personal-info.cy.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. /**
  2. * @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
  3. *
  4. * @author Ferdinand Thiessen <opensource@fthiessen.de>
  5. *
  6. * @license AGPL-3.0-or-later
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. import type { User } from '@nextcloud/cypress'
  23. import { handlePasswordConfirmation } from './usersUtils.ts'
  24. let user: User
  25. enum Visibility {
  26. Private = 'Private',
  27. Local = 'Local',
  28. Federated = 'Federated',
  29. Public = 'Published'
  30. }
  31. const ALL_VISIBILITIES = [Visibility.Public, Visibility.Private, Visibility.Local, Visibility.Federated]
  32. /**
  33. * Get the input connected to a specific label
  34. * @param label The content of the label
  35. */
  36. const inputForLabel = (label: string) => cy.contains('label', label).then((el) => cy.get(`#${el.attr('for')}`))
  37. /**
  38. * Get the property visibility button
  39. * @param property The property to which to look for the button
  40. */
  41. const getVisibilityButton = (property: string) => cy.get(`button[aria-label*="Change scope level of ${property.toLowerCase()}"`)
  42. /**
  43. * Validate a specifiy visibility is set for a property
  44. * @param property The property
  45. * @param active The active visibility
  46. */
  47. const validateActiveVisibility = (property: string, active: Visibility) => {
  48. getVisibilityButton(property)
  49. .should('have.attr', 'aria-label')
  50. .and('match', new RegExp(`current scope is ${active}`, 'i'))
  51. getVisibilityButton(property)
  52. .click()
  53. cy.get('ul[role="menu"]')
  54. .contains('button', active)
  55. .should('have.attr', 'aria-checked', 'true')
  56. // close menu
  57. getVisibilityButton(property)
  58. .click()
  59. }
  60. /**
  61. * Set a specific visibility for a property
  62. * @param property The property
  63. * @param active The visibility to set
  64. */
  65. const setActiveVisibility = (property: string, active: Visibility) => {
  66. getVisibilityButton(property)
  67. .click()
  68. cy.get('ul[role="menu"]')
  69. .contains('button', active)
  70. .click({ force: true })
  71. handlePasswordConfirmation(user.password)
  72. cy.wait('@submitSetting')
  73. }
  74. /**
  75. * Helper to check that setting all visibilities on a property is possible
  76. * @param property The property to test
  77. * @param defaultVisibility The default visibility of that property
  78. * @param allowedVisibility Visibility that is allowed and need to be checked
  79. */
  80. const checkSettingsVisibility = (property: string, defaultVisibility: Visibility = Visibility.Local, allowedVisibility: Visibility[] = ALL_VISIBILITIES) => {
  81. getVisibilityButton(property)
  82. .scrollIntoView()
  83. validateActiveVisibility(property, defaultVisibility)
  84. allowedVisibility.forEach((active) => {
  85. setActiveVisibility(property, active)
  86. cy.reload()
  87. getVisibilityButton(property).scrollIntoView()
  88. validateActiveVisibility(property, active)
  89. })
  90. // TODO: Fix this in vue library then enable this test again
  91. /* // Test that not allowed options are disabled
  92. ALL_VISIBILITIES.filter((v) => !allowedVisibility.includes(v)).forEach((disabled) => {
  93. getVisibilityButton(property)
  94. .click()
  95. cy.get('ul[role="dialog"')
  96. .contains('button', disabled)
  97. .should('exist')
  98. .and('have.attr', 'disabled', 'true')
  99. }) */
  100. }
  101. const genericProperties = ['Location', 'X (formerly Twitter)', 'Fediverse']
  102. const nonfederatedProperties = ['Organisation', 'Role', 'Headline', 'About']
  103. describe('Settings: Change personal information', { testIsolation: true }, () => {
  104. before(() => {
  105. // ensure we can set locale and language
  106. cy.runOccCommand('config:system:delete force_language')
  107. cy.runOccCommand('config:system:delete force_locale')
  108. })
  109. after(() => {
  110. cy.runOccCommand('config:system:set force_language --value en')
  111. cy.runOccCommand('config:system:set force_locale --value en_US')
  112. })
  113. beforeEach(() => {
  114. cy.createRandomUser().then(($user) => {
  115. user = $user
  116. cy.modifyUser(user, 'language', 'en')
  117. cy.modifyUser(user, 'locale', 'en_US')
  118. cy.login($user)
  119. cy.visit('/settings/user')
  120. })
  121. cy.intercept('PUT', /ocs\/v2.php\/cloud\/users\//).as('submitSetting')
  122. })
  123. it('Can dis- and enable the profile', () => {
  124. cy.visit(`/u/${user.userId}`)
  125. cy.contains('h2', user.userId).should('be.visible')
  126. cy.visit('/settings/user')
  127. cy.contains('Enable profile').click()
  128. handlePasswordConfirmation(user.password)
  129. cy.visit(`/u/${user.userId}`, { failOnStatusCode: false })
  130. cy.contains('h2', 'Profile not found').should('be.visible')
  131. cy.visit('/settings/user')
  132. cy.contains('Enable profile').click()
  133. handlePasswordConfirmation(user.password)
  134. cy.visit(`/u/${user.userId}`, { failOnStatusCode: false })
  135. cy.contains('h2', user.userId).should('be.visible')
  136. })
  137. it('Can change language', () => {
  138. cy.intercept('GET', /settings\/user/).as('reload')
  139. inputForLabel('Language').scrollIntoView()
  140. inputForLabel('Language').type('Ned')
  141. cy.contains('li[role="option"]', 'Nederlands')
  142. .click()
  143. cy.wait('@reload')
  144. // expect language changed
  145. inputForLabel('Taal').scrollIntoView()
  146. cy.contains('section', 'Help met vertalen')
  147. })
  148. it('Can change locale', () => {
  149. cy.intercept('GET', /settings\/user/).as('reload')
  150. cy.clock(new Date(2024, 0, 10))
  151. // Default is US
  152. cy.contains('section', '01/10/2024')
  153. inputForLabel('Locale').scrollIntoView()
  154. inputForLabel('Locale').type('German')
  155. cy.contains('li[role="option"]', 'German (Germany')
  156. .click()
  157. cy.wait('@reload')
  158. // expect locale changed
  159. inputForLabel('Locale').scrollIntoView()
  160. cy.contains('section', '10.01.2024')
  161. })
  162. it('Can set primary email and change its visibility', () => {
  163. cy.contains('label', 'Email').scrollIntoView()
  164. // Check invalid input
  165. inputForLabel('Email').type('foo bar')
  166. inputForLabel('Email').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false)
  167. // handle valid input
  168. inputForLabel('Email').type('{selectAll}hello@example.com')
  169. handlePasswordConfirmation(user.password)
  170. cy.wait('@submitSetting')
  171. cy.reload()
  172. inputForLabel('Email').should('have.value', 'hello@example.com')
  173. checkSettingsVisibility(
  174. 'Email',
  175. Visibility.Federated,
  176. // It is not possible to set it as private
  177. ALL_VISIBILITIES.filter((v) => v !== Visibility.Private),
  178. )
  179. // check it is visible on the profile
  180. cy.visit(`/u/${user.userId}`)
  181. cy.contains('a', 'hello@example.com').should('be.visible').and('have.attr', 'href', 'mailto:hello@example.com')
  182. })
  183. it('Can delete primary email', () => {
  184. cy.contains('label', 'Email').scrollIntoView()
  185. inputForLabel('Email').type('{selectAll}hello@example.com')
  186. handlePasswordConfirmation(user.password)
  187. cy.wait('@submitSetting')
  188. // check after reload
  189. cy.reload()
  190. inputForLabel('Email').should('have.value', 'hello@example.com')
  191. // delete email
  192. cy.get('button[aria-label="Remove primary email"]').click({ force: true })
  193. cy.wait('@submitSetting')
  194. // check after reload
  195. cy.reload()
  196. inputForLabel('Email').should('have.value', '')
  197. })
  198. it('Can set and delete additional emails', () => {
  199. cy.get('button[aria-label="Add additional email"]').should('be.disabled')
  200. // we need a primary email first
  201. cy.contains('label', 'Email').scrollIntoView()
  202. inputForLabel('Email').type('{selectAll}primary@example.com')
  203. handlePasswordConfirmation(user.password)
  204. cy.wait('@submitSetting')
  205. // add new email
  206. cy.get('button[aria-label="Add additional email"]')
  207. .click()
  208. // without any value we should not be able to add a second additional
  209. cy.get('button[aria-label="Add additional email"]').should('be.disabled')
  210. // fill the first additional
  211. inputForLabel('Additional email address 1')
  212. .type('1@example.com')
  213. handlePasswordConfirmation(user.password)
  214. cy.wait('@submitSetting')
  215. // add second additional email
  216. cy.get('button[aria-label="Add additional email"]')
  217. .click()
  218. // fill the second additional
  219. inputForLabel('Additional email address 2')
  220. .type('2@example.com')
  221. handlePasswordConfirmation(user.password)
  222. cy.wait('@submitSetting')
  223. // check the content is saved
  224. cy.reload()
  225. inputForLabel('Additional email address 1')
  226. .should('have.value', '1@example.com')
  227. inputForLabel('Additional email address 2')
  228. .should('have.value', '2@example.com')
  229. // delete the first
  230. cy.get('button[aria-label="Options for additional email address 1"]')
  231. .click({ force: true })
  232. cy.contains('button[role="menuitem"]', 'Delete email')
  233. .click({ force: true })
  234. handlePasswordConfirmation(user.password)
  235. cy.reload()
  236. inputForLabel('Additional email address 1')
  237. .should('have.value', '2@example.com')
  238. })
  239. it('Can set Full name and change its visibility', () => {
  240. cy.contains('label', 'Full name').scrollIntoView()
  241. // handle valid input
  242. inputForLabel('Full name').type('{selectAll}Jane Doe')
  243. handlePasswordConfirmation(user.password)
  244. cy.wait('@submitSetting')
  245. cy.reload()
  246. inputForLabel('Full name').should('have.value', 'Jane Doe')
  247. checkSettingsVisibility(
  248. 'Full name',
  249. Visibility.Federated,
  250. // It is not possible to set it as private
  251. ALL_VISIBILITIES.filter((v) => v !== Visibility.Private),
  252. )
  253. // check it is visible on the profile
  254. cy.visit(`/u/${user.userId}`)
  255. cy.contains('h2', 'Jane Doe').should('be.visible')
  256. })
  257. it('Can set Phone number and its visibility', () => {
  258. cy.contains('label', 'Phone number').scrollIntoView()
  259. // Check invalid input
  260. inputForLabel('Phone number').type('foo bar')
  261. inputForLabel('Phone number').should('have.attr', 'class').and('contain', '--error')
  262. // handle valid input
  263. inputForLabel('Phone number').type('{selectAll}+49 89 721010 99701')
  264. inputForLabel('Phone number').should('have.attr', 'class').and('not.contain', '--error')
  265. handlePasswordConfirmation(user.password)
  266. cy.wait('@submitSetting')
  267. cy.reload()
  268. inputForLabel('Phone number').should('have.value', '+498972101099701')
  269. checkSettingsVisibility('Phone number')
  270. // check it is visible on the profile
  271. cy.visit(`/u/${user.userId}`)
  272. cy.get('a[href="tel:+498972101099701"]').should('be.visible')
  273. })
  274. it('Can set Website and change its visibility', () => {
  275. cy.contains('label', 'Website').scrollIntoView()
  276. // Check invalid input
  277. inputForLabel('Website').type('foo bar')
  278. inputForLabel('Website').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false)
  279. // handle valid input
  280. inputForLabel('Website').type('{selectAll}http://example.com')
  281. handlePasswordConfirmation(user.password)
  282. cy.wait('@submitSetting')
  283. cy.reload()
  284. inputForLabel('Website').should('have.value', 'http://example.com')
  285. checkSettingsVisibility('Website')
  286. // check it is visible on the profile
  287. cy.visit(`/u/${user.userId}`)
  288. cy.contains('http://example.com').should('be.visible')
  289. })
  290. // Check generic properties that allow any visibility and any value
  291. genericProperties.forEach((property) => {
  292. it(`Can set ${property} and change its visibility`, () => {
  293. const uniqueValue = `${property.toUpperCase()} ${property.toLowerCase()}`
  294. cy.contains('label', property).scrollIntoView()
  295. inputForLabel(property).type(uniqueValue)
  296. handlePasswordConfirmation(user.password)
  297. cy.wait('@submitSetting')
  298. cy.reload()
  299. inputForLabel(property).should('have.value', uniqueValue)
  300. checkSettingsVisibility(property)
  301. // check it is visible on the profile
  302. cy.visit(`/u/${user.userId}`)
  303. cy.contains(uniqueValue).should('be.visible')
  304. })
  305. })
  306. // Check non federated properties - those where we need special configuration and only support local visibility
  307. nonfederatedProperties.forEach((property) => {
  308. it(`Can set ${property} and change its visibility`, () => {
  309. const uniqueValue = `${property.toUpperCase()} ${property.toLowerCase()}`
  310. cy.contains('label', property).scrollIntoView()
  311. inputForLabel(property).type(uniqueValue)
  312. handlePasswordConfirmation(user.password)
  313. cy.wait('@submitSetting')
  314. cy.reload()
  315. inputForLabel(property).should('have.value', uniqueValue)
  316. checkSettingsVisibility(property, Visibility.Local, [Visibility.Private, Visibility.Local])
  317. // check it is visible on the profile
  318. cy.visit(`/u/${user.userId}`)
  319. cy.contains(uniqueValue).should('be.visible')
  320. })
  321. })
  322. })