mirror of
https://github.com/nextcloud/server.git
synced 2026-10-08 20:51:14 +02:00
The dialog only appears once a session's confirmation window has expired, so the old helper probed for it for 500 ms and silently skipped it on a slow shard, leaving the guarded request unsent; its wait for the dialog to close was unbounded. Install a locator handler per logged-in page instead, and use awaitPasswordGuardedRequest() where a response promise is awaited, since handlers never run during a bare waitForResponse. Assisted-by: ClaudeCode:claude-opus-5 Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
222 lines
8.6 KiB
TypeScript
222 lines
8.6 KiB
TypeScript
/*
|
|
* 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 { runOcc } from '@nextcloud/e2e-test-server/docker'
|
|
import { createRandomUser } from '@nextcloud/e2e-test-server/playwright'
|
|
import { expect } from '@playwright/test'
|
|
import { test } from '../../support/fixtures/admin-with-user.ts'
|
|
import { SettingsUsersPage } from '../../support/sections/SettingsUsersPage.ts'
|
|
import { awaitPasswordGuardedRequest } from '../../support/utils/password-confirmation.ts'
|
|
import { getToast } from '../../support/utils/toast.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 awaitPasswordGuardedRequest(page, createGroupsResponsePromise)
|
|
|
|
await expect(settingsPage.customGroupsList()).toContainText(groupName)
|
|
} finally {
|
|
await runOcc(['group:delete', groupName]).catch(() => {})
|
|
}
|
|
})
|
|
|
|
// ── Assign user to group ──────────────────────────────────────────────────────
|
|
|
|
const userGroupTest = test.extend<{ testGroup: string }>({
|
|
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, user }) => {
|
|
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.groupMemberCount(testGroup)).toHaveCount(0)
|
|
// user is in the list
|
|
await expect(settingsPage.userRow(user.userId)).toBeVisible()
|
|
|
|
// can assign the group via the edit dialog
|
|
await settingsPage.openEditDialog(user.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 settingsPage.saveEditDialog()
|
|
await expect(getToast(page, /Account updated/i)).toBeVisible()
|
|
|
|
// user is now group now shows 1 member
|
|
await expect(settingsPage.groupMemberCount(testGroup)).toHaveText('1')
|
|
// backend confirms the user is in the group
|
|
const { stdout: jsonList } = await runOcc(['user:info', '--output=json', user.userId])
|
|
const { groups } = JSON.parse(jsonList)
|
|
expect(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()
|
|
|
|
await expect(settingsPage.groupListItem(groupName)).toBeVisible()
|
|
|
|
await settingsPage.openGroupActionsMenu(groupName)
|
|
|
|
// and delete the group
|
|
await page.getByRole('button', { name: 'Delete group' }).click()
|
|
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click()
|
|
|
|
// Group must be gone from the UI
|
|
await expect(settingsPage.groupListItem(groupName)).toHaveCount(0)
|
|
|
|
// Verify backend
|
|
// `group:list --output=json` returns the group map directly ({"name":[members]}),
|
|
// with no `groups` wrapper — unlike `user:info`.
|
|
const { stdout: jsonList } = await runOcc(['group:list', '--output=json'])
|
|
const groups = JSON.parse(jsonList)
|
|
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()
|
|
|
|
await expect(settingsPage.groupListItem(groupName)).toBeVisible()
|
|
|
|
await settingsPage.openGroupActionsMenu(groupName)
|
|
|
|
// and delete the group
|
|
await page.getByRole('button', { name: 'Delete group' }).click()
|
|
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click()
|
|
|
|
await expect(settingsPage.groupListItem(groupName)).toHaveCount(0)
|
|
|
|
// `group:list --output=json` returns the group map directly ({"name":[members]}),
|
|
// with no `groups` wrapper — unlike `user:info`.
|
|
const { stdout: jsonList } = await runOcc(['group:list', '--output=json'])
|
|
const groups = JSON.parse(jsonList)
|
|
expect(Object.keys(groups)).not.toContain(groupName)
|
|
})
|
|
})
|
|
|
|
// ── Sort groups ───────────────────────────────────────────────────────────────
|
|
const sortGroupsTest = test.extend<{ testGroups: [string, string] }>({
|
|
async testGroups({ user }, 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, user.userId])
|
|
await use([groupA, groupB])
|
|
await runOcc(['group:delete', groupA]).catch(() => {})
|
|
await runOcc(['group:delete', groupB]).catch(() => {})
|
|
},
|
|
})
|
|
|
|
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)
|
|
}
|