@entifix/testing-e2e 0.5.0 → 0.7.0

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.
@@ -15,7 +15,7 @@ export interface AssertExpectedServerOptions {
15
15
  * a production build every time. So the reuse stays and the *assumption* is
16
16
  * checked instead: the `mock` profile's whole claim is that it is hermetic, and
17
17
  * attached to a dev server it is not. Turning a silent wrong answer into a loud
18
- * failure is the trade — see issue #69.
18
+ * failure is the trade.
19
19
  *
20
20
  * `live` is exempt on purpose: it expects an already-running app and makes no
21
21
  * hermeticity claim, so which build answers is the operator's business.
@@ -38,7 +38,7 @@ const describeMismatch = (baseURL)=>[
38
38
  * a production build every time. So the reuse stays and the *assumption* is
39
39
  * checked instead: the `mock` profile's whole claim is that it is hermetic, and
40
40
  * attached to a dev server it is not. Turning a silent wrong answer into a loud
41
- * failure is the trade — see issue #69.
41
+ * failure is the trade.
42
42
  *
43
43
  * `live` is exempt on purpose: it expects an already-running app and makes no
44
44
  * hermeticity claim, so which build answers is the operator's business.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/playwright/assert-expected-server.ts"],"sourcesContent":["import { type E2eEnv, isMockProfile } from '../profile/profile.js';\n\n/** Opts out of the guard, for a developer who knows what they are attaching to. */\nexport const ALLOW_DEV_SERVER_VAR = 'ENTIFIX_E2E_ALLOW_DEV_SERVER';\n\n/**\n * Liveness, not the config's `readyPath`: every app answers it, it answers from\n * the process alone, and it carries the one field this check needs. `readyPath`\n * differs per app and defaults to `/`, which returns HTML.\n */\nconst LIVENESS_PATH = '/api/health/live';\n\n/**\n * The second signal, which needs no cooperation from the app at all.\n *\n * `next dev` serves its static assets under the literal segment `development`;\n * a production build uses a hashed build id, so this 404s under `next start`.\n * It is a Next internal and therefore the *fallback*, but it is the one that\n * survives the case that motivates it: an `nx e2e` run builds into the same\n * `.next` the running dev server owns, which corrupts that server's Turbopack\n * cache and makes every app route — liveness included — answer `500`. A guard\n * that only asked the app would degrade to \"cannot tell\" at exactly the moment\n * it is needed.\n */\nconst DEV_ASSET_PATH = '/_next/static/development/_buildManifest.js';\n\n/** A probe that hangs must not hold the whole run hostage. */\nconst PROBE_TIMEOUT_MS = 5_000;\n\nexport interface AssertExpectedServerOptions {\n /** Where the suite is pointed, e.g. `http://localhost:3001`. */\n readonly baseURL: string;\n readonly env?: E2eEnv;\n readonly fetchImpl?: typeof fetch;\n readonly warn?: (message: string) => void;\n}\n\nconst describeMismatch = (baseURL: string): string =>\n [\n `The hermetic e2e profile attached to a DEVELOPMENT server at ${baseURL}.`,\n '',\n 'Playwright reuses whatever already listens on the app port, so this run would',\n 'test `next dev` — a different bundle, no prerender, and with the dev fleet',\n \"behind it the real services instead of this profile's fakes. It can fail, or\",\n 'pass, for reasons that have nothing to do with the code under test. The build',\n 'this run just made also writes the same `.next` the dev server owns, which',\n 'corrupts its Turbopack cache on the way past.',\n '',\n 'Stop the dev server on that port and run again, or set',\n `${ALLOW_DEV_SERVER_VAR}=1 to accept the risk.`,\n ].join('\\n');\n\n/**\n * Refuses to run the hermetic suite against a development server.\n *\n * `reuseExistingServer` is what makes a local run fast, and turning it off costs\n * a production build every time. So the reuse stays and the *assumption* is\n * checked instead: the `mock` profile's whole claim is that it is hermetic, and\n * attached to a dev server it is not. Turning a silent wrong answer into a loud\n * failure is the trade — see issue #69.\n *\n * `live` is exempt on purpose: it expects an already-running app and makes no\n * hermeticity claim, so which build answers is the operator's business.\n *\n * Only when **both** signals are unreadable does the run continue, with a\n * warning: an app that exposes neither is not a reason to refuse to test it.\n */\nexport const assertExpectedServer = async ({\n baseURL,\n env = process.env,\n fetchImpl = fetch,\n warn = message => console.warn(message),\n}: AssertExpectedServerOptions): Promise<void> => {\n if (!isMockProfile(env)) return;\n\n const override = env[ALLOW_DEV_SERVER_VAR];\n if (override !== undefined && override !== '') return;\n\n const get = async (path: string): Promise<Response | undefined> => {\n try {\n return await fetchImpl(`${baseURL}${path}`, {\n signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),\n });\n } catch {\n return undefined;\n }\n };\n\n const liveness = await get(LIVENESS_PATH);\n if (liveness?.ok === true) {\n const mode = ((await liveness.json()) as { mode?: unknown }).mode;\n if (mode === 'production') return;\n if (mode === 'development') throw new Error(describeMismatch(baseURL));\n }\n\n // The app could not answer for itself. Ask Next directly.\n const devAsset = await get(DEV_ASSET_PATH);\n if (devAsset === undefined) {\n warn(\n `e2e: could not reach ${baseURL}; cannot tell which build is serving.`,\n );\n return;\n }\n\n if (devAsset.ok) throw new Error(describeMismatch(baseURL));\n};\n\n/** One probe per worker process, whatever the outcome. */\nlet verdict: Promise<void> | undefined;\n\n/**\n * The form `defineEntifixE2eTest` calls, from an auto fixture.\n *\n * A fixture rather than `globalSetup`, which is where this naturally belongs:\n * Playwright loads a `globalSetup` file through a different module path than\n * the config and the specs, and this package is `\"type\": \"module\"` while an\n * app's `playwright.config.ts` is CJS — the same file transpiles for one and\n * throws `exports is not defined in ES module scope` for the other. The\n * fixture rides the path that already works, at the cost of reporting the\n * refusal once per test rather than once per run.\n *\n * The promise is cached, so the probe happens once and every subsequent test\n * rejects with the same error instead of re-asking a server that will not have\n * changed mid-run.\n */\nexport const assertExpectedServerOnce = (\n baseURL: string | undefined,\n): Promise<void> => {\n if (baseURL === undefined || baseURL === '') return Promise.resolve();\n\n verdict ??= assertExpectedServer({ baseURL });\n return verdict;\n};\n"],"names":["isMockProfile","ALLOW_DEV_SERVER_VAR","LIVENESS_PATH","DEV_ASSET_PATH","PROBE_TIMEOUT_MS","describeMismatch","baseURL","join","assertExpectedServer","env","process","fetchImpl","fetch","warn","message","console","override","undefined","get","path","signal","AbortSignal","timeout","liveness","ok","mode","json","Error","devAsset","verdict","assertExpectedServerOnce","Promise","resolve"],"mappings":"AAAA,SAAsBA,aAAa,QAAQ,wBAAwB;AAEnE,iFAAiF,GACjF,OAAO,MAAMC,uBAAuB,+BAA+B;AAEnE;;;;CAIC,GACD,MAAMC,gBAAgB;AAEtB;;;;;;;;;;;CAWC,GACD,MAAMC,iBAAiB;AAEvB,4DAA4D,GAC5D,MAAMC,mBAAmB;AAUzB,MAAMC,mBAAmB,CAACC,UACxB;QACE,CAAC,6DAA6D,EAAEA,QAAQ,CAAC,CAAC;QAC1E;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,GAAGL,qBAAqB,sBAAsB,CAAC;KAChD,CAACM,IAAI,CAAC;AAET;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,uBAAuB,OAAO,EACzCF,OAAO,EACPG,MAAMC,QAAQD,GAAG,EACjBE,YAAYC,KAAK,EACjBC,OAAOC,CAAAA,UAAWC,QAAQF,IAAI,CAACC,QAAQ,EACX;IAC5B,IAAI,CAACd,cAAcS,MAAM;IAEzB,MAAMO,WAAWP,GAAG,CAACR,qBAAqB;IAC1C,IAAIe,aAAaC,aAAaD,aAAa,IAAI;IAE/C,MAAME,MAAM,OAAOC;QACjB,IAAI;YACF,OAAO,MAAMR,UAAU,GAAGL,UAAUa,MAAM,EAAE;gBAC1CC,QAAQC,YAAYC,OAAO,CAAClB;YAC9B;QACF,EAAE,eAAM;YACN,OAAOa;QACT;IACF;IAEA,MAAMM,WAAW,MAAML,IAAIhB;IAC3B,IAAIqB,CAAAA,4BAAAA,SAAUC,EAAE,MAAK,MAAM;QACzB,MAAMC,OAAO,AAAE,CAAA,MAAMF,SAASG,IAAI,EAAC,EAA0BD,IAAI;QACjE,IAAIA,SAAS,cAAc;QAC3B,IAAIA,SAAS,eAAe,MAAM,IAAIE,MAAMtB,iBAAiBC;IAC/D;IAEA,0DAA0D;IAC1D,MAAMsB,WAAW,MAAMV,IAAIf;IAC3B,IAAIyB,aAAaX,WAAW;QAC1BJ,KACE,CAAC,qBAAqB,EAAEP,QAAQ,qCAAqC,CAAC;QAExE;IACF;IAEA,IAAIsB,SAASJ,EAAE,EAAE,MAAM,IAAIG,MAAMtB,iBAAiBC;AACpD,EAAE;AAEF,wDAAwD,GACxD,IAAIuB;AAEJ;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,2BAA2B,CACtCxB;IAEA,IAAIA,YAAYW,aAAaX,YAAY,IAAI,OAAOyB,QAAQC,OAAO;IAEnEH,kBAAAA,UAAAA,UAAYrB,qBAAqB;QAAEF;IAAQ;IAC3C,OAAOuB;AACT,EAAE"}
1
+ {"version":3,"sources":["../../src/playwright/assert-expected-server.ts"],"sourcesContent":["import { type E2eEnv, isMockProfile } from '../profile/profile.js';\n\n/** Opts out of the guard, for a developer who knows what they are attaching to. */\nexport const ALLOW_DEV_SERVER_VAR = 'ENTIFIX_E2E_ALLOW_DEV_SERVER';\n\n/**\n * Liveness, not the config's `readyPath`: every app answers it, it answers from\n * the process alone, and it carries the one field this check needs. `readyPath`\n * differs per app and defaults to `/`, which returns HTML.\n */\nconst LIVENESS_PATH = '/api/health/live';\n\n/**\n * The second signal, which needs no cooperation from the app at all.\n *\n * `next dev` serves its static assets under the literal segment `development`;\n * a production build uses a hashed build id, so this 404s under `next start`.\n * It is a Next internal and therefore the *fallback*, but it is the one that\n * survives the case that motivates it: an `nx e2e` run builds into the same\n * `.next` the running dev server owns, which corrupts that server's Turbopack\n * cache and makes every app route — liveness included — answer `500`. A guard\n * that only asked the app would degrade to \"cannot tell\" at exactly the moment\n * it is needed.\n */\nconst DEV_ASSET_PATH = '/_next/static/development/_buildManifest.js';\n\n/** A probe that hangs must not hold the whole run hostage. */\nconst PROBE_TIMEOUT_MS = 5_000;\n\nexport interface AssertExpectedServerOptions {\n /** Where the suite is pointed, e.g. `http://localhost:3001`. */\n readonly baseURL: string;\n readonly env?: E2eEnv;\n readonly fetchImpl?: typeof fetch;\n readonly warn?: (message: string) => void;\n}\n\nconst describeMismatch = (baseURL: string): string =>\n [\n `The hermetic e2e profile attached to a DEVELOPMENT server at ${baseURL}.`,\n '',\n 'Playwright reuses whatever already listens on the app port, so this run would',\n 'test `next dev` — a different bundle, no prerender, and with the dev fleet',\n \"behind it the real services instead of this profile's fakes. It can fail, or\",\n 'pass, for reasons that have nothing to do with the code under test. The build',\n 'this run just made also writes the same `.next` the dev server owns, which',\n 'corrupts its Turbopack cache on the way past.',\n '',\n 'Stop the dev server on that port and run again, or set',\n `${ALLOW_DEV_SERVER_VAR}=1 to accept the risk.`,\n ].join('\\n');\n\n/**\n * Refuses to run the hermetic suite against a development server.\n *\n * `reuseExistingServer` is what makes a local run fast, and turning it off costs\n * a production build every time. So the reuse stays and the *assumption* is\n * checked instead: the `mock` profile's whole claim is that it is hermetic, and\n * attached to a dev server it is not. Turning a silent wrong answer into a loud\n * failure is the trade.\n *\n * `live` is exempt on purpose: it expects an already-running app and makes no\n * hermeticity claim, so which build answers is the operator's business.\n *\n * Only when **both** signals are unreadable does the run continue, with a\n * warning: an app that exposes neither is not a reason to refuse to test it.\n */\nexport const assertExpectedServer = async ({\n baseURL,\n env = process.env,\n fetchImpl = fetch,\n warn = message => console.warn(message),\n}: AssertExpectedServerOptions): Promise<void> => {\n if (!isMockProfile(env)) return;\n\n const override = env[ALLOW_DEV_SERVER_VAR];\n if (override !== undefined && override !== '') return;\n\n const get = async (path: string): Promise<Response | undefined> => {\n try {\n return await fetchImpl(`${baseURL}${path}`, {\n signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),\n });\n } catch {\n return undefined;\n }\n };\n\n const liveness = await get(LIVENESS_PATH);\n if (liveness?.ok === true) {\n const mode = ((await liveness.json()) as { mode?: unknown }).mode;\n if (mode === 'production') return;\n if (mode === 'development') throw new Error(describeMismatch(baseURL));\n }\n\n // The app could not answer for itself. Ask Next directly.\n const devAsset = await get(DEV_ASSET_PATH);\n if (devAsset === undefined) {\n warn(\n `e2e: could not reach ${baseURL}; cannot tell which build is serving.`,\n );\n return;\n }\n\n if (devAsset.ok) throw new Error(describeMismatch(baseURL));\n};\n\n/** One probe per worker process, whatever the outcome. */\nlet verdict: Promise<void> | undefined;\n\n/**\n * The form `defineEntifixE2eTest` calls, from an auto fixture.\n *\n * A fixture rather than `globalSetup`, which is where this naturally belongs:\n * Playwright loads a `globalSetup` file through a different module path than\n * the config and the specs, and this package is `\"type\": \"module\"` while an\n * app's `playwright.config.ts` is CJS — the same file transpiles for one and\n * throws `exports is not defined in ES module scope` for the other. The\n * fixture rides the path that already works, at the cost of reporting the\n * refusal once per test rather than once per run.\n *\n * The promise is cached, so the probe happens once and every subsequent test\n * rejects with the same error instead of re-asking a server that will not have\n * changed mid-run.\n */\nexport const assertExpectedServerOnce = (\n baseURL: string | undefined,\n): Promise<void> => {\n if (baseURL === undefined || baseURL === '') return Promise.resolve();\n\n verdict ??= assertExpectedServer({ baseURL });\n return verdict;\n};\n"],"names":["isMockProfile","ALLOW_DEV_SERVER_VAR","LIVENESS_PATH","DEV_ASSET_PATH","PROBE_TIMEOUT_MS","describeMismatch","baseURL","join","assertExpectedServer","env","process","fetchImpl","fetch","warn","message","console","override","undefined","get","path","signal","AbortSignal","timeout","liveness","ok","mode","json","Error","devAsset","verdict","assertExpectedServerOnce","Promise","resolve"],"mappings":"AAAA,SAAsBA,aAAa,QAAQ,wBAAwB;AAEnE,iFAAiF,GACjF,OAAO,MAAMC,uBAAuB,+BAA+B;AAEnE;;;;CAIC,GACD,MAAMC,gBAAgB;AAEtB;;;;;;;;;;;CAWC,GACD,MAAMC,iBAAiB;AAEvB,4DAA4D,GAC5D,MAAMC,mBAAmB;AAUzB,MAAMC,mBAAmB,CAACC,UACxB;QACE,CAAC,6DAA6D,EAAEA,QAAQ,CAAC,CAAC;QAC1E;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,GAAGL,qBAAqB,sBAAsB,CAAC;KAChD,CAACM,IAAI,CAAC;AAET;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,uBAAuB,OAAO,EACzCF,OAAO,EACPG,MAAMC,QAAQD,GAAG,EACjBE,YAAYC,KAAK,EACjBC,OAAOC,CAAAA,UAAWC,QAAQF,IAAI,CAACC,QAAQ,EACX;IAC5B,IAAI,CAACd,cAAcS,MAAM;IAEzB,MAAMO,WAAWP,GAAG,CAACR,qBAAqB;IAC1C,IAAIe,aAAaC,aAAaD,aAAa,IAAI;IAE/C,MAAME,MAAM,OAAOC;QACjB,IAAI;YACF,OAAO,MAAMR,UAAU,GAAGL,UAAUa,MAAM,EAAE;gBAC1CC,QAAQC,YAAYC,OAAO,CAAClB;YAC9B;QACF,EAAE,eAAM;YACN,OAAOa;QACT;IACF;IAEA,MAAMM,WAAW,MAAML,IAAIhB;IAC3B,IAAIqB,CAAAA,4BAAAA,SAAUC,EAAE,MAAK,MAAM;QACzB,MAAMC,OAAO,AAAE,CAAA,MAAMF,SAASG,IAAI,EAAC,EAA0BD,IAAI;QACjE,IAAIA,SAAS,cAAc;QAC3B,IAAIA,SAAS,eAAe,MAAM,IAAIE,MAAMtB,iBAAiBC;IAC/D;IAEA,0DAA0D;IAC1D,MAAMsB,WAAW,MAAMV,IAAIf;IAC3B,IAAIyB,aAAaX,WAAW;QAC1BJ,KACE,CAAC,qBAAqB,EAAEP,QAAQ,qCAAqC,CAAC;QAExE;IACF;IAEA,IAAIsB,SAASJ,EAAE,EAAE,MAAM,IAAIG,MAAMtB,iBAAiBC;AACpD,EAAE;AAEF,wDAAwD,GACxD,IAAIuB;AAEJ;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,2BAA2B,CACtCxB;IAEA,IAAIA,YAAYW,aAAaX,YAAY,IAAI,OAAOyB,QAAQC,OAAO;IAEnEH,kBAAAA,UAAAA,UAAYrB,qBAAqB;QAAEF;IAAQ;IAC3C,OAAOuB;AACT,EAAE"}
@@ -44,8 +44,8 @@ export interface SeedSessionOptions {
44
44
  */
45
45
  partyId?: string | null;
46
46
  /**
47
- * The domains the seeded organization is provisioned for — ADR 0007's second
48
- * ceiling, which the back-office nav reads.
47
+ * The domains the seeded organization is provisioned for — the second
48
+ * ceiling beside the role, which the back-office nav reads.
49
49
  *
50
50
  * Defaults to the one domain `entitlementSeedData` gives the demo
51
51
  * organization, so the fixture keeps mirroring what a real sign-in produces.
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/playwright/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,kBAAkB,CAAC;AAI7D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,eAAe,CAAC;AAC1C,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAC5C,eAAO,MAAM,aAAa,mBAAmB,CAAC;AAgB9C,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAoND;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GACtB,SAAS,cAAc,EACvB,iHAWG,kBAAuB,KACzB,OAAO,CAAC,IAAI,CAuCd,CAAC"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/playwright/session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,kBAAkB,CAAC;AAI7D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,eAAe,CAAC;AAC1C,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAC5C,eAAO,MAAM,aAAa,mBAAmB,CAAC;AAgB9C,MAAM,WAAW,kBAAkB;IACjC,+CAA+C;IAC/C,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAmND;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,GACtB,SAAS,cAAc,EACvB,iHAWG,kBAAuB,KACzB,OAAO,CAAC,IAAI,CAuCd,CAAC"}
@@ -114,9 +114,8 @@ const base64url = (value)=>Buffer.from(value).toString('base64').replace(/\+/g,
114
114
  /**
115
115
  * Sign in for real, through Zitadel's hosted login page.
116
116
  *
117
- * There is no API shortcut left and that is by design: no r10c endpoint accepts
118
- * a password, because r10c holds none
119
- * ([ADR 0016](../../../../../docs/adr/0016-zitadel-authenticates-r10c-authorizes.md)).
117
+ * There is no API shortcut left and that is by design: no host endpoint accepts
118
+ * a password, because a host that delegates authentication holds none.
120
119
  * The only way to obtain a real session is the flow a person performs, so the
121
120
  * `live` profile performs it — which also means this fixture exercises the
122
121
  * redirect, the PKCE exchange and the callback rather than skipping them.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/playwright/session.ts"],"sourcesContent":["import type { BrowserContext, Page } from '@playwright/test';\n\nimport { isMockProfile } from '../profile/profile.js';\n\n/**\n * The cookies a signed-in session travels in — the same names\n * `@entifix/core` declares, **written out rather than imported**.\n *\n * ⚠️ **This module must import no built package at runtime.** Nx builds its\n * project graph by loading every e2e project's `playwright.config.ts` in plain\n * Node — no source condition, nothing built — and that config reaches this\n * file. An `import … from '@entifix/core'` here resolves core's `dist`, which a\n * CI job that only lints never builds, and the whole graph fails with\n * `Cannot find module …/entifix-ts-core/dist/index.js`. It passed locally only\n * because a developer machine always has `dist` from a previous build.\n *\n * `session-cookies.spec.ts` imports core and fails if these drift, so the\n * duplication is checked rather than trusted.\n */\nexport const ACCESS_COOKIE = 'entifix_at';\nexport const SESSION_COOKIE = 'entifix_sid';\nexport const LOCALE_COOKIE = 'entifix_locale';\n\n/** Give a hosted-UI component time to hydrate before its input is retyped. */\nconst STEP_SETTLE_MS = 1_000;\n\n/**\n * Ceiling for a single hosted-UI action. Short on purpose: an action that cannot\n * complete has lost a race with a navigation, and the step machine wants to look\n * again rather than sit on it until the test's own timeout fires.\n */\nconst ACTION_TIMEOUT_MS = 10_000;\n\n/** How long a step waits for the next screen: 40 × 250ms = 10s. */\nconst STEP_POLLS = 40;\nconst STEP_POLL_MS = 250;\n\nexport interface SeedSessionOptions {\n /** Roles the seeded principal should carry. */\n roles?: readonly string[];\n /**\n * The organization the seeded session acts for. Tenant-plane routes resolve\n * their storage from this claim, so a spec that exercises one and omits it\n * gets `409 noActiveOrganization` rather than data. Pass `null` to seed a\n * session with no tenant scope on purpose — an operator, or a buyer.\n */\n activeOrganizationId?: string | null;\n /**\n * Which population the seeded principal belongs to — `customer`, `vendor` or\n * `operator`. Defaults to `vendor`, matching the default organization: a\n * session that acts for one is a vendor member's. A spec exercising platform\n * staff passes `operator` together with `activeOrganizationId: null`.\n */\n partyRole?: string;\n /**\n * The party the seeded principal is — the `Individual` id a route scoping a\n * read to its caller matches against. Defaults to the seeded vendor party, so\n * the fixture keeps mirroring what a real sign-in produces.\n *\n * Pass `null` to seed an account with no party record, which is the case a\n * scoped read must answer nothing for rather than everything.\n */\n partyId?: string | null;\n /**\n * The domains the seeded organization is provisioned for — ADR 0007's second\n * ceiling, which the back-office nav reads.\n *\n * Defaults to the one domain `entitlementSeedData` gives the demo\n * organization, so the fixture keeps mirroring what a real sign-in produces.\n * Pass `[]` to seed a member of an organization provisioned for nothing;\n * that is a different principal from one with `activeOrganizationId: null`,\n * who is outside the ceiling rather than refused by it.\n */\n entitlements?: readonly string[];\n /**\n * Locale to pin the run to. Seeded in both profiles so a run does not depend\n * on the CI machine's `Accept-Language` — without it the same spec renders\n * Spanish locally and English on a differently-configured runner.\n */\n locale?: string;\n /**\n * Identifier + password for the `live` profile's real sign-in, performed\n * against Zitadel's hosted login page. The default is the seeded super-admin\n * and the dev password `auth-service` provisions them with.\n */\n identifier?: string;\n password?: string;\n /** Where the sign-in surface is served in `live`. */\n signInAppUrl?: string;\n}\n\nconst base64url = (value: string): string =>\n Buffer.from(value)\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\n/**\n * A structurally valid, **deliberately unsigned** access token. It satisfies\n * exactly the two things the mock profile exercises — the middleware's cookie\n * presence check and the server-rendered nav's unverified claim read — and\n * nothing else: no service verifies it, because in `mock` the services are msw\n * fixtures. Signing it would need the private key, which the suite has no\n * business knowing and which is exactly what asymmetric signing exists to keep\n * out of places like this.\n */\nconst fabricateToken = (\n roles: readonly string[],\n activeOrganizationId: string | null,\n partyRole: string,\n partyId: string | null,\n entitlements: readonly string[],\n): string => {\n const header = base64url(\n JSON.stringify({ alg: 'RS256', kid: 'e2e-not-a-real-key', typ: 'JWT' }),\n );\n const payload = base64url(\n JSON.stringify({\n userId: 'e2e-user',\n subject: 'e2e-user',\n sessionId: 'e2e-session',\n roles,\n partyRole,\n ...(partyId === null ? {} : { partyId }),\n ...(activeOrganizationId === null\n ? {}\n : { activeOrganizationId, entitlements }),\n exp: Math.floor(Date.now() / 1000) + 3600,\n }),\n );\n return `${header}.${payload}.e2e-not-a-real-signature`;\n};\n\n/**\n * Fill one field of the hosted login and submit it.\n *\n * The retype is load-bearing, not defensive. v2 disables Continue on\n * `!formState.isValid` — react-hook-form state, not the DOM value — and\n * Playwright can type into the input before Next has hydrated the component.\n * Those keystrokes reach the DOM and never reach the form, so the button never\n * enables, and the failure screenshot shows the value sitting in the field as if\n * nothing were wrong. Typing again once the component is live registers it.\n *\n * `pressSequentially` rather than `fill` for the same reason at a smaller scale,\n * and because it is what the login's own acceptance suite uses.\n *\n * Every action is bounded and nothing here throws: losing a race with a\n * navigation is normal (the screen this was called for is already gone), and the\n * caller re-reads the route and decides what to do next. An unbounded action\n * would instead hang until the whole test times out, and report the wrong step.\n */\nconst submitHostedField = async (\n page: Page,\n testId: string,\n value: string,\n): Promise<void> => {\n const input = page.getByTestId(testId);\n const submit = page.getByTestId('submit-button');\n try {\n for (let attempt = 0; attempt < 4; attempt += 1) {\n await input.pressSequentially(value, { timeout: ACTION_TIMEOUT_MS });\n if (await submit.isEnabled()) break;\n // The keystrokes went to a component that was not listening yet. Clear\n // what the DOM kept and type it again into the hydrated one.\n await page.waitForTimeout(STEP_SETTLE_MS);\n await input.fill('', { timeout: ACTION_TIMEOUT_MS });\n }\n await submit.click({ timeout: ACTION_TIMEOUT_MS });\n } catch {\n // Deliberately swallowed — see above.\n }\n};\n\n/**\n * Wait for the hosted login to actually move on.\n *\n * A fixed sleep after each step is a race, not a wait: when the redirect is slow\n * the loop re-reads the same route and acts on a screen that is mid-navigation,\n * and when it is fast the sleep is wasted. This returns as soon as the route\n * changes or the session lands, whichever comes first.\n */\nconst waitForNextStep = async (\n context: BrowserContext,\n page: Page,\n fromPath: string,\n): Promise<void> => {\n for (let i = 0; i < STEP_POLLS; i += 1) {\n if ((await context.cookies()).some(c => c.name === ACCESS_COOKIE)) return;\n if (new URL(page.url()).pathname !== fromPath) return;\n await page.waitForTimeout(STEP_POLL_MS);\n }\n};\n\n/**\n * Sign in for real, through Zitadel's hosted login page.\n *\n * There is no API shortcut left and that is by design: no r10c endpoint accepts\n * a password, because r10c holds none\n * ([ADR 0016](../../../../../docs/adr/0016-zitadel-authenticates-r10c-authorizes.md)).\n * The only way to obtain a real session is the flow a person performs, so the\n * `live` profile performs it — which also means this fixture exercises the\n * redirect, the PKCE exchange and the callback rather than skipping them.\n *\n * Written against the **v2** hosted login (`ghcr.io/zitadel/zitadel-login`,\n * `infra/local/zitadel-login`). Screens are told apart by their **route**, not by\n * what is on them: v2 gives each step its own path under `/ui/v2/login`, while\n * its `data-testid`s are reused across screens (`reset-button` is \"Reset\n * password\" on one and \"Skip\" on another). Routes are also what the login app's\n * own acceptance suite drives, so they are the part of its surface most likely\n * to survive a release.\n */\nconst signInThroughHostedUi = async (\n context: BrowserContext,\n options: {\n readonly signInAppUrl: string;\n readonly identifier: string;\n readonly password: string;\n },\n): Promise<void> => {\n const page = await context.newPage();\n try {\n await page.goto(`${options.signInAppUrl}/api/auth/oidc/start`);\n\n // A step machine rather than a fixed sequence, because the hosted UI's path\n // is not fixed. Four shapes a scripted login/password pair walks into:\n //\n // - **`/loginname`**, the identifier screen, where a cold run starts.\n // - **`/password`**, after it.\n // - **`/mfa/set`**, offered after the password to anyone with no enrolled\n // factor. `forceMfa` is off, so it is a prompt with a skip.\n // - **`/accounts`**, when the browser still holds a provider session from an\n // earlier spec. There is no login field on it at all.\n // - **no screen at all**, when the provider session is still live and the\n // authorization completes on the redirect.\n //\n // The loop's exit condition is the access cookie rather than the URL,\n // because that is what \"signed in\" actually means here and it is the only\n // one of the three that a landing origin does not lie about — the callback\n // lands on whichever app the redirect names.\n for (let step = 0; step < 8; step += 1) {\n if ((await context.cookies()).some(c => c.name === ACCESS_COOKIE)) break;\n\n const path = new URL(page.url()).pathname;\n\n if (path.endsWith('/accounts')) {\n await page\n .getByRole('link', { name: /add another account/i })\n .click({ timeout: ACTION_TIMEOUT_MS });\n await waitForNextStep(context, page, path);\n continue;\n }\n\n // Skipping the enrolment offer, not declining MFA: a user who HAS a\n // factor is challenged at `/otp/...` or `/u2f` instead, and those screens\n // have no skip — which is why a spec needing an enrolled user must seed\n // its own code.\n if (path.includes('/mfa/set')) {\n await page\n .getByTestId('reset-button')\n .click({ timeout: ACTION_TIMEOUT_MS });\n await waitForNextStep(context, page, path);\n continue;\n }\n\n if (path.endsWith('/loginname')) {\n await submitHostedField(\n page,\n 'username-text-input',\n options.identifier,\n );\n await waitForNextStep(context, page, path);\n continue;\n }\n\n if (path.endsWith('/password')) {\n await submitHostedField(page, 'password-text-input', options.password);\n await waitForNextStep(context, page, path);\n continue;\n }\n\n // No cookie yet and on a route this fixture has not been taught — an MFA\n // challenge, a password change demand, or a screen v2 grew since. Name the\n // route, because a bare timeout would send the reader looking in the wrong\n // place.\n throw new Error(\n `seedSession: stuck on the hosted login at ${page.url()} (\"${await page.title()}\"). ` +\n `Route \"${path}\" is not one this fixture handles — a second factor, most likely.`,\n );\n }\n\n if (!(await context.cookies()).some(c => c.name === ACCESS_COOKIE)) {\n throw new Error(\n `seedSession: finished the hosted login but no ${ACCESS_COOKIE} cookie was set. Is auth-service running and seeded into Zitadel?`,\n );\n }\n } finally {\n await page.close();\n }\n};\n\n/**\n * Give the browser context a signed-in session before the first navigation.\n *\n * Profile-aware, because \"being signed in\" means different things in each:\n * `mock` fabricates the cookie, while `live` performs a **real sign-in** through\n * the provider's hosted page so the token is one auth-service actually minted\n * and the downstream `requirePermission` checks are genuinely exercised.\n *\n * Call it before `page.goto` — an app whose middleware protects the route will\n * otherwise redirect away before the spec can assert anything.\n */\nexport const seedSession = async (\n context: BrowserContext,\n {\n roles = ['user'],\n activeOrganizationId = 'e2e-organization',\n partyRole = 'vendor',\n partyId = 'party-user-2',\n entitlements = ['product-configuration-management'],\n locale = 'es',\n identifier = 'ada@example.com',\n password = 'Password123!',\n signInAppUrl = process.env['BACK_OFFICE_APP_URL'] ??\n 'http://localhost:3001',\n }: SeedSessionOptions = {},\n): Promise<void> => {\n if (!isMockProfile()) {\n await signInThroughHostedUi(context, {\n signInAppUrl,\n identifier,\n password,\n });\n await context.addCookies([\n { name: LOCALE_COOKIE, value: locale, domain: 'localhost', path: '/' },\n ]);\n return;\n }\n\n await context.addCookies([\n {\n name: ACCESS_COOKIE,\n value: fabricateToken(\n roles,\n activeOrganizationId,\n partyRole,\n partyId,\n entitlements,\n ),\n domain: 'localhost',\n path: '/',\n },\n {\n name: SESSION_COOKIE,\n value: 'e2e-session',\n domain: 'localhost',\n path: '/',\n },\n {\n name: LOCALE_COOKIE,\n value: locale,\n domain: 'localhost',\n path: '/',\n },\n ]);\n};\n"],"names":["process","isMockProfile","ACCESS_COOKIE","SESSION_COOKIE","LOCALE_COOKIE","STEP_SETTLE_MS","ACTION_TIMEOUT_MS","STEP_POLLS","STEP_POLL_MS","base64url","value","Buffer","from","toString","replace","fabricateToken","roles","activeOrganizationId","partyRole","partyId","entitlements","header","JSON","stringify","alg","kid","typ","payload","userId","subject","sessionId","exp","Math","floor","Date","now","submitHostedField","page","testId","input","getByTestId","submit","attempt","pressSequentially","timeout","isEnabled","waitForTimeout","fill","click","waitForNextStep","context","fromPath","i","cookies","some","c","name","URL","url","pathname","signInThroughHostedUi","options","newPage","goto","signInAppUrl","step","path","endsWith","getByRole","includes","identifier","password","Error","title","close","seedSession","locale","env","addCookies","domain"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/playwright/session.ts"],"sourcesContent":["import type { BrowserContext, Page } from '@playwright/test';\n\nimport { isMockProfile } from '../profile/profile.js';\n\n/**\n * The cookies a signed-in session travels in — the same names\n * `@entifix/core` declares, **written out rather than imported**.\n *\n * ⚠️ **This module must import no built package at runtime.** Nx builds its\n * project graph by loading every e2e project's `playwright.config.ts` in plain\n * Node — no source condition, nothing built — and that config reaches this\n * file. An `import … from '@entifix/core'` here resolves core's `dist`, which a\n * CI job that only lints never builds, and the whole graph fails with\n * `Cannot find module …/entifix-ts-core/dist/index.js`. It passed locally only\n * because a developer machine always has `dist` from a previous build.\n *\n * `session-cookies.spec.ts` imports core and fails if these drift, so the\n * duplication is checked rather than trusted.\n */\nexport const ACCESS_COOKIE = 'entifix_at';\nexport const SESSION_COOKIE = 'entifix_sid';\nexport const LOCALE_COOKIE = 'entifix_locale';\n\n/** Give a hosted-UI component time to hydrate before its input is retyped. */\nconst STEP_SETTLE_MS = 1_000;\n\n/**\n * Ceiling for a single hosted-UI action. Short on purpose: an action that cannot\n * complete has lost a race with a navigation, and the step machine wants to look\n * again rather than sit on it until the test's own timeout fires.\n */\nconst ACTION_TIMEOUT_MS = 10_000;\n\n/** How long a step waits for the next screen: 40 × 250ms = 10s. */\nconst STEP_POLLS = 40;\nconst STEP_POLL_MS = 250;\n\nexport interface SeedSessionOptions {\n /** Roles the seeded principal should carry. */\n roles?: readonly string[];\n /**\n * The organization the seeded session acts for. Tenant-plane routes resolve\n * their storage from this claim, so a spec that exercises one and omits it\n * gets `409 noActiveOrganization` rather than data. Pass `null` to seed a\n * session with no tenant scope on purpose — an operator, or a buyer.\n */\n activeOrganizationId?: string | null;\n /**\n * Which population the seeded principal belongs to — `customer`, `vendor` or\n * `operator`. Defaults to `vendor`, matching the default organization: a\n * session that acts for one is a vendor member's. A spec exercising platform\n * staff passes `operator` together with `activeOrganizationId: null`.\n */\n partyRole?: string;\n /**\n * The party the seeded principal is — the `Individual` id a route scoping a\n * read to its caller matches against. Defaults to the seeded vendor party, so\n * the fixture keeps mirroring what a real sign-in produces.\n *\n * Pass `null` to seed an account with no party record, which is the case a\n * scoped read must answer nothing for rather than everything.\n */\n partyId?: string | null;\n /**\n * The domains the seeded organization is provisioned for — the second\n * ceiling beside the role, which the back-office nav reads.\n *\n * Defaults to the one domain `entitlementSeedData` gives the demo\n * organization, so the fixture keeps mirroring what a real sign-in produces.\n * Pass `[]` to seed a member of an organization provisioned for nothing;\n * that is a different principal from one with `activeOrganizationId: null`,\n * who is outside the ceiling rather than refused by it.\n */\n entitlements?: readonly string[];\n /**\n * Locale to pin the run to. Seeded in both profiles so a run does not depend\n * on the CI machine's `Accept-Language` — without it the same spec renders\n * Spanish locally and English on a differently-configured runner.\n */\n locale?: string;\n /**\n * Identifier + password for the `live` profile's real sign-in, performed\n * against Zitadel's hosted login page. The default is the seeded super-admin\n * and the dev password `auth-service` provisions them with.\n */\n identifier?: string;\n password?: string;\n /** Where the sign-in surface is served in `live`. */\n signInAppUrl?: string;\n}\n\nconst base64url = (value: string): string =>\n Buffer.from(value)\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\n/**\n * A structurally valid, **deliberately unsigned** access token. It satisfies\n * exactly the two things the mock profile exercises — the middleware's cookie\n * presence check and the server-rendered nav's unverified claim read — and\n * nothing else: no service verifies it, because in `mock` the services are msw\n * fixtures. Signing it would need the private key, which the suite has no\n * business knowing and which is exactly what asymmetric signing exists to keep\n * out of places like this.\n */\nconst fabricateToken = (\n roles: readonly string[],\n activeOrganizationId: string | null,\n partyRole: string,\n partyId: string | null,\n entitlements: readonly string[],\n): string => {\n const header = base64url(\n JSON.stringify({ alg: 'RS256', kid: 'e2e-not-a-real-key', typ: 'JWT' }),\n );\n const payload = base64url(\n JSON.stringify({\n userId: 'e2e-user',\n subject: 'e2e-user',\n sessionId: 'e2e-session',\n roles,\n partyRole,\n ...(partyId === null ? {} : { partyId }),\n ...(activeOrganizationId === null\n ? {}\n : { activeOrganizationId, entitlements }),\n exp: Math.floor(Date.now() / 1000) + 3600,\n }),\n );\n return `${header}.${payload}.e2e-not-a-real-signature`;\n};\n\n/**\n * Fill one field of the hosted login and submit it.\n *\n * The retype is load-bearing, not defensive. v2 disables Continue on\n * `!formState.isValid` — react-hook-form state, not the DOM value — and\n * Playwright can type into the input before Next has hydrated the component.\n * Those keystrokes reach the DOM and never reach the form, so the button never\n * enables, and the failure screenshot shows the value sitting in the field as if\n * nothing were wrong. Typing again once the component is live registers it.\n *\n * `pressSequentially` rather than `fill` for the same reason at a smaller scale,\n * and because it is what the login's own acceptance suite uses.\n *\n * Every action is bounded and nothing here throws: losing a race with a\n * navigation is normal (the screen this was called for is already gone), and the\n * caller re-reads the route and decides what to do next. An unbounded action\n * would instead hang until the whole test times out, and report the wrong step.\n */\nconst submitHostedField = async (\n page: Page,\n testId: string,\n value: string,\n): Promise<void> => {\n const input = page.getByTestId(testId);\n const submit = page.getByTestId('submit-button');\n try {\n for (let attempt = 0; attempt < 4; attempt += 1) {\n await input.pressSequentially(value, { timeout: ACTION_TIMEOUT_MS });\n if (await submit.isEnabled()) break;\n // The keystrokes went to a component that was not listening yet. Clear\n // what the DOM kept and type it again into the hydrated one.\n await page.waitForTimeout(STEP_SETTLE_MS);\n await input.fill('', { timeout: ACTION_TIMEOUT_MS });\n }\n await submit.click({ timeout: ACTION_TIMEOUT_MS });\n } catch {\n // Deliberately swallowed — see above.\n }\n};\n\n/**\n * Wait for the hosted login to actually move on.\n *\n * A fixed sleep after each step is a race, not a wait: when the redirect is slow\n * the loop re-reads the same route and acts on a screen that is mid-navigation,\n * and when it is fast the sleep is wasted. This returns as soon as the route\n * changes or the session lands, whichever comes first.\n */\nconst waitForNextStep = async (\n context: BrowserContext,\n page: Page,\n fromPath: string,\n): Promise<void> => {\n for (let i = 0; i < STEP_POLLS; i += 1) {\n if ((await context.cookies()).some(c => c.name === ACCESS_COOKIE)) return;\n if (new URL(page.url()).pathname !== fromPath) return;\n await page.waitForTimeout(STEP_POLL_MS);\n }\n};\n\n/**\n * Sign in for real, through Zitadel's hosted login page.\n *\n * There is no API shortcut left and that is by design: no host endpoint accepts\n * a password, because a host that delegates authentication holds none.\n * The only way to obtain a real session is the flow a person performs, so the\n * `live` profile performs it — which also means this fixture exercises the\n * redirect, the PKCE exchange and the callback rather than skipping them.\n *\n * Written against the **v2** hosted login (`ghcr.io/zitadel/zitadel-login`,\n * `infra/local/zitadel-login`). Screens are told apart by their **route**, not by\n * what is on them: v2 gives each step its own path under `/ui/v2/login`, while\n * its `data-testid`s are reused across screens (`reset-button` is \"Reset\n * password\" on one and \"Skip\" on another). Routes are also what the login app's\n * own acceptance suite drives, so they are the part of its surface most likely\n * to survive a release.\n */\nconst signInThroughHostedUi = async (\n context: BrowserContext,\n options: {\n readonly signInAppUrl: string;\n readonly identifier: string;\n readonly password: string;\n },\n): Promise<void> => {\n const page = await context.newPage();\n try {\n await page.goto(`${options.signInAppUrl}/api/auth/oidc/start`);\n\n // A step machine rather than a fixed sequence, because the hosted UI's path\n // is not fixed. Four shapes a scripted login/password pair walks into:\n //\n // - **`/loginname`**, the identifier screen, where a cold run starts.\n // - **`/password`**, after it.\n // - **`/mfa/set`**, offered after the password to anyone with no enrolled\n // factor. `forceMfa` is off, so it is a prompt with a skip.\n // - **`/accounts`**, when the browser still holds a provider session from an\n // earlier spec. There is no login field on it at all.\n // - **no screen at all**, when the provider session is still live and the\n // authorization completes on the redirect.\n //\n // The loop's exit condition is the access cookie rather than the URL,\n // because that is what \"signed in\" actually means here and it is the only\n // one of the three that a landing origin does not lie about — the callback\n // lands on whichever app the redirect names.\n for (let step = 0; step < 8; step += 1) {\n if ((await context.cookies()).some(c => c.name === ACCESS_COOKIE)) break;\n\n const path = new URL(page.url()).pathname;\n\n if (path.endsWith('/accounts')) {\n await page\n .getByRole('link', { name: /add another account/i })\n .click({ timeout: ACTION_TIMEOUT_MS });\n await waitForNextStep(context, page, path);\n continue;\n }\n\n // Skipping the enrolment offer, not declining MFA: a user who HAS a\n // factor is challenged at `/otp/...` or `/u2f` instead, and those screens\n // have no skip — which is why a spec needing an enrolled user must seed\n // its own code.\n if (path.includes('/mfa/set')) {\n await page\n .getByTestId('reset-button')\n .click({ timeout: ACTION_TIMEOUT_MS });\n await waitForNextStep(context, page, path);\n continue;\n }\n\n if (path.endsWith('/loginname')) {\n await submitHostedField(\n page,\n 'username-text-input',\n options.identifier,\n );\n await waitForNextStep(context, page, path);\n continue;\n }\n\n if (path.endsWith('/password')) {\n await submitHostedField(page, 'password-text-input', options.password);\n await waitForNextStep(context, page, path);\n continue;\n }\n\n // No cookie yet and on a route this fixture has not been taught — an MFA\n // challenge, a password change demand, or a screen v2 grew since. Name the\n // route, because a bare timeout would send the reader looking in the wrong\n // place.\n throw new Error(\n `seedSession: stuck on the hosted login at ${page.url()} (\"${await page.title()}\"). ` +\n `Route \"${path}\" is not one this fixture handles — a second factor, most likely.`,\n );\n }\n\n if (!(await context.cookies()).some(c => c.name === ACCESS_COOKIE)) {\n throw new Error(\n `seedSession: finished the hosted login but no ${ACCESS_COOKIE} cookie was set. Is auth-service running and seeded into Zitadel?`,\n );\n }\n } finally {\n await page.close();\n }\n};\n\n/**\n * Give the browser context a signed-in session before the first navigation.\n *\n * Profile-aware, because \"being signed in\" means different things in each:\n * `mock` fabricates the cookie, while `live` performs a **real sign-in** through\n * the provider's hosted page so the token is one auth-service actually minted\n * and the downstream `requirePermission` checks are genuinely exercised.\n *\n * Call it before `page.goto` — an app whose middleware protects the route will\n * otherwise redirect away before the spec can assert anything.\n */\nexport const seedSession = async (\n context: BrowserContext,\n {\n roles = ['user'],\n activeOrganizationId = 'e2e-organization',\n partyRole = 'vendor',\n partyId = 'party-user-2',\n entitlements = ['product-configuration-management'],\n locale = 'es',\n identifier = 'ada@example.com',\n password = 'Password123!',\n signInAppUrl = process.env['BACK_OFFICE_APP_URL'] ??\n 'http://localhost:3001',\n }: SeedSessionOptions = {},\n): Promise<void> => {\n if (!isMockProfile()) {\n await signInThroughHostedUi(context, {\n signInAppUrl,\n identifier,\n password,\n });\n await context.addCookies([\n { name: LOCALE_COOKIE, value: locale, domain: 'localhost', path: '/' },\n ]);\n return;\n }\n\n await context.addCookies([\n {\n name: ACCESS_COOKIE,\n value: fabricateToken(\n roles,\n activeOrganizationId,\n partyRole,\n partyId,\n entitlements,\n ),\n domain: 'localhost',\n path: '/',\n },\n {\n name: SESSION_COOKIE,\n value: 'e2e-session',\n domain: 'localhost',\n path: '/',\n },\n {\n name: LOCALE_COOKIE,\n value: locale,\n domain: 'localhost',\n path: '/',\n },\n ]);\n};\n"],"names":["process","isMockProfile","ACCESS_COOKIE","SESSION_COOKIE","LOCALE_COOKIE","STEP_SETTLE_MS","ACTION_TIMEOUT_MS","STEP_POLLS","STEP_POLL_MS","base64url","value","Buffer","from","toString","replace","fabricateToken","roles","activeOrganizationId","partyRole","partyId","entitlements","header","JSON","stringify","alg","kid","typ","payload","userId","subject","sessionId","exp","Math","floor","Date","now","submitHostedField","page","testId","input","getByTestId","submit","attempt","pressSequentially","timeout","isEnabled","waitForTimeout","fill","click","waitForNextStep","context","fromPath","i","cookies","some","c","name","URL","url","pathname","signInThroughHostedUi","options","newPage","goto","signInAppUrl","step","path","endsWith","getByRole","includes","identifier","password","Error","title","close","seedSession","locale","env","addCookies","domain"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/testing-e2e",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "End-to-end testing with one set of journeys and two profiles: mocked backends by default, real infrastructure on demand.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -67,9 +67,9 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@swc/helpers": "0.5.23",
70
- "@entifix/core": "0.5.0",
71
- "@entifix/testing-unit": "0.5.0",
72
- "@entifix/business": "0.5.0"
70
+ "@entifix/business": "0.7.0",
71
+ "@entifix/testing-unit": "0.7.0",
72
+ "@entifix/core": "0.7.0"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@nx/devkit": ">=23 <24",
@@ -80,9 +80,9 @@
80
80
  "msw": ">=3",
81
81
  "@msw/playwright": ">=0.7",
82
82
  "vitest": "*",
83
- "@entifix/amqp": "0.5.0",
84
- "@entifix/redis": "0.5.0",
85
- "@entifix/mongo": "0.5.0"
83
+ "@entifix/amqp": "0.7.0",
84
+ "@entifix/mongo": "0.7.0",
85
+ "@entifix/redis": "0.7.0"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@msw/playwright": {
@@ -120,8 +120,8 @@
120
120
  "@nx/devkit": "23.3.0",
121
121
  "@nx/playwright": "23.3.0",
122
122
  "effect": "4.0.5",
123
- "@entifix/amqp": "0.5.0",
124
- "@entifix/mongo": "0.5.0",
125
- "@entifix/redis": "0.5.0"
123
+ "@entifix/mongo": "0.7.0",
124
+ "@entifix/amqp": "0.7.0",
125
+ "@entifix/redis": "0.7.0"
126
126
  }
127
127
  }