Files
authentik/web/tests/pageobjects/controls.ts
Teffen Ellis 1bc2daae98 web: Form validation regressions, consistency fixes (#15894)
* web: Clean up naming of inputs.

* web: Flesh out label components, slots.

* web: Expand clickable area of labels.

* web: Fix issue where launch URL is required.

* web: Surface string errors verbatim.

* web: Fix issues surrounding client-side error reporting, form validation,
server-side error reporting.

* web: Clarify property visibility.

* web: Fix issue where provider errors are not surfaced.

* web: Fix issue where wizard steps do not consistently use form validation.

* web: Fix issue where render root is not preferred.

* web: Fix import path.

* web: Fix selectors.

* keep labels aligned

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: Fix field nesting.

* web: Fix issue where required secret text inputs fail validation when editing existing entities.

- We need to make the component have a better understanding of this concept.

* web: Fix slot alignment on legacy elements.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2025-08-12 14:34:08 +00:00

228 lines
7.5 KiB
TypeScript

import { browser } from "@wdio/globals";
import { Key } from "webdriverio";
export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) {
const element = await el;
browser.execute((element) => element.blur(), element);
}
export function tap<A>(a: A) {
console.log("TAP:", a);
return a;
}
const makeComparator = (value: string | RegExp) =>
typeof value === "string"
? (sample: string) => sample === value
: (sample: string) => value.test(sample);
export async function checkIsPresent(name: string) {
await expect(await $(name)).toBeDisplayed();
}
export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
const context = ctx ?? browser;
const button = await (async () => {
for await (const button of context.$$("button")) {
if ((await button.isDisplayed()) && (await button.getText()).indexOf(name) !== -1) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(`Unable to find button '${name}'`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function clickToggleGroup(name: string, value: string | RegExp) {
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`>>>[data-ouid-component-name=${name}]`).$$(
">>>.pf-c-toggle-group__button",
)) {
if (comparator(await button.$(">>>.pf-c-toggle-group__text").getText())) {
return button;
}
}
})();
if (!(button && (await button?.isDisplayed()))) {
throw new Error(`Unable to locate toggle button ${name}:${value.toString()}`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function setFormGroup(name: string | RegExp, setting: "open" | "closed") {
const comparator = makeComparator(name);
const formGroup = await (async () => {
for await (const group of browser.$$(">>>ak-form-group")) {
// Delightfully, wizards may have slotted elements that *exist* but are not *attached*,
// and this can break the damn tests.
if (!(await group.isDisplayed())) {
continue;
}
if (
comparator(
await group.$(">>>div.pf-c-form__field-group-header-title-text").getText(),
)
) {
return group;
}
}
})();
if (!(formGroup && (await formGroup.isDisplayed()))) {
throw new Error(`Unable to find ak-form-group[name="${name}"]`);
}
await formGroup.scrollIntoView();
const open = await formGroup.getAttribute("open").then((value) => value !== null);
if ((setting === "open" && !open) || (setting === "closed" && open)) {
await formGroup.$(">>>summary").click();
}
await doBlur(formGroup);
}
export async function setRadio(name: string, value: string | RegExp) {
const control = await $(`>>>ak-radio[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const item = await (async () => {
for await (const item of control.$$(">>>div.pf-c-radio")) {
if (comparator(await item.$(">>>.pf-c-radio__label").getText())) {
return item;
}
}
})();
if (!(item && (await item.isDisplayed()))) {
throw new Error(`Unable to find a radio that matches ${name}:${value.toString()}`);
}
await item.scrollIntoView();
await item.click();
await doBlur(control);
}
export async function setSearchSelect(name: string, value: string | RegExp) {
const control = await (async () => {
try {
const control = await $(`>>>ak-search-select[name="${name}"]`);
await control.waitForExist({ timeout: 500 });
return control;
} catch (_e: unknown) {
const control = await $(`>>>ak-search-selects-ez[name="${name}"]`);
return control;
}
})();
if (!(control && (await control.isExisting()))) {
throw new Error(`Unable to find an ak-search-select variant matching ${name}}`);
}
// Find the search select input control and activate it.
const view = await control.$(">>>ak-search-select-view");
const input = await view.$('>>>input[type="text"]');
await input.scrollIntoView();
await input.click();
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`>>>div[data-managed-for*="${name}"]`)
.$(">>>ak-list-select")
.$$("button")) {
if (comparator(await button.getText())) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(
`Unable to find an ak-search-select entry matching ${name}:${value.toString()}`,
);
}
await (await button).click();
await browser.keys(Key.Tab);
await doBlur(control);
}
export async function setTextInput(name: string, value: string) {
const control = await $(`>>>input[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setTextareaInput(name: string, value: string) {
const control = await $(`>>>textarea[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setToggle(name: string, set: boolean) {
const toggle = await $(`>>>input[name="${name}"]`);
await toggle.scrollIntoView();
await expect(await toggle.getAttribute("type")).toBe("checkbox");
const state = await toggle.isSelected();
if (set !== state) {
const control = await (await toggle.parentElement()).$(">>>.pf-c-switch__toggle");
await control.click();
await doBlur(control);
}
}
export async function setTypeCreate(name: string, value: string | RegExp) {
const control = await $(`>>>ak-wizard-page-type-create[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const card = await (async () => {
for await (const card of $(">>>ak-wizard-page-type-create").$$(
'>>>[data-ouid-component-type="ak-type-create-grid-card"]',
)) {
if (comparator(await card.$(">>>.pf-c-card__title").getText())) {
return card;
}
}
})();
if (!(card && (await card.isDisplayed()))) {
throw new Error(`Unable to locate radio card ${name}:${value.toString()}`);
}
await card.scrollIntoView();
await card.click();
await doBlur(control);
}
export type TestInteraction =
| [typeof checkIsPresent, ...Parameters<typeof checkIsPresent>]
| [typeof clickButton, ...Parameters<typeof clickButton>]
| [typeof clickToggleGroup, ...Parameters<typeof clickToggleGroup>]
| [typeof setFormGroup, ...Parameters<typeof setFormGroup>]
| [typeof setRadio, ...Parameters<typeof setRadio>]
| [typeof setSearchSelect, ...Parameters<typeof setSearchSelect>]
| [typeof setTextInput, ...Parameters<typeof setTextInput>]
| [typeof setTextareaInput, ...Parameters<typeof setTextareaInput>]
| [typeof setToggle, ...Parameters<typeof setToggle>]
| [typeof setTypeCreate, ...Parameters<typeof setTypeCreate>];
export type TestSequence = TestInteraction[];
export type TestProvider = () => TestSequence;