mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
1c82199852
* web/table: fetch on first render when already visible Tables inside `<ak-modal>` rendered empty until the user clicked the refresh button. The 2026.5 RC native-`<dialog>` migration taught `AKModal.updated()` to force `visible = true` on its slotted child, but `Table.firstUpdated()` was delegating to `#synchronizeRefreshSchedule()`, which only flushes a *previously deferred* refresh. With visibility forced on before the first update cycle, no deferred refresh was ever queued, so the synchronizer no-op'd and nothing fetched. Switch the first-update hook to call `fetch()` directly. `fetch()` already handles both states correctly: if the table is visible it issues the request immediately, and if it isn't it queues the deferred refresh that the synchronizer flushes when visibility flips on. Beyond the modal case this also covers any future caller that mounts a Table already-visible. Reproduced and verified against the user-library RAC endpoint launcher (the surface from the beta report). Added a Playwright e2e (`rac-launch-modal.test.ts`) that seeds a RAC provider + two endpoints via the API, opens the launcher, and asserts the endpoint rows appear without a manual refresh — fails on `main`, passes with this change. A 2026.5 backport will follow as a separate PR. Co-Authored-By: Agent (authentik-m-triage-rac-proper-shared-lilac) <279763771+playpen-agent@users.noreply.github.com> * web/test: silence cspell on AK_TEST_BOOTSTRAP_TOKEN fallback `changeme` in the playpen-specific default for `AK_TEST_BOOTSTRAP_TOKEN` trips the spellcheck lint job. Add an inline `cspell:ignore` directive so the fallback can stay (CI sets the env var so the default is only used locally inside playpen sandboxes). * Flesh out RAC test coverage. * Use simple search for applications list. * Add order. * Ignore playwright result. * Remove unused. * Tidy for test. * Fix test selectors. * Fix overlap. * Defer to connected callback. * Use consistent Patternfly input outline. * Clean up labels. * Only trigger navigation on non-current entries. * Ensure that selected type is retained. --------- Co-authored-by: Agent (authentik-m-triage-rac-proper-shared-lilac) <279763771+playpen-agent@users.noreply.github.com>
237 lines
6.8 KiB
TypeScript
237 lines
6.8 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 textbox = context.getByRole("textbox", { name: fieldName });
|
|
const searchbox = context.getByRole("searchbox", { name: fieldName });
|
|
const spinbutton = context.getByRole("spinbutton", { name: fieldName });
|
|
// Comboboxes (e.g. the Query Language input) wrap an inner textbox.
|
|
const comboboxTextbox = context
|
|
.getByRole("combobox", { name: fieldName })
|
|
.getByRole("textbox");
|
|
|
|
const control = textbox.or(searchbox).or(spinbutton).or(comboboxTextbox);
|
|
|
|
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.
|
|
*
|
|
* @returns A locator for the row entry matching the query.
|
|
*/
|
|
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
|
|
}
|