@owlmeans/test-ui 0.1.18-rc.5 → 0.1.18-rc.51

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.
Files changed (69) hide show
  1. package/README.md +10 -7
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/testing-ui/SKILL.md +214 -24
  4. package/build/browser/types.d.ts +22 -0
  5. package/build/browser/types.d.ts.map +1 -0
  6. package/build/browser/types.js +2 -0
  7. package/build/browser/types.js.map +1 -0
  8. package/build/browser.d.ts +8 -16
  9. package/build/browser.d.ts.map +1 -1
  10. package/build/browser.js +38 -41
  11. package/build/browser.js.map +1 -1
  12. package/build/consent/types.d.ts +36 -0
  13. package/build/consent/types.d.ts.map +1 -0
  14. package/build/consent/types.js +2 -0
  15. package/build/consent/types.js.map +1 -0
  16. package/build/consent.d.ts +4 -0
  17. package/build/consent.d.ts.map +1 -0
  18. package/build/consent.js +41 -0
  19. package/build/consent.js.map +1 -0
  20. package/build/consts.local.d.ts +9 -0
  21. package/build/consts.local.d.ts.map +1 -0
  22. package/build/consts.local.js +9 -0
  23. package/build/consts.local.js.map +1 -0
  24. package/build/index.d.ts +8 -0
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js +3 -0
  27. package/build/index.js.map +1 -1
  28. package/build/marketing-consent.d.ts +5 -0
  29. package/build/marketing-consent.d.ts.map +1 -0
  30. package/build/marketing-consent.js +4 -0
  31. package/build/marketing-consent.js.map +1 -0
  32. package/build/mount.d.ts +2 -27
  33. package/build/mount.d.ts.map +1 -1
  34. package/build/mount.js +11 -4
  35. package/build/mount.js.map +1 -1
  36. package/build/page/types.d.ts +141 -0
  37. package/build/page/types.d.ts.map +1 -0
  38. package/build/page/types.js +2 -0
  39. package/build/page/types.js.map +1 -0
  40. package/build/page.d.ts +4 -0
  41. package/build/page.d.ts.map +1 -0
  42. package/build/page.js +168 -0
  43. package/build/page.js.map +1 -0
  44. package/build/supervisor/types.d.ts +56 -0
  45. package/build/supervisor/types.d.ts.map +1 -0
  46. package/build/supervisor/types.js +2 -0
  47. package/build/supervisor/types.js.map +1 -0
  48. package/build/supervisor.d.ts +9 -86
  49. package/build/supervisor.d.ts.map +1 -1
  50. package/build/supervisor.js +65 -123
  51. package/build/supervisor.js.map +1 -1
  52. package/build/types.d.ts +46 -0
  53. package/build/types.d.ts.map +1 -0
  54. package/build/types.js +2 -0
  55. package/build/types.js.map +1 -0
  56. package/package.json +11 -11
  57. package/src/browser/types.ts +22 -0
  58. package/src/browser.ts +43 -40
  59. package/src/consent/types.ts +38 -0
  60. package/src/consent.ts +48 -0
  61. package/src/consts.local.ts +12 -0
  62. package/src/index.ts +8 -0
  63. package/src/marketing-consent.ts +8 -0
  64. package/src/mount.ts +12 -32
  65. package/src/page/types.ts +144 -0
  66. package/src/page.ts +194 -0
  67. package/src/supervisor/types.ts +55 -0
  68. package/src/supervisor.ts +83 -187
  69. package/src/types.ts +47 -0
package/README.md CHANGED
@@ -6,19 +6,22 @@ These are **not** end-to-end UI tests — they exercise individual components in
6
6
 
7
7
  ## Exports
8
8
 
9
- - `launchBrowser(opts?)` — return the shared chromium instance for this `bun test` process. Idempotent.
10
- - `closeBrowser()` — tear it down. Call from `afterAll`.
11
- - `withPage(fn)` — convenience: lease a fresh context+page for the duration of `fn`, then dispose the context.
12
- - `mountComponent({ url, component?, props? })` — opens a fresh context, navigates to a harness URL with the component / props encoded, returns `{ page, close }` so the spec can assert against `page.locator(...)` and dispose the context when done.
9
+ - `browserHelper.launchBrowser(opts?)` — return the shared chromium instance for this `bun test` process. Idempotent.
10
+ - `browserHelper.closeBrowser()` — tear it down. Call from `afterAll`.
11
+ - `browserHelper.withPage(fn)` — convenience: lease a fresh context+page for the duration of `fn`, then dispose the context.
12
+ - `mountComponent({ url, component?, props?, consentGeo? })` — opens a fresh context, navigates to a harness URL with the component / props encoded, returns `{ page, close }` so the spec can assert against `page.locator(...)` and dispose the context when done. `consentGeo` pins where the visitor is before the first navigation.
13
+ - `consentGeoTestHelper.mockConsentGeo(pageOrContext, geo)` / `.traceCalls(page)` — answer the page's same-origin `/cdn-cgi/trace` from an init script (`'US'`, or `{ country?, fail?, delayMs?, gpc? }`), so a consent test does not depend on the runner's own country; count the trace requests.
14
+ - `makePageHelper(page)` — drives one page of an OwlMeans app: `.acceptConsent(opts?)`, `.answerMarketingConsent(opts?)`, `.loginViaDispatcher(baseUrl, token, opts?)`, `.loginViaSupervisorForm(opts)`, `.saveScreenshot(dir, name)`.
15
+ - `supervisorAuthHelper` — bearers without a browser: `.pregenerateAuthToken(opts)` (signed offline) and `.authenticateViaSupervisorApi(opts)` (the live supervisor flow over the API).
13
16
  - `Browser`, `BrowserContext`, `Page`, `Locator` — re-exports of the Playwright types so consumers don't need a direct `playwright` import.
14
17
 
15
18
  ## Spec shape
16
19
 
17
20
  ```ts
18
21
  import { afterAll, describe, expect, test } from 'bun:test'
19
- import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
22
+ import { browserHelper, mountComponent } from '@owlmeans/test-ui'
20
23
 
21
- afterAll(async () => { await closeBrowser() })
24
+ afterAll(async () => { await browserHelper.closeBrowser() })
22
25
 
23
26
  describe('LoginForm — acceptance', () => {
24
27
  test('renders the heading from the SKILL example', async () => {
@@ -58,7 +61,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
58
61
  your project's skill store (`.agents/skills/`):
59
62
 
60
63
  ```sh
61
- npx @owlmeans/agent-skills
64
+ npx @owlmeans/agent-skills@^0.1.18-rc.52
62
65
  ```
63
66
 
64
67
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/test-ui",
4
- "version": "0.1.18-rc.0",
5
- "generatedAt": "2026-08-16T22:20:50.499Z",
4
+ "version": "0.1.18-rc.51",
5
+ "generatedAt": "2026-10-08T21:27:54.284Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,16 +1,29 @@
1
1
  ---
2
2
  name: testing-ui
3
- description: Category-D component-level acceptance tests for OwlMeans Common UI packages (web-panel, web-client, client-panel, client-wl, web-flow, web-wl, client, client-i18n) and new shadcn UI + Tailwind v4 packages. Run under bun test, drive a real chromium via Playwright as a library — not the Playwright runner. Auto-invoked when writing tests in those packages.
3
+ description: Category-D component-level acceptance tests for OwlMeans Common UI packages (web-panel, web-client, web-consent, web-router, mui-panel, client-panel, client-wl, web-flow, web-wl, client, client-i18n) and shadcn UI + Tailwind v4 packages. Run under bun test, drive a real chromium via Playwright as a library — not the Playwright runner. Auto-invoked when writing tests in those packages.
4
4
  ---
5
5
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
6
6
 
7
7
  # UI Acceptance Tests — Category D (bun test + Playwright as a library)
8
8
 
9
- Category D applies to packages that ship rendered React UI: `client`, `client-i18n`, `client-panel`, `client-wl`, `web-client`, `web-flow`, `web-panel`, `web-wl`. Tests are **component-level acceptance** — they mount one component in a real browser and assert against rendered DOM. They are **not** end-to-end tests (no router-deep navigation, no live backend).
9
+ **Install:** `"@owlmeans/test-ui": "^0.1.18-rc.51"` in `devDependencies`
10
+
11
+ `@owlmeans/test-ui` depends on `playwright`, so the browser library arrives with it; add
12
+ `playwright` to `devDependencies` as well when a spec imports a launcher itself. The Vite harness
13
+ below is the consuming package's own, so a package that builds one also declares `vite` and
14
+ `@vitejs/plugin-react`, plus `@tailwindcss/vite` and `tailwindcss` for a shadcn package. A
15
+ smoke-only package that serves a `data:` URL needs none of those.
16
+
17
+ Category D applies to packages that ship a rendered surface: `client`, `client-i18n`, `client-panel`, `client-wl`, `mui-panel`, `web-client`, `web-consent`, `web-flow`, `web-oauth`, `web-panel`, `web-router`, `web-wl`. Tests are **component-level acceptance** — they mount one component in a real browser and assert against rendered DOM. They are **not** end-to-end tests (no live backend).
10
18
 
11
19
  The runner is **`bun test`** — same as categories A/B/C — kept consistent so contributors only learn one harness. Playwright is consumed as a **library** (`playwright` package, not `@playwright/test`). The `playwright` library exposes `chromium`, `firefox`, `webkit` browser launchers; specs drive them directly from inside `bun:test` blocks.
12
20
 
13
- Service-only "client-*" packages (`client-flow`, `client-socket`, `web-router`, `web-db`) belong to category A — they have no rendered UI.
21
+ A client-side package that ships no component belongs to category A, however browser-flavoured its
22
+ name — `client-config`, `client-context`, `client-entrypoint`, `client-flow`, `client-job`,
23
+ `client-resource`, `client-route`, `client-socket`, `web-db`, `web-gtm` and
24
+ `web-router-react-router` are all services, models and adapters. `@owlmeans/web-router` is the one
25
+ that is not: it drives the real History API, so its routing behaviour is only observable in a
26
+ browser and its specs mount a full harness here.
14
27
 
15
28
  Auth-related UI packages (`web-oidc-rp`, `web-oidc-provider`, `client-auth`, `client-payment`) belong to category B and use `bun test` + `@owlmeans/test-auth`, not the chromium harness.
16
29
 
@@ -20,9 +33,9 @@ Auth-related UI packages (`web-oidc-rp`, `web-oidc-provider`, `client-auth`, `cl
20
33
  <your-package>/
21
34
  ├── src/...
22
35
  └── tests/
23
- ├── harness/ # per-package: components registry + mount config
36
+ ├── harness/ # per-package: the app the specs drive
24
37
  │ ├── index.html # copy of @owlmeans/test-ui/harness/index.html
25
- │ └── mount.tsx # registers components, reads ?component= and ?props=
38
+ │ └── mount.tsx # boots the package's real surface into #root
26
39
  ├── context.ts # one place that boots the harness server, exports HARNESS_URL
27
40
  └── <area>.spec.ts # *.spec.ts only
28
41
  ```
@@ -39,38 +52,69 @@ Downloads the chromium binary the `playwright` library drives. CI installs it in
39
52
 
40
53
  | Helper | Purpose |
41
54
  |---|---|
42
- | `launchBrowser(opts?)` | Return the shared chromium for this `bun test` process. Idempotent. |
43
- | `closeBrowser()` | Tear it down. Call from `afterAll`. |
44
- | `withPage(fn)` | Lease a fresh context+page for the duration of `fn`, dispose context on completion. |
45
- | `mountComponent({ url, component?, props? })` | Open a fresh context, navigate to the harness URL with `?component=` and `?props=` set, return `{ page, close }`. |
55
+ | `browserHelper.launchBrowser(opts?)` | Return the shared chromium for this `bun test` process. Idempotent. |
56
+ | `browserHelper.closeBrowser()` | Tear it down. Call from `afterAll`. |
57
+ | `browserHelper.withPage(fn)` | Lease a fresh context+page for the duration of `fn`, dispose context on completion. |
58
+ | `mountComponent(opts)` | Open a fresh context, navigate to `opts.url`, return `Mounted` = `{ page, close }`. `component` and `props`, when given, are appended as `?component=` and `?props=`; omit them when the harness mounts a fixed root, which is what every harness here does. |
59
+ | `MountOptions` | `{ url, component?, props?, waitUntil?, timeout? }` — `waitUntil` defaults to `domcontentloaded`, see below. |
60
+ | `makePageHelper(page).acceptConsent({ timeout? })` | Wait up to `timeout` (default 5s) for the consent store to SETTLE — `[data-consent-dialog]` up (bar or window), or `<html data-consent="decided">` — then accept all and wait for it to detach. Returns whether it answered one; a decision made without UI returns `false` at once. |
61
+ | `consentGeoTestHelper.mockConsentGeo(pageOrContext, geo)` / `.traceCalls(page)` | Pin where the visitor is: an init script answering the same-origin `/cdn-cgi/trace` (`'US'`, or `{ country?, fail?: true \| 'hang', delayMs?, gpc? }`), installed for the NEXT navigation; counts the calls in the page. `mountComponent({ …, consentGeo })` installs it before the first one. |
62
+ | `makePageHelper(page).saveScreenshot(dir, name)` | Full-page PNG to `<dir>/<name>.png`, creating `dir`. Returns the absolute path. |
46
63
  | Re-exports: `Browser`, `BrowserContext`, `Page`, `Locator` | Playwright types — no direct `playwright` import needed. |
47
64
 
65
+ ### Authenticated specs
66
+
67
+ `@owlmeans/test-ui` also carries the supervisor-login helpers, so a spec can reach a screen that
68
+ sits behind auth without hand-rolling a token:
69
+
70
+ | Helper | Purpose |
71
+ |---|---|
72
+ | `supervisorAuthHelper.pregenerateAuthToken(opts)` | Mint an `ED25519-BASIC-TOKEN …` bearer offline from a trusted private key — no browser, no round trip. Options: `{ userId, pk, scopes?, role?, entityId?, profileId?, source? }`. |
73
+ | `supervisorAuthHelper.authenticateViaSupervisorApi(opts)` | Drive the live PK supervisor flow over the backend API (init → sign → authenticate → dispatch), registering the user on first use. Options: `{ apiBaseUrl, userId, pk, paths?, fetchImpl? }`. |
74
+ | `makePageHelper(page).loginViaDispatcher(baseUrl, token, opts?)` | Inject a bearer through the standard `/dispatcher?token=…` route, wait for the app to navigate away, then answer marketing consent (see below). Options: `{ dispatcherPath?, waitUntil?, marketingConsent? }`. |
75
+ | `makePageHelper(page).loginViaSupervisorForm(opts)` | Drive the real login form end-to-end — navigate, answer consent, fill user id + key, submit, wait for the landing, then answer marketing consent (see below). Options: `{ baseUrl, userId, pk, path?, expectPath?, timeout?, waitUntil?, consent?, screenshotDir?, marketingConsent? }`. |
76
+ | `makePageHelper(page).answerMarketingConsent(opts?)` | Answer the `@owlmeans/web-marketing-consent` full-page step if it is shown right after login. Options: `{ accept?: 'all' \| 'none' \| 'skip', terms?: 'accept' \| 'leave', timeout? }`, default `accept: 'all'`, `terms: 'accept'`, `timeout: 30_000`. Returns whether the step was actually shown and answered — `false` means it never appeared. Throws if a Terms box could not be confirmed. |
77
+
78
+ These need a project whose backend trusts the private key they sign with, so they belong to specs
79
+ that run against a real app rather than a mounted component. See `[[supervisor-auth]]` for the
80
+ server and web wiring they assume.
81
+
48
82
  ## Spec shape
49
83
 
50
84
  ```ts
51
85
  import { afterAll, describe, expect, test } from 'bun:test'
52
- import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
86
+ import { browserHelper, mountComponent } from '@owlmeans/test-ui'
53
87
  import { HARNESS_URL } from './context.js'
54
88
 
55
- afterAll(async () => { await closeBrowser() })
89
+ // Browser work does not fit bun's 5s default: a cold harness compiles the app on first request,
90
+ // and the first test to run pays for the Vite boot happening in the same process.
91
+ const TIMEOUT = 30_000
92
+
93
+ afterAll(async () => { await browserHelper.closeBrowser() })
94
+
95
+ // The harness is a real app, so a spec picks its case by opening a path on it.
96
+ const open = async (path: string) =>
97
+ mountComponent({ url: `${HARNESS_URL.replace(/\/$/, '')}${path}` })
56
98
 
57
99
  describe('<package> — <component>', () => {
58
100
  test('renders the SKILL.md happy-path example', async () => {
59
- const { page, close } = await mountComponent({
60
- url: HARNESS_URL,
61
- component: 'LoginForm',
62
- props: { redirect: '/' },
63
- })
101
+ const { page, close } = await open('/login')
64
102
  try {
65
103
  expect(await page.locator('h1').textContent()).toBe('Sign in')
66
104
  expect(await page.getByRole('button', { name: /sign in/i }).isVisible()).toBe(true)
67
105
  } finally {
68
106
  await close()
69
107
  }
70
- })
108
+ }, TIMEOUT)
71
109
  })
72
110
  ```
73
111
 
112
+ **Every chromium test carries its own timeout** — the third argument to `test`. Bun's default is
113
+ 5s, which a cold harness blows through before the first assertion, and `bunfig.toml`'s
114
+ `[test] timeout` does not raise it (bun 1.4.0 reads the section but ignores that key). 30s covers a
115
+ mounted component against a Vite harness; a suite that drives a login form or a consent gate needs
116
+ 60s. `bun test --timeout=<ms>` raises a whole run from the command line.
117
+
74
118
  Use `bun:test`'s `expect` against awaited Playwright DOM queries (`textContent()`, `isVisible()`, `getAttribute()`). Don't import `expect` from `@playwright/test` — that would pull in the full runner and create two parallel test toolchains.
75
119
 
76
120
  ## Per-package `tests/context.ts`
@@ -93,7 +137,12 @@ export const getHarnessUrl = async (): Promise<string> => {
93
137
  configFile: false,
94
138
  root: resolve(here, './harness'),
95
139
  plugins: [react()],
140
+ // Pre-bundle the third-party runtime deps the mounted components pull in. A dependency
141
+ // discovered mid-transform makes Vite re-optimize and reload the page underneath the
142
+ // navigation, and the first `goto` of a cold run pays for that as a timeout.
143
+ optimizeDeps: { include: ['<runtime dep the components import>'] },
96
144
  server: { port: 0 },
145
+ logLevel: 'warn',
97
146
  })
98
147
  await server.listen()
99
148
  const local = server.resolvedUrls?.local?.[0]
@@ -105,29 +154,44 @@ export const getHarnessUrl = async (): Promise<string> => {
105
154
  export const HARNESS_URL = await getHarnessUrl()
106
155
  ```
107
156
 
108
- The per-package `tests/harness/mount.tsx` reads `?component=<name>` and `?props=<json>`, dynamic-imports the component from the consumer's `src/`, and renders it via `react-dom/client` inside `<div id="root">`. Each consuming UI package wires its own providers (MUI theme for legacy packages, or Tailwind CSS + `@` alias for shadcn packages; plus i18n, router) the way the real app does — that's why mount.tsx is per-package, not in `@owlmeans/test-ui`.
157
+ List in `optimizeDeps.include` every non-`@owlmeans` runtime package the components reach — the
158
+ toast library, the icon set, a form library. It is the difference between a cold run that passes
159
+ and one that times out on its first navigation for no visible reason.
160
+
161
+ The per-package `tests/harness/mount.tsx` statically imports what the package ships and renders it
162
+ via `react-dom/client` into the `<div id="root">` that `index.html` provides. It builds a real
163
+ application — `@owlmeans/web-panel` wires a context, the framework entrypoints and a `NavLayout`
164
+ over its screens; `@owlmeans/web-router` compiles a route tree behind the browser router plugin —
165
+ so a spec selects its case by the path it opens rather than by naming a component. A harness that
166
+ needs variants of one surface reads its own query string for them, the way `@owlmeans/web-consent`
167
+ picks a locale, a category set or the policy view. `mountComponent`'s `component` and `props`
168
+ options are the ready-made form of that convention, for a harness written to read them.
169
+ Each consuming UI package wires its own providers (a theme provider for the `mui-*` packages, or Tailwind CSS + the `@` alias for shadcn packages; plus i18n, router) the way the real app does — that's why mount.tsx is per-package, not in `@owlmeans/test-ui`.
109
170
 
110
171
  ## Smoke-only baseline
111
172
 
112
- For a package that only needs to prove the bun-test + chromium pipeline works (e.g. `web-panel`'s pilot), skip the Vite harness and serve a `data:text/html` URL inline:
173
+ For a package that only needs to prove the bun-test + chromium pipeline works, skip the Vite harness and serve a `data:text/html` URL inline. Use this for a first spec in a package that has no harness yet — `web-panel` keeps one alongside a full Vite + chromium harness (`tests/harness/` plus a `tests/context.ts` wiring the Tailwind plugin, the `@` alias and a react dedupe), and that harness is the reference to copy for any package rendering real components:
113
174
 
114
175
  ```ts
115
176
  import { afterAll, describe, expect, test } from 'bun:test'
116
- import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
177
+ import { browserHelper, mountComponent } from '@owlmeans/test-ui'
117
178
 
118
179
  const harness = `data:text/html,${encodeURIComponent('<h1 id="t">hello</h1>')}`
119
180
 
120
- afterAll(async () => { await closeBrowser() })
181
+ afterAll(async () => { await browserHelper.closeBrowser() })
121
182
 
122
183
  describe('chromium smoke', () => {
123
184
  test('reads a heading from a data: URL', async () => {
124
185
  const { page, close } = await mountComponent({ url: harness })
125
186
  try { expect(await page.locator('#t').textContent()).toBe('hello') }
126
187
  finally { await close() }
127
- })
188
+ }, 30_000)
128
189
  })
129
190
  ```
130
191
 
192
+ A smoke spec needs the same explicit timeout as any other: bun runs every spec file of a package
193
+ in one process, so the first test to start pays for whatever harness the sibling files boot.
194
+
131
195
  ## Wiring `bun test`
132
196
 
133
197
  Per-package `package.json`:
@@ -142,7 +206,7 @@ Same line as categories A/B/C. No `playwright.config.ts`, no `bun x playwright t
142
206
 
143
207
  ## Tailwind + shadcn packages
144
208
 
145
- For shadcn-based OwlMeans packages the `tests/context.ts` Vite config needs two additions beyond the standard setup:
209
+ For shadcn-based OwlMeans packages the `tests/context.ts` Vite config needs three additions beyond the standard setup:
146
210
 
147
211
  ```ts
148
212
  import tailwindcss from '@tailwindcss/vite'
@@ -154,11 +218,16 @@ const server = await createServer({
154
218
  plugins: [react(), tailwindcss()], // add Tailwind v4 Vite plugin
155
219
  resolve: {
156
220
  alias: { '@': resolve(here, '../src/@') }, // resolve @/ to the package's local copy
221
+ dedupe: ['react', 'react-dom'], // one React across the workspace links
157
222
  },
223
+ optimizeDeps: { include: ['sonner'] }, // pre-bundle, never discover mid-navigation
158
224
  server: { port: 0 },
225
+ logLevel: 'warn',
159
226
  })
160
227
  ```
161
228
 
229
+ `dedupe` is not optional in a workspace: symlinked sibling packages otherwise resolve their own React copy and every hook throws.
230
+
162
231
  `tests/harness/mount.tsx` imports the package CSS so components render with Tailwind styles:
163
232
 
164
233
  ```tsx
@@ -166,6 +235,15 @@ import '../../src/@/globals.css'
166
235
  // ... dynamic component mounting ...
167
236
  ```
168
237
 
238
+ ### Testing routed UI (navigation, layouts)
239
+
240
+ A spec that exercises navigation opens a **path** on the harness origin instead of a `?component=` query, and `mount.tsx` builds a real context with real entrypoints — the OwlMeans router drives the History API, so nothing about the routing may be faked. Two shapes the harness must get right, because a spec that trips them fails obscurely:
241
+
242
+ - **Leave the context un-initialized** (`ready` false). The Router compiles the entrypoint tree into routes only while the context is un-initialized; a pre-readied context renders a blank page.
243
+ - **Give every entrypoint that has children one child declared `default: true`**, or the parent's own path renders blank.
244
+
245
+ Spread the framework's own `entrypoints` (exported by the panel package) into the harness list: the panel context registers the api-config middleware, which resolves one of them during init and throws before any route is compiled if they are missing.
246
+
169
247
  Shadcn components are plain React — no theme provider needed. Assert via `getByRole` / text as usual. To verify that Tailwind classes took visual effect, read computed styles:
170
248
 
171
249
  ```ts
@@ -182,5 +260,117 @@ See `[[shadcn-web]]` for the full `@` alias contract and `tests/context.ts` patt
182
260
  - **No mocks.** Render the real component with real client-context wiring (state, i18n, router) the way the app does. For network calls, use Playwright's `page.route(...)` to a static fixture — never point at a live backend.
183
261
  - **Cover SKILL.md and README.md cases first.** Each component the package documents gets at least one acceptance spec for its happy path.
184
262
  - **Max 3-4 tests per component**: render, primary interaction, one edge case.
185
- - **Always `close()` the page and `closeBrowser()` in `afterAll`.** A leaked context blocks the bun process from exiting.
263
+ - **Always pass a timeout as `test`'s third argument.** 30s for a mounted component, 60s for a
264
+ suite that drives a login or a consent gate. Bun's 5s default is for in-process work, and
265
+ `bunfig.toml`'s `[test] timeout` is not a way to change it.
266
+ - **Always `close()` the page and `browserHelper.closeBrowser()` in `afterAll`.** A leaked context blocks the bun process from exiting.
186
267
  - **Chromium only by default.** Add `firefox` or `webkit` per package only when the feature has documented cross-browser concerns — call `chromium.launch` / `firefox.launch` explicitly in that case.
268
+
269
+ ## `mountComponent` waits for `domcontentloaded`, not `load`
270
+
271
+ Playwright's own default is `load`, and it is wrong for any page that is a real application:
272
+ `load` waits for EVERY subresource, so one analytics beacon, long-poll or third-party pixel that
273
+ never settles holds the navigation open until it times out — with the page fully rendered and
274
+ working the whole time. The failure reads as "the site is down", and the first instinct is to go
275
+ looking at the server.
276
+
277
+ `mountComponent` therefore navigates with `domcontentloaded` and lets the spec wait for the
278
+ selector it actually needs. That wait IS the assertion; the load event never was. Pass
279
+ `waitUntil` explicitly for the rare case that wants otherwise.
280
+
281
+ The rule is not `mountComponent`'s alone — **every** navigation helper here follows it, including
282
+ `makePageHelper(page).loginViaSupervisorForm` and `.loginViaDispatcher`. A helper that navigates on playwright's default
283
+ turns "someone added a tag manager" into "the whole login-driven suite times out", with the browser
284
+ showing a working login form the entire time. Any new helper that calls `page.goto` passes
285
+ `domcontentloaded` and takes a `waitUntil` override.
286
+
287
+ `makePageHelper(page).saveScreenshot(dir, name)` is the fastest way to see what the browser actually had on
288
+ screen when an assertion timed out; `.loginViaSupervisorForm` takes a `screenshotDir` that captures
289
+ the filled form just before submit.
290
+
291
+ ## The consent dialog blocks the login form, and the failure blames the button
292
+
293
+ An OwlMeans app asks for cookie consent **before** it will start an authentication flow, and the
294
+ surface — the bar on its transparent overlay, or the preferences window — offers no dismissal: a
295
+ decision is what the gate is waiting for. Until it is
296
+ answered the overlay intercepts pointer events, so a login form renders, resolves, reports itself
297
+ `visible, enabled and stable`, and still cannot be clicked. Playwright retries for the full timeout
298
+ and then reports `click: Timeout … waiting for getByTestId('supervisor-submit')`, naming the
299
+ button. Nothing is wrong with the button; read the `subtree intercepts pointer events` line, which
300
+ names `[data-consent-dialog]`.
301
+
302
+ `makePageHelper(page).loginViaSupervisorForm` therefore calls `.acceptConsent()` after navigating. `.acceptConsent` is
303
+ public for specs that drive their own login: it waits for the consent store to SETTLE — never while
304
+ the visitor is still being located, when the transparent overlay is up and would take the click —
305
+ then clicks `[data-consent-accept-all]` on whichever surface asks, waits for it to detach, and
306
+ reports whether it answered one. It accepts **all** categories deliberately — a spec asserting a
307
+ narrower decision must make that decision itself rather than inherit a silent minimum.
308
+
309
+ **Where the app asks depends on where the runner is.** With the geo gate on, a Cloudflare-proxied
310
+ app asks only visitors located in a consent country — and the trace answers with the RUNNER's
311
+ country. A spec whose assertions need the surface pins it: `mountComponent({ url, consentGeo: 'PL' })`
312
+ or `consentGeoTestHelper.mockConsentGeo(context, 'PL')` before the navigation; a spec about the
313
+ automatic decision pins `'US'`. Never `page.route` for it — interception disables the cache for
314
+ every request, and a vite-served application stalls on its hundreds of unbundled modules.
315
+
316
+ **A `false` is cheap when the app says so, and costs the full wait when it cannot.** An app on a
317
+ consent package that marks `<html data-consent>` settles at once on a stored or automatic
318
+ decision. One older than that marker — or one that ships no consent widget — only ever settles by
319
+ showing a dialog, so a page with none pays the timeout: 5s by `.acceptConsent`'s own default, but
320
+ `.loginViaSupervisorForm` passes its `timeout` straight through and that defaults to `60_000`.
321
+ Drive an app with no widget with `consent: 'ignore'`, which skips the call entirely; that is also
322
+ the switch a spec flips when it wants to answer the dialog itself.
323
+
324
+ ## `makePageHelper(page).answerMarketingConsent` — cheap absence detection, unlike the cookie dialog
325
+
326
+ `makePageHelper(page).loginViaSupervisorForm` and `.loginViaDispatcher` both call `.answerMarketingConsent({ accept:
327
+ 'all', timeout })` right after they confirm the app has navigated away from login/dispatch, passing
328
+ their **own** `timeout` through explicitly rather than relying on this helper's internal default —
329
+ both of them already budget more than one round trip (login + terms + items) into the value they
330
+ pick. Both also default `marketingConsent` (their own option, not `.answerMarketingConsent`'s) to
331
+ `'save'` — pass `'ignore'` to skip the call in a spec that drives the step itself.
332
+ `@owlmeans/web-marketing-consent` renders a full-page step, when shown, with `[data-marketing-consent]`
333
+ (root, carrying `data-state="loading"` while its status read is outstanding and `"ready"` once it can
334
+ render), `[data-marketing-consent-terms]` (the Terms checkbox, Terms mode only, with `data-version`),
335
+ `[data-marketing-consent-terms-error]` (Terms record failed — the person stays and retries),
336
+ `[data-marketing-consent-all]` (select-all, never ticks Terms), `[data-marketing-consent-item="<key>"]`
337
+ (per-consent), `[data-marketing-consent-save]`, `[data-marketing-consent-skip]` (shown whenever the
338
+ step is optional-only — i.e. `!loading && !terms.needed` — not only after a failed save),
339
+ `[data-marketing-consent-skip-note]`, `[data-marketing-consent-signout]` (shown while a Terms box or
340
+ the loading state is up) and `[data-marketing-consent-error]`.
341
+
342
+ Unlike `makePageHelper(page).acceptConsent`, whose `false` return deliberately costs the full wait (see above),
343
+ `.answerMarketingConsent` does NOT pay the full `timeout` (default `30_000`) when the step is absent.
344
+ The screen is opt-in per app and most applications and environments don't have it wired in yet, and this helper
345
+ is called unconditionally by both login helpers — a full-timeout tax on every login, on every app,
346
+ whether or not the screen exists, would be an unacceptable regression to every existing suite. It
347
+ instead races `[data-marketing-consent]` against the generic "the app has already landed" /
348
+ "still on the login screen" markers the login helpers themselves already rely on (`#app-prompt`,
349
+ `[data-login-method]`) via the same `locator('a, b, c').first().waitFor({ state: 'visible' })` idiom
350
+ `.loginViaSupervisorForm` uses for its own dialog-vs-form race. By the time a login helper calls this,
351
+ one of those markers has typically already rendered, so on an app without the screen the race
352
+ resolves as soon as it does — no fixed sleep, and no meaningful added latency over what the login
353
+ helper already paid to get there.
354
+
355
+ Once the step's root is present, it may still be `data-state="loading"` (its own status read is
356
+ outstanding) — the helper races that attribute clearing against the whole root detaching on its own
357
+ (nothing was pending, so it auto-continued while this waited), via two `page.waitForSelector` calls
358
+ rather than an in-page `waitForFunction`/`evaluate` (this package's `tsconfig` has no `lib: dom`, so
359
+ no TS runs inside the browser context here). An older `web-marketing-consent` that predates
360
+ `data-state` entirely satisfies the "not loading" selector immediately, so this costs nothing there.
361
+
362
+ When a Terms box is present, `opts.terms` (default `'accept'`) ticks it before saving; `'leave'`
363
+ keeps it unticked, for a spec that means to exercise the blocked state itself — the confirm click
364
+ still fires (with `{ force: true }`, since the control is never natively `disabled`), and the helper
365
+ then *expects* the box to still be up afterward. `opts.accept` (default `'all'`) additionally clicks
366
+ select-all when present, `'none'` saves with every item unchecked, and `'skip'` clicks
367
+ `[data-marketing-consent-skip]` instead of saving — valid only when no Terms box is on screen.
368
+
369
+ Returns `false` when the step never appeared. Otherwise it **throws** in two cases rather than
370
+ silently swallowing them: a `[data-marketing-consent-terms-error]` after attempting to confirm
371
+ Terms, and a Terms box still on screen after a failed/blocked confirm (Skip is never offered there
372
+ on purpose, so there is no fallback and no honest way to report success). Short of those, it returns
373
+ `true` whether the save succeeded outright or fell back to clicking
374
+ `[data-marketing-consent-skip]` after a save error on an optional-only screen (logged as a console
375
+ warning, never thrown) — the step is "answered" either way, so a spec is never permanently blocked
376
+ behind a flaky save.
@@ -0,0 +1,22 @@
1
+ import type { Browser, LaunchOptions, Page } from 'playwright';
2
+ /** The one chromium a `bun test` process shares, and the pages specs drive off it. */
3
+ export interface BrowserHelper {
4
+ /**
5
+ * Launch (or return) a single shared chromium instance for the current
6
+ * `bun test` process. UI specs hold a fixture page off this browser; the
7
+ * suite calls {@link BrowserHelper.closeBrowser} from `afterAll` to tear it down once.
8
+ */
9
+ launchBrowser: (opts?: LaunchOptions) => Promise<Browser>;
10
+ /**
11
+ * Tear down the shared browser. Idempotent — safe to call from multiple
12
+ * `afterAll` hooks.
13
+ */
14
+ closeBrowser: () => Promise<void>;
15
+ /**
16
+ * Convenience that launches the browser, runs a callback with a fresh
17
+ * context + page, and closes the context on completion. The browser
18
+ * itself stays alive across calls so multiple specs share it cheaply.
19
+ */
20
+ withPage: <T>(fn: (page: Page) => Promise<T>) => Promise<T>;
21
+ }
22
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/browser/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAE9D,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACzD;;;OAGG;IACH,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC;;;;OAIG;IACH,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;CAC5D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/browser/types.ts"],"names":[],"mappings":""}
@@ -1,19 +1,11 @@
1
- import type { Browser, LaunchOptions } from 'playwright';
2
- /**
3
- * Launch (or return) a single shared chromium instance for the current
4
- * `bun test` process. UI specs hold a fixture page off this browser; the
5
- * suite calls `closeBrowser()` from `afterAll` to tear it down once.
6
- */
1
+ import type { Browser, LaunchOptions, Page } from 'playwright';
2
+ import type { BrowserHelper } from './browser/types.js';
3
+ export declare const createBrowserHelper: () => BrowserHelper;
4
+ export declare const browserHelper: BrowserHelper;
5
+ /** @deprecated compat:factory-refactor — use `browserHelper.launchBrowser(…)` */
7
6
  export declare const launchBrowser: (opts?: LaunchOptions) => Promise<Browser>;
8
- /**
9
- * Tear down the shared browser. Idempotent — safe to call from multiple
10
- * `afterAll` hooks.
11
- */
7
+ /** @deprecated compat:factory-refactor — use `browserHelper.closeBrowser()` */
12
8
  export declare const closeBrowser: () => Promise<void>;
13
- /**
14
- * Convenience that launches the browser, runs a callback with a fresh
15
- * context + page, and closes the context on completion. The browser
16
- * itself stays alive across calls so multiple specs share it cheaply.
17
- */
18
- export declare const withPage: <T>(fn: (page: import('playwright').Page) => Promise<T>) => Promise<T>;
9
+ /** @deprecated compat:factory-refactor — use `browserHelper.withPage(…)` */
10
+ export declare const withPage: <T>(fn: (page: Page) => Promise<T>) => Promise<T>;
19
11
  //# sourceMappingURL=browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAKxD;;;;GAIG;AACH,eAAO,MAAM,aAAa,UAAgB,aAAa,KAAQ,OAAO,CAAC,OAAO,CAO7E,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,QAAa,OAAO,CAAC,IAAI,CAOjD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAU,CAAC,MAC1B,CAAC,IAAI,EAAE,OAAO,YAAY,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAClD,OAAO,CAAC,CAAC,CASX,CAAA"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAMvD,eAAO,MAAM,mBAAmB,QAAO,aA+BtC,CAAA;AAED,eAAO,MAAM,aAAa,eAAwB,CAAA;AAElD,iFAAiF;AACjF,eAAO,MAAM,aAAa,UAAgB,aAAa,KAAQ,OAAO,CAAC,OAAO,CACrC,CAAA;AAEzC,+EAA+E;AAC/E,eAAO,MAAM,YAAY,QAAa,OAAO,CAAC,IAAI,CAAuC,CAAA;AAEzF,4EAA4E;AAC5E,eAAO,MAAM,QAAQ,GAAU,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAC1C,CAAA"}
package/build/browser.js CHANGED
@@ -1,47 +1,44 @@
1
1
  import { chromium } from 'playwright';
2
+ // Process-wide on purpose: every spec of a `bun test` run shares one browser.
2
3
  let shared;
3
4
  let inflight;
4
- /**
5
- * Launch (or return) a single shared chromium instance for the current
6
- * `bun test` process. UI specs hold a fixture page off this browser; the
7
- * suite calls `closeBrowser()` from `afterAll` to tear it down once.
8
- */
9
- export const launchBrowser = async (opts = {}) => {
10
- if (shared != null)
5
+ export const createBrowserHelper = () => {
6
+ const launchBrowser = async (opts = {}) => {
7
+ if (shared != null)
8
+ return shared;
9
+ if (inflight != null)
10
+ return inflight;
11
+ inflight = chromium.launch({ headless: true, ...opts });
12
+ shared = await inflight;
13
+ inflight = undefined;
11
14
  return shared;
12
- if (inflight != null)
13
- return inflight;
14
- inflight = chromium.launch({ headless: true, ...opts });
15
- shared = await inflight;
16
- inflight = undefined;
17
- return shared;
18
- };
19
- /**
20
- * Tear down the shared browser. Idempotent — safe to call from multiple
21
- * `afterAll` hooks.
22
- */
23
- export const closeBrowser = async () => {
24
- const b = shared;
25
- shared = undefined;
26
- inflight = undefined;
27
- if (b != null) {
28
- await b.close();
29
- }
30
- };
31
- /**
32
- * Convenience that launches the browser, runs a callback with a fresh
33
- * context + page, and closes the context on completion. The browser
34
- * itself stays alive across calls so multiple specs share it cheaply.
35
- */
36
- export const withPage = async (fn) => {
37
- const browser = await launchBrowser();
38
- const context = await browser.newContext();
39
- const page = await context.newPage();
40
- try {
41
- return await fn(page);
42
- }
43
- finally {
44
- await context.close();
45
- }
15
+ };
16
+ const closeBrowser = async () => {
17
+ const b = shared;
18
+ shared = undefined;
19
+ inflight = undefined;
20
+ if (b != null) {
21
+ await b.close();
22
+ }
23
+ };
24
+ const withPage = async (fn) => {
25
+ const browser = await launchBrowser();
26
+ const context = await browser.newContext();
27
+ const page = await context.newPage();
28
+ try {
29
+ return await fn(page);
30
+ }
31
+ finally {
32
+ await context.close();
33
+ }
34
+ };
35
+ return { launchBrowser, closeBrowser, withPage };
46
36
  };
37
+ export const browserHelper = createBrowserHelper();
38
+ /** @deprecated compat:factory-refactor — use `browserHelper.launchBrowser(…)` */
39
+ export const launchBrowser = async (opts = {}) => await browserHelper.launchBrowser(opts);
40
+ /** @deprecated compat:factory-refactor — use `browserHelper.closeBrowser()` */
41
+ export const closeBrowser = async () => await browserHelper.closeBrowser();
42
+ /** @deprecated compat:factory-refactor — use `browserHelper.withPage(…)` */
43
+ export const withPage = async (fn) => await browserHelper.withPage(fn);
47
44
  //# sourceMappingURL=browser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGrC,IAAI,MAA2B,CAAA;AAC/B,IAAI,QAAsC,CAAA;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,IAAI,GAAkB,EAAE,EAAoB,EAAE;IAChF,IAAI,MAAM,IAAI,IAAI;QAAE,OAAO,MAAM,CAAA;IACjC,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,QAAQ,CAAA;IACrC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;IACvD,MAAM,GAAG,MAAM,QAAQ,CAAA;IACvB,QAAQ,GAAG,SAAS,CAAA;IACpB,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,IAAmB,EAAE;IACpD,MAAM,CAAC,GAAG,MAAM,CAAA;IAChB,MAAM,GAAG,SAAS,CAAA;IAClB,QAAQ,GAAG,SAAS,CAAA;IACpB,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QACd,MAAM,CAAC,CAAC,KAAK,EAAE,CAAA;IACjB,CAAC;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAC3B,EAAmD,EACvC,EAAE;IACd,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAA;IACrC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAA;IAC1C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IACpC,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;IACvB,CAAC;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIrC,8EAA8E;AAC9E,IAAI,MAA2B,CAAA;AAC/B,IAAI,QAAsC,CAAA;AAE1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAkB,EAAE;IACrD,MAAM,aAAa,GAAG,KAAK,EAAE,IAAI,GAAkB,EAAE,EAAoB,EAAE;QACzE,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,MAAM,CAAA;QACjC,IAAI,QAAQ,IAAI,IAAI;YAAE,OAAO,QAAQ,CAAA;QACrC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;QACvD,MAAM,GAAG,MAAM,QAAQ,CAAA;QACvB,QAAQ,GAAG,SAAS,CAAA;QACpB,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,KAAK,IAAmB,EAAE;QAC7C,MAAM,CAAC,GAAG,MAAM,CAAA;QAChB,MAAM,GAAG,SAAS,CAAA;QAClB,QAAQ,GAAG,SAAS,CAAA;QACpB,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;YACd,MAAM,CAAC,CAAC,KAAK,EAAE,CAAA;QACjB,CAAC;IACH,CAAC,CAAA;IAED,MAAM,QAAQ,GAAG,KAAK,EAAK,EAA8B,EAAc,EAAE;QACvE,MAAM,OAAO,GAAG,MAAM,aAAa,EAAE,CAAA;QACrC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAA;QAC1C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;QACpC,IAAI,CAAC;YACH,OAAO,MAAM,EAAE,CAAC,IAAI,CAAC,CAAA;QACvB,CAAC;gBAAS,CAAC;YACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;QACvB,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAA;AAClD,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,EAAE,CAAA;AAElD,iFAAiF;AACjF,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,IAAI,GAAkB,EAAE,EAAoB,EAAE,CAChF,MAAM,aAAa,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;AAEzC,+EAA+E;AAC/E,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,IAAmB,EAAE,CAAC,MAAM,aAAa,CAAC,YAAY,EAAE,CAAA;AAEzF,4EAA4E;AAC5E,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,EAAK,EAA8B,EAAc,EAAE,CAC9E,MAAM,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA"}
@@ -0,0 +1,36 @@
1
+ import type { BrowserContext, Page } from 'playwright';
2
+ /**
3
+ * A fake answer from Cloudflare's trace endpoint: a country code (`'US'`), or
4
+ * `{ country?, fail?, delayMs?, gpc? }`.
5
+ */
6
+ export type ConsentGeoMock = string | ConsentGeoMockOptions;
7
+ export interface ConsentGeoMockOptions {
8
+ /** The `loc=` the fake trace answers with. Defaults to `PL` — a consent country. */
9
+ country?: string;
10
+ /** `true` answers 404 (the host is not behind Cloudflare); `'hang'` never answers. */
11
+ fail?: boolean | 'hang';
12
+ /** Answer only after this many ms — long enough to see the spinner. */
13
+ delayMs?: number;
14
+ /** Send Global Privacy Control (`navigator.globalPrivacyControl`). */
15
+ gpc?: boolean;
16
+ /** The trace path the page asks. Defaults to `/cdn-cgi/trace`. */
17
+ path?: string;
18
+ }
19
+ /**
20
+ * Pinning where a visitor is, so a consent test does not depend on where its runner happens to be:
21
+ * every app behind Cloudflare answers the trace with the RUNNER's country.
22
+ */
23
+ export interface ConsentGeoTestHelper {
24
+ /**
25
+ * Answer the page's same-origin trace request from inside the page: an init script that wraps
26
+ * `window.fetch` before any page script runs, and counts the calls in
27
+ * `window.__consentTraceCalls`. Never `page.route` — interception disables the cache for every
28
+ * request, and a vite-served application stalls on its hundreds of unbundled modules.
29
+ *
30
+ * Applies to the NEXT navigation of the page or context it is installed on.
31
+ */
32
+ mockConsentGeo: (target: Page | BrowserContext, geo: ConsentGeoMock) => Promise<void>;
33
+ /** How many trace requests the page made since it loaded (`mockConsentGeo` only). */
34
+ traceCalls: (page: Page) => Promise<number>;
35
+ }
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/consent/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtD;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,qBAAqB,CAAA;AAE3D,MAAM,WAAW,qBAAqB;IACpC,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sFAAsF;IACtF,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IACvB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sEAAsE;IACtE,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;;OAOG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,IAAI,GAAG,cAAc,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACrF,qFAAqF;IACrF,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/consent/types.ts"],"names":[],"mappings":""}