Files
authentik/web/test/browser/101-session-lifecycle.test.ts
Teffen Ellis 1c82199852 web/table: fetch on first render when already visible (#22376)
* 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>
2026-05-18 11:10:17 +00:00

135 lines
5.1 KiB
TypeScript

import { expect, test } from "#e2e";
import { FormFixture } from "#e2e/fixtures/FormFixture";
import { NavigatorFixture } from "#e2e/fixtures/NavigatorFixture";
import { GOOD_USERNAME, SessionFixture } from "#e2e/fixtures/SessionFixture";
import type { Page } from "@playwright/test";
const REMEMBER_ME_USER_KEY = "authentik-remember-me-user";
const REMEMBER_ME_SESSION_KEY = "authentik-remember-me-session";
const IDENTIFICATION_STAGE_NAME = "default-authentication-identification";
const readStoredUserIdentifier = (page: Page) =>
page.evaluate((k) => localStorage.getItem(k), REMEMBER_ME_USER_KEY);
test.describe("Session Lifecycle", () => {
test.beforeAll(
'Ensure "Enable Remember me on this device" is on for the default identification stage',
async ({ browser }, { title: testName }) => {
const context = await browser.newContext();
const page = await context.newPage();
const navigator = new NavigatorFixture(page, testName);
const form = new FormFixture(page, testName);
const session = new SessionFixture({ page, testName, navigator });
await test.step("Authenticate", async () =>
session.login({
to: "/if/admin/#/flow/stages",
page,
}));
const $stage = await test.step("Find stage via search", () =>
form.search(IDENTIFICATION_STAGE_NAME, page));
await $stage.getByRole("button", { name: "Edit Stage" }).click();
const dialog = page.getByRole("dialog", { name: "Edit Identification Stage" });
await expect(dialog, "Edit modal opens after clicking edit").toBeVisible();
await form.setInputCheck(`Enable "Remember me on this device"`, true, dialog);
await dialog.getByRole("button", { name: "Save Changes" }).click();
await expect(dialog, "Edit modal closes after save").toBeHidden();
await context.close();
},
);
test.beforeEach(async ({ session, page }) => {
await session.toLoginPage();
await page.evaluate(
([userKey, sessionKey]) => {
localStorage.removeItem(userKey);
localStorage.removeItem(sessionKey);
},
[REMEMBER_ME_USER_KEY, REMEMBER_ME_SESSION_KEY],
);
await page.reload();
await session.$identificationStage.waitFor({ state: "visible" });
});
test("Remember me persists username", async ({ navigator, session, page }) => {
await test.step("Verify identification stage", async () => {
await expect(
session.$rememberMeCheckbox,
"Remember me checkbox is visible",
).toBeVisible();
await expect(
session.$rememberMeCheckbox,
"Remember me checkbox is not checked by default",
).not.toBeChecked();
});
await test.step("Identify with remember-me enabled", async () => {
await session.login(
{
rememberMe: true,
to: "if/user/#/library",
},
page,
);
const storedUserIdentifier = await readStoredUserIdentifier(page);
expect(
storedUserIdentifier,
"username persists to localStorage when remember-me is checked",
).toBe(GOOD_USERNAME);
});
await test.step("Sign out and verify username is remembered", async () => {
const signOutLink = page.getByRole("link", { name: "Sign out" });
await expect(signOutLink, "Sign out link is visible").toBeVisible();
await signOutLink.click();
await navigator.waitForPathname("/if/flow/default-authentication-flow/?next=%2F");
await session.$identificationStage.waitFor({ state: "visible" });
const passwordEmbedded = await session.$passwordField.isVisible();
if (passwordEmbedded) {
// Password is embedded in the identification stage, so the Not-you UI never renders.
// Remember-me's only observable effect is the pre-filled username field.
await expect(
session.$usernameField,
"Username pre-filled from remember-me",
).toHaveValue(GOOD_USERNAME);
return;
}
await session.$submitButton.click();
await session.$passwordStage.waitFor({ state: "visible" });
const notYouLink = page.getByRole("link", { name: "Not you?" });
await expect(notYouLink, "Not you? link is visible after sign out").toBeVisible();
await notYouLink.click();
await expect(
session.$identificationStage,
"Identification stage is visible after clicking not you link",
).toBeVisible();
const storedUserIdentifier = await readStoredUserIdentifier(page);
expect(storedUserIdentifier, "Removed after clicking not you link").toBeNull();
});
});
});