Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>pull/61522/head
parent
b54e98a64b
commit
b245d2dcc9
@ -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) |
||||
}) |
||||
}) |
||||
@ -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() |
||||
}) |
||||
}) |
||||
@ -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) |
||||
}) |
||||
}) |
||||
@ -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) |
||||
}) |
||||
}) |
||||
@ -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]) |
||||
}) |
||||
}) |
||||
@ -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) |
||||
} |
||||
@ -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() |
||||
}) |
||||
}) |
||||
@ -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]) |
||||
} |
||||
}) |
||||
}) |
||||
@ -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) |
||||
}) |
||||
}) |
||||
@ -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(() => {}) |
||||
} |
||||
}) |
||||
}) |
||||
@ -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 new issue