You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

415 lines
13 KiB

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