Files
punimtag/e2e/pages/ManageUsersPanel.ts
T
ilia 37f565dd04
CI / skip-ci-check (pull_request) Successful in 5s
CI / docker-ci (pull_request) Successful in 7s
CI / secret-scan (pull_request) Successful in 14s
CI / viewer-unit (pull_request) Successful in 54s
CI / admin-unit (pull_request) Successful in 2m35s
CI / e2e (pull_request) Failing after 4m31s
test: Manage Users real CRUD + causal cross-session e2e coverage
admin.manage-users.spec.ts only opened/closed the panel; the actual
create/edit/delete flow through the UI was untested. Adds a page object
(ManageUsersPanel) and two specs: full CRUD through the panel, and a
causal cross-session check that deactivating a user via the admin UI
immediately revokes that user's already-open session (NextAuth jwt
callback re-checks isActive on every request, per issue #57's fix).

Every run creates and tears down its own disposable e2e-manage-test-*
account since punimtagdev shares its Postgres DB with MirrorMatch.
2026-07-14 22:08:34 -04:00

110 lines
4.0 KiB
TypeScript

import { type Page, type Locator } from '@playwright/test';
import { BasePage, waitForVisible, waitForHidden } from '@levkin/playkit';
export interface NewUserInput {
email: string;
password: string;
name: string;
isAdmin?: boolean;
hasWriteAccess?: boolean;
}
/**
* Page object for the "Manage Users" overlay content
* (`viewer-frontend/app/admin/users/ManageUsersContent.tsx`).
*
* Talks to the NextAuth-backed `/api/users` store — a separate account
* database from the FastAPI `/api/v1/users` store covered by
* `api.role-permissions.spec.ts`.
*/
export class ManageUsersPanel extends BasePage {
constructor(page: Page, baseUrl: string) {
super(page, baseUrl);
}
row(email: string): Locator {
return this.page.locator('tbody tr').filter({ hasText: email });
}
/** Scopes to the currently-open dialog matching a heading, to avoid
* ambiguous matches against hidden/portalled sibling dialogs. */
private dialog(headingText: string): Locator {
return this.page
.getByRole('dialog')
.filter({ has: this.page.getByRole('heading', { name: headingText, exact: true }) });
}
async createUser(input: NewUserInput): Promise<void> {
await this.click(this.page.getByRole('button', { name: 'Add User', exact: true }));
const dialog = this.dialog('Add New User');
await waitForVisible(dialog);
await this.fill(dialog.locator('#add-email'), input.email);
await this.fill(dialog.locator('#add-password'), input.password);
await this.fill(dialog.locator('#add-name'), input.name);
if (input.isAdmin) {
await this.click(dialog.locator('#add-role'));
await this.click(this.page.getByRole('option', { name: 'Admin', exact: true }));
}
if (input.hasWriteAccess) {
await this.click(dialog.locator('#add-write-access'));
}
await this.click(dialog.getByRole('button', { name: 'Create User', exact: true }));
await waitForVisible(this.row(input.email), { timeout: 15_000 });
}
async openEditDialog(email: string): Promise<void> {
await this.click(this.row(email).getByRole('button', { name: `Edit ${email}`, exact: true }));
await waitForVisible(this.dialog('Edit User'));
}
private async setCheckbox(locator: Locator, checked: boolean): Promise<void> {
const isChecked = await locator.isChecked();
if (isChecked !== checked) {
await this.click(locator);
}
}
async setActive(active: boolean): Promise<void> {
await this.setCheckbox(this.dialog('Edit User').locator('#edit-active'), active);
}
async setWriteAccess(enabled: boolean): Promise<void> {
await this.setCheckbox(this.dialog('Edit User').locator('#edit-write-access'), enabled);
}
async saveEdit(): Promise<void> {
const dialog = this.dialog('Edit User');
await this.click(dialog.getByRole('button', { name: 'Save Changes', exact: true }));
await waitForHidden(dialog);
}
/** Clicks the row's delete/deactivate icon and confirms in the dialog.
* Hard-deletes if the account has no related records, otherwise the
* backend soft-deactivates it — either way `isActive` ends up false. */
async deleteUser(email: string): Promise<void> {
await this.click(
this.row(email).getByRole('button', { name: `Deactivate ${email}`, exact: true }),
);
const dialog = this.dialog('Delete User');
await waitForVisible(dialog);
await this.click(dialog.getByRole('button', { name: 'Delete', exact: true }));
await waitForHidden(dialog);
}
async statusBadgeText(email: string): Promise<string> {
return (await this.row(email).locator('td').nth(2).innerText()).trim();
}
async roleBadgeText(email: string): Promise<string> {
return (await this.row(email).locator('td').nth(3).innerText()).trim();
}
async writeAccessText(email: string): Promise<string> {
return (await this.row(email).locator('td').nth(4).innerText()).trim();
}
async waitForRowGone(email: string, timeout = 15_000): Promise<void> {
await waitForHidden(this.row(email), { timeout });
}
}