@zitadel/testing 0.0.0 → 0.1.0-alpha.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"playwright.cjs","names":["HANDSHAKE_ENV","SUPERVISOR_CONFIG_ENV","APP_RUNNER_CONFIG_ENV","base","connectZitadel","readHandshakeSync"],"sources":["../src/playwright-config.ts","../src/playwright.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { extname, isAbsolute, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { PlaywrightTestConfig } from \"@playwright/test\";\n\nimport type { AppEnvTemplate } from \"./app-env\";\nimport {\n APP_RUNNER_CONFIG_ENV,\n HANDSHAKE_ENV,\n SUPERVISOR_CONFIG_ENV,\n type AppRunnerConfig,\n type SupervisorConfig,\n} from \"./orchestration\";\n\ntype WebServerEntry = Extract<\n NonNullable<PlaywrightTestConfig[\"webServer\"]>,\n readonly unknown[]\n>[number];\n\nexport interface WithZitadelOptions {\n /**\n * The Playwright config's directory (`import.meta.dirname`). Anchors the\n * default handshake location and the working directory of the generated\n * webServer entries.\n */\n configDir: string;\n /**\n * Fixed TCP port for the instance. Required (unlike `startLocalZitadel`,\n * which defaults to a free port) because Playwright's readiness URL must be\n * known while the config is evaluated, before anything boots.\n */\n port: number;\n /**\n * Origin the browser will use for the app under test. Registered as the\n * project's preview origin (the backend's origin check rejects forwarded\n * requests from unregistered origins) and the base of `app.readyPath`.\n */\n appOrigin: string;\n /** Boot/bootstrap options forwarded to the instance supervisor. */\n zitadel?: {\n /** Absolute path to the server binary. */\n serverBinary?: string;\n /** Appended to the missing-binary error, e.g. \"run `moon run server:build` first.\" */\n serverBinaryHint?: string;\n projectName?: string;\n preset?: SupervisorConfig[\"preset\"];\n useCase?: SupervisorConfig[\"useCase\"];\n /** Absolute state directory; defaults to a fresh temp dir removed on stop. */\n dir?: string;\n /** Keep the owned temp dir after stop (debugging). */\n keep?: boolean;\n /** webServer readiness timeout for the boot; cold boot dominates it. */\n bootTimeoutMs?: number;\n };\n /** The app dev server to run against the instance. */\n app: {\n /** Spawn argv (no shell), e.g. [\"corepack\", \"pnpm\", \"--filter\", \"my-app\", \"dev\"]. */\n command: string[];\n /** Working directory for the app command. */\n cwd: string;\n /** Path on `appOrigin` Playwright polls for readiness, e.g. \"/login\". */\n readyPath: string;\n /**\n * Env vars the app needs, as a template mapping env names to\n * InstanceHandle fields — see `nextAppEnv` for the `@zitadel/sdk-next`\n * shape. A template (not a callback) because it crosses into the app\n * runner process.\n */\n env: AppEnvTemplate;\n readyTimeoutMs?: number;\n /** How long SIGTERM gets before the app is killed on teardown. */\n gracefulShutdownMs?: number;\n };\n /** Absolute path; defaults to `<configDir>/.zitadel-testing/handshake.json`. */\n handshakePath?: string;\n}\n\n/**\n * Generate the Playwright `webServer` entries that boot an ephemeral seeded\n * Zitadel and run the app against it, replacing the per-suite wrapper\n * scripts. Spread the result into `defineConfig`:\n *\n * ```ts\n * export default defineConfig({\n * ...withZitadel({ configDir: import.meta.dirname, port: 8092, ... }),\n * testDir: \"./src-real\",\n * });\n * ```\n *\n * Also points ZITADEL_TESTING_HANDSHAKE at the handshake file so the\n * `@zitadel/testing/playwright` fixtures resolve the instance — Playwright\n * workers re-evaluate the config, which re-applies this for every process\n * that needs it. The returned value is plain data; append your own entries\n * to `webServer` if the suite needs additional servers.\n */\nexport function withZitadel(\n options: WithZitadelOptions,\n /** Test seam: alternative executable resolution. */\n resolveEntry: (name: \"supervisor\" | \"app-runner\") => string = entryPoint,\n): { webServer: WebServerEntry[] } {\n const { configDir, port, appOrigin, app } = options;\n if (!isAbsolute(configDir)) {\n throw new Error(`withZitadel: configDir must be absolute, got \"${configDir}\"`);\n }\n if (!Number.isInteger(port) || port <= 0) {\n throw new Error(`withZitadel: port must be a positive integer, got ${port}`);\n }\n const origin = URL.canParse(appOrigin) ? new URL(appOrigin) : undefined;\n if (\n !origin ||\n (origin.protocol !== \"http:\" && origin.protocol !== \"https:\") ||\n origin.pathname !== \"/\" ||\n origin.search !== \"\" ||\n origin.hash !== \"\"\n ) {\n throw new Error(\n `withZitadel: appOrigin must be an origin like \"http://localhost:3002\", got \"${appOrigin}\"`,\n );\n }\n if (!app.readyPath.startsWith(\"/\")) {\n throw new Error(`withZitadel: app.readyPath must start with \"/\", got \"${app.readyPath}\"`);\n }\n if (app.command.length === 0) {\n throw new Error(\"withZitadel: app.command must not be empty\");\n }\n if (!isAbsolute(app.cwd)) {\n throw new Error(`withZitadel: app.cwd must be absolute, got \"${app.cwd}\"`);\n }\n // Path options are consumed by the executables, whose cwd is configDir —\n // a relative path would silently resolve against that, not the project.\n for (const [label, value] of [\n [\"zitadel.serverBinary\", options.zitadel?.serverBinary],\n [\"zitadel.dir\", options.zitadel?.dir],\n [\"handshakePath\", options.handshakePath],\n ] as const) {\n if (value !== undefined && !isAbsolute(value)) {\n throw new Error(`withZitadel: ${label} must be an absolute path, got \"${value}\"`);\n }\n }\n\n const handshakePath =\n options.handshakePath ?? join(configDir, \".zitadel-testing\", \"handshake.json\");\n // Workers inherit the runner's env; the fixtures resolve the instance from it.\n process.env[HANDSHAKE_ENV] = handshakePath;\n\n const supervisorConfig: SupervisorConfig = {\n port,\n appOrigins: [appOrigin],\n serverBinary: options.zitadel?.serverBinary,\n serverBinaryHint: options.zitadel?.serverBinaryHint,\n dir: options.zitadel?.dir,\n keep: options.zitadel?.keep,\n projectName: options.zitadel?.projectName,\n preset: options.zitadel?.preset,\n useCase: options.zitadel?.useCase,\n };\n const appRunnerConfig: AppRunnerConfig = {\n command: app.command,\n cwd: app.cwd,\n env: app.env,\n handshakeTimeoutMs: app.readyTimeoutMs ?? 180_000,\n };\n\n return {\n webServer: [\n {\n command: `node ${JSON.stringify(resolveEntry(\"supervisor\"))}`,\n url: `http://localhost:${port}/healthz`,\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n // Cold boot (fresh data dir: migrations + health wait) dominates.\n timeout: options.zitadel?.bootTimeoutMs ?? 120_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [SUPERVISOR_CONFIG_ENV]: JSON.stringify(supervisorConfig),\n },\n // SIGTERM first so the supervisor can stop the instance; the default\n // hard kill would orphan the server process group.\n gracefulShutdown: { signal: \"SIGTERM\", timeout: 30_000 },\n },\n {\n command: `node ${JSON.stringify(resolveEntry(\"app-runner\"))}`,\n url: new URL(app.readyPath, appOrigin).toString(),\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n timeout: app.readyTimeoutMs ?? 180_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [APP_RUNNER_CONFIG_ENV]: JSON.stringify(appRunnerConfig),\n },\n gracefulShutdown: {\n signal: \"SIGTERM\",\n timeout: app.gracefulShutdownMs ?? 15_000,\n },\n },\n ],\n };\n}\n\n/**\n * Resolve a sibling dist entry in the same module format this file was loaded\n * as (dist/supervisor.mjs next to dist/playwright.mjs, .cjs next to .cjs), so\n * the spawned process needs no package-manager bin plumbing.\n */\nfunction entryPoint(name: \"supervisor\" | \"app-runner\"): string {\n const self = fileURLToPath(import.meta.url);\n const ext = extname(self);\n if (ext !== \".mjs\" && ext !== \".cjs\") {\n throw new Error(\n `withZitadel: expected to run from the built package (got ${self}); ` +\n \"build @zitadel/testing first (in-repo: `moon run testing:build`).\",\n );\n }\n const path = fileURLToPath(new URL(`./${name}${ext}`, import.meta.url));\n if (!existsSync(path)) {\n throw new Error(\n `withZitadel: missing ${path}; rebuild @zitadel/testing (in-repo: \\`moon run testing:build\\`).`,\n );\n }\n return path;\n}\n","import { test as base, type Page } from \"@playwright/test\";\n\nimport { readHandshakeSync } from \"./handshake\";\nimport { connectZitadel } from \"./index\";\nimport type {\n ConnectedZitadel,\n Identity,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n\nexport interface AuthenticatedPage {\n /** A page in its own context, already carrying the session cookie. */\n page: Page;\n user: SeededUser;\n session: MintedSession;\n}\n\nexport interface ZitadelTestFixtures {\n /** Per-test seeding; each call mints unique data on the shared instance. */\n seed: {\n user(input?: SeedUserInput): Promise<SeededUser>;\n users(count: number, template?: SeedUsersTemplate): Promise<SeededUser[]>;\n /** Unused email+password for registration flows — creates nothing. */\n identity(): Identity;\n /** Seeded user + headless real-flow login; password flows only. */\n session(input?: SeedSessionInput): Promise<MintedSession>;\n };\n /**\n * Start the test authenticated: a fresh user, a real session minted through\n * the flow API, and the cookie injected into a dedicated browser context —\n * the default `page` stays signed out for login-flow tests. Requires\n * `use.baseURL` (every withZitadel consumer sets it).\n */\n authenticatedPage: AuthenticatedPage;\n}\n\nexport interface ZitadelWorkerFixtures {\n /** Connection to the suite's instance, resolved once per worker. */\n zitadel: ConnectedZitadel;\n}\n\nexport const test = base.extend<ZitadelTestFixtures, ZitadelWorkerFixtures>({\n zitadel: [\n // Playwright derives fixture dependencies from the destructuring pattern,\n // so the empty pattern is required here.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n const handshakePath = process.env.ZITADEL_TESTING_HANDSHAKE;\n if (!handshakePath) {\n throw new Error(\n \"ZITADEL_TESTING_HANDSHAKE is not set. Point it at the handshake file \" +\n \"written by the script that boots the instance (see @zitadel/testing docs).\",\n );\n }\n await use(connectZitadel(readHandshakeSync(handshakePath)));\n },\n { scope: \"worker\" },\n ],\n seed: async ({ zitadel, baseURL }, use) => {\n await use({\n user: (input) => zitadel.seedUser(input),\n users: (count, template) => zitadel.seedUsers(count, template),\n identity: () => zitadel.identity(),\n // The suite's baseURL is the app origin the project allowlists.\n session: (input) => zitadel.seedSession({ origin: baseURL, ...input }),\n });\n },\n authenticatedPage: async ({ browser, zitadel, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n \"authenticatedPage requires `use.baseURL` so the session cookie can be \" +\n \"scoped to the app under test.\",\n );\n }\n const session = await zitadel.seedSession({ origin: baseURL });\n const context = await browser.newContext({ baseURL });\n // `addCookies` takes either url or domain/path; url derives the rest.\n const { path: _path, ...cookie } = session.cookie;\n await context.addCookies([{ ...cookie, url: baseURL }]);\n const page = await context.newPage();\n await use({ page, user: session.user, session });\n await context.close();\n },\n});\n\nexport { expect } from \"@playwright/test\";\nexport { applyAppEnvTemplate, nextAppEnv } from \"./app-env\";\nexport type { AppEnvTemplate } from \"./app-env\";\nexport { withZitadel } from \"./playwright-config\";\nexport type { WithZitadelOptions } from \"./playwright-config\";\nexport type {\n ConnectedZitadel,\n Identity,\n InstanceHandle,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"playwright.cjs","names":["HANDSHAKE_ENV","SUPERVISOR_CONFIG_ENV","APP_RUNNER_CONFIG_ENV","base","connectZitadel","readHandshakeSync"],"sources":["../src/passkey.ts","../src/flows.ts","../src/playwright-config.ts","../src/playwright.ts"],"sourcesContent":["import type { CDPSession, Page } from \"@playwright/test\";\n\n/**\n * A virtual WebAuthn authenticator attached to one page. Ceremonies started\n * from that page (passkey registration, passkey login) complete automatically\n * — no OS authenticator dialog, no touch.\n */\nexport interface VirtualPasskey {\n /** CDP id of the virtual authenticator, for advanced raw-protocol use. */\n authenticatorId: string;\n /** Number of credentials the authenticator currently stores. */\n credentialCount(): Promise<number>;\n /** Remove the authenticator and detach the CDP session. */\n dispose(): Promise<void>;\n}\n\n/**\n * Attach a virtual passkey authenticator to the page via the Chrome DevTools\n * Protocol. The options mirror a platform authenticator with discoverable\n * credentials and automatic user presence — the profile the consumer journey\n * has run in CI since passkey coverage became mandatory there.\n *\n * Chromium only: the CDP WebAuthn domain does not exist in WebKit or Firefox.\n * The authenticator is bound to this page — drive registration and the later\n * login from the same page, or the credential is gone. Serve the app under\n * test on an origin WebAuthn accepts as a relying-party ID: HTTPS on a real\n * domain, or `http://localhost` for local runs — raw IP origins such as\n * `http://127.0.0.1` are invalid RP IDs.\n */\nexport async function enableVirtualPasskey(page: Page): Promise<VirtualPasskey> {\n let client: CDPSession;\n try {\n client = await page.context().newCDPSession(page);\n } catch (error) {\n throw new Error(\n \"enableVirtualPasskey: could not open a CDP session — passkey testing \" +\n \"needs Chromium's virtual authenticator, so run passkey specs in a \" +\n \"Chromium project.\",\n { cause: error },\n );\n }\n let authenticatorId: string;\n try {\n await client.send(\"WebAuthn.enable\");\n ({ authenticatorId } = await client.send(\"WebAuthn.addVirtualAuthenticator\", {\n options: {\n protocol: \"ctap2\",\n transport: \"internal\",\n hasResidentKey: true,\n hasUserVerification: true,\n isUserVerified: true,\n automaticPresenceSimulation: true,\n },\n }));\n } catch (error) {\n // Don't leave a half-initialized CDP session attached behind a throw.\n await client.detach().catch(() => undefined);\n throw error;\n }\n\n return {\n authenticatorId,\n async credentialCount() {\n const { credentials } = await client.send(\"WebAuthn.getCredentials\", {\n authenticatorId,\n });\n return credentials.length;\n },\n async dispose() {\n // Best-effort: the page (and with it the CDP target) may already be\n // gone when teardown runs after a failed test; never mask that failure.\n // Detach even when the removal fails, so the session never outlives it.\n try {\n await client.send(\"WebAuthn.removeVirtualAuthenticator\", { authenticatorId });\n } catch {\n // ignore\n } finally {\n await client.detach().catch(() => undefined);\n }\n },\n };\n}\n","import type { Locator, Page } from \"@playwright/test\";\n\n/**\n * Helpers that drive the `<zitadel-login>` widget through complete auth\n * ceremonies, built on the widget's documented automation hooks\n * (`zitadel-field-*` / `zitadel-input-*` on fields, `zitadel-action-*` on\n * actions — see packages/components/README.md). Field names use the\n * normalised hook token: the flow engine names credential fields\n * `x-auth-methods#password`, but the hook (and this API) says `password`.\n *\n * The ceremony helpers assume the default flow vocabulary — steps that\n * declare `submit` / `passkey` / `passkey_register` actions and `email` /\n * password fields, as `default-login.json` does. They branch only on\n * widget-observable state (which fields and actions the flow renders),\n * because customer flow configurations legitimately vary; they never\n * assert app state. Callers navigate to the widget first and assert their\n * own signed-in surface afterwards. Flows with renamed actions or custom\n * steps drive the widget directly via `flowAction` / `flowField`.\n */\n\nexport interface FlowActionOptions {\n /**\n * Accessible-name fallback for templates that do not emit the documented\n * `data-testid` hooks: adds role-based button/link candidates.\n */\n name?: RegExp;\n}\n\nexport interface FlowFieldOptions {\n /** Label fallback for templates that do not emit the documented hooks. */\n label?: RegExp;\n}\n\n/**\n * One optional registration field, filled only when the flow renders it.\n * The value's type picks the control: a boolean drives a checkbox\n * (`zl-checkbox`), a string first tries a select (`zl-select`) and falls\n * back to filling a text-like input.\n */\nexport interface ProfileEntry {\n /** Normalised field hook token, e.g. `givenName`. */\n field: string;\n value: string | boolean;\n /** Label fallback for text-like inputs on templates without hooks. */\n label?: RegExp;\n}\n\nexport interface LoginCredentials {\n email: string;\n password: string;\n}\n\nexport interface RegistrationDetails {\n email: string;\n /** Extra fields some flows require at registration, filled if rendered. */\n profile?: ProfileEntry[];\n}\n\nexport interface PasswordRegistrationDetails extends RegistrationDetails {\n password: string;\n}\n\n/**\n * A union locator resolves in DOM order across the whole page, so every\n * candidate built from a broad selector — accessible names, labels, the\n * generic `data-action` attribute — is scoped to the `<zitadel-login>`\n * host; otherwise a same-named control in the app's own chrome (header\n * nav, footer forms) could win the union. The host element exists no\n * matter what the tenant's template renders, so these fallbacks keep\n * working for custom templates that emit no automation hooks — the case\n * they exist for. The `zitadel-*` testid hooks and the `zl-button` atom\n * are namespaced and stay page-global.\n */\nfunction widgetRoot(page: Page): Locator {\n return page.locator(\"zitadel-login\");\n}\n\n/**\n * Escape a value for use inside a double-quoted CSS attribute selector,\n * per the CSSOM \"serialize a string\" rules: NUL becomes U+FFFD, control\n * characters become hex code-point escapes, quote and backslash are\n * backslash-escaped. C1 controls (U+0080–U+009F) are escaped too — CSSOM\n * itself leaves them literal, but the escaped form is equivalent and\n * survives stricter-than-spec selector parsers. Anything the flow schema\n * accepts as an action name yields a parseable selector.\n */\nfunction cssAttributeValue(value: string): string {\n let out = \"\";\n for (const ch of value) {\n const code = ch.codePointAt(0) ?? 0;\n if (code === 0) {\n out += \"�\";\n } else if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {\n out += `\\\\${code.toString(16)} `;\n } else if (ch === '\"' || ch === \"\\\\\") {\n out += `\\\\${ch}`;\n } else {\n out += ch;\n }\n }\n return out;\n}\n\n/**\n * Locator for a flow action control by its declared action name. Matches\n * every hook shape the default template emits — host testid, native\n * shadow button/link testids, and the raw `action`/`data-action`\n * attributes (the recover link carries only `data-action`).\n */\nexport function flowAction(page: Page, action: string, options: FlowActionOptions = {}): Locator {\n // Action names are free-form in the flow schema; escape them before\n // interpolating into attribute selectors so an exotic name cannot break\n // the whole union's parsing.\n const attributeSafe = cssAttributeValue(action);\n let candidates = page\n .getByTestId(`zitadel-action-${action}`)\n .or(page.getByTestId(`zitadel-action-${action}-button`))\n .or(page.getByTestId(`zitadel-action-${action}-link`))\n .or(page.locator(`zl-button[action=\"${attributeSafe}\"]`))\n .or(widgetRoot(page).locator(`[data-action=\"${attributeSafe}\"]`));\n if (options.name) {\n candidates = candidates\n .or(widgetRoot(page).getByRole(\"button\", { name: options.name }))\n .or(widgetRoot(page).getByRole(\"link\", { name: options.name }));\n }\n return candidates.first();\n}\n\n/**\n * Locator for a flow field's input by its normalised hook token\n * (`email`, `password`, a user-schema property name).\n */\nexport function flowField(page: Page, field: string, options: FlowFieldOptions = {}): Locator {\n let candidates = page\n .getByTestId(`zitadel-input-${field}`)\n .or(page.getByTestId(`zitadel-field-${field}`).locator(\"input\"));\n if (options.label) {\n candidates = candidates.or(widgetRoot(page).getByLabel(options.label));\n }\n return candidates.first();\n}\n\n/** Click a flow action (auto-waits like any locator click). */\nexport async function clickFlowAction(\n page: Page,\n action: string,\n options?: FlowActionOptions,\n): Promise<void> {\n await flowAction(page, action, options).click();\n}\n\n/** Fill a flow field (auto-waits like any locator fill). */\nexport async function fillFlowField(\n page: Page,\n field: string,\n value: string,\n options?: FlowFieldOptions,\n): Promise<void> {\n await flowField(page, field, options).fill(value);\n}\n\n/**\n * Sign in with email and password from the flow's entry step. Handles both\n * the default split shape (identifier → password) and flows that render\n * the password field on the entry step.\n */\nexport async function loginWithPassword(\n page: Page,\n { email, password }: LoginCredentials,\n): Promise<void> {\n await emailField(page).fill(email);\n const password_ = passwordField(page);\n if (!(await password_.isVisible().catch(() => false))) {\n await flowAction(page, \"submit\").click();\n }\n await password_.fill(password);\n await flowAction(page, \"submit\").click();\n}\n\n/**\n * Sign in with a passkey. With `email`, fills the identifier and takes the\n * step's passkey action; without, taps the entry step's passkey action\n * directly (discoverable-credential one-tap, e.g. the passkey-first\n * preset). Pair with `enableVirtualPasskey` / the `passkey` fixture in\n * headless runs — the ceremony completes automatically once the widget\n * issues the WebAuthn challenge.\n */\nexport async function loginWithPasskey(page: Page, options: { email?: string } = {}): Promise<void> {\n if (options.email !== undefined) {\n await emailField(page).fill(options.email);\n }\n await flowAction(page, \"passkey\").click();\n}\n\n/**\n * Register a new user with a password: enter the (unknown) identifier,\n * advance into the registration step, continue on the password path, and\n * submit the password. Ends when the final submit is clicked — assert your\n * app's signed-in surface afterwards. Flows that route through steps the\n * default flow does not (e.g. a passkey upsell) need caller-side handling\n * after this returns.\n */\nexport async function registerWithPassword(\n page: Page,\n { email, password, profile }: PasswordRegistrationDetails,\n): Promise<void> {\n await advanceToRegistration(page, email, profile);\n // Continue with password unless this flow renders the password field on\n // the registration step itself.\n const password_ = passwordField(page);\n if (!(await password_.isVisible().catch(() => false))) {\n await flowAction(page, \"submit\").click();\n }\n await password_.fill(password);\n await flowAction(page, \"submit\").click();\n}\n\n/**\n * Register a new user with a passkey: enter the (unknown) identifier,\n * advance into the registration step, and take its `passkey_register`\n * action. Requires an authenticator (see `loginWithPasskey`). The default\n * flow completes registration from the ceremony directly.\n */\nexport async function registerWithPasskey(\n page: Page,\n { email, profile }: RegistrationDetails,\n): Promise<void> {\n await advanceToRegistration(page, email, profile);\n await flowAction(page, \"passkey_register\").click();\n}\n\nfunction emailField(page: Page): Locator {\n return flowField(page, \"email\", { label: /email/i });\n}\n\nfunction passwordField(page: Page): Locator {\n return flowField(page, \"password\", { label: /password/i });\n}\n\n/**\n * From the entry step: submit the unknown identifier (the default flow's\n * `user_not_found` transition routes to registration), or take an explicit\n * `register` navigate action when the entry step is a combined\n * email+password step — there, submitting would attempt a password sign-in\n * instead. Then wait for the registration step and fill what it renders.\n */\nasync function advanceToRegistration(\n page: Page,\n email: string,\n profile: ProfileEntry[] | undefined,\n): Promise<void> {\n await emailField(page).fill(email);\n if (await passwordField(page).isVisible().catch(() => false)) {\n await flowAction(page, \"register\").click();\n } else {\n await flowAction(page, \"submit\").click();\n }\n await expectRegistrationStep(page);\n // The engine echoes the attempted identifier into the registration step's\n // email field; refill defensively for flows that render it empty.\n await fillIfVisible(emailField(page), email);\n for (const entry of profile ?? []) {\n await fillProfileEntry(page, entry);\n }\n}\n\n/**\n * Fill one registration field with the verb its control needs: booleans\n * check the `zl-checkbox` native input, strings prefer the `zl-select`\n * native select (option matched by value, falling back to label) and\n * otherwise fill a text-like input. The select/checkbox natives carry the\n * name-first testids the atoms document (`zitadel-select-*`,\n * `zitadel-checkbox-*`); templates without those hooks drive such fields\n * via their own locators.\n */\nasync function fillProfileEntry(page: Page, entry: ProfileEntry): Promise<void> {\n if (typeof entry.value === \"boolean\") {\n const checkbox = page.getByTestId(`zitadel-checkbox-${entry.field}`).first();\n if (await checkbox.isVisible().catch(() => false)) {\n await checkbox.setChecked(entry.value);\n }\n return;\n }\n const select = page.getByTestId(`zitadel-select-${entry.field}`).first();\n if (await select.isVisible().catch(() => false)) {\n await select.selectOption(entry.value);\n return;\n }\n await fillIfVisible(flowField(page, entry.field, { label: entry.label }), entry.value);\n}\n\n/**\n * Barrier between the entry submit and probing the registration step's\n * fields: without it, optional-field probes race the re-render and read\n * the outgoing step. The default flow's registration step declares\n * `passkey_register` — a structural, locale-independent signal; the\n * heading regex covers password-only flows on the default English\n * template.\n */\nasync function expectRegistrationStep(page: Page): Promise<void> {\n await flowAction(page, \"passkey_register\")\n .or(widgetRoot(page).getByRole(\"heading\", { name: /create|register|sign up|no-account/i }))\n .first()\n .waitFor({ state: \"visible\", timeout: 30_000 });\n}\n\nasync function fillIfVisible(field: Locator, value: string): Promise<void> {\n if (await field.isVisible().catch(() => false)) {\n await field.fill(value);\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { extname, isAbsolute, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport type { PlaywrightTestConfig } from \"@playwright/test\";\n\nimport type { AppEnvTemplate } from \"./app-env\";\nimport {\n APP_RUNNER_CONFIG_ENV,\n HANDSHAKE_ENV,\n SUPERVISOR_CONFIG_ENV,\n type AppRunnerConfig,\n type SupervisorConfig,\n} from \"./orchestration\";\n\ntype WebServerEntry = Extract<\n NonNullable<PlaywrightTestConfig[\"webServer\"]>,\n readonly unknown[]\n>[number];\n\nexport interface WithZitadelOptions {\n /**\n * The Playwright config's directory (`import.meta.dirname`). Anchors the\n * default handshake location and the working directory of the generated\n * webServer entries.\n */\n configDir: string;\n /**\n * Fixed TCP port for the instance. Required (unlike `startLocalZitadel`,\n * which defaults to a free port) because Playwright's readiness URL must be\n * known while the config is evaluated, before anything boots.\n */\n port: number;\n /**\n * Origin the browser will use for the app under test. Registered as the\n * project's preview origin (the backend's origin check rejects forwarded\n * requests from unregistered origins) and the base of `app.readyPath`.\n */\n appOrigin: string;\n /** Boot/bootstrap options forwarded to the instance supervisor. */\n zitadel?: {\n /** Absolute path to the server binary. */\n serverBinary?: string;\n /** Appended to the missing-binary error, e.g. \"run `moon run server:build` first.\" */\n serverBinaryHint?: string;\n projectName?: string;\n preset?: SupervisorConfig[\"preset\"];\n useCase?: SupervisorConfig[\"useCase\"];\n /** Absolute state directory; defaults to a fresh temp dir removed on stop. */\n dir?: string;\n /** Keep the owned temp dir after stop (debugging). */\n keep?: boolean;\n /** webServer readiness timeout for the boot; cold boot dominates it. */\n bootTimeoutMs?: number;\n };\n /** The app dev server to run against the instance. */\n app: {\n /** Spawn argv (no shell), e.g. [\"corepack\", \"pnpm\", \"--filter\", \"my-app\", \"dev\"]. */\n command: string[];\n /** Working directory for the app command. */\n cwd: string;\n /** Path on `appOrigin` Playwright polls for readiness, e.g. \"/login\". */\n readyPath: string;\n /**\n * Env vars the app needs, as a template mapping env names to\n * InstanceHandle fields — see `nextAppEnv` for the `@zitadel/sdk-next`\n * shape. A template (not a callback) because it crosses into the app\n * runner process.\n */\n env: AppEnvTemplate;\n readyTimeoutMs?: number;\n /** How long SIGTERM gets before the app is killed on teardown. */\n gracefulShutdownMs?: number;\n };\n /** Absolute path; defaults to `<configDir>/.zitadel-testing/handshake.json`. */\n handshakePath?: string;\n}\n\n/**\n * Generate the Playwright `webServer` entries that boot an ephemeral seeded\n * Zitadel and run the app against it, replacing the per-suite wrapper\n * scripts. Spread the result into `defineConfig`:\n *\n * ```ts\n * export default defineConfig({\n * ...withZitadel({ configDir: import.meta.dirname, port: 8092, ... }),\n * testDir: \"./src-real\",\n * });\n * ```\n *\n * Also points ZITADEL_TESTING_HANDSHAKE at the handshake file so the\n * `@zitadel/testing/playwright` fixtures resolve the instance — Playwright\n * workers re-evaluate the config, which re-applies this for every process\n * that needs it. The returned value is plain data; append your own entries\n * to `webServer` if the suite needs additional servers.\n */\nexport function withZitadel(\n options: WithZitadelOptions,\n /** Test seam: alternative executable resolution. */\n resolveEntry: (name: \"supervisor\" | \"app-runner\") => string = entryPoint,\n): { webServer: WebServerEntry[] } {\n const { configDir, port, appOrigin, app } = options;\n if (!isAbsolute(configDir)) {\n throw new Error(`withZitadel: configDir must be absolute, got \"${configDir}\"`);\n }\n if (!Number.isInteger(port) || port <= 0) {\n throw new Error(`withZitadel: port must be a positive integer, got ${port}`);\n }\n const origin = URL.canParse(appOrigin) ? new URL(appOrigin) : undefined;\n if (\n !origin ||\n (origin.protocol !== \"http:\" && origin.protocol !== \"https:\") ||\n origin.pathname !== \"/\" ||\n origin.search !== \"\" ||\n origin.hash !== \"\"\n ) {\n throw new Error(\n `withZitadel: appOrigin must be an origin like \"http://localhost:3002\", got \"${appOrigin}\"`,\n );\n }\n if (!app.readyPath.startsWith(\"/\")) {\n throw new Error(`withZitadel: app.readyPath must start with \"/\", got \"${app.readyPath}\"`);\n }\n if (app.command.length === 0) {\n throw new Error(\"withZitadel: app.command must not be empty\");\n }\n if (!isAbsolute(app.cwd)) {\n throw new Error(`withZitadel: app.cwd must be absolute, got \"${app.cwd}\"`);\n }\n // Path options are consumed by the executables, whose cwd is configDir —\n // a relative path would silently resolve against that, not the project.\n for (const [label, value] of [\n [\"zitadel.serverBinary\", options.zitadel?.serverBinary],\n [\"zitadel.dir\", options.zitadel?.dir],\n [\"handshakePath\", options.handshakePath],\n ] as const) {\n if (value !== undefined && !isAbsolute(value)) {\n throw new Error(`withZitadel: ${label} must be an absolute path, got \"${value}\"`);\n }\n }\n\n const handshakePath =\n options.handshakePath ?? join(configDir, \".zitadel-testing\", \"handshake.json\");\n // Workers inherit the runner's env; the fixtures resolve the instance from it.\n process.env[HANDSHAKE_ENV] = handshakePath;\n\n const supervisorConfig: SupervisorConfig = {\n port,\n appOrigins: [appOrigin],\n serverBinary: options.zitadel?.serverBinary,\n serverBinaryHint: options.zitadel?.serverBinaryHint,\n dir: options.zitadel?.dir,\n keep: options.zitadel?.keep,\n projectName: options.zitadel?.projectName,\n preset: options.zitadel?.preset,\n useCase: options.zitadel?.useCase,\n };\n const appRunnerConfig: AppRunnerConfig = {\n command: app.command,\n cwd: app.cwd,\n env: app.env,\n handshakeTimeoutMs: app.readyTimeoutMs ?? 180_000,\n };\n\n return {\n webServer: [\n {\n command: `node ${JSON.stringify(resolveEntry(\"supervisor\"))}`,\n url: `http://localhost:${port}/healthz`,\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n // Cold boot (fresh data dir: migrations + health wait) dominates.\n timeout: options.zitadel?.bootTimeoutMs ?? 120_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [SUPERVISOR_CONFIG_ENV]: JSON.stringify(supervisorConfig),\n },\n // SIGTERM first so the supervisor can stop the instance; the default\n // hard kill would orphan the server process group.\n gracefulShutdown: { signal: \"SIGTERM\", timeout: 30_000 },\n },\n {\n command: `node ${JSON.stringify(resolveEntry(\"app-runner\"))}`,\n url: new URL(app.readyPath, appOrigin).toString(),\n reuseExistingServer: false,\n cwd: configDir,\n stdout: \"pipe\",\n stderr: \"pipe\",\n timeout: app.readyTimeoutMs ?? 180_000,\n env: {\n [HANDSHAKE_ENV]: handshakePath,\n [APP_RUNNER_CONFIG_ENV]: JSON.stringify(appRunnerConfig),\n },\n gracefulShutdown: {\n signal: \"SIGTERM\",\n timeout: app.gracefulShutdownMs ?? 15_000,\n },\n },\n ],\n };\n}\n\n/**\n * Resolve a sibling dist entry in the same module format this file was loaded\n * as (dist/supervisor.mjs next to dist/playwright.mjs, .cjs next to .cjs), so\n * the spawned process needs no package-manager bin plumbing.\n */\nfunction entryPoint(name: \"supervisor\" | \"app-runner\"): string {\n const self = fileURLToPath(import.meta.url);\n const ext = extname(self);\n if (ext !== \".mjs\" && ext !== \".cjs\") {\n throw new Error(\n `withZitadel: expected to run from the built package (got ${self}); ` +\n \"build @zitadel/testing first (in-repo: `moon run testing:build`).\",\n );\n }\n const path = fileURLToPath(new URL(`./${name}${ext}`, import.meta.url));\n if (!existsSync(path)) {\n throw new Error(\n `withZitadel: missing ${path}; rebuild @zitadel/testing (in-repo: \\`moon run testing:build\\`).`,\n );\n }\n return path;\n}\n","import { test as base, type Page } from \"@playwright/test\";\n\nimport { readHandshakeSync } from \"./handshake\";\nimport { connectZitadel } from \"./index\";\nimport { enableVirtualPasskey, type VirtualPasskey } from \"./passkey\";\nimport type {\n ConnectedZitadel,\n Identity,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n\nexport interface AuthenticatedPage {\n /** A page in its own context, already carrying the session cookie. */\n page: Page;\n user: SeededUser;\n session: MintedSession;\n}\n\nexport interface ZitadelTestFixtures {\n /** Per-test seeding; each call mints unique data on the shared instance. */\n seed: {\n user(input?: SeedUserInput): Promise<SeededUser>;\n users(count: number, template?: SeedUsersTemplate): Promise<SeededUser[]>;\n /** Unused email+password for registration flows — creates nothing. */\n identity(): Identity;\n /** Seeded user + headless real-flow login; password flows only. */\n session(input?: SeedSessionInput): Promise<MintedSession>;\n };\n /**\n * Start the test authenticated: a fresh user, a real session minted through\n * the flow API, and the cookie injected into a dedicated browser context —\n * the default `page` stays signed out for login-flow tests. Requires\n * `use.baseURL` (every withZitadel consumer sets it).\n */\n authenticatedPage: AuthenticatedPage;\n /**\n * Virtual passkey authenticator attached to the default `page`, disposed on\n * teardown. On-demand: tests that don't request it pay nothing. Chromium\n * only — see `enableVirtualPasskey` for the constraints.\n */\n passkey: VirtualPasskey;\n}\n\nexport interface ZitadelWorkerFixtures {\n /** Connection to the suite's instance, resolved once per worker. */\n zitadel: ConnectedZitadel;\n}\n\nexport const test = base.extend<ZitadelTestFixtures, ZitadelWorkerFixtures>({\n zitadel: [\n // Playwright derives fixture dependencies from the destructuring pattern,\n // so the empty pattern is required here.\n // oxlint-disable-next-line no-empty-pattern\n async ({}, use) => {\n const handshakePath = process.env.ZITADEL_TESTING_HANDSHAKE;\n if (!handshakePath) {\n throw new Error(\n \"ZITADEL_TESTING_HANDSHAKE is not set. Point it at the handshake file \" +\n \"written by the script that boots the instance (see @zitadel/testing docs).\",\n );\n }\n await use(connectZitadel(readHandshakeSync(handshakePath)));\n },\n { scope: \"worker\" },\n ],\n seed: async ({ zitadel, baseURL }, use) => {\n await use({\n user: (input) => zitadel.seedUser(input),\n users: (count, template) => zitadel.seedUsers(count, template),\n identity: () => zitadel.identity(),\n // The suite's baseURL is the app origin the project allowlists.\n session: (input) => zitadel.seedSession({ origin: baseURL, ...input }),\n });\n },\n passkey: async ({ page }, use) => {\n const passkey = await enableVirtualPasskey(page);\n await use(passkey);\n await passkey.dispose();\n },\n authenticatedPage: async ({ browser, zitadel, baseURL }, use) => {\n if (!baseURL) {\n throw new Error(\n \"authenticatedPage requires `use.baseURL` so the session cookie can be \" +\n \"scoped to the app under test.\",\n );\n }\n const session = await zitadel.seedSession({ origin: baseURL });\n const context = await browser.newContext({ baseURL });\n // `addCookies` takes either url or domain/path; url derives the rest.\n const { path: _path, ...cookie } = session.cookie;\n await context.addCookies([{ ...cookie, url: baseURL }]);\n const page = await context.newPage();\n await use({ page, user: session.user, session });\n await context.close();\n },\n});\n\nexport { expect } from \"@playwright/test\";\nexport { applyAppEnvTemplate, nextAppEnv } from \"./app-env\";\nexport type { AppEnvTemplate } from \"./app-env\";\nexport {\n clickFlowAction,\n fillFlowField,\n flowAction,\n flowField,\n loginWithPassword,\n loginWithPasskey,\n registerWithPassword,\n registerWithPasskey,\n} from \"./flows\";\nexport type {\n FlowActionOptions,\n FlowFieldOptions,\n LoginCredentials,\n PasswordRegistrationDetails,\n ProfileEntry,\n RegistrationDetails,\n} from \"./flows\";\nexport { enableVirtualPasskey } from \"./passkey\";\nexport type { VirtualPasskey } from \"./passkey\";\nexport { withZitadel } from \"./playwright-config\";\nexport type { WithZitadelOptions } from \"./playwright-config\";\nexport type {\n ConnectedZitadel,\n Identity,\n InstanceHandle,\n MintedSession,\n SeededUser,\n SeedSessionInput,\n SeedUserInput,\n SeedUsersTemplate,\n} from \"./types\";\n"],"mappings":"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"}
@@ -1,8 +1,151 @@
1
1
  import { a as Identity, d as SeedUserInput, f as SeedUsersTemplate, i as ConnectedZitadel, l as MintedSession, n as applyAppEnvTemplate, o as InstanceHandle, p as SeededUser, r as nextAppEnv, t as AppEnvTemplate, u as SeedSessionInput } from "./app-env-D3W0GYhA.mjs";
2
2
  import { SetupPreset, SetupUseCase } from "@zitadel/config/defaults";
3
3
  import * as _$_playwright_test0 from "@playwright/test";
4
- import { Page, PlaywrightTestConfig, expect } from "@playwright/test";
4
+ import { Locator, Page, PlaywrightTestConfig, expect } from "@playwright/test";
5
5
 
6
+ //#region src/passkey.d.ts
7
+ /**
8
+ * A virtual WebAuthn authenticator attached to one page. Ceremonies started
9
+ * from that page (passkey registration, passkey login) complete automatically
10
+ * — no OS authenticator dialog, no touch.
11
+ */
12
+ interface VirtualPasskey {
13
+ /** CDP id of the virtual authenticator, for advanced raw-protocol use. */
14
+ authenticatorId: string;
15
+ /** Number of credentials the authenticator currently stores. */
16
+ credentialCount(): Promise<number>;
17
+ /** Remove the authenticator and detach the CDP session. */
18
+ dispose(): Promise<void>;
19
+ }
20
+ /**
21
+ * Attach a virtual passkey authenticator to the page via the Chrome DevTools
22
+ * Protocol. The options mirror a platform authenticator with discoverable
23
+ * credentials and automatic user presence — the profile the consumer journey
24
+ * has run in CI since passkey coverage became mandatory there.
25
+ *
26
+ * Chromium only: the CDP WebAuthn domain does not exist in WebKit or Firefox.
27
+ * The authenticator is bound to this page — drive registration and the later
28
+ * login from the same page, or the credential is gone. Serve the app under
29
+ * test on an origin WebAuthn accepts as a relying-party ID: HTTPS on a real
30
+ * domain, or `http://localhost` for local runs — raw IP origins such as
31
+ * `http://127.0.0.1` are invalid RP IDs.
32
+ */
33
+ declare function enableVirtualPasskey(page: Page): Promise<VirtualPasskey>;
34
+ //#endregion
35
+ //#region src/flows.d.ts
36
+ /**
37
+ * Helpers that drive the `<zitadel-login>` widget through complete auth
38
+ * ceremonies, built on the widget's documented automation hooks
39
+ * (`zitadel-field-*` / `zitadel-input-*` on fields, `zitadel-action-*` on
40
+ * actions — see packages/components/README.md). Field names use the
41
+ * normalised hook token: the flow engine names credential fields
42
+ * `x-auth-methods#password`, but the hook (and this API) says `password`.
43
+ *
44
+ * The ceremony helpers assume the default flow vocabulary — steps that
45
+ * declare `submit` / `passkey` / `passkey_register` actions and `email` /
46
+ * password fields, as `default-login.json` does. They branch only on
47
+ * widget-observable state (which fields and actions the flow renders),
48
+ * because customer flow configurations legitimately vary; they never
49
+ * assert app state. Callers navigate to the widget first and assert their
50
+ * own signed-in surface afterwards. Flows with renamed actions or custom
51
+ * steps drive the widget directly via `flowAction` / `flowField`.
52
+ */
53
+ interface FlowActionOptions {
54
+ /**
55
+ * Accessible-name fallback for templates that do not emit the documented
56
+ * `data-testid` hooks: adds role-based button/link candidates.
57
+ */
58
+ name?: RegExp;
59
+ }
60
+ interface FlowFieldOptions {
61
+ /** Label fallback for templates that do not emit the documented hooks. */
62
+ label?: RegExp;
63
+ }
64
+ /**
65
+ * One optional registration field, filled only when the flow renders it.
66
+ * The value's type picks the control: a boolean drives a checkbox
67
+ * (`zl-checkbox`), a string first tries a select (`zl-select`) and falls
68
+ * back to filling a text-like input.
69
+ */
70
+ interface ProfileEntry {
71
+ /** Normalised field hook token, e.g. `givenName`. */
72
+ field: string;
73
+ value: string | boolean;
74
+ /** Label fallback for text-like inputs on templates without hooks. */
75
+ label?: RegExp;
76
+ }
77
+ interface LoginCredentials {
78
+ email: string;
79
+ password: string;
80
+ }
81
+ interface RegistrationDetails {
82
+ email: string;
83
+ /** Extra fields some flows require at registration, filled if rendered. */
84
+ profile?: ProfileEntry[];
85
+ }
86
+ interface PasswordRegistrationDetails extends RegistrationDetails {
87
+ password: string;
88
+ }
89
+ /**
90
+ * Locator for a flow action control by its declared action name. Matches
91
+ * every hook shape the default template emits — host testid, native
92
+ * shadow button/link testids, and the raw `action`/`data-action`
93
+ * attributes (the recover link carries only `data-action`).
94
+ */
95
+ declare function flowAction(page: Page, action: string, options?: FlowActionOptions): Locator;
96
+ /**
97
+ * Locator for a flow field's input by its normalised hook token
98
+ * (`email`, `password`, a user-schema property name).
99
+ */
100
+ declare function flowField(page: Page, field: string, options?: FlowFieldOptions): Locator;
101
+ /** Click a flow action (auto-waits like any locator click). */
102
+ declare function clickFlowAction(page: Page, action: string, options?: FlowActionOptions): Promise<void>;
103
+ /** Fill a flow field (auto-waits like any locator fill). */
104
+ declare function fillFlowField(page: Page, field: string, value: string, options?: FlowFieldOptions): Promise<void>;
105
+ /**
106
+ * Sign in with email and password from the flow's entry step. Handles both
107
+ * the default split shape (identifier → password) and flows that render
108
+ * the password field on the entry step.
109
+ */
110
+ declare function loginWithPassword(page: Page, {
111
+ email,
112
+ password
113
+ }: LoginCredentials): Promise<void>;
114
+ /**
115
+ * Sign in with a passkey. With `email`, fills the identifier and takes the
116
+ * step's passkey action; without, taps the entry step's passkey action
117
+ * directly (discoverable-credential one-tap, e.g. the passkey-first
118
+ * preset). Pair with `enableVirtualPasskey` / the `passkey` fixture in
119
+ * headless runs — the ceremony completes automatically once the widget
120
+ * issues the WebAuthn challenge.
121
+ */
122
+ declare function loginWithPasskey(page: Page, options?: {
123
+ email?: string;
124
+ }): Promise<void>;
125
+ /**
126
+ * Register a new user with a password: enter the (unknown) identifier,
127
+ * advance into the registration step, continue on the password path, and
128
+ * submit the password. Ends when the final submit is clicked — assert your
129
+ * app's signed-in surface afterwards. Flows that route through steps the
130
+ * default flow does not (e.g. a passkey upsell) need caller-side handling
131
+ * after this returns.
132
+ */
133
+ declare function registerWithPassword(page: Page, {
134
+ email,
135
+ password,
136
+ profile
137
+ }: PasswordRegistrationDetails): Promise<void>;
138
+ /**
139
+ * Register a new user with a passkey: enter the (unknown) identifier,
140
+ * advance into the registration step, and take its `passkey_register`
141
+ * action. Requires an authenticator (see `loginWithPasskey`). The default
142
+ * flow completes registration from the ceremony directly.
143
+ */
144
+ declare function registerWithPasskey(page: Page, {
145
+ email,
146
+ profile
147
+ }: RegistrationDetails): Promise<void>;
148
+ //#endregion
6
149
  //#region src/orchestration.d.ts
7
150
  interface SupervisorConfig {
8
151
  /** Fixed port: the config process must know the health URL up front. */
@@ -87,7 +230,9 @@ interface WithZitadelOptions {
87
230
  * that needs it. The returned value is plain data; append your own entries
88
231
  * to `webServer` if the suite needs additional servers.
89
232
  */
90
- declare function withZitadel(options: WithZitadelOptions, /** Test seam: alternative executable resolution. */resolveEntry?: (name: "supervisor" | "app-runner") => string): {
233
+ declare function withZitadel(options: WithZitadelOptions, /** Test seam: alternative executable resolution. */
234
+
235
+ resolveEntry?: (name: "supervisor" | "app-runner") => string): {
91
236
  webServer: WebServerEntry[];
92
237
  };
93
238
  //#endregion
@@ -113,6 +258,12 @@ interface ZitadelTestFixtures {
113
258
  * `use.baseURL` (every withZitadel consumer sets it).
114
259
  */
115
260
  authenticatedPage: AuthenticatedPage;
261
+ /**
262
+ * Virtual passkey authenticator attached to the default `page`, disposed on
263
+ * teardown. On-demand: tests that don't request it pay nothing. Chromium
264
+ * only — see `enableVirtualPasskey` for the constraints.
265
+ */
266
+ passkey: VirtualPasskey;
116
267
  }
117
268
  interface ZitadelWorkerFixtures {
118
269
  /** Connection to the suite's instance, resolved once per worker. */
@@ -120,5 +271,5 @@ interface ZitadelWorkerFixtures {
120
271
  }
121
272
  declare const test: _$_playwright_test0.TestType<_$_playwright_test0.PlaywrightTestArgs & _$_playwright_test0.PlaywrightTestOptions & ZitadelTestFixtures, _$_playwright_test0.PlaywrightWorkerArgs & _$_playwright_test0.PlaywrightWorkerOptions & ZitadelWorkerFixtures>;
122
273
  //#endregion
123
- export { type AppEnvTemplate, AuthenticatedPage, type ConnectedZitadel, type Identity, type InstanceHandle, type MintedSession, type SeedSessionInput, type SeedUserInput, type SeedUsersTemplate, type SeededUser, type WithZitadelOptions, ZitadelTestFixtures, ZitadelWorkerFixtures, applyAppEnvTemplate, expect, nextAppEnv, test, withZitadel };
274
+ export { type AppEnvTemplate, AuthenticatedPage, type ConnectedZitadel, type FlowActionOptions, type FlowFieldOptions, type Identity, type InstanceHandle, type LoginCredentials, type MintedSession, type PasswordRegistrationDetails, type ProfileEntry, type RegistrationDetails, type SeedSessionInput, type SeedUserInput, type SeedUsersTemplate, type SeededUser, type VirtualPasskey, type WithZitadelOptions, ZitadelTestFixtures, ZitadelWorkerFixtures, applyAppEnvTemplate, clickFlowAction, enableVirtualPasskey, expect, fillFlowField, flowAction, flowField, loginWithPasskey, loginWithPassword, nextAppEnv, registerWithPasskey, registerWithPassword, test, withZitadel };
124
275
  //# sourceMappingURL=playwright.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"playwright.d.mts","names":[],"sources":["../src/orchestration.ts","../src/playwright-config.ts","../src/playwright.ts"],"mappings":";;;;;;UAeiB,gBAAA;EAMf;EAJA,IAAA;EACA,UAAA;EACA,YAAA;EAMA;EAJA,gBAAA;EACA,GAAA;EACA,IAAA;EACA,WAAA;EACA,MAAA,GAAS,WAAA;EACT,OAAA,GAAU,YAAA;EACV,SAAA;AAAA;;;KCZG,cAAA,GAAiB,OAAA,CACpB,WAAA,CAAY,oBAAA;AAAA,UAIG,kBAAA;;ADLjB;;;;ECWE,SAAA;EDRA;;;;;ECcA,IAAA;EDPA;;;;;ECaA,SAAA;EDXS;ECaT,OAAA;8CAEE,YAAA,WA3BC;IA6BD,gBAAA;IACA,WAAA;IACA,MAAA,GAAS,gBAAA;IACT,OAAA,GAAU,gBAAA,aAhCQ;IAkClB,GAAA,WAlCyB;IAoCzB,IAAA,YAnCF;IAqCE,aAAA;EAAA;EArC8B;EAwChC,GAAA;IApCiC,qFAsC/B,OAAA,YAZS;IAcT,GAAA,UASK;IAPL,SAAA;IAOmB;;;;;;IAAnB,GAAA,EAAK,cAAA;IACL,cAAA,WAxBA;IA0BA,kBAAA;EAAA;EAzBU;EA4BZ,aAAA;AAAA;;;;;;;;;;;;;AAqBF;;;;;;iBAAgB,WAAA,CACd,OAAA,EAAS,kBAAA,sDAET,YAAA,IAAe,IAAA;EACZ,SAAA,EAAW,cAAA;AAAA;;;UCtFC,iBAAA;;EAEf,IAAA,EAAM,IAAA;EACN,IAAA,EAAM,UAAA;EACN,OAAA,EAAS,aAAA;AAAA;AAAA,UAGM,mBAAA;EFKO;EEHtB,IAAA;IACE,IAAA,CAAK,KAAA,GAAQ,aAAA,GAAgB,OAAA,CAAQ,UAAA;IACrC,KAAA,CAAM,KAAA,UAAe,QAAA,GAAW,iBAAA,GAAoB,OAAA,CAAQ,UAAA,KFJ9D;IEME,QAAA,IAAY,QAAA,EFJd;IEME,OAAA,CAAQ,KAAA,GAAQ,gBAAA,GAAmB,OAAA,CAAQ,aAAA;EAAA;EFJpC;;;;;;EEYT,iBAAA,EAAmB,iBAAA;AAAA;AAAA,UAGJ,qBAAA;EDzBZ;EC2BH,OAAA,EAAS,gBAAA;AAAA;AAAA,cAGE,IAAA,EAAI,mBAAA,CAAA,QAAA,CA0Cf,mBAAA,CA1Ce,kBAAA,GAAA,mBAAA,CAAA,qBAAA,GAAA,mBAAA,EAAA,mBAAA,CAAA,oBAAA,GAAA,mBAAA,CAAA,uBAAA,GAAA,qBAAA"}
1
+ {"version":3,"file":"playwright.d.mts","names":[],"sources":["../src/passkey.ts","../src/flows.ts","../src/orchestration.ts","../src/playwright-config.ts","../src/playwright.ts"],"mappings":"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"}
@@ -1,10 +1,236 @@
1
- import { a as nextAppEnv, i as applyAppEnvTemplate, t as readHandshakeSync } from "./handshake-BPtWruO8.mjs";
2
- import { t as connectZitadel } from "./src-DkG0V4A6.mjs";
1
+ import { a as nextAppEnv, i as applyAppEnvTemplate, t as readHandshakeSync } from "./handshake-ClzWvG8z.mjs";
2
+ import { t as connectZitadel } from "./src-eTcdx-ZS.mjs";
3
3
  import { n as HANDSHAKE_ENV, r as SUPERVISOR_CONFIG_ENV, t as APP_RUNNER_CONFIG_ENV } from "./orchestration-C640_1Lg.mjs";
4
4
  import { extname, isAbsolute, join } from "node:path";
5
5
  import { existsSync } from "node:fs";
6
6
  import { expect, test as test$1 } from "@playwright/test";
7
7
  import { fileURLToPath } from "node:url";
8
+ //#region src/passkey.ts
9
+ /**
10
+ * Attach a virtual passkey authenticator to the page via the Chrome DevTools
11
+ * Protocol. The options mirror a platform authenticator with discoverable
12
+ * credentials and automatic user presence — the profile the consumer journey
13
+ * has run in CI since passkey coverage became mandatory there.
14
+ *
15
+ * Chromium only: the CDP WebAuthn domain does not exist in WebKit or Firefox.
16
+ * The authenticator is bound to this page — drive registration and the later
17
+ * login from the same page, or the credential is gone. Serve the app under
18
+ * test on an origin WebAuthn accepts as a relying-party ID: HTTPS on a real
19
+ * domain, or `http://localhost` for local runs — raw IP origins such as
20
+ * `http://127.0.0.1` are invalid RP IDs.
21
+ */
22
+ async function enableVirtualPasskey(page) {
23
+ let client;
24
+ try {
25
+ client = await page.context().newCDPSession(page);
26
+ } catch (error) {
27
+ throw new Error("enableVirtualPasskey: could not open a CDP session — passkey testing needs Chromium's virtual authenticator, so run passkey specs in a Chromium project.", { cause: error });
28
+ }
29
+ let authenticatorId;
30
+ try {
31
+ await client.send("WebAuthn.enable");
32
+ ({authenticatorId} = await client.send("WebAuthn.addVirtualAuthenticator", { options: {
33
+ protocol: "ctap2",
34
+ transport: "internal",
35
+ hasResidentKey: true,
36
+ hasUserVerification: true,
37
+ isUserVerified: true,
38
+ automaticPresenceSimulation: true
39
+ } }));
40
+ } catch (error) {
41
+ await client.detach().catch(() => void 0);
42
+ throw error;
43
+ }
44
+ return {
45
+ authenticatorId,
46
+ async credentialCount() {
47
+ const { credentials } = await client.send("WebAuthn.getCredentials", { authenticatorId });
48
+ return credentials.length;
49
+ },
50
+ async dispose() {
51
+ try {
52
+ await client.send("WebAuthn.removeVirtualAuthenticator", { authenticatorId });
53
+ } catch {} finally {
54
+ await client.detach().catch(() => void 0);
55
+ }
56
+ }
57
+ };
58
+ }
59
+ //#endregion
60
+ //#region src/flows.ts
61
+ /**
62
+ * A union locator resolves in DOM order across the whole page, so every
63
+ * candidate built from a broad selector — accessible names, labels, the
64
+ * generic `data-action` attribute — is scoped to the `<zitadel-login>`
65
+ * host; otherwise a same-named control in the app's own chrome (header
66
+ * nav, footer forms) could win the union. The host element exists no
67
+ * matter what the tenant's template renders, so these fallbacks keep
68
+ * working for custom templates that emit no automation hooks — the case
69
+ * they exist for. The `zitadel-*` testid hooks and the `zl-button` atom
70
+ * are namespaced and stay page-global.
71
+ */
72
+ function widgetRoot(page) {
73
+ return page.locator("zitadel-login");
74
+ }
75
+ /**
76
+ * Escape a value for use inside a double-quoted CSS attribute selector,
77
+ * per the CSSOM "serialize a string" rules: NUL becomes U+FFFD, control
78
+ * characters become hex code-point escapes, quote and backslash are
79
+ * backslash-escaped. C1 controls (U+0080–U+009F) are escaped too — CSSOM
80
+ * itself leaves them literal, but the escaped form is equivalent and
81
+ * survives stricter-than-spec selector parsers. Anything the flow schema
82
+ * accepts as an action name yields a parseable selector.
83
+ */
84
+ function cssAttributeValue(value) {
85
+ let out = "";
86
+ for (const ch of value) {
87
+ const code = ch.codePointAt(0) ?? 0;
88
+ if (code === 0) out += "�";
89
+ else if (code <= 31 || code >= 127 && code <= 159) out += `\\${code.toString(16)} `;
90
+ else if (ch === "\"" || ch === "\\") out += `\\${ch}`;
91
+ else out += ch;
92
+ }
93
+ return out;
94
+ }
95
+ /**
96
+ * Locator for a flow action control by its declared action name. Matches
97
+ * every hook shape the default template emits — host testid, native
98
+ * shadow button/link testids, and the raw `action`/`data-action`
99
+ * attributes (the recover link carries only `data-action`).
100
+ */
101
+ function flowAction(page, action, options = {}) {
102
+ const attributeSafe = cssAttributeValue(action);
103
+ let candidates = page.getByTestId(`zitadel-action-${action}`).or(page.getByTestId(`zitadel-action-${action}-button`)).or(page.getByTestId(`zitadel-action-${action}-link`)).or(page.locator(`zl-button[action="${attributeSafe}"]`)).or(widgetRoot(page).locator(`[data-action="${attributeSafe}"]`));
104
+ if (options.name) candidates = candidates.or(widgetRoot(page).getByRole("button", { name: options.name })).or(widgetRoot(page).getByRole("link", { name: options.name }));
105
+ return candidates.first();
106
+ }
107
+ /**
108
+ * Locator for a flow field's input by its normalised hook token
109
+ * (`email`, `password`, a user-schema property name).
110
+ */
111
+ function flowField(page, field, options = {}) {
112
+ let candidates = page.getByTestId(`zitadel-input-${field}`).or(page.getByTestId(`zitadel-field-${field}`).locator("input"));
113
+ if (options.label) candidates = candidates.or(widgetRoot(page).getByLabel(options.label));
114
+ return candidates.first();
115
+ }
116
+ /** Click a flow action (auto-waits like any locator click). */
117
+ async function clickFlowAction(page, action, options) {
118
+ await flowAction(page, action, options).click();
119
+ }
120
+ /** Fill a flow field (auto-waits like any locator fill). */
121
+ async function fillFlowField(page, field, value, options) {
122
+ await flowField(page, field, options).fill(value);
123
+ }
124
+ /**
125
+ * Sign in with email and password from the flow's entry step. Handles both
126
+ * the default split shape (identifier → password) and flows that render
127
+ * the password field on the entry step.
128
+ */
129
+ async function loginWithPassword(page, { email, password }) {
130
+ await emailField(page).fill(email);
131
+ const password_ = passwordField(page);
132
+ if (!await password_.isVisible().catch(() => false)) await flowAction(page, "submit").click();
133
+ await password_.fill(password);
134
+ await flowAction(page, "submit").click();
135
+ }
136
+ /**
137
+ * Sign in with a passkey. With `email`, fills the identifier and takes the
138
+ * step's passkey action; without, taps the entry step's passkey action
139
+ * directly (discoverable-credential one-tap, e.g. the passkey-first
140
+ * preset). Pair with `enableVirtualPasskey` / the `passkey` fixture in
141
+ * headless runs — the ceremony completes automatically once the widget
142
+ * issues the WebAuthn challenge.
143
+ */
144
+ async function loginWithPasskey(page, options = {}) {
145
+ if (options.email !== void 0) await emailField(page).fill(options.email);
146
+ await flowAction(page, "passkey").click();
147
+ }
148
+ /**
149
+ * Register a new user with a password: enter the (unknown) identifier,
150
+ * advance into the registration step, continue on the password path, and
151
+ * submit the password. Ends when the final submit is clicked — assert your
152
+ * app's signed-in surface afterwards. Flows that route through steps the
153
+ * default flow does not (e.g. a passkey upsell) need caller-side handling
154
+ * after this returns.
155
+ */
156
+ async function registerWithPassword(page, { email, password, profile }) {
157
+ await advanceToRegistration(page, email, profile);
158
+ const password_ = passwordField(page);
159
+ if (!await password_.isVisible().catch(() => false)) await flowAction(page, "submit").click();
160
+ await password_.fill(password);
161
+ await flowAction(page, "submit").click();
162
+ }
163
+ /**
164
+ * Register a new user with a passkey: enter the (unknown) identifier,
165
+ * advance into the registration step, and take its `passkey_register`
166
+ * action. Requires an authenticator (see `loginWithPasskey`). The default
167
+ * flow completes registration from the ceremony directly.
168
+ */
169
+ async function registerWithPasskey(page, { email, profile }) {
170
+ await advanceToRegistration(page, email, profile);
171
+ await flowAction(page, "passkey_register").click();
172
+ }
173
+ function emailField(page) {
174
+ return flowField(page, "email", { label: /email/i });
175
+ }
176
+ function passwordField(page) {
177
+ return flowField(page, "password", { label: /password/i });
178
+ }
179
+ /**
180
+ * From the entry step: submit the unknown identifier (the default flow's
181
+ * `user_not_found` transition routes to registration), or take an explicit
182
+ * `register` navigate action when the entry step is a combined
183
+ * email+password step — there, submitting would attempt a password sign-in
184
+ * instead. Then wait for the registration step and fill what it renders.
185
+ */
186
+ async function advanceToRegistration(page, email, profile) {
187
+ await emailField(page).fill(email);
188
+ if (await passwordField(page).isVisible().catch(() => false)) await flowAction(page, "register").click();
189
+ else await flowAction(page, "submit").click();
190
+ await expectRegistrationStep(page);
191
+ await fillIfVisible(emailField(page), email);
192
+ for (const entry of profile ?? []) await fillProfileEntry(page, entry);
193
+ }
194
+ /**
195
+ * Fill one registration field with the verb its control needs: booleans
196
+ * check the `zl-checkbox` native input, strings prefer the `zl-select`
197
+ * native select (option matched by value, falling back to label) and
198
+ * otherwise fill a text-like input. The select/checkbox natives carry the
199
+ * name-first testids the atoms document (`zitadel-select-*`,
200
+ * `zitadel-checkbox-*`); templates without those hooks drive such fields
201
+ * via their own locators.
202
+ */
203
+ async function fillProfileEntry(page, entry) {
204
+ if (typeof entry.value === "boolean") {
205
+ const checkbox = page.getByTestId(`zitadel-checkbox-${entry.field}`).first();
206
+ if (await checkbox.isVisible().catch(() => false)) await checkbox.setChecked(entry.value);
207
+ return;
208
+ }
209
+ const select = page.getByTestId(`zitadel-select-${entry.field}`).first();
210
+ if (await select.isVisible().catch(() => false)) {
211
+ await select.selectOption(entry.value);
212
+ return;
213
+ }
214
+ await fillIfVisible(flowField(page, entry.field, { label: entry.label }), entry.value);
215
+ }
216
+ /**
217
+ * Barrier between the entry submit and probing the registration step's
218
+ * fields: without it, optional-field probes race the re-render and read
219
+ * the outgoing step. The default flow's registration step declares
220
+ * `passkey_register` — a structural, locale-independent signal; the
221
+ * heading regex covers password-only flows on the default English
222
+ * template.
223
+ */
224
+ async function expectRegistrationStep(page) {
225
+ await flowAction(page, "passkey_register").or(widgetRoot(page).getByRole("heading", { name: /create|register|sign up|no-account/i })).first().waitFor({
226
+ state: "visible",
227
+ timeout: 3e4
228
+ });
229
+ }
230
+ async function fillIfVisible(field, value) {
231
+ if (await field.isVisible().catch(() => false)) await field.fill(value);
232
+ }
233
+ //#endregion
8
234
  //#region src/playwright-config.ts
9
235
  /**
10
236
  * Generate the Playwright `webServer` entries that boot an ephemeral seeded
@@ -123,6 +349,11 @@ const test = test$1.extend({
123
349
  })
124
350
  });
125
351
  },
352
+ passkey: async ({ page }, use) => {
353
+ const passkey = await enableVirtualPasskey(page);
354
+ await use(passkey);
355
+ await passkey.dispose();
356
+ },
126
357
  authenticatedPage: async ({ browser, zitadel, baseURL }, use) => {
127
358
  if (!baseURL) throw new Error("authenticatedPage requires `use.baseURL` so the session cookie can be scoped to the app under test.");
128
359
  const session = await zitadel.seedSession({ origin: baseURL });
@@ -141,6 +372,6 @@ const test = test$1.extend({
141
372
  }
142
373
  });
143
374
  //#endregion
144
- export { applyAppEnvTemplate, expect, nextAppEnv, test, withZitadel };
375
+ export { applyAppEnvTemplate, clickFlowAction, enableVirtualPasskey, expect, fillFlowField, flowAction, flowField, loginWithPasskey, loginWithPassword, nextAppEnv, registerWithPasskey, registerWithPassword, test, withZitadel };
145
376
 
146
377
  //# sourceMappingURL=playwright.mjs.map