mirror of
https://github.com/nextcloud/server.git
synced 2026-06-24 16:09:50 -04:00
test(settings): migrate end-to-end tests to PlayWright
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
b54e98a64b
commit
b245d2dcc9
22 changed files with 1046 additions and 1701 deletions
4
.github/workflows/cypress.yml
vendored
4
.github/workflows/cypress.yml
vendored
|
|
@ -141,10 +141,10 @@ jobs:
|
|||
matrix:
|
||||
# Run multiple copies of the current job in parallel
|
||||
# Please increase the number or runners as your tests suite grows (0 based index for e2e tests)
|
||||
containers: ['setup', '0', '1', '2', '3', '4', '5']
|
||||
containers: ['setup', '0', '1', '2', '3', '4']
|
||||
# Hack as strategy.job-total includes the "setup" and GitHub does not allow math expressions
|
||||
# Always align this number with the total of e2e runners (max. index + 1)
|
||||
total-containers: [6]
|
||||
total-containers: [5]
|
||||
|
||||
services:
|
||||
mysql:
|
||||
|
|
|
|||
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
|
|
@ -82,8 +82,8 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4]
|
||||
shardTotal: [4]
|
||||
shardIndex: [1, 2, 3, 4, 5]
|
||||
shardTotal: [5]
|
||||
outputs:
|
||||
node-version: ${{ steps.versions.outputs.node-version }}
|
||||
package-manager-version: ${{ steps.versions.outputs.package-manager-version }}
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { clearState, getNextcloudUserMenu, getNextcloudUserMenuToggle } from '../../support/commonUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: Ensure only administrator can see the administration settings section', { testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
clearState()
|
||||
})
|
||||
|
||||
it('Regular users cannot see admin-level items on the Settings page', () => {
|
||||
// Given I am logged in
|
||||
cy.createRandomUser().then(($user) => {
|
||||
cy.login($user)
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
// I open the settings menu
|
||||
getNextcloudUserMenuToggle().click()
|
||||
// I navigate to the settings panel
|
||||
getNextcloudUserMenu()
|
||||
.findByRole('link', { name: /settings/i })
|
||||
.click()
|
||||
cy.url().should('match', /\/settings\/user$/)
|
||||
|
||||
cy.findAllByRole('navigation')
|
||||
.filter('#app-navigation-vue')
|
||||
.as('appNavigation')
|
||||
.findByRole('list', { name: 'Personal' })
|
||||
.should('be.visible')
|
||||
.findByRole('link', { name: /Personal info/i })
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'aria-current', 'page')
|
||||
|
||||
cy.get('@appNavigation')
|
||||
.findByRole('list', { name: 'Administration' })
|
||||
.should('not.exist')
|
||||
})
|
||||
|
||||
it('Admin users can see admin-level items on the Settings page', () => {
|
||||
// Given I am logged in
|
||||
cy.login(admin)
|
||||
cy.visit('/')
|
||||
|
||||
// I open the settings menu
|
||||
getNextcloudUserMenuToggle().click()
|
||||
// I navigate to the settings panel
|
||||
getNextcloudUserMenu()
|
||||
.findByRole('link', { name: /Personal settings/i })
|
||||
.click()
|
||||
cy.url().should('match', /\/settings\/user$/)
|
||||
|
||||
cy.findAllByRole('navigation')
|
||||
.filter('#app-navigation-vue')
|
||||
.as('appNavigation')
|
||||
.findByRole('list', { name: 'Personal' })
|
||||
.should('be.visible')
|
||||
.findByRole('link', { name: /Personal info/i })
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'aria-current', 'page')
|
||||
|
||||
cy.get('@appNavigation')
|
||||
.findByRole('list', { name: 'Administration' })
|
||||
.should('be.visible')
|
||||
.findByRole('link', { name: /Overview/i })
|
||||
.should('be.visible')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,449 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
|
||||
import { handlePasswordConfirmation } from '../core-utils.ts'
|
||||
|
||||
let user: User
|
||||
|
||||
enum Visibility {
|
||||
Private = 'Private',
|
||||
Local = 'Local',
|
||||
Federated = 'Federated',
|
||||
Public = 'Published',
|
||||
}
|
||||
|
||||
const ALL_VISIBILITIES = [Visibility.Public, Visibility.Private, Visibility.Local, Visibility.Federated]
|
||||
|
||||
/**
|
||||
* Get the input connected to a specific label
|
||||
* @param label The content of the label
|
||||
*/
|
||||
const inputForLabel = (label: string) => cy.contains('label', label).then((el) => cy.get(`#${el.attr('for')}`))
|
||||
|
||||
/**
|
||||
* Get the property visibility button
|
||||
* @param property The property to which to look for the button
|
||||
*/
|
||||
const getVisibilityButton = (property: string) => cy.get(`button[aria-label*="Change scope level of ${property.toLowerCase()}"`)
|
||||
|
||||
/**
|
||||
* Validate a specifiy visibility is set for a property
|
||||
* @param property The property
|
||||
* @param active The active visibility
|
||||
*/
|
||||
function validateActiveVisibility(property: string, active: Visibility) {
|
||||
getVisibilityButton(property)
|
||||
.should('have.attr', 'aria-label')
|
||||
.and('match', new RegExp(`current scope is ${active}`, 'i'))
|
||||
getVisibilityButton(property)
|
||||
.click()
|
||||
cy.get('ul[role="menu"]')
|
||||
.contains('button', active)
|
||||
.should('have.attr', 'aria-checked', 'true')
|
||||
|
||||
// close menu
|
||||
getVisibilityButton(property)
|
||||
.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific visibility for a property
|
||||
* @param property The property
|
||||
* @param active The visibility to set
|
||||
*/
|
||||
function setActiveVisibility(property: string, active: Visibility) {
|
||||
getVisibilityButton(property)
|
||||
.click()
|
||||
cy.get('ul[role="menu"]')
|
||||
.contains('button', active)
|
||||
.click({ force: true })
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check that setting all visibilities on a property is possible
|
||||
* @param property The property to test
|
||||
* @param defaultVisibility The default visibility of that property
|
||||
* @param allowedVisibility Visibility that is allowed and need to be checked
|
||||
*/
|
||||
function checkSettingsVisibility(property: string, defaultVisibility: Visibility = Visibility.Local, allowedVisibility: Visibility[] = ALL_VISIBILITIES) {
|
||||
getVisibilityButton(property)
|
||||
.scrollIntoView()
|
||||
|
||||
validateActiveVisibility(property, defaultVisibility)
|
||||
|
||||
allowedVisibility.forEach((active) => {
|
||||
setActiveVisibility(property, active)
|
||||
|
||||
cy.reload()
|
||||
getVisibilityButton(property).scrollIntoView()
|
||||
|
||||
validateActiveVisibility(property, active)
|
||||
})
|
||||
|
||||
// TODO: Fix this in vue library then enable this test again
|
||||
/* // Test that not allowed options are disabled
|
||||
ALL_VISIBILITIES.filter((v) => !allowedVisibility.includes(v)).forEach((disabled) => {
|
||||
getVisibilityButton(property)
|
||||
.click()
|
||||
cy.get('ul[role="dialog"')
|
||||
.contains('button', disabled)
|
||||
.should('exist')
|
||||
.and('have.attr', 'disabled', 'true')
|
||||
}) */
|
||||
}
|
||||
|
||||
const genericProperties = [
|
||||
['Location', 'Berlin'],
|
||||
['X (formerly Twitter)', 'nextclouders'],
|
||||
['Fediverse', 'nextcloud@mastodon.xyz'],
|
||||
]
|
||||
const nonfederatedProperties = ['Organisation', 'Role', 'Headline', 'About']
|
||||
|
||||
describe('Settings: Change personal information', { testIsolation: true }, () => {
|
||||
let snapshot: string = ''
|
||||
|
||||
before(() => {
|
||||
// make sure the fediverse check does not do http requests
|
||||
cy.runOccCommand('config:system:set has_internet_connection --type bool --value false')
|
||||
// ensure we can set locale and language
|
||||
cy.runOccCommand('config:system:delete force_language')
|
||||
cy.runOccCommand('config:system:delete force_locale')
|
||||
cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
cy.modifyUser(user, 'language', 'en')
|
||||
cy.modifyUser(user, 'locale', 'en_US')
|
||||
|
||||
// Make sure the user is logged in at least once
|
||||
// before the snapshot is taken to speed up the tests
|
||||
cy.login(user)
|
||||
cy.visit('/settings/user')
|
||||
|
||||
cy.saveState().then(($snapshot) => {
|
||||
snapshot = $snapshot
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.runOccCommand('config:system:delete has_internet_connection')
|
||||
|
||||
cy.runOccCommand('config:system:set force_language --value en')
|
||||
cy.runOccCommand('config:system:set force_locale --value en_US')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login(user)
|
||||
cy.visit('/settings/user')
|
||||
cy.intercept('PUT', /ocs\/v2.php\/cloud\/users\//).as('submitSetting')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cy.restoreState(snapshot)
|
||||
})
|
||||
|
||||
it('Can dis- and enable the profile', () => {
|
||||
cy.visit(`/u/${user.userId}`)
|
||||
cy.contains('h2', user.userId).should('be.visible')
|
||||
|
||||
cy.visit('/settings/user')
|
||||
cy.contains('Enable profile').click()
|
||||
handlePasswordConfirmation(user.password)
|
||||
cy.wait('@submitSetting')
|
||||
|
||||
cy.visit(`/u/${user.userId}`, { failOnStatusCode: false })
|
||||
cy.contains('h2', 'Profile not found').should('be.visible')
|
||||
|
||||
cy.visit('/settings/user')
|
||||
cy.contains('Enable profile').click()
|
||||
handlePasswordConfirmation(user.password)
|
||||
cy.wait('@submitSetting')
|
||||
|
||||
cy.visit(`/u/${user.userId}`, { failOnStatusCode: false })
|
||||
cy.contains('h2', user.userId).should('be.visible')
|
||||
})
|
||||
|
||||
it('Can change language', () => {
|
||||
cy.intercept('GET', /settings\/user/).as('reload')
|
||||
inputForLabel('Language').scrollIntoView()
|
||||
inputForLabel('Language').type('Ned')
|
||||
cy.contains('li[role="option"]', 'Nederlands')
|
||||
.click()
|
||||
cy.wait('@reload')
|
||||
|
||||
// expect language changed
|
||||
inputForLabel('Taal').scrollIntoView()
|
||||
cy.contains('section', 'Help met vertalen')
|
||||
})
|
||||
|
||||
it('Can change locale', () => {
|
||||
cy.intercept('GET', /settings\/user/).as('reload')
|
||||
cy.clock(new Date(2024, 0, 10))
|
||||
|
||||
// Default is US
|
||||
cy.contains('section', '01/10/2024')
|
||||
|
||||
inputForLabel('Locale').scrollIntoView()
|
||||
inputForLabel('Locale').type('German')
|
||||
cy.contains('li[role="option"]', 'German (Germany')
|
||||
.click()
|
||||
cy.wait('@reload')
|
||||
|
||||
// expect locale changed
|
||||
inputForLabel('Locale').scrollIntoView()
|
||||
cy.contains('section', '10.01.2024')
|
||||
})
|
||||
|
||||
it('Can set primary email and change its visibility', () => {
|
||||
cy.contains('label', 'Email').scrollIntoView()
|
||||
// Check invalid input
|
||||
inputForLabel('Email').type('foo bar')
|
||||
inputForLabel('Email').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false)
|
||||
// handle valid input
|
||||
inputForLabel('Email').type('{selectAll}hello@example.com')
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Email').should('have.value', 'hello@example.com')
|
||||
|
||||
checkSettingsVisibility(
|
||||
'Email',
|
||||
Visibility.Federated,
|
||||
// It is not possible to set it as private
|
||||
ALL_VISIBILITIES.filter((v) => v !== Visibility.Private),
|
||||
)
|
||||
|
||||
// check it is visible on the profile
|
||||
cy.visit(`/u/${user.userId}`)
|
||||
cy.contains('a', 'hello@example.com').should('be.visible').and('have.attr', 'href', 'mailto:hello@example.com')
|
||||
})
|
||||
|
||||
it('Can delete primary email', () => {
|
||||
cy.contains('label', 'Email').scrollIntoView()
|
||||
inputForLabel('Email').type('{selectAll}hello@example.com')
|
||||
handlePasswordConfirmation(user.password)
|
||||
cy.wait('@submitSetting')
|
||||
|
||||
// check after reload
|
||||
cy.reload()
|
||||
inputForLabel('Email').should('have.value', 'hello@example.com')
|
||||
|
||||
// delete email
|
||||
cy.get('button[aria-label="Remove primary email"]').click({ force: true })
|
||||
cy.wait('@submitSetting')
|
||||
|
||||
// check after reload
|
||||
cy.reload()
|
||||
inputForLabel('Email').should('have.value', '')
|
||||
})
|
||||
|
||||
it('Can set and delete additional emails', () => {
|
||||
cy.get('button[aria-label="Add additional email"]').should('be.disabled')
|
||||
// we need a primary email first
|
||||
cy.contains('label', 'Email').scrollIntoView()
|
||||
inputForLabel('Email').type('{selectAll}primary@example.com')
|
||||
handlePasswordConfirmation(user.password)
|
||||
cy.wait('@submitSetting')
|
||||
|
||||
// add new email
|
||||
cy.get('button[aria-label="Add additional email"]')
|
||||
.click()
|
||||
|
||||
// without any value we should not be able to add a second additional
|
||||
cy.get('button[aria-label="Add additional email"]').should('be.disabled')
|
||||
|
||||
// fill the first additional
|
||||
inputForLabel('Additional email address 1')
|
||||
.type('1@example.com')
|
||||
handlePasswordConfirmation(user.password)
|
||||
cy.wait('@submitSetting')
|
||||
|
||||
// add second additional email
|
||||
cy.get('button[aria-label="Add additional email"]')
|
||||
.click()
|
||||
|
||||
// fill the second additional
|
||||
inputForLabel('Additional email address 2')
|
||||
.type('2@example.com')
|
||||
handlePasswordConfirmation(user.password)
|
||||
cy.wait('@submitSetting')
|
||||
|
||||
// check the content is saved
|
||||
cy.reload()
|
||||
inputForLabel('Additional email address 1')
|
||||
.should('have.value', '1@example.com')
|
||||
inputForLabel('Additional email address 2')
|
||||
.should('have.value', '2@example.com')
|
||||
|
||||
// delete the first
|
||||
cy.get('button[aria-label="Options for additional email address 1"]')
|
||||
.click({ force: true })
|
||||
cy.contains('button[role="menuitem"]', 'Delete email')
|
||||
.click({ force: true })
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.reload()
|
||||
inputForLabel('Additional email address 1')
|
||||
.should('have.value', '2@example.com')
|
||||
})
|
||||
|
||||
it('Can set Full name and change its visibility', () => {
|
||||
cy.contains('label', 'Full name').scrollIntoView()
|
||||
// handle valid input
|
||||
inputForLabel('Full name').type('{selectAll}Jane Doe')
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Full name').should('have.value', 'Jane Doe')
|
||||
|
||||
checkSettingsVisibility(
|
||||
'Full name',
|
||||
Visibility.Federated,
|
||||
// It is not possible to set it as private
|
||||
ALL_VISIBILITIES.filter((v) => v !== Visibility.Private),
|
||||
)
|
||||
|
||||
// check it is visible on the profile
|
||||
cy.visit(`/u/${user.userId}`)
|
||||
cy.contains('h2', 'Jane Doe').should('be.visible')
|
||||
})
|
||||
|
||||
it('Can set Phone number and its visibility', () => {
|
||||
cy.contains('label', 'Phone number').scrollIntoView()
|
||||
// Check invalid input
|
||||
inputForLabel('Phone number').type('foo bar')
|
||||
inputForLabel('Phone number').should('have.attr', 'class').and('contain', '--error')
|
||||
// handle valid input
|
||||
inputForLabel('Phone number').type('{selectAll}+49 89 721010 99701')
|
||||
inputForLabel('Phone number').should('have.attr', 'class').and('not.contain', '--error')
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Phone number').should('have.value', '+498972101099701')
|
||||
|
||||
checkSettingsVisibility('Phone number')
|
||||
|
||||
// check it is visible on the profile
|
||||
cy.visit(`/u/${user.userId}`)
|
||||
cy.get('a[href="tel:+498972101099701"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Can set phone number with phone region', () => {
|
||||
cy.contains('label', 'Phone number').scrollIntoView()
|
||||
inputForLabel('Phone number').type('{selectAll}0 40 428990')
|
||||
inputForLabel('Phone number').should('have.attr', 'class').and('contain', '--error')
|
||||
|
||||
cy.runOccCommand('config:system:set default_phone_region --value DE')
|
||||
cy.reload()
|
||||
|
||||
cy.contains('label', 'Phone number').scrollIntoView()
|
||||
inputForLabel('Phone number').type('{selectAll}0 40 428990')
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Phone number').should('have.value', '+4940428990')
|
||||
})
|
||||
|
||||
it('Can reset phone number', () => {
|
||||
cy.contains('label', 'Phone number').scrollIntoView()
|
||||
inputForLabel('Phone number').type('{selectAll}+49 40 428990')
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Phone number').should('have.value', '+4940428990')
|
||||
|
||||
inputForLabel('Phone number').clear()
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Phone number').should('have.value', '')
|
||||
})
|
||||
|
||||
it('Can reset social media property', () => {
|
||||
cy.contains('label', 'Fediverse').scrollIntoView()
|
||||
inputForLabel('Fediverse').type('{selectAll}@nextcloud@mastodon.social')
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Fediverse').should('have.value', 'nextcloud@mastodon.social')
|
||||
|
||||
inputForLabel('Fediverse').clear()
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Fediverse').should('have.value', '')
|
||||
})
|
||||
|
||||
it('Can set Website and change its visibility', () => {
|
||||
cy.contains('label', 'Website').scrollIntoView()
|
||||
// Check invalid input
|
||||
inputForLabel('Website').type('foo bar')
|
||||
inputForLabel('Website').then(($el) => expect(($el.get(0) as HTMLInputElement).checkValidity()).to.be.false)
|
||||
// handle valid input
|
||||
inputForLabel('Website').type('{selectAll}http://example.com')
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel('Website').should('have.value', 'http://example.com')
|
||||
|
||||
checkSettingsVisibility('Website')
|
||||
|
||||
// check it is visible on the profile
|
||||
cy.visit(`/u/${user.userId}`)
|
||||
cy.contains('http://example.com').should('be.visible')
|
||||
})
|
||||
|
||||
// Check generic properties that allow any visibility and any value
|
||||
genericProperties.forEach(([property, value]) => {
|
||||
it(`Can set ${property} and change its visibility`, () => {
|
||||
cy.contains('label', property).scrollIntoView()
|
||||
inputForLabel(property).type(value)
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel(property).should('have.value', value)
|
||||
|
||||
checkSettingsVisibility(property)
|
||||
|
||||
// check it is visible on the profile
|
||||
cy.visit(`/u/${user.userId}`)
|
||||
cy.contains(value).should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
// Check non federated properties - those where we need special configuration and only support local visibility
|
||||
nonfederatedProperties.forEach((property) => {
|
||||
it(`Can set ${property} and change its visibility`, () => {
|
||||
const uniqueValue = `${property.toUpperCase()} ${property.toLowerCase()}`
|
||||
cy.contains('label', property).scrollIntoView()
|
||||
inputForLabel(property).type(uniqueValue)
|
||||
handlePasswordConfirmation(user.password)
|
||||
|
||||
cy.wait('@submitSetting')
|
||||
cy.reload()
|
||||
inputForLabel(property).should('have.value', uniqueValue)
|
||||
|
||||
checkSettingsVisibility(property, Visibility.Local, [Visibility.Private, Visibility.Local])
|
||||
|
||||
// check it is visible on the profile
|
||||
cy.visit(`/u/${user.userId}`)
|
||||
cy.contains(uniqueValue).should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { randomString } from '../../support/utils/randomString.ts'
|
||||
import { handlePasswordConfirmation } from '../core-utils.ts'
|
||||
import { getUserListRow } from './usersUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
const john = new User('john', '123456')
|
||||
|
||||
/**
|
||||
* Make a user subadmin of a group.
|
||||
*
|
||||
* @param user - The user to make subadmin
|
||||
* @param group - The group the user should be subadmin of
|
||||
*/
|
||||
function makeSubAdmin(user: User, group: string): void {
|
||||
cy.request({
|
||||
url: `${Cypress.config('baseUrl')!.replace('/index.php', '')}/ocs/v2.php/cloud/users/${user.userId}/subadmins`,
|
||||
method: 'POST',
|
||||
auth: {
|
||||
user: admin.userId,
|
||||
password: admin.userId,
|
||||
},
|
||||
headers: {
|
||||
'OCS-ApiRequest': 'true',
|
||||
},
|
||||
body: {
|
||||
groupid: group,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('Settings: Create accounts as a group admin', function() {
|
||||
let subadmin: User
|
||||
let group: string
|
||||
|
||||
beforeEach(() => {
|
||||
group = randomString(7)
|
||||
cy.deleteUser(john)
|
||||
cy.createRandomUser().then((user) => {
|
||||
subadmin = user
|
||||
cy.runOccCommand(`group:add '${group}'`)
|
||||
cy.runOccCommand(`group:adduser '${group}' '${subadmin.userId}'`)
|
||||
makeSubAdmin(subadmin, group)
|
||||
})
|
||||
})
|
||||
|
||||
it('Can create a user with prefilled single group', () => {
|
||||
cy.login(subadmin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
|
||||
// open the New user modal
|
||||
cy.get('button#new-user-button').click()
|
||||
|
||||
cy.get('form[data-test="form"]').within(() => {
|
||||
// see that the correct group is preselected
|
||||
cy.contains('[data-test="groups"] .vs__selected', group).should('be.visible')
|
||||
// see that the username is ""
|
||||
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
|
||||
// set the username to john
|
||||
cy.get('input[data-test="username"]').type(john.userId)
|
||||
// see that the username is john
|
||||
cy.get('input[data-test="username"]').should('have.value', john.userId)
|
||||
// see that the password is ""
|
||||
cy.get('input[type="password"]').should('exist').and('have.value', '')
|
||||
// set the password to 123456
|
||||
cy.get('input[type="password"]').type(john.password)
|
||||
// see that the password is 123456
|
||||
cy.get('input[type="password"]').should('have.value', john.password)
|
||||
})
|
||||
|
||||
cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => {
|
||||
// submit the new user form
|
||||
cy.get('button[type="submit"]').click({ force: true })
|
||||
})
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
// see that the created user is in the list
|
||||
getUserListRow(john.userId)
|
||||
// see that the list of users contains the user john
|
||||
.contains(john.userId).should('exist')
|
||||
})
|
||||
|
||||
// Skiping as this crash the webengine in the CI
|
||||
it.skip('Can create a new user when member of multiple groups', () => {
|
||||
const group2 = randomString(7)
|
||||
cy.runOccCommand(`group:add '${group2}'`)
|
||||
cy.runOccCommand(`group:adduser '${group2}' '${subadmin.userId}'`)
|
||||
makeSubAdmin(subadmin, group2)
|
||||
|
||||
cy.login(subadmin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
|
||||
// open the New user modal
|
||||
cy.get('button#new-user-button').click()
|
||||
|
||||
cy.get('form[data-test="form"]').within(() => {
|
||||
// see that no group is pre-selected
|
||||
cy.get('[data-test="groups"] .vs__selected').should('not.exist')
|
||||
// see both groups are available
|
||||
cy.findByRole('combobox', { name: /member of the following groups/i })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
// can select both groups
|
||||
cy.document().its('body')
|
||||
.findByRole('listbox', { name: 'Options' })
|
||||
.should('be.visible')
|
||||
.as('options')
|
||||
.findAllByRole('option')
|
||||
.should('have.length', 2)
|
||||
.get('@options')
|
||||
.findByRole('option', { name: group })
|
||||
.should('be.visible')
|
||||
.get('@options')
|
||||
.findByRole('option', { name: group2 })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
// see group is selected
|
||||
cy.contains('[data-test="groups"] .vs__selected', group2).should('be.visible')
|
||||
|
||||
// see that the username is ""
|
||||
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
|
||||
// set the username to john
|
||||
cy.get('input[data-test="username"]').type(john.userId)
|
||||
// see that the username is john
|
||||
cy.get('input[data-test="username"]').should('have.value', john.userId)
|
||||
// see that the password is ""
|
||||
cy.get('input[type="password"]').should('exist').and('have.value', '')
|
||||
// set the password to 123456
|
||||
cy.get('input[type="password"]').type(john.password)
|
||||
// see that the password is 123456
|
||||
cy.get('input[type="password"]').should('have.value', john.password)
|
||||
})
|
||||
|
||||
cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => {
|
||||
// submit the new user form
|
||||
cy.get('button[type="submit"]').click({ force: true })
|
||||
})
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
// see that the created user is in the list
|
||||
getUserListRow(john.userId)
|
||||
// see that the list of users contains the user john
|
||||
.contains(john.userId).should('exist')
|
||||
})
|
||||
|
||||
it.skip('Only sees groups they are subadmin of', () => {
|
||||
const group2 = randomString(7)
|
||||
cy.runOccCommand(`group:add '${group2}'`)
|
||||
cy.runOccCommand(`group:adduser '${group2}' '${subadmin.userId}'`)
|
||||
// not a subadmin!
|
||||
|
||||
cy.login(subadmin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
|
||||
// open the New user modal
|
||||
cy.get('button#new-user-button').click()
|
||||
|
||||
cy.get('form[data-test="form"]').within(() => {
|
||||
// see that the subadmin group is pre-selected
|
||||
cy.contains('[data-test="groups"] .vs__selected', group).should('be.visible')
|
||||
// see only the subadmin group is available
|
||||
cy.findByRole('combobox', { name: /member of the following groups/i })
|
||||
.should('be.visible')
|
||||
.click()
|
||||
// can select both groups
|
||||
cy.document().its('body')
|
||||
.findByRole('listbox', { name: 'Options' })
|
||||
.should('be.visible')
|
||||
.as('options')
|
||||
.findAllByRole('option')
|
||||
.should('have.length', 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
/// <reference types="cypress-if" />
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { handlePasswordConfirmation } from '../core-utils.ts'
|
||||
import { getUserListRow } from './usersUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
const john = new User('john', '123456')
|
||||
|
||||
describe('Settings: Create and delete accounts', function() {
|
||||
beforeEach(function() {
|
||||
cy.listUsers().then((users) => {
|
||||
if ((users as string[]).includes(john.userId)) {
|
||||
// ensure created user is deleted
|
||||
cy.deleteUser(john)
|
||||
}
|
||||
})
|
||||
cy.login(admin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
it('Can create a user', function() {
|
||||
// open the New user modal
|
||||
cy.get('button#new-user-button').click()
|
||||
|
||||
cy.get('form[data-test="form"]').within(() => {
|
||||
// see that the username is ""
|
||||
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
|
||||
// set the username to john
|
||||
cy.get('input[data-test="username"]').type(john.userId)
|
||||
// see that the username is john
|
||||
cy.get('input[data-test="username"]').should('have.value', john.userId)
|
||||
// see that the password is ""
|
||||
cy.get('input[type="password"]').should('exist').and('have.value', '')
|
||||
// set the password to 123456
|
||||
cy.get('input[type="password"]').type(john.password)
|
||||
// see that the password is 123456
|
||||
cy.get('input[type="password"]').should('have.value', john.password)
|
||||
})
|
||||
|
||||
cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => {
|
||||
// submit the new user form
|
||||
cy.get('button[type="submit"]').click({ force: true })
|
||||
})
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
// see that the created user is in the list
|
||||
getUserListRow(john.userId)
|
||||
// see that the list of users contains the user john
|
||||
.contains(john.userId).should('exist')
|
||||
})
|
||||
|
||||
it('Can create a user with additional field data', function() {
|
||||
// open the New user modal
|
||||
cy.get('button#new-user-button').click()
|
||||
|
||||
cy.get('form[data-test="form"]').within(() => {
|
||||
// set the username
|
||||
cy.get('input[data-test="username"]').should('exist').and('have.value', '')
|
||||
cy.get('input[data-test="username"]').type(john.userId)
|
||||
cy.get('input[data-test="username"]').should('have.value', john.userId)
|
||||
// set the display name
|
||||
cy.get('input[data-test="displayName"]').should('exist').and('have.value', '')
|
||||
cy.get('input[data-test="displayName"]').type('John Smith')
|
||||
cy.get('input[data-test="displayName"]').should('have.value', 'John Smith')
|
||||
// set the email
|
||||
cy.get('input[data-test="email"]').should('exist').and('have.value', '')
|
||||
cy.get('input[data-test="email"]').type('john@example.org')
|
||||
cy.get('input[data-test="email"]').should('have.value', 'john@example.org')
|
||||
// set the password
|
||||
cy.get('input[type="password"]').should('exist').and('have.value', '')
|
||||
cy.get('input[type="password"]').type(john.password)
|
||||
cy.get('input[type="password"]').should('have.value', john.password)
|
||||
})
|
||||
|
||||
cy.get('form[data-test="form"]').parents('[role="dialog"]').within(() => {
|
||||
// submit the new user form
|
||||
cy.get('button[type="submit"]').click({ force: true })
|
||||
})
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
// see that the created user is in the list
|
||||
getUserListRow(john.userId)
|
||||
// see that the list of users contains the user john
|
||||
.contains(john.userId)
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('Can delete a user', function() {
|
||||
let testUser
|
||||
// create user
|
||||
cy.createRandomUser()
|
||||
.then(($user) => {
|
||||
testUser = $user
|
||||
})
|
||||
cy.login(admin)
|
||||
// ensure created user is present
|
||||
cy.reload().then(() => {
|
||||
// see that the user is in the list
|
||||
getUserListRow(testUser.userId).within(() => {
|
||||
// see that the list of users contains the user testUser
|
||||
cy.contains(testUser.userId).should('exist')
|
||||
// open the actions menu for the user
|
||||
cy.get('[data-cy-user-list-cell-actions]')
|
||||
.find('button.action-item__menutoggle')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
// The "Delete account" action in the actions menu is shown and clicked
|
||||
cy.get('.action-item__popper .action').contains('Delete account').should('exist').click({ force: true })
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
|
||||
// And confirmation dialog accepted
|
||||
cy.get('.nc-generic-dialog button').contains(`Delete ${testUser.userId}`).click({ force: true })
|
||||
|
||||
// deleted clicked the user is not shown anymore
|
||||
getUserListRow(testUser.userId).should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { assertNotExistOrNotVisible, getUserList } from './usersUtils.js'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: Show and hide columns', function() {
|
||||
before(function() {
|
||||
cy.login(admin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
// open the settings dialog
|
||||
cy.contains('button', 'Account management settings').click()
|
||||
// reset all visibility toggles
|
||||
cy.get('.modal-container #settings-section_visibility-settings input[type="checkbox"]').uncheck({ force: true })
|
||||
|
||||
cy.contains('.modal-container', 'Account management settings').within(() => {
|
||||
// enable the last login toggle
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').check({ force: true })
|
||||
// close the settings dialog
|
||||
cy.get('button.modal-container__close').click()
|
||||
})
|
||||
cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el)))
|
||||
})
|
||||
|
||||
it('Can show a column', function() {
|
||||
// see that the language column is not in the header
|
||||
cy.get('[data-cy-user-list-header-languages]').should('not.exist')
|
||||
|
||||
// see that the language column is not in all user rows
|
||||
cy.get('tbody.user-list__body tr').each(($row) => {
|
||||
cy.wrap($row).get('[data-test="language"]').should('not.exist')
|
||||
})
|
||||
|
||||
// open the settings dialog
|
||||
cy.contains('button', 'Account management settings').click()
|
||||
|
||||
cy.contains('.modal-container', 'Account management settings').within(() => {
|
||||
// enable the language toggle
|
||||
cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('not.be.checked')
|
||||
cy.get('[data-test="showLanguages"] input[type="checkbox"]').check({ force: true })
|
||||
cy.get('[data-test="showLanguages"] input[type="checkbox"]').should('be.checked')
|
||||
// close the settings dialog
|
||||
cy.get('button.modal-container__close').click()
|
||||
})
|
||||
cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el)))
|
||||
|
||||
// see that the language column is in the header
|
||||
cy.get('[data-cy-user-list-header-languages]').should('exist')
|
||||
|
||||
// see that the language column is in all user rows
|
||||
getUserList().find('tbody tr').each(($row) => {
|
||||
cy.wrap($row).get('[data-cy-user-list-cell-language]').should('exist')
|
||||
})
|
||||
|
||||
// Clear local storage and reload to verify user settings DB persistence
|
||||
cy.clearLocalStorage()
|
||||
cy.reload()
|
||||
cy.get('[data-cy-user-list-header-languages]').should('exist')
|
||||
})
|
||||
|
||||
it('Can hide a column', function() {
|
||||
// see that the last login column is in the header
|
||||
cy.get('[data-cy-user-list-header-last-login]').should('exist')
|
||||
|
||||
// see that the last login column is in all user rows
|
||||
getUserList().find('tbody tr').each(($row) => {
|
||||
cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('exist')
|
||||
})
|
||||
|
||||
// open the settings dialog
|
||||
cy.contains('button', 'Account management settings').click()
|
||||
|
||||
cy.contains('.modal-container', 'Account management settings').within(() => {
|
||||
// disable the last login toggle
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('be.checked')
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').uncheck({ force: true })
|
||||
cy.get('[data-test="showLastLogin"] input[type="checkbox"]').should('not.be.checked')
|
||||
// close the settings dialog
|
||||
cy.get('button.modal-container__close').click()
|
||||
})
|
||||
cy.waitUntil(() => cy.contains('.modal-container', 'Account management settings').should((el) => assertNotExistOrNotVisible(el)))
|
||||
|
||||
// see that the last login column is not in the header
|
||||
cy.get('[data-cy-user-list-header-last-login]').should('not.exist')
|
||||
|
||||
// see that the last login column is not in all user rows
|
||||
getUserList().find('tbody tr').each(($row) => {
|
||||
cy.wrap($row).get('[data-cy-user-list-cell-last-login]').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { clearState } from '../../support/commonUtils.ts'
|
||||
import { getUserListRow } from './usersUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: Disable and enable users', function() {
|
||||
let testUser: User
|
||||
|
||||
beforeEach(function() {
|
||||
clearState()
|
||||
cy.createRandomUser().then(($user) => {
|
||||
testUser = $user
|
||||
})
|
||||
cy.login(admin)
|
||||
// open the User settings
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
// Not guranteed to run but would be nice to cleanup
|
||||
after(() => {
|
||||
cy.deleteUser(testUser)
|
||||
})
|
||||
|
||||
it('Can disable the user', function() {
|
||||
// ensure user is enabled
|
||||
cy.enableUser(testUser)
|
||||
|
||||
// see that the user is in the list of active users
|
||||
getUserListRow(testUser.userId).within(() => {
|
||||
// see that the list of users contains the user testUser
|
||||
cy.contains(testUser.userId).should('exist')
|
||||
// open the actions menu for the user
|
||||
cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' })
|
||||
})
|
||||
|
||||
// The "Disable account" action in the actions menu is shown and clicked
|
||||
cy.get('.action-item__popper .action').contains('Disable account').should('exist').click()
|
||||
// When clicked the section is not shown anymore
|
||||
getUserListRow(testUser.userId).should('not.exist')
|
||||
// But the disabled user section now exists
|
||||
cy.get('#disabled').should('exist')
|
||||
// Open disabled users section
|
||||
cy.get('#disabled a').click()
|
||||
cy.url().should('match', /\/disabled/)
|
||||
// The list of disabled users should now contain the user
|
||||
getUserListRow(testUser.userId).should('exist')
|
||||
})
|
||||
|
||||
it('Can enable the user', function() {
|
||||
// ensure user is disabled
|
||||
cy.enableUser(testUser, false).reload()
|
||||
|
||||
// Open disabled users section
|
||||
cy.get('#disabled a').click()
|
||||
cy.url().should('match', /\/disabled/)
|
||||
|
||||
// see that the user is in the list of active users
|
||||
getUserListRow(testUser.userId).within(() => {
|
||||
// see that the list of disabled users contains the user testUser
|
||||
cy.contains(testUser.userId).should('exist')
|
||||
// open the actions menu for the user
|
||||
cy.get('[data-cy-user-list-cell-actions] button.action-item__menutoggle').click({ scrollBehavior: 'center' })
|
||||
})
|
||||
|
||||
// The "Enable account" action in the actions menu is shown and clicked
|
||||
cy.get('.action-item__popper .action').contains('Enable account').should('exist').click()
|
||||
// When clicked the section is not shown anymore
|
||||
cy.get('#disabled').should('not.exist')
|
||||
// Make sure it is still gone after the reload reload
|
||||
cy.reload().login(admin)
|
||||
cy.get('#disabled').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,305 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { clearState } from '../../support/commonUtils.ts'
|
||||
import { randomString } from '../../support/utils/randomString.ts'
|
||||
import { handlePasswordConfirmation } from '../core-utils.ts'
|
||||
import { assertNotExistOrNotVisible, getUserListRow, openEditDialog, saveEditDialog } from './usersUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: Create groups', () => {
|
||||
let groupName: string
|
||||
|
||||
after(() => {
|
||||
cy.runOccCommand(`group:delete '${groupName!}'`)
|
||||
})
|
||||
|
||||
before(() => {
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
it('Can create a group', () => {
|
||||
cy.intercept('POST', '**/ocs/v2.php/cloud/groups').as('createGroups')
|
||||
|
||||
groupName = randomString(7)
|
||||
// open the Create group menu
|
||||
cy.get('button[aria-label="Create group"]').click()
|
||||
|
||||
cy.get('li[data-cy-users-settings-new-group-name]').within(() => {
|
||||
// see that the group name is ""
|
||||
cy.get('input').should('exist').and('have.value', '')
|
||||
// set the group name to foo
|
||||
cy.get('input').type(groupName)
|
||||
// see that the group name is foo
|
||||
cy.get('input').should('have.value', groupName)
|
||||
// submit the group name
|
||||
cy.get('input ~ button').click()
|
||||
})
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
cy.wait('@createGroups').its('response.statusCode').should('eq', 200)
|
||||
|
||||
// see that the created group is in the list
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
// see that the list of groups contains the group foo
|
||||
cy.contains(groupName).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Settings: Assign user to a group', { testIsolation: false }, () => {
|
||||
const groupName = randomString(7)
|
||||
let testUser: User
|
||||
|
||||
after(() => {
|
||||
cy.deleteUser(testUser)
|
||||
cy.runOccCommand(`group:delete '${groupName}'`)
|
||||
})
|
||||
|
||||
before(() => {
|
||||
clearState()
|
||||
|
||||
cy.createRandomUser().then((user) => {
|
||||
testUser = user
|
||||
})
|
||||
cy.runOccCommand(`group:add '${groupName}'`)
|
||||
cy.login(admin)
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups')
|
||||
cy.visit('/settings/users')
|
||||
cy.wait('@loadGroups')
|
||||
})
|
||||
|
||||
it('see that the group is in the list', () => {
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('exist')
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('.counter-bubble__counter')
|
||||
.should('not.exist') // is hidden when 0
|
||||
})
|
||||
|
||||
it('see that the user is in the list', () => {
|
||||
getUserListRow(testUser.userId)
|
||||
.contains(testUser.userId)
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('assign the group via the edit dialog', () => {
|
||||
openEditDialog(testUser)
|
||||
|
||||
// Type part of the group name in the groups NcSelect
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
cy.get('[data-test="groups"] input[type="search"]').click({ force: true })
|
||||
cy.get('[data-test="groups"] input[type="search"]').type(groupName.slice(0, 5))
|
||||
})
|
||||
|
||||
// Select the group from the floating dropdown
|
||||
cy.get('.vs__dropdown-menu').should('be.visible')
|
||||
.contains('li', groupName).click({ force: true })
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
})
|
||||
|
||||
it('see the group was successfully assigned', () => {
|
||||
// see a new member
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('.counter-bubble__counter')
|
||||
.should('contain', '1')
|
||||
})
|
||||
|
||||
it('validate the user was added on backend', () => {
|
||||
cy.runOccCommand(`user:info --output=json '${testUser.userId}'`).then((output) => {
|
||||
cy.wrap(output.exitCode).should('eq', 0)
|
||||
cy.wrap(JSON.parse(output.stdout)?.groups).should('include', groupName)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Settings: Delete an empty group', { testIsolation: false }, () => {
|
||||
const groupName = randomString(7)
|
||||
|
||||
after(() => {
|
||||
cy.runOccCommand(`group:delete '${groupName}'`, { failOnNonZeroExit: false })
|
||||
})
|
||||
before(() => {
|
||||
cy.runOccCommand(`group:add '${groupName}'`)
|
||||
cy.login(admin)
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups')
|
||||
cy.visit('/settings/users')
|
||||
cy.wait('@loadGroups')
|
||||
})
|
||||
|
||||
it('see that the group is in the list', () => {
|
||||
// see that the list of groups contains the group foo
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
// open the actions menu for the group
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('button.action-item__menutoggle')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
it('can delete the group', () => {
|
||||
// The "Delete group" action in the actions menu is shown and clicked
|
||||
cy.get('.action-item__popper button').contains('Delete group').should('exist').click({ force: true })
|
||||
// And confirmation dialog accepted
|
||||
cy.get('.modal-container button').contains('Confirm').click({ force: true })
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
})
|
||||
|
||||
it('deleted group is not shown anymore', () => {
|
||||
// see that the list of groups does not contain the group
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
|
||||
.find('li')
|
||||
.not('.app-navigation-caption')
|
||||
.should('not.exist')
|
||||
// and also not in database
|
||||
cy.runOccCommand('group:list --output=json').then(($response) => {
|
||||
const groups: string[] = Object.keys(JSON.parse($response.stdout))
|
||||
expect(groups).to.not.include(groupName)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Settings: Delete a non empty group', () => {
|
||||
let testUser: User
|
||||
const groupName = randomString(7)
|
||||
|
||||
after(() => {
|
||||
cy.runOccCommand(`group:delete '${groupName}'`, { failOnNonZeroExit: false })
|
||||
})
|
||||
|
||||
before(() => {
|
||||
cy.runOccCommand(`group:add '${groupName}'`)
|
||||
cy.createRandomUser().then(($user) => {
|
||||
testUser = $user
|
||||
cy.runOccCommand(`group:addUser '${groupName}' '${$user.userId}'`)
|
||||
})
|
||||
cy.login(admin)
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups')
|
||||
cy.visit('/settings/users')
|
||||
cy.wait('@loadGroups')
|
||||
})
|
||||
after(() => cy.deleteUser(testUser))
|
||||
|
||||
it('see that the group is in the list', () => {
|
||||
// see that the list of groups contains the group
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('can delete the group', () => {
|
||||
// open the menu
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('button.action-item__menutoggle')
|
||||
.click({ force: true })
|
||||
|
||||
// The "Delete group" action in the actions menu is shown and clicked
|
||||
cy.get('.action-item__popper button').contains('Delete group').should('exist').click({ force: true })
|
||||
// And confirmation dialog accepted
|
||||
cy.get('.modal-container button').contains('Confirm').click({ force: true })
|
||||
|
||||
// Make sure no confirmation modal is shown
|
||||
handlePasswordConfirmation(admin.password)
|
||||
})
|
||||
|
||||
it('deleted group is not shown anymore', () => {
|
||||
// see that the list of groups does not contain the group foo
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
|
||||
.find('li')
|
||||
.not('.app-navigation-caption')
|
||||
.should('not.exist')
|
||||
// and also not in database
|
||||
cy.runOccCommand('group:list --output=json').then(($response) => {
|
||||
const groups: string[] = Object.keys(JSON.parse($response.stdout))
|
||||
expect(groups).to.not.include(groupName)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Settings: Sort groups in the UI', () => {
|
||||
before(() => {
|
||||
// Clear state
|
||||
clearState()
|
||||
|
||||
// Add two groups and add one user to group B
|
||||
cy.runOccCommand('group:add A')
|
||||
cy.runOccCommand('group:add B')
|
||||
cy.createRandomUser().then((user) => {
|
||||
cy.runOccCommand(`group:adduser B '${user.userId}'`)
|
||||
})
|
||||
|
||||
// Visit the settings as admin
|
||||
cy.login(admin)
|
||||
cy.visit('/settings/users')
|
||||
})
|
||||
|
||||
it('Can set sort by member count', () => {
|
||||
// open the settings dialog
|
||||
cy.contains('button', 'Account management settings').click()
|
||||
|
||||
cy.contains('.modal-container', 'Account management settings').within(() => {
|
||||
cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').scrollIntoView()
|
||||
cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').check({ force: true })
|
||||
// close the settings dialog
|
||||
cy.get('button.modal-container__close').click()
|
||||
})
|
||||
cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el)))
|
||||
})
|
||||
|
||||
it('See that the groups are sorted by the member count', () => {
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member
|
||||
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members
|
||||
})
|
||||
})
|
||||
|
||||
it('See that the order is preserved after a reload', () => {
|
||||
cy.reload()
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'B') // 1 member
|
||||
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'A') // 0 members
|
||||
})
|
||||
})
|
||||
|
||||
it('Can set sort by group name', () => {
|
||||
// open the settings dialog
|
||||
cy.contains('button', 'Account management settings').click()
|
||||
|
||||
cy.contains('.modal-container', 'Account management settings').within(() => {
|
||||
cy.get('[data-test="sortGroupsByName"] input[type="radio"]').scrollIntoView()
|
||||
cy.get('[data-test="sortGroupsByName"] input[type="radio"]').check({ force: true })
|
||||
// close the settings dialog
|
||||
cy.get('button.modal-container__close').click()
|
||||
})
|
||||
cy.waitUntil(() => cy.get('.modal-container').should((el) => assertNotExistOrNotVisible(el)))
|
||||
})
|
||||
|
||||
it('See that the groups are sorted by the user count', () => {
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A')
|
||||
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B')
|
||||
})
|
||||
})
|
||||
|
||||
it('See that the order is preserved after a reload', () => {
|
||||
cy.reload()
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
cy.get('li').not('.app-navigation-caption').eq(0).should('contain', 'A')
|
||||
cy.get('li').not('.app-navigation-caption').eq(1).should('contain', 'B')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { clearState } from '../../support/commonUtils.ts'
|
||||
import { handlePasswordConfirmation } from '../core-utils.ts'
|
||||
import { openEditDialog, saveEditDialog } from './usersUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: User Manager Management', function() {
|
||||
let user: User
|
||||
let manager: User
|
||||
|
||||
beforeEach(function() {
|
||||
clearState()
|
||||
cy.createRandomUser().then(($user) => {
|
||||
manager = $user
|
||||
return cy.createRandomUser()
|
||||
}).then(($user) => {
|
||||
user = $user
|
||||
cy.login(admin)
|
||||
})
|
||||
})
|
||||
|
||||
it('Can assign a manager through the edit dialog', function() {
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
// Open the Manager NcSelect and type manager name
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
cy.findByRole('combobox', { name: /Manager/i }).click({ force: true })
|
||||
cy.findByRole('combobox', { name: /Manager/i }).type(manager.userId)
|
||||
})
|
||||
|
||||
// Select the manager from the floating dropdown
|
||||
cy.get('.vs__dropdown-menu').should('be.visible')
|
||||
.contains('li', manager.userId).click({ force: true })
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify backend
|
||||
cy.getUserData(user).then(($result) => {
|
||||
expect($result.body).to.contain(`<manager>${manager.userId}</manager>`)
|
||||
})
|
||||
})
|
||||
|
||||
it('Can remove a manager through the edit dialog', function() {
|
||||
// Set manager via backend first.
|
||||
// User::getManagerUids() decodes this with JSON_THROW_ON_ERROR, so we
|
||||
// must store a JSON array, matching what setManagerUids() writes.
|
||||
// Double-quotes are escaped because runOccCommand passes the command
|
||||
// through `bash -c "..."`, which would otherwise eat them.
|
||||
cy.runOccCommand(`user:setting '${user.userId}' settings manager '[\\"${manager.userId}\\"]'`)
|
||||
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
// Clear the manager selection inside the dialog
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
cy.get('.user-form__managers .vs__clear').click({ force: true })
|
||||
})
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify backend
|
||||
cy.getUserData(user).then(($result) => {
|
||||
expect($result.body).to.not.contain(`<manager>${manager.userId}</manager>`)
|
||||
expect($result.body).to.contain('<manager></manager>')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { clearState } from '../../support/commonUtils.ts'
|
||||
import { handlePasswordConfirmation } from '../core-utils.ts'
|
||||
import { openEditDialog, saveEditDialog } from './usersUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
describe('Settings: Change user properties', function() {
|
||||
let user: User
|
||||
|
||||
beforeEach(function() {
|
||||
clearState()
|
||||
cy.createRandomUser().then(($user) => {
|
||||
user = $user
|
||||
})
|
||||
cy.login(admin)
|
||||
})
|
||||
|
||||
it('Can change the display name', function() {
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
cy.get('input[data-test="displayName"]').should('have.value', user.userId)
|
||||
cy.get('input[data-test="displayName"]').clear()
|
||||
cy.get('input[data-test="displayName"]').type('John Doe')
|
||||
cy.get('input[data-test="displayName"]').should('have.value', 'John Doe')
|
||||
})
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify backend
|
||||
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
|
||||
expect($result.exitCode).to.equal(0)
|
||||
const info = JSON.parse($result.stdout)
|
||||
expect(info?.display_name).to.equal('John Doe')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can change the password', function() {
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
cy.get('input[data-test="password"]').should('have.value', '')
|
||||
cy.get('input[data-test="password"]').type('newpassword123')
|
||||
})
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify by logging in with the new password
|
||||
cy.login(new User(user.userId, 'newpassword123'))
|
||||
cy.visit('/apps/dashboard')
|
||||
cy.url().should('include', '/apps/dashboard')
|
||||
})
|
||||
|
||||
it('Can change the email address', function() {
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
cy.get('input[data-test="email"]').should('have.value', '')
|
||||
cy.get('input[data-test="email"]').type('mymail@example.com')
|
||||
cy.get('input[data-test="email"]').should('have.value', 'mymail@example.com')
|
||||
})
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify backend
|
||||
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
|
||||
expect($result.exitCode).to.equal(0)
|
||||
const info = JSON.parse($result.stdout)
|
||||
expect(info?.email).to.equal('mymail@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can change the user quota to a predefined one', function() {
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
// Open the quota selector
|
||||
cy.get('.vs__selected').contains('Unlimited').should('exist')
|
||||
cy.findByRole('combobox', { name: /Quota/i }).click({ force: true })
|
||||
})
|
||||
|
||||
// Dropdown is floating outside the form — select 5 GB
|
||||
cy.get('.vs__dropdown-menu').should('be.visible')
|
||||
.contains('li', '5 GB').click({ force: true })
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify backend
|
||||
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
|
||||
expect($result.exitCode).to.equal(0)
|
||||
const info = JSON.parse($result.stdout)
|
||||
expect(info?.quota).to.equal('5 GB')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can change the user quota to a custom value', function() {
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
// Type a custom quota value
|
||||
cy.findByRole('combobox', { name: /Quota/i }).type('4 MB{enter}')
|
||||
})
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify backend
|
||||
cy.runOccCommand(`user:info --output=json '${user.userId}'`).then(($result) => {
|
||||
expect($result.exitCode).to.equal(0)
|
||||
// Quota value is stored as bytes, verify it was set
|
||||
const info = JSON.parse($result.stdout)
|
||||
expect(info?.quota).to.not.equal('none')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can make user a subadmin of a group', function() {
|
||||
const groupName = 'userstestgroup'
|
||||
cy.runOccCommand(`group:add '${groupName}'`)
|
||||
|
||||
cy.visit('/settings/users')
|
||||
|
||||
openEditDialog(user)
|
||||
|
||||
cy.get('.edit-dialog [data-test="form"]').within(() => {
|
||||
// Find the subadmin NcSelect by its label and open the dropdown
|
||||
cy.findByRole('combobox', { name: /Admin of the following groups/i }).click({ force: true })
|
||||
cy.findByRole('combobox', { name: /Admin of the following groups/i }).type('userstestgroup')
|
||||
})
|
||||
|
||||
// Select the group from the floating dropdown
|
||||
cy.get('.vs__dropdown-menu').should('be.visible')
|
||||
.contains('li', groupName).click({ force: true })
|
||||
|
||||
handlePasswordConfirmation(admin.password)
|
||||
saveEditDialog()
|
||||
|
||||
cy.get('.toastify.toast-success').contains(/Account updated/i).should('exist')
|
||||
|
||||
// Verify backend
|
||||
cy.getUserData(user).then(($response) => {
|
||||
expect($response.status).to.equal(200)
|
||||
const dom = (new DOMParser()).parseFromString($response.body, 'text/xml')
|
||||
expect(dom.querySelector('subadmin element')?.textContent).to.contain(groupName)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
/// <reference types="cypress-if" />
|
||||
|
||||
import { User } from '@nextcloud/e2e-test-server/cypress'
|
||||
import { clearState } from '../../support/commonUtils.ts'
|
||||
import { randomString } from '../../support/utils/randomString.ts'
|
||||
import { getUserList, getUserListRow } from './usersUtils.ts'
|
||||
|
||||
const admin = new User('admin', 'admin')
|
||||
|
||||
/** Scope role queries to the account management sidebar so they don't match
|
||||
* unrelated elements (e.g. the global unified search bar at the top of the page).
|
||||
*/
|
||||
function accountNav() {
|
||||
return cy.findByRole('navigation', { name: /account management/i })
|
||||
}
|
||||
|
||||
function waitForSearchRequest(alias: string, expectedSearch: string) {
|
||||
return cy.wait(alias).then(({ request }) => {
|
||||
expect(new URL(request.url).searchParams.get('search')).to.equal(expectedSearch)
|
||||
})
|
||||
}
|
||||
|
||||
describe('Settings: Unified search for accounts and groups', { testIsolation: false }, () => {
|
||||
// Use a stable, searchable prefix in the group name so we can match
|
||||
// it independently from the random user id below.
|
||||
const matchingGroup = `zzz-match-${randomString(5)}`
|
||||
const otherGroup = `aaa-other-${randomString(5)}`
|
||||
let alice: User
|
||||
let bob: User
|
||||
|
||||
after(() => {
|
||||
cy.deleteUser(alice)
|
||||
cy.deleteUser(bob)
|
||||
cy.runOccCommand(`group:delete '${matchingGroup}'`, { failOnNonZeroExit: false })
|
||||
cy.runOccCommand(`group:delete '${otherGroup}'`, { failOnNonZeroExit: false })
|
||||
})
|
||||
|
||||
before(() => {
|
||||
clearState()
|
||||
|
||||
cy.createRandomUser().then((user) => {
|
||||
alice = user
|
||||
})
|
||||
cy.createRandomUser().then((user) => {
|
||||
bob = user
|
||||
})
|
||||
|
||||
cy.runOccCommand(`group:add '${matchingGroup}'`)
|
||||
cy.runOccCommand(`group:add '${otherGroup}'`)
|
||||
|
||||
cy.login(admin)
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=*').as('initialLoadGroups')
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/users/details?*').as('initialLoadUsers')
|
||||
cy.visit('/settings/users')
|
||||
cy.wait('@initialLoadGroups')
|
||||
cy.wait('@initialLoadUsers')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Intercept aliases reset between tests even with testIsolation: false,
|
||||
// so re-register them here to capture requests triggered inside each test.
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=*').as('loadGroups')
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/users/details?*').as('loadUsers')
|
||||
})
|
||||
|
||||
it('shows the search input in the navigation sidebar', () => {
|
||||
accountNav().findByRole('searchbox', { name: /search accounts and groups/i })
|
||||
.should('be.visible')
|
||||
.and('have.value', '')
|
||||
})
|
||||
|
||||
it('dispatches the query to both the users and groups API', () => {
|
||||
accountNav().findByRole('searchbox', { name: /search accounts and groups/i })
|
||||
.type(alice.userId)
|
||||
|
||||
// A single keystroke sequence debounces once (300ms), then fans out
|
||||
// to both APIs — both requests must carry the same search term.
|
||||
cy.wait('@loadUsers').its('request.url').should('include', `search=${alice.userId}`)
|
||||
cy.wait('@loadGroups').its('request.url').should('include', `search=${alice.userId}`)
|
||||
|
||||
// The user list reflects what the backend returned for this query.
|
||||
getUserListRow(alice.userId).should('exist')
|
||||
getUserList().should('not.contain', bob.userId)
|
||||
})
|
||||
|
||||
it('filters the group list when the query matches a group name', () => {
|
||||
accountNav().findByRole('searchbox', { name: /search accounts and groups/i })
|
||||
.clear()
|
||||
.type(matchingGroup)
|
||||
|
||||
cy.wait('@loadGroups').its('request.url').should('include', `search=${matchingGroup}`)
|
||||
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
|
||||
.should('contain', matchingGroup)
|
||||
.and('not.contain', otherGroup)
|
||||
})
|
||||
|
||||
it('resets both lists when the clear button is clicked', () => {
|
||||
accountNav().findByRole('button', { name: /clear search/i }).click()
|
||||
|
||||
accountNav().findByRole('searchbox', { name: /search accounts and groups/i })
|
||||
.should('have.value', '')
|
||||
|
||||
waitForSearchRequest('@loadUsers', '')
|
||||
waitForSearchRequest('@loadGroups', '')
|
||||
|
||||
getUserListRow(alice.userId).should('exist')
|
||||
getUserListRow(bob.userId).should('exist')
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
|
||||
.should('contain', matchingGroup)
|
||||
.and('contain', otherGroup)
|
||||
})
|
||||
})
|
||||
42
tests/playwright/e2e/settings/access-levels.spec.ts
Normal file
42
tests/playwright/e2e/settings/access-levels.spec.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { test as userTest } from '../../support/fixtures/random-user-session.ts'
|
||||
import { test as adminTest } from '../../support/fixtures/admin-session.ts'
|
||||
import { AccountMenuPage } from '../../support/sections/AccountMenuPage.ts'
|
||||
|
||||
userTest.describe('Settings: Access levels – regular user', () => {
|
||||
userTest('cannot see the Administration section in the settings navigation', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const accountMenu = new AccountMenuPage(page)
|
||||
await accountMenu.open()
|
||||
await accountMenu.entry('Settings').getByRole('link').click()
|
||||
await expect(page).toHaveURL(/\/settings\/user$/)
|
||||
|
||||
const appNavigation = page.locator('#app-navigation-vue')
|
||||
await expect(appNavigation.getByRole('list', { name: 'Personal' })).toBeVisible()
|
||||
await expect(appNavigation.getByRole('link', { name: /Personal info/i })).toBeVisible()
|
||||
// Regular users must not see the Administration section
|
||||
await expect(appNavigation.getByRole('list', { name: 'Administration' })).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
adminTest.describe('Settings: Access levels – admin user', () => {
|
||||
adminTest('can see the Administration section in the settings navigation', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
const accountMenu = new AccountMenuPage(page)
|
||||
await accountMenu.open()
|
||||
await accountMenu.entry('Personal settings').getByRole('link').click()
|
||||
await expect(page).toHaveURL(/\/settings\/user$/)
|
||||
|
||||
const appNavigation = page.locator('#app-navigation-vue')
|
||||
await expect(appNavigation.getByRole('list', { name: 'Personal' })).toBeVisible()
|
||||
await expect(appNavigation.getByRole('link', { name: /Personal info/i })).toBeVisible()
|
||||
// Admins must see the Administration section
|
||||
await expect(appNavigation.getByRole('list', { name: 'Administration' })).toBeVisible()
|
||||
await expect(appNavigation.getByRole('link', { name: /Overview/i })).toBeVisible()
|
||||
})
|
||||
})
|
||||
77
tests/playwright/e2e/users/users-columns.spec.ts
Normal file
77
tests/playwright/e2e/users/users-columns.spec.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { test } from '../../support/fixtures/admin-session.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
test.describe('Settings: Show and hide columns', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
// Reset: open settings, uncheck all optional columns, re-enable last-login
|
||||
await settingsPage.openSettingsDialog()
|
||||
const dialog = settingsPage.settingsDialog()
|
||||
|
||||
// Uncheck both optional columns
|
||||
for (const name of ['Show language', 'Show last login']) {
|
||||
const checkbox = dialog.getByRole('checkbox', { name })
|
||||
if (await checkbox.isChecked()) {
|
||||
await checkbox.uncheck({ force: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable last-login so each test starts from a known baseline
|
||||
await dialog.getByRole('checkbox', { name: 'Show last login' }).check({ force: true })
|
||||
await settingsPage.closeSettingsDialog()
|
||||
})
|
||||
|
||||
test('can show the Language column', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
|
||||
// Language column must not be visible before the toggle
|
||||
await expect(page.getByRole('columnheader', { name: /Language/i })).toHaveCount(0)
|
||||
await expect(page.locator('[data-cy-user-list-cell-language]').first()).toHaveCount(0)
|
||||
|
||||
await settingsPage.openSettingsDialog()
|
||||
const dialog = settingsPage.settingsDialog()
|
||||
const checkbox = dialog.getByRole('checkbox', { name: 'Show language' })
|
||||
await expect(checkbox).not.toBeChecked()
|
||||
await checkbox.check({ force: true })
|
||||
await expect(checkbox).toBeChecked()
|
||||
await settingsPage.closeSettingsDialog()
|
||||
|
||||
// Language column header must now be visible
|
||||
await expect(page.getByRole('columnheader', { name: /Language/i })).toBeVisible()
|
||||
// Every row must have a language cell
|
||||
await expect(page.locator('[data-cy-user-list-cell-language]').first()).toBeVisible()
|
||||
|
||||
// Reload to verify the preference is persisted (stored in DB, not just localStorage)
|
||||
await page.evaluate(() => localStorage.clear())
|
||||
await page.reload()
|
||||
await expect(page.getByRole('columnheader', { name: /Language/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('can hide the Last login column', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
|
||||
// Last login column must be visible (enabled in beforeEach)
|
||||
await expect(page.getByRole('columnheader', { name: /Last login/i })).toBeVisible()
|
||||
await expect(page.locator('[data-cy-user-list-cell-last-login]').first()).toBeVisible()
|
||||
|
||||
await settingsPage.openSettingsDialog()
|
||||
const dialog = settingsPage.settingsDialog()
|
||||
const checkbox = dialog.getByRole('checkbox', { name: 'Show last login' })
|
||||
await expect(checkbox).toBeChecked()
|
||||
await checkbox.uncheck({ force: true })
|
||||
await expect(checkbox).not.toBeChecked()
|
||||
await settingsPage.closeSettingsDialog()
|
||||
|
||||
// Column header must now be gone
|
||||
await expect(page.getByRole('columnheader', { name: /Last login/i })).toHaveCount(0)
|
||||
await expect(page.locator('[data-cy-user-list-cell-last-login]').first()).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
76
tests/playwright/e2e/users/users-disable.spec.ts
Normal file
76
tests/playwright/e2e/users/users-disable.spec.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect, test as baseTest } from '@playwright/test'
|
||||
import { type User } from '@nextcloud/e2e-test-server'
|
||||
import { createRandomUser } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { test as adminTest } from '../../support/fixtures/admin-session.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
const test = adminTest.extend<{ testUser: User }>({
|
||||
testUser: async ({}, use) => {
|
||||
const user = await createRandomUser()
|
||||
await use(user)
|
||||
await runOcc(['user:delete', user.userId])
|
||||
},
|
||||
})
|
||||
|
||||
test.describe('Settings: Disable and enable users', () => {
|
||||
test('can disable a user', async ({ page, testUser }) => {
|
||||
// Ensure user is enabled
|
||||
await runOcc(['user:enable', testUser.userId])
|
||||
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await expect(settingsPage.userRow(testUser.userId)).toBeVisible()
|
||||
|
||||
await settingsPage.openActionsMenu(testUser.userId)
|
||||
await page.getByRole('menuitem', { name: 'Disable account' }).click()
|
||||
|
||||
// User should no longer be in the main list
|
||||
await expect(settingsPage.userRow(testUser.userId)).toHaveCount(0)
|
||||
|
||||
// Disabled accounts nav link should now appear
|
||||
const disabledLink = settingsPage.navigation().getByRole('link', { name: /Disabled accounts/i })
|
||||
await expect(disabledLink).toBeVisible()
|
||||
|
||||
// Navigate to disabled users
|
||||
await disabledLink.click()
|
||||
await expect(page).toHaveURL(/\/disabled/)
|
||||
|
||||
// The disabled user should be in the list
|
||||
await settingsPage.userList().waitFor({ state: 'visible' })
|
||||
await expect(settingsPage.userRow(testUser.userId)).toBeVisible()
|
||||
})
|
||||
|
||||
test('can enable a user', async ({ page, testUser }) => {
|
||||
// Ensure user is disabled
|
||||
await runOcc(['user:disable', testUser.userId])
|
||||
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
// Navigate to disabled users
|
||||
const disabledLink = settingsPage.navigation().getByRole('link', { name: /Disabled accounts/i })
|
||||
await expect(disabledLink).toBeVisible()
|
||||
await disabledLink.click()
|
||||
await expect(page).toHaveURL(/\/disabled/)
|
||||
await settingsPage.userList().waitFor({ state: 'visible' })
|
||||
|
||||
const waitForEnableRequest = page.waitForResponse((r) => r.request().url().match(/\/ocs\/v2\.php\/cloud\/users\/[^/]+\/enable/) !== null)
|
||||
await settingsPage.openActionsMenu(testUser.userId)
|
||||
await page.getByRole('menuitem', { name: 'Enable account' }).click()
|
||||
await waitForEnableRequest
|
||||
|
||||
// Disabled accounts section should disappear (no more disabled users)
|
||||
await expect(settingsPage.navigation().getByRole('link', { name: /Disabled accounts/i })).toHaveCount(0)
|
||||
|
||||
// After reload, still no disabled accounts section
|
||||
await page.reload()
|
||||
await expect(settingsPage.navigation().getByRole('link', { name: /Disabled accounts/i })).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
65
tests/playwright/e2e/users/users-group-admin.spec.ts
Normal file
65
tests/playwright/e2e/users/users-group-admin.spec.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server'
|
||||
|
||||
import { expect, test as baseTest } from '@playwright/test'
|
||||
import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { handlePasswordConfirmation } from '../../support/utils/password-confirmation.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
const test = baseTest.extend<{ subadmin: User; group: string }>({
|
||||
group: async ({}, use) => {
|
||||
const groupName = crypto.randomUUID()
|
||||
await runOcc(['group:add', groupName])
|
||||
await use(groupName)
|
||||
await runOcc(['group:delete', groupName]).catch(() => {})
|
||||
},
|
||||
subadmin: async ({ group, request }, use) => {
|
||||
const user = await createRandomUser()
|
||||
await runOcc(['group:adduser', group, user.userId])
|
||||
// Grant subadmin rights via OCS API authenticated as admin
|
||||
await request.post(`/ocs/v2.php/cloud/users/${user.userId}/subadmins`, {
|
||||
headers: {
|
||||
'OCS-APIRequest': 'true',
|
||||
Authorization: 'Basic ' + Buffer.from('admin:admin').toString('base64'),
|
||||
},
|
||||
form: { groupid: group },
|
||||
})
|
||||
await use(user)
|
||||
await runOcc(['user:delete', user.userId])
|
||||
},
|
||||
})
|
||||
|
||||
test.describe('Settings: Create accounts as a group admin', () => {
|
||||
test('can create a user with the group pre-filled', async ({ page, context, subadmin, group }) => {
|
||||
// Log in as the subadmin (not as admin)
|
||||
await login(context.request, subadmin)
|
||||
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openNewUserDialog()
|
||||
const dialog = settingsPage.newUserDialog()
|
||||
|
||||
// The subadmin's single group must be pre-selected in the groups field.
|
||||
// NcSelect renders selected values as .vs__selected (no accessible role).
|
||||
await expect(dialog.locator('.vs__selected').filter({ hasText: group })).toBeVisible()
|
||||
|
||||
// Fill in the new user details and submit
|
||||
const newUserId = crypto.randomUUID()
|
||||
await dialog.getByLabel(/Account name/).fill(newUserId)
|
||||
await dialog.getByLabel(/Password/).and(page.locator('input')).fill('password123')
|
||||
|
||||
await dialog.getByRole('button', { name: 'Add new account' }).click()
|
||||
await handlePasswordConfirmation(page, subadmin.password)
|
||||
await dialog.waitFor({ state: 'hidden' })
|
||||
|
||||
await expect(settingsPage.userRow(newUserId)).toContainText(newUserId)
|
||||
|
||||
await runOcc(['user:delete', newUserId])
|
||||
})
|
||||
})
|
||||
235
tests/playwright/e2e/users/users-groups.spec.ts
Normal file
235
tests/playwright/e2e/users/users-groups.spec.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { User } from '@nextcloud/e2e-test-server'
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { createRandomUser } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { test } from '../../support/fixtures/admin-session.ts'
|
||||
import { handlePasswordConfirmation } from '../../support/utils/password-confirmation.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
// ── Create group ──────────────────────────────────────────────────────────────
|
||||
|
||||
test('Account Management: Can create a group', async ({ page }) => {
|
||||
const groupName = crypto.randomUUID()
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
try {
|
||||
const createGroupsResponsePromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/groups($|\?)/)
|
||||
|
||||
await page.getByRole('button', { name: 'Create group' }).click()
|
||||
await page.getByLabel('Group name').fill(groupName)
|
||||
await page.getByLabel('Group name').press('Enter')
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await createGroupsResponsePromise
|
||||
|
||||
await expect(settingsPage.customGroupsList()).toContainText(groupName)
|
||||
} finally {
|
||||
await runOcc(['group:delete', groupName]).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
// ── Assign user to group ──────────────────────────────────────────────────────
|
||||
|
||||
const userGroupTest = test.extend<{ testUser: User, testGroup: string }>({
|
||||
async testUser({}, use) {
|
||||
const testUser = await createRandomUser()
|
||||
await use(testUser)
|
||||
await runOcc(['user:delete', testUser.userId])
|
||||
},
|
||||
async testGroup({}, use) {
|
||||
const testGroup = crypto.randomUUID()
|
||||
await runOcc(['group:add', testGroup])
|
||||
await use(testGroup)
|
||||
await runOcc(['group:delete', testGroup])
|
||||
},
|
||||
})
|
||||
|
||||
userGroupTest('Account Management: Assign user to a group', async ({ page, testGroup, testUser }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
// group is in the list with no members
|
||||
await expect(settingsPage.groupListItem(testGroup)).toBeVisible()
|
||||
// Counter bubble is absent when member count is 0
|
||||
await expect(settingsPage.groupListItem(testGroup).locator('.counter-bubble__counter')).toHaveCount(0)
|
||||
// user is in the list
|
||||
await expect(settingsPage.userRow(testUser.userId)).toBeVisible()
|
||||
|
||||
// can assign the group via the edit dialog
|
||||
await settingsPage.openEditDialog(testUser.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
const groupsCombobox = dialog.getByRole('combobox', { name: /Member of the following groups/i })
|
||||
const searchRequest = page.waitForResponse((r) => r.request().url().match(new RegExp('/ocs/v2\\.php/cloud/groups/details\\?(.+&|)search=' + testGroup.slice(0, 5))) !== null)
|
||||
await groupsCombobox.fill(testGroup.slice(0, 5))
|
||||
await searchRequest
|
||||
|
||||
await page.getByRole('option', { name: new RegExp(testGroup.slice(0, 8)) }).click()
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// user is now group now shows 1 member
|
||||
await expect(settingsPage.groupListItem(testGroup).locator('.counter-bubble__counter')).toHaveText('1')
|
||||
// backend confirms the user is in the group
|
||||
const info = JSON.parse(await runOcc(['user:info', '--output=json', testUser.userId]))
|
||||
expect(info?.groups).toContain(testGroup)
|
||||
})
|
||||
|
||||
// ── Delete an empty group ─────────────────────────────────────────────────────
|
||||
|
||||
test.describe('Settings: Delete an empty group', () => {
|
||||
const groupName = crypto.randomUUID()
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await runOcc(['group:add', groupName])
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await runOcc(['group:delete', groupName]).catch(() => {})
|
||||
})
|
||||
|
||||
test('can delete an empty group', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
const groupItem = settingsPage.groupListItem(groupName)
|
||||
await expect(groupItem).toBeVisible()
|
||||
|
||||
// Open the group's actions menu
|
||||
await groupItem.hover()
|
||||
await expect(groupItem.getByRole('button', { name: /Actions/i })).toBeVisible()
|
||||
await groupItem.getByRole('button', { name: /Actions/i }).click()
|
||||
|
||||
// and delete the group
|
||||
await page.getByRole('button', { name: 'Delete group' }).click()
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click()
|
||||
await handlePasswordConfirmation(page)
|
||||
|
||||
// Group must be gone from the UI
|
||||
await expect(settingsPage.groupListItem(groupName)).toHaveCount(0)
|
||||
|
||||
// Verify backend
|
||||
const groups: Record<string, unknown> = JSON.parse(await runOcc(['group:list', '--output=json']))
|
||||
expect(Object.keys(groups)).not.toContain(groupName)
|
||||
})
|
||||
})
|
||||
|
||||
// ── Delete a non-empty group ──────────────────────────────────────────────────
|
||||
|
||||
test.describe('Settings: Delete a non-empty group', () => {
|
||||
const groupName = crypto.randomUUID()
|
||||
let testUser: User
|
||||
|
||||
test.beforeAll(async () => {
|
||||
testUser = await createRandomUser()
|
||||
await runOcc(['group:add', groupName])
|
||||
await runOcc(['group:adduser', groupName, testUser.userId])
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await runOcc(['user:delete', testUser.userId])
|
||||
await runOcc(['group:delete', groupName]).catch(() => {})
|
||||
})
|
||||
|
||||
test('can delete a non-empty group', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
const groupItem = settingsPage.groupListItem(groupName)
|
||||
await expect(groupItem).toBeVisible()
|
||||
|
||||
// Open the group's actions menu
|
||||
await groupItem.hover()
|
||||
expect(groupItem.getByRole('button', { name: /Actions/i })).toBeVisible()
|
||||
await groupItem.getByRole('button', { name: /Actions/i }).click()
|
||||
|
||||
// and delete the group
|
||||
await page.getByRole('button', { name: 'Delete group' }).click()
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click()
|
||||
await handlePasswordConfirmation(page)
|
||||
|
||||
await expect(settingsPage.groupListItem(groupName)).toHaveCount(0)
|
||||
|
||||
const groups: Record<string, unknown> = JSON.parse(await runOcc(['group:list', '--output=json']))
|
||||
expect(Object.keys(groups)).not.toContain(groupName)
|
||||
})
|
||||
})
|
||||
|
||||
// ── Sort groups ───────────────────────────────────────────────────────────────
|
||||
const sortGroupsTest = test.extend<{ testUser: User, testGroups: [string, string] }>({
|
||||
async testGroups({ testUser }, use) {
|
||||
const suffix = crypto.randomUUID().slice(0, 8)
|
||||
const groupA = `A-${suffix}`
|
||||
const groupB = `B-${suffix}`
|
||||
|
||||
await runOcc(['group:add', groupA])
|
||||
await runOcc(['group:add', groupB])
|
||||
await runOcc(['group:adduser', groupB, testUser.userId])
|
||||
await use([groupA, groupB])
|
||||
await runOcc(['group:delete', groupA]).catch(() => {})
|
||||
await runOcc(['group:delete', groupB]).catch(() => {})
|
||||
},
|
||||
testUser: async ({}, use) => {
|
||||
const testUser = await createRandomUser()
|
||||
await use(testUser)
|
||||
await runOcc(['user:delete', testUser.userId])
|
||||
},
|
||||
})
|
||||
|
||||
sortGroupsTest('Settings: Sort groups by member count and then by name', async ({ page, testGroups }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
// ── sort by member count ──
|
||||
await settingsPage.openSettingsDialog()
|
||||
await settingsPage.settingsDialog()
|
||||
.getByRole('radio', { name: 'By member count' })
|
||||
.check({ force: true })
|
||||
await settingsPage.closeSettingsDialog()
|
||||
|
||||
// B (1 member) must come before A (0 members)
|
||||
await checkGroupOrder([testGroups[1], testGroups[0]], settingsPage)
|
||||
|
||||
// Reload to confirm persistence
|
||||
await page.reload()
|
||||
await checkGroupOrder([testGroups[1], testGroups[0]], settingsPage)
|
||||
|
||||
// ── sort by name ──
|
||||
await settingsPage.openSettingsDialog()
|
||||
await settingsPage.settingsDialog().getByRole('radio', { name: 'By name' }).check({ force: true })
|
||||
await settingsPage.closeSettingsDialog()
|
||||
|
||||
// A comes before B alphabetically
|
||||
await checkGroupOrder([testGroups[0], testGroups[1]], settingsPage)
|
||||
|
||||
// Reload to confirm persistence
|
||||
await page.reload()
|
||||
await checkGroupOrder([testGroups[0], testGroups[1]], settingsPage)
|
||||
})
|
||||
|
||||
/**
|
||||
* Check that the groups are in the expected order in the UI.
|
||||
*
|
||||
* @param order - The expected group order
|
||||
* @param settingsPage - The settings page
|
||||
*/
|
||||
async function checkGroupOrder(order: string[], settingsPage: SettingsUsersPage) {
|
||||
// B (1 member) must come before A (0 members)
|
||||
const listItems = settingsPage.customGroupsList().getByRole('listitem')
|
||||
for (const group of order) {
|
||||
await expect(listItems.filter({ hasText: group })).toHaveCount(1)
|
||||
}
|
||||
|
||||
const contents = (await listItems.allTextContents())
|
||||
.map((text) => text.trim().replaceAll(/\s+.*/g, '')) // trim and remove member count
|
||||
.filter((text) => order.includes(text)) // filter out other groups that might be in the list
|
||||
expect(contents).toEqual(order)
|
||||
}
|
||||
82
tests/playwright/e2e/users/users-manager.spec.ts
Normal file
82
tests/playwright/e2e/users/users-manager.spec.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { type User } from '@nextcloud/e2e-test-server'
|
||||
import { createRandomUser } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { test as adminTest } from '../../support/fixtures/admin-session.ts'
|
||||
import { handlePasswordConfirmation } from '../../support/utils/password-confirmation.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
const test = adminTest.extend<{ user: User; manager: User }>({
|
||||
user: async ({}, use) => {
|
||||
const u = await createRandomUser()
|
||||
await use(u)
|
||||
await runOcc(['user:delete', u.userId])
|
||||
},
|
||||
manager: async ({}, use) => {
|
||||
const u = await createRandomUser()
|
||||
await use(u)
|
||||
await runOcc(['user:delete', u.userId])
|
||||
},
|
||||
})
|
||||
|
||||
test.describe('Settings: User Manager Management', () => {
|
||||
test('can assign a manager through the edit dialog', async ({ page, user, manager }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
|
||||
const managerCombobox = dialog.getByRole('combobox', { name: /Manager/i })
|
||||
await managerCombobox.fill(manager.userId)
|
||||
await page.getByRole('option', { name: manager.userId }).click()
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify via OCS API (page shares admin auth cookies)
|
||||
const response = await page.request.get(
|
||||
`/ocs/v2.php/cloud/users/${user.userId}`,
|
||||
{ headers: { 'OCS-APIRequest': 'true', Accept: 'application/json' } },
|
||||
)
|
||||
const data = await response.json()
|
||||
expect(data?.ocs?.data?.manager).toBe(manager.userId)
|
||||
})
|
||||
|
||||
test('can remove a manager through the edit dialog', async ({ page, user, manager }) => {
|
||||
// Set manager via OCC first
|
||||
await runOcc([
|
||||
'user:setting', user.userId, 'settings', 'manager',
|
||||
`["${manager.userId}"]`,
|
||||
])
|
||||
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
|
||||
// Clear the currently-set manager using the NcSelect's clear button
|
||||
await dialog.getByRole('button', { name: /Clear Selected/i }).click()
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify backend: manager must be empty
|
||||
const response = await page.request.get(
|
||||
`/ocs/v2.php/cloud/users/${user.userId}`,
|
||||
{ headers: { 'OCS-APIRequest': 'true', Accept: 'application/json' } },
|
||||
)
|
||||
const data = await response.json()
|
||||
expect(data?.ocs?.data?.manager).toBeFalsy()
|
||||
})
|
||||
})
|
||||
165
tests/playwright/e2e/users/users-modify.spec.ts
Normal file
165
tests/playwright/e2e/users/users-modify.spec.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { type User } from '@nextcloud/e2e-test-server'
|
||||
import { createRandomUser, login } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { test as adminTest } from '../../support/fixtures/admin-session.ts'
|
||||
import { handlePasswordConfirmation } from '../../support/utils/password-confirmation.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
const test = adminTest.extend<{ user: User }>({
|
||||
user: async ({}, use) => {
|
||||
const user = await createRandomUser()
|
||||
await use(user)
|
||||
await runOcc(['user:delete', user.userId])
|
||||
},
|
||||
})
|
||||
|
||||
test.describe('Settings: Change user properties', () => {
|
||||
test('can change the display name', async ({ page, user }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
const displayNameInput = dialog.getByLabel('Display name')
|
||||
await expect(displayNameInput).toHaveValue(user.userId)
|
||||
await displayNameInput.fill('John Doe')
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify backend
|
||||
const info = JSON.parse(await runOcc(['user:info', '--output=json', user.userId]))
|
||||
expect(info?.display_name).toBe('John Doe')
|
||||
})
|
||||
|
||||
test('can change the password', async ({ page, user, context }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
const passwordInput = dialog.getByLabel(/New password/i).and(page.locator('input')) // hack because there is no accessible role for input fields with type=password
|
||||
await expect(passwordInput).toHaveValue('')
|
||||
await passwordInput.fill('newpassword123')
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify by logging in with the new password
|
||||
await login(context.request, { ...user, password: 'newpassword123' })
|
||||
await page.goto('/apps/dashboard')
|
||||
await expect(page).toHaveURL(/\/apps\/dashboard/)
|
||||
})
|
||||
|
||||
test('can change the email address', async ({ page, user }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
const emailInput = dialog.getByLabel(/Email/)
|
||||
await expect(emailInput).toHaveValue('')
|
||||
await emailInput.fill('mymail@example.com')
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify backend
|
||||
const info = JSON.parse(await runOcc(['user:info', '--output=json', user.userId]))
|
||||
expect(info?.email).toBe('mymail@example.com')
|
||||
})
|
||||
|
||||
test('can change the user quota to a predefined value', async ({ page, user }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
|
||||
// Open the Quota NcSelect and choose 5 GB
|
||||
const quotaCombobox = dialog.getByRole('combobox', { name: /Quota/i })
|
||||
await quotaCombobox.click()
|
||||
await page.getByRole('option', { name: '5 GB' }).click()
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify backend
|
||||
const info = JSON.parse(await runOcc(['user:info', '--output=json', user.userId]))
|
||||
expect(info?.quota).toBe('5 GB')
|
||||
})
|
||||
|
||||
test('can change the user quota to a custom value', async ({ page, user }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
|
||||
// Type a custom value directly into the combobox
|
||||
const quotaCombobox = dialog.getByRole('combobox', { name: /Quota/i })
|
||||
await quotaCombobox.fill('4 MB')
|
||||
await quotaCombobox.press('Enter')
|
||||
|
||||
await handlePasswordConfirmation(page)
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify backend (stored as bytes)
|
||||
const info = JSON.parse(await runOcc(['user:info', '--output=json', user.userId]))
|
||||
expect(info?.quota).not.toBe('none')
|
||||
})
|
||||
|
||||
test('can make user a subadmin of a group', async ({ page, user }) => {
|
||||
const groupName = crypto.randomUUID().slice(0, 6)
|
||||
const shortName = groupName.slice(0, 4)
|
||||
await runOcc(['group:add', groupName])
|
||||
|
||||
try {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openEditDialog(user.userId)
|
||||
const dialog = settingsPage.editUserDialog()
|
||||
|
||||
// Open the subadmin NcSelect and pick the group
|
||||
const subadminCombobox = dialog.getByRole('combobox', { name: /Admin of the following groups/i })
|
||||
await subadminCombobox.click()
|
||||
|
||||
const waitForSearch = page
|
||||
.waitForResponse((r) => r.request().url().includes(`ocs/v2.php/cloud/groups/details?search=${shortName}`))
|
||||
await subadminCombobox.fill(shortName)
|
||||
await waitForSearch
|
||||
await page.getByRole('option', { name: new RegExp(groupName) }).click()
|
||||
|
||||
await settingsPage.saveEditDialog()
|
||||
|
||||
await expect(page.getByText(/Account updated/i)).toBeVisible()
|
||||
|
||||
// Verify backend via OCS API (page shares admin auth state)
|
||||
const response = await page.request.get(
|
||||
`/ocs/v2.php/cloud/users/${user.userId}/subadmins`,
|
||||
{ headers: { 'OCS-APIRequest': 'true', Accept: 'application/json' } },
|
||||
)
|
||||
const data = await response.json()
|
||||
expect(data?.ocs?.data).toContain(groupName)
|
||||
} finally {
|
||||
await runOcc(['group:delete', groupName])
|
||||
}
|
||||
})
|
||||
})
|
||||
108
tests/playwright/e2e/users/users-search.spec.ts
Normal file
108
tests/playwright/e2e/users/users-search.spec.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { type User } from '@nextcloud/e2e-test-server'
|
||||
import { createRandomUser } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { test as adminTest } from '../../support/fixtures/admin-session.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
adminTest.describe.configure({ mode: 'serial' })
|
||||
|
||||
adminTest.describe('Settings: Unified search for accounts and groups', () => {
|
||||
// Stable, searchable prefix so we can match the group independently of the random suffix
|
||||
const matchingGroup = `zzz-match-${crypto.randomUUID().slice(0, 5)}`
|
||||
const otherGroup = `aaa-other-${crypto.randomUUID().slice(0, 5)}`
|
||||
let alice: User
|
||||
let bob: User
|
||||
|
||||
adminTest.beforeAll(async () => {
|
||||
alice = await createRandomUser()
|
||||
bob = await createRandomUser()
|
||||
await runOcc(['group:add', matchingGroup])
|
||||
await runOcc(['group:add', otherGroup])
|
||||
})
|
||||
|
||||
adminTest.afterAll(async () => {
|
||||
await runOcc(['user:delete', alice.userId])
|
||||
await runOcc(['user:delete', bob.userId])
|
||||
await runOcc(['group:delete', matchingGroup])
|
||||
await runOcc(['group:delete', otherGroup])
|
||||
})
|
||||
|
||||
adminTest('shows the search input in the navigation sidebar', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
const searchbox = settingsPage.navigation().getByRole('searchbox', { name: /search accounts and groups/i })
|
||||
await expect(searchbox).toBeVisible()
|
||||
await expect(searchbox).toHaveValue('')
|
||||
})
|
||||
|
||||
adminTest('dispatches the query to both the users and groups API', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
const searchbox = settingsPage.navigation().getByRole('searchbox', { name: /search accounts and groups/i })
|
||||
|
||||
const usersRespPromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/users\/details/)
|
||||
const groupsRespPromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/groups\/details/)
|
||||
await searchbox.fill(alice.userId)
|
||||
const usersResp = await usersRespPromise
|
||||
const groupsResp = await groupsRespPromise
|
||||
|
||||
expect(new URL(usersResp.url()).searchParams.get('search')).toBe(alice.userId)
|
||||
expect(new URL(groupsResp.url()).searchParams.get('search')).toBe(alice.userId)
|
||||
|
||||
// User list reflects the filtered result
|
||||
await expect(settingsPage.userRow(alice.userId)).toBeVisible()
|
||||
await expect(settingsPage.userList()).not.toContainText(bob.userId)
|
||||
})
|
||||
|
||||
adminTest('filters the group list when the query matches a group name', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
const searchbox = settingsPage.navigation().getByRole('searchbox', { name: /search accounts and groups/i })
|
||||
|
||||
const groupsRespPromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/groups\/details/)
|
||||
await searchbox.fill(matchingGroup)
|
||||
const groupsResp = await groupsRespPromise
|
||||
|
||||
expect(new URL(groupsResp.url()).searchParams.get('search')).toBe(matchingGroup)
|
||||
|
||||
await expect(settingsPage.customGroupsList()).toContainText(matchingGroup)
|
||||
await expect(settingsPage.customGroupsList()).not.toContainText(otherGroup)
|
||||
})
|
||||
|
||||
adminTest('resets both lists when the clear button is clicked', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
const searchbox = settingsPage.navigation().getByRole('searchbox', { name: /search accounts and groups/i })
|
||||
|
||||
// Prime the search box with a term first
|
||||
const primeUsersPromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/users\/details/)
|
||||
const primeGroupsPromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/groups\/details/)
|
||||
await searchbox.fill(alice.userId)
|
||||
await primeUsersPromise
|
||||
await primeGroupsPromise
|
||||
|
||||
// Now clear
|
||||
const usersRespPromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/users\/details/)
|
||||
const groupsRespPromise = page.waitForResponse(/ocs\/v2\.php\/cloud\/groups\/details/)
|
||||
await settingsPage.navigation().getByRole('button', { name: /clear search/i }).click()
|
||||
await usersRespPromise
|
||||
await groupsRespPromise
|
||||
|
||||
await expect(searchbox).toHaveValue('')
|
||||
// Both users and both groups must be visible again
|
||||
await expect(settingsPage.userRow(alice.userId)).toBeVisible()
|
||||
await expect(settingsPage.userRow(bob.userId)).toBeVisible()
|
||||
await expect(settingsPage.customGroupsList()).toContainText(matchingGroup)
|
||||
await expect(settingsPage.customGroupsList()).toContainText(otherGroup)
|
||||
})
|
||||
})
|
||||
77
tests/playwright/e2e/users/users.spec.ts
Normal file
77
tests/playwright/e2e/users/users.spec.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test'
|
||||
import { createRandomUser } from '@nextcloud/e2e-test-server/playwright'
|
||||
import { runOcc } from '@nextcloud/e2e-test-server/docker'
|
||||
import { test } from '../../support/fixtures/admin-session.ts'
|
||||
import { handlePasswordConfirmation } from '../../support/utils/password-confirmation.ts'
|
||||
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
||||
|
||||
test.describe('Settings: Create and delete accounts', () => {
|
||||
test('can create a user with username and password', async ({ page }) => {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openNewUserDialog()
|
||||
|
||||
const dialog = settingsPage.newUserDialog()
|
||||
await dialog.getByLabel(/Account name/).fill('newuser-basic')
|
||||
await dialog.getByLabel(/Password/).and(page.locator('input')).fill('password123')
|
||||
|
||||
await dialog.getByRole('button', { name: 'Add new account' }).click()
|
||||
await handlePasswordConfirmation(page)
|
||||
await dialog.waitFor({ state: 'hidden' })
|
||||
|
||||
await expect(settingsPage.userRow('newuser-basic')).toContainText('newuser-basic')
|
||||
|
||||
await runOcc(['user:delete', 'newuser-basic'])
|
||||
})
|
||||
|
||||
test('can create a user with display name and email', async ({ page }) => {
|
||||
const newUserId = crypto.randomUUID()
|
||||
try {
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
await settingsPage.open()
|
||||
|
||||
await settingsPage.openNewUserDialog()
|
||||
|
||||
const dialog = settingsPage.newUserDialog()
|
||||
await dialog.getByLabel(/Account name/).fill(newUserId)
|
||||
await dialog.getByLabel('Display name').fill('John Smith')
|
||||
await dialog.getByLabel(/Email/).fill('john@example.org')
|
||||
await dialog.getByLabel(/Password/).and(page.locator('input')).fill('password123')
|
||||
|
||||
await dialog.getByRole('button', { name: 'Add new account' }).click()
|
||||
await handlePasswordConfirmation(page)
|
||||
await dialog.waitFor({ state: 'hidden' })
|
||||
|
||||
await expect(settingsPage.userRow(newUserId)).toContainText(newUserId)
|
||||
} finally {
|
||||
await runOcc(['user:delete', newUserId])
|
||||
}
|
||||
})
|
||||
|
||||
test('can delete a user', async ({ page }) => {
|
||||
const testUser = await createRandomUser()
|
||||
const settingsPage = new SettingsUsersPage(page)
|
||||
|
||||
try {
|
||||
await settingsPage.open()
|
||||
await expect(settingsPage.userRow(testUser.userId)).toBeVisible()
|
||||
|
||||
await settingsPage.openActionsMenu(testUser.userId)
|
||||
await page.getByRole('menuitem', { name: 'Delete account' }).click()
|
||||
await handlePasswordConfirmation(page)
|
||||
|
||||
// Confirm the deletion in the confirmation dialog
|
||||
await page.getByRole('dialog').getByRole('button', { name: `Delete ${testUser.userId}` }).click()
|
||||
|
||||
await expect(settingsPage.userRow(testUser.userId)).toHaveCount(0)
|
||||
} finally {
|
||||
await runOcc(['user:delete', testUser.userId]).catch(() => {})
|
||||
}
|
||||
})
|
||||
})
|
||||
115
tests/playwright/support/sections/SettingsUsersPage.ts
Normal file
115
tests/playwright/support/sections/SettingsUsersPage.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { expect, type Locator, type Page } from '@playwright/test'
|
||||
import { handlePasswordConfirmation } from '../utils/password-confirmation'
|
||||
|
||||
/**
|
||||
* Page object for the Admin Users Management page (/settings/users).
|
||||
*
|
||||
* Selector strategy:
|
||||
* - Prefer role / label / text selectors.
|
||||
* - `data-cy-user-row` and `data-cy-user-list` are the only data-attribute
|
||||
* selectors used — the virtual-scroll list and individual rows have no
|
||||
* semantic ARIA alternative.
|
||||
* - `data-cy-users-settings-navigation-groups` is used for the custom groups
|
||||
* list because the list has no distinct accessible name.
|
||||
*/
|
||||
export class SettingsUsersPage {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async open(): Promise<void> {
|
||||
await this.page.goto('/settings/users')
|
||||
await this.userList().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
// ── Sidebar navigation ──────────────────────────────────────────────────
|
||||
|
||||
navigation(): Locator {
|
||||
return this.page.getByRole('navigation', { name: 'Account management' })
|
||||
}
|
||||
|
||||
/** Click a named link in the account management sidebar. */
|
||||
async navigateTo(name: string | RegExp): Promise<void> {
|
||||
await this.navigation().getByRole('link', { name }).click()
|
||||
}
|
||||
|
||||
/** The custom groups section in the sidebar navigation. */
|
||||
customGroupsList(): Locator {
|
||||
return this.page.locator('[data-cy-users-settings-navigation-groups="custom"]')
|
||||
}
|
||||
|
||||
groupListItem(groupName: string): Locator {
|
||||
return this.customGroupsList().getByRole('listitem').filter({ hasText: groupName })
|
||||
}
|
||||
|
||||
// ── User list ────────────────────────────────────────────────────────────
|
||||
|
||||
userList(): Locator {
|
||||
return this.page.locator('[data-cy-user-list]')
|
||||
}
|
||||
|
||||
userRow(userId: string): Locator {
|
||||
return this.page.locator(`[data-cy-user-row="${userId}"]`)
|
||||
}
|
||||
|
||||
// ── Dialogs ──────────────────────────────────────────────────────────────
|
||||
|
||||
/** Open the "New account" dialog and wait for it to appear. */
|
||||
async openNewUserDialog(): Promise<void> {
|
||||
await this.page.getByRole('navigation')
|
||||
.getByRole('button', { name: 'New account' })
|
||||
.click()
|
||||
await this.newUserDialog().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
newUserDialog(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'New account' })
|
||||
}
|
||||
|
||||
/** Open the edit dialog for `userId` by clicking its inline Edit button. */
|
||||
async openEditDialog(userId: string): Promise<void> {
|
||||
await this.userRow(userId).getByRole('button', { name: 'Edit' }).click()
|
||||
await this.editUserDialog().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
editUserDialog(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Edit account' })
|
||||
}
|
||||
|
||||
/** Save and close the currently open edit dialog. */
|
||||
async saveEditDialog(): Promise<void> {
|
||||
const dialog = this.editUserDialog()
|
||||
const button = dialog.getByRole('button', { name: 'Save' })
|
||||
await button.focus()
|
||||
await button.click({ force: true })
|
||||
await handlePasswordConfirmation(this.page)
|
||||
await dialog.waitFor({ state: 'hidden' })
|
||||
}
|
||||
|
||||
/** Open the actions dropdown for `userId`. */
|
||||
async openActionsMenu(userId: string): Promise<void> {
|
||||
const button = this.userRow(userId).getByRole('button', { name: 'Toggle account actions menu' })
|
||||
await button.click()
|
||||
await expect(button).toHaveAttribute('aria-controls')
|
||||
await expect(this.page.getByRole('menu').and(this.page.locator('#' + await button.getAttribute('aria-controls')))).toBeVisible()
|
||||
}
|
||||
|
||||
/** Open the "Account management settings" dialog. */
|
||||
async openSettingsDialog(): Promise<void> {
|
||||
await this.page.getByRole('button', { name: 'Account management settings' }).click()
|
||||
await this.settingsDialog().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
settingsDialog(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Account management settings' })
|
||||
}
|
||||
|
||||
/** Close the "Account management settings" dialog. */
|
||||
async closeSettingsDialog(): Promise<void> {
|
||||
await this.settingsDialog().getByRole('button', { name: 'Close' }).click()
|
||||
await this.settingsDialog().waitFor({ state: 'hidden' })
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue