import { expect, type Page } from "@playwright/test"; import { REAL_UA } from "./env"; // Authentik's default login flow is two stages (identification -> password), // both rendered inside open shadow roots. Playwright's CSS engine pierces // open shadow roots, so plain input[name=...] locators work. Each login runs // in a FRESH browser context: reusing a half-finished flow poisons authentik's // pending_user stage and the second login hangs (see the // authentik-flow-ui-automation skill). Anubis sits in front and solves its // proof-of-work automatically in a real Chromium, which is why the // uidField wait can legitimately take a while on a cold challenge. export async function akLogin(page: Page, baseUrl: string, user: string, pass: string): Promise { await page.goto(baseUrl + "/"); const uid = page.locator('input[name="uidField"]'); await uid.waitFor({ state: "visible", timeout: 90_000 }); await uid.fill(user); await page.locator('button[type="submit"]').first().click(); const pwd = page.locator('input[name="password"]'); await pwd.waitFor({ state: "visible", timeout: 30_000 }); // authentik's flow executor (Lit) can re-render the stage right after it // mounts, wiping a programmatic fill; clicking the submit button then // POSTs an empty form ("This field is required"). Fill, VERIFY the value // survived, and submit via Enter (native form path), retrying if the // stage reset under us. for (let attempt = 0; attempt < 3; attempt++) { await pwd.fill(pass); await expect(pwd).toHaveValue(pass, { timeout: 5_000 }); await pwd.press("Enter"); try { await page.waitForURL(/:\/\/www\.franksdank\.cloud\//, { timeout: 20_000 }); break; } catch { if (attempt === 2) throw new Error("password stage never advanced after 3 attempts"); } } // Back on the homepage with panels rendered. await expect(page.locator("#grid .panel").first()).toBeVisible({ timeout: 30_000 }); } export function launchOptions() { return { userAgent: REAL_UA, ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 }, }; }