import { expect, Locator, Page, Request } from "@playwright/test"; import { RUNTIME_CONFIG_SCRIPT_ID, type RuntimePublicConfig, } from "@/lib/runtime-config.shared"; import { BasePage } from "../base-page"; export { RUNTIME_CONFIG_SCRIPT_ID }; /** Keys the runtime data island is expected to expose (the allowlist). */ export const RUNTIME_CONFIG_KEYS = [ "sentryDsn", "sentryEnvironment", "googleTagManagerId", "apiBaseUrl", "apiDocsUrl", "posthogKey", "posthogHost", "reoDevClientId", "cloudBillingEnabled", ] as const satisfies ReadonlyArray; /** * Page object for the runtime public-config data island. The island is rendered * into `` by both root layouts, so any unauthenticated route works; the * sign-in page is used because it needs no session. */ export class RuntimeConfigPage extends BasePage { readonly island: Locator; constructor(page: Page) { super(page); this.island = page.locator(`script#${RUNTIME_CONFIG_SCRIPT_ID}`); } async goto(): Promise { await super.goto("/sign-in"); } /** Parsed island JSON, or null when the island is missing/malformed. */ async readConfig(): Promise | null> { return this.page.evaluate((id) => { const el = document.getElementById(id); if (!el?.textContent) return null; try { return JSON.parse(el.textContent) as Record; } catch { return null; } }, RUNTIME_CONFIG_SCRIPT_ID); } /** DSN the browser Sentry client initialized with, or null if uninitialized. */ async sentryInitializedDsn(): Promise { return this.page.evaluate(() => { const sentry = (window as unknown as { __SENTRY__?: unknown }).__SENTRY__; if (!sentry || typeof sentry !== "object") return null; const hub = sentry as { getClient?: () => unknown; hub?: { getClient?: () => unknown }; }; const client = (hub.getClient?.() ?? hub.hub?.getClient?.()) as | { getOptions?: () => { dsn?: string } } | undefined; return client?.getOptions?.().dsn ?? null; }); } async verifyIslandInHead(): Promise { // type="application/json" ⇒ inert, not governed by CSP script-src. await expect(this.island).toHaveAttribute("type", "application/json"); const parentTag = await this.island.evaluate( (el) => el.parentElement?.tagName.toLowerCase() ?? "", ); expect(parentTag).toBe("head"); } /** * The island must precede the first ordered (non-`async`) bundle * `