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

132 lines
4.8 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
/**
* 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()
await dialog.getByRole('button', { name: 'Save' }).click()
await expect(dialog).toBeHidden({ timeout: 30_000 })
}
/**
* Open the NcActions flyout for a custom group nav item.
* NcActions inside NcAppNavigationItem falls back to the default `t('Actions')` label.
*/
async openGroupActionsMenu(groupName: string): Promise<void> {
const item = this.groupListItem(groupName)
await item.hover()
await item.getByRole('button', { name: /Actions/i }).click()
}
/**
* Locator for the member count inside a group list item.
* NcCounterBubble has no accessible role — CSS class is a last resort.
*/
groupMemberCount(groupName: string): Locator {
return this.groupListItem(groupName).locator('.counter-bubble__counter')
}
/** 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' })
}
}