Files
Ferdinand Thiessen 49dec7388e test(playwright): answer password confirmation via a locator handler
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>
2026-09-16 00:57:55 +02:00

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)
}