Files
authentik/web/e2e/fixtures/FormFixture.ts
Teffen Ellis b88d082947 web/a11y: Modals, Command Palette (Merge branch) (#17812)
* Use project relative paths.

* Fix tests.

* Fix types.

* Clean up admin imports.

* Move admin import.

* Remove or replace references to admin.

* Typo fix.

* Flesh out ak-modal, about modal.

* Flesh out lazy modal.

* Fix portal elements not using dialog scope.

* Fix url parameters, wizards.

* Fix invokers, lazy load.

* Fix theming.

* Add placeholders, help.

* Flesh out command palette.

Flesh out styles, command invokers.

Continue clean up.

Allow slotted content.

Flesh out.

* Flesh out edit invoker. Prep groups.

* Fix odd labeling, legacy situations.

* Prepare deprecation of table modal. Clean up serialization.

* Tidy types.

* Port provider select modal.

* Port member select form.

* Flesh out role modal. Fix loading state.

* Port user group form.

* Fix spellcheck.

* Fix dialog detection.

* Revise types.

* Port rac launch modal.

* Remove deprecated table modal.

* Consistent form action placement.

* Consistent casing.

* Consistent alignment.

* Use more appropriate description.

* Flesh out icon. Fix alignment, colors.

* Flesh out user search.

* Consistent save button.

* Clean up labels.

* Reduce warning noise.

* Clean up label.

* Use attribute e2e expects.

* Use directive. Fix lifecycle

* Fix frequent un-memoized entries.

* Fix up closedBy detection.

* Tidy alignment.

* Fix types, composition.

* Fix labels, tests.

* Fix up impersonation, labels.

* Flesh out. Fix refresh after submit.

* Flesh out basic modal test.

* Fix ARIA.

* Flesh out roles test.

* Revise selectors.

* Clean up selectors.

* Fix impersonation labels, form references.

* Fix messages appearing under modals.

* Ensure reason is parsed.

* Flesh out impersonation test.

* Flesh out impersonate test.

* Flesh out application tests. Clean up toolbar header, ARIA.

* Flesh out wizard test.

* Refine weight, order.

* Fix up initial values, selectors.

* Fix tests.

* Fix selector.
2026-03-25 06:07:29 +00:00

251 lines
7.0 KiB
TypeScript

import { PageFixture } from "#e2e/fixtures/PageFixture";
import type { LocatorContext } from "#e2e/selectors/types";
import { expect, Locator, Page } from "@playwright/test";
export class FormFixture extends PageFixture {
static fixtureName = "Form";
//#region Selector Methods
//#endregion
//#region Field Methods
/**
* Set the value of a text input.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public findTextualInput = async (
fieldName: string | RegExp,
context: LocatorContext = this.page,
) => {
const control = context
.getByLabel(fieldName, { exact: true })
.filter({
hasNot: context.getByRole("presentation"),
})
.and(context.locator(":not(button)"))
.or(
context.getByRole("textbox", {
name: fieldName,
}),
)
.or(
context.getByRole("spinbutton", {
name: fieldName,
}),
);
const role = await control.getAttribute("role");
if (role === "combobox") {
// Comboboxes, such as our Query Language input need additional handling...
const textbox = control.getByRole("textbox");
return textbox;
}
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
return control;
};
/**
* Set the value of a text input.
*
* @param target The name of the form element.
* @param value the value to set.
*/
public fill = async (
target: string | RegExp | Locator,
value: string,
context: LocatorContext = this.page,
): Promise<void> => {
let control: Locator;
if (typeof target === "string" || target instanceof RegExp) {
control = await this.findTextualInput(target, context);
} else {
control = target;
}
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.
*
* @param fieldName The name of the form element.
* @param value the value to set.
*/
public setInputCheck = async (
fieldName: string,
value: boolean = true,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.locator("ak-switch-input", {
hasText: fieldName,
});
await control.scrollIntoViewIfNeeded();
await expect(control, `Field (${fieldName}) should be visible`).toBeVisible();
const currentChecked = await control
.getAttribute("checked")
.then((value) => value !== null);
if (currentChecked === value) {
return;
}
await control.click();
};
/**
* Set the value of a radio or checkbox input.
*
* @param fieldName The name of the form element.
* @param pattern the value to set.
*/
public setRadio = async (
groupName: string,
fieldName: string,
parent: LocatorContext = this.page,
): Promise<void> => {
const group = parent.getByRole("radiogroup", { name: groupName });
await expect(group, `Field "${groupName}" should be visible`).toBeVisible();
const control = parent.getByRole("radio", { name: fieldName });
await control.setChecked(true);
};
/**
* Set the value of a search select input.
*
* @param fieldLabel The name of the search select element.
* @param pattern The text to match against the search select entry.
*/
public selectSearchValue = async (
fieldLabel: string,
pattern: string | RegExp,
parent: LocatorContext = this.page,
): Promise<void> => {
const control = parent.getByRole("textbox", { name: fieldLabel });
await expect(
control,
`Search select control (${fieldLabel}) should be visible`,
).toBeVisible();
const fieldName = await control.getAttribute("name");
if (!fieldName) {
throw new Error(`Unable to find name attribute on search select (${fieldLabel})`);
}
// Find the search select input control and activate it.
await control.click();
if (typeof pattern === "string") {
this.fill(control, pattern, parent);
}
const button = this.page
// ---
.locator(`div[data-managed-for*="${fieldName}"] button`, {
hasText: pattern,
});
await expect(button, `Search select entry (${pattern}) should be visible`).toBeVisible();
await button.click();
await this.page.keyboard.press("Tab");
await control.blur();
};
public setFormGroup = async (
pattern: string | RegExp,
value: boolean = true,
parent: LocatorContext = this.page,
) => {
const control = parent
.locator("ak-form-group", {
hasText: pattern,
})
.first();
const currentOpen = await control.getAttribute("open").then((value) => value !== null);
if (currentOpen === value) {
this.logger.debug(`Form group ${pattern} is already ${value ? "open" : "closed"}`);
return;
}
this.logger.debug(`Toggling form group ${pattern} to ${value ? "open" : "closed"}`);
await control.click();
if (value) {
await expect(control).toHaveAttribute("open");
} else {
await expect(control).not.toHaveAttribute("open");
}
};
//#endregion
//#region Lifecycle
constructor(page: Page, testName: string) {
super({ page, testName });
}
//#endregion
}