web: Fix numeric values in search select inputs, search input fixes (#16928)

* web: Fix numeric values in search select inputs.

* web: Fix ARIA attributes on menu items.

* web: Fix issues surrounding nested modal actions, selectors, labels.

* web: Prepare group forms for testing, ARIA, etc.

* web: Clarify when spinner buttons are busy.

* web: Fix dark theme toggle input visibility.

* web: Fix issue where tests complete before optional search inputs load.

* web: Add user creation tests, group creation. Flesh out fixtures.
This commit is contained in:
Teffen Ellis
2025-10-02 05:04:38 +02:00
committed by GitHub
parent 9e4b6098fd
commit 2e8a1d80a3
24 changed files with 661 additions and 191 deletions

View File

@@ -78,6 +78,47 @@ export class FormFixture extends PageFixture {
await control.fill(value);
};
/**
* Search for a row containing the given text.
*/
public search = async (
query: string,
context: LocatorContext = this.page,
): Promise<Locator> => {
const searchInput = await this.findTextualInput(/search/i, context);
// We have to wait for the user to appear in the table,
// but several UI elements will be rendered asynchronously.
// We attempt several times to find the user to avoid flakiness.
const tries = 10;
let found = false;
for (let i = 0; i < tries; i++) {
await this.fill(searchInput, query);
await searchInput.press("Enter");
const $rowEntry = context.getByRole("row", {
name: query,
});
this.logger.info(`${i + 1}/${tries} Waiting for "${query}" to appear in the table`);
found = await $rowEntry
.waitFor({
timeout: 1500,
})
.then(() => true)
.catch(() => false);
if (found) {
this.logger.info(`"${query}" found in the table`);
return $rowEntry;
}
}
throw new Error(`"${query}" not found in the table`);
};
/**
* Set the value of a radio or checkbox input.
*

View File

@@ -0,0 +1,56 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import { Page } from "@playwright/test";
export const GOOD_USERNAME = "test-admin@goauthentik.io";
export const GOOD_PASSWORD = "test-runner";
export const BAD_USERNAME = "bad-username@bad-login.io";
export const BAD_PASSWORD = "-this-is-a-bad-password-";
export interface LoginInit {
username?: string;
password?: string;
to?: URL | string;
}
export class NavigatorFixture extends PageFixture {
static fixtureName = "Navigator";
constructor(page: Page, testName: string) {
super({ page, testName });
}
/**
* Wait for the current page to navigate to the given pathname.
*
* This method is useful to verify that a navigation has completed after an action
* automatically updates the URL, such as form submissions or link clicks.
*
* @see {@linkcode navigate} for navigation.
*
* @param to The pathname or URL to wait for.
*/
public waitForPathname = async (to: string | URL): Promise<void> => {
const expectedPathname = typeof to === "string" ? to : to.pathname;
this.logger.info(`Waiting for URL to change to ${expectedPathname}`);
await this.page.waitForURL(`**${expectedPathname}**`);
this.logger.info(`URL changed to ${this.page.url()}`);
};
/**
* Navigate to the given URL or pathname, and wait for the navigation to complete.
*/
public navigate = async (to: URL | string | null | undefined): Promise<void> => {
if (!to) {
throw new TypeError("No URL or pathname given to navigate to.");
}
await this.page.goto(to.toString());
await this.waitForPathname(to);
};
}

View File

@@ -2,7 +2,7 @@ import { ConsoleLogger, FixtureLogger } from "#logger/node";
import { Page } from "@playwright/test";
export interface PageFixtureOptions {
export interface PageFixtureInit {
page: Page;
testName: string;
}
@@ -19,7 +19,7 @@ export abstract class PageFixture {
protected readonly page: Page;
protected readonly testName: string;
constructor({ page, testName }: PageFixtureOptions) {
constructor({ page, testName }: PageFixtureInit) {
this.page = page;
this.testName = testName;

View File

@@ -17,6 +17,9 @@ export type ClickByRole = (
export class PointerFixture extends PageFixture {
public static fixtureName = "Pointer";
/**
* A high-level click function that simplifies clicking on buttons and links.
*/
public click = (
name: string | RegExp,
optionsOrRole?: ARIAOptions | ARIARole,

View File

@@ -1,6 +1,5 @@
import { PageFixture } from "#e2e/fixtures/PageFixture";
import { Page } from "@playwright/test";
import { NavigatorFixture } from "#e2e/fixtures/NavigatorFixture";
import { PageFixture, PageFixtureInit } from "#e2e/fixtures/PageFixture";
export const GOOD_USERNAME = "test-admin@goauthentik.io";
export const GOOD_PASSWORD = "test-runner";
@@ -14,11 +13,17 @@ export interface LoginInit {
to?: URL | string;
}
export interface SessionFixtureInit extends PageFixtureInit {
navigator: NavigatorFixture;
}
export class SessionFixture extends PageFixture {
static fixtureName = "Session";
public static readonly pathname = "/if/flow/default-authentication-flow/";
protected navigator: NavigatorFixture;
//#region Selectors
public $identificationStage = this.page.locator("ak-stage-identification");
@@ -46,8 +51,9 @@ export class SessionFixture extends PageFixture {
//#endregion
constructor(page: Page, testName: string) {
constructor({ page, testName, navigator }: SessionFixtureInit) {
super({ page, testName });
this.navigator = navigator;
}
//#region Specific interactions
@@ -89,13 +95,7 @@ export class SessionFixture extends PageFixture {
await this.$submitButton.click();
const expectedPathname = typeof to === "string" ? to : to.pathname;
this.logger.info(`Waiting for URL to change to ${expectedPathname}`);
await this.page.waitForURL(`**${expectedPathname}**`);
this.logger.info(`URL changed to ${this.page.url()}`);
await this.navigator.waitForPathname(to);
}
//#endregion

View File

@@ -3,6 +3,7 @@
*/
import { FormFixture } from "#e2e/fixtures/FormFixture";
import { NavigatorFixture } from "#e2e/fixtures/NavigatorFixture";
import { PointerFixture } from "#e2e/fixtures/PointerFixture";
import { SessionFixture } from "#e2e/fixtures/SessionFixture";
@@ -13,6 +14,7 @@ export { expect } from "@playwright/test";
/* eslint-disable react-hooks/rules-of-hooks */
interface E2EFixturesTestScope {
navigator: NavigatorFixture;
session: SessionFixture;
pointer: PointerFixture;
form: FormFixture;
@@ -23,15 +25,19 @@ interface E2EWorkerScope {
}
export const test = base.extend<E2EFixturesTestScope, E2EWorkerScope>({
session: async ({ page }, use, { title }) => {
await use(new SessionFixture(page, title));
navigator: async ({ page }, use, { title }) => {
await use(new NavigatorFixture(page, title));
},
session: async ({ page, navigator }, use, { title: testName }) => {
await use(new SessionFixture({ page, testName, navigator }));
},
form: async ({ page }, use, { title }) => {
await use(new FormFixture(page, title));
},
pointer: async ({ page }, use, { title }) => {
await use(new PointerFixture({ page, testName: title }));
pointer: async ({ page }, use, { title: testName }) => {
await use(new PointerFixture({ page, testName }));
},
});