@owlmeans/test-ui 0.1.18-rc.4 → 0.1.18-rc.41

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.
package/README.md CHANGED
@@ -58,7 +58,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
58
58
  your project's skill store (`.agents/skills/`):
59
59
 
60
60
  ```sh
61
- npx @owlmeans/agent-skills
61
+ npx @owlmeans/agent-skills@^0.1.18-rc.40
62
62
  ```
63
63
 
64
64
  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.41",
5
+ "generatedAt": "2026-09-25T21:19:37.945Z",
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.41"` 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
  ```
@@ -42,9 +55,29 @@ Downloads the chromium binary the `playwright` library drives. CI installs it in
42
55
  | `launchBrowser(opts?)` | Return the shared chromium for this `bun test` process. Idempotent. |
43
56
  | `closeBrowser()` | Tear it down. Call from `afterAll`. |
44
57
  | `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 }`. |
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
+ | `acceptConsent(page, { timeout? })` | Wait up to `timeout` (default 5s) for `[data-consent-dialog]`, accept all, wait for it to detach. Returns whether it answered one — and costs that whole wait when it does not. |
61
+ | `saveScreenshot(page, dir, name)` | Full-page PNG to `<dir>/<name>.png`, creating `dir`. Returns the absolute path. |
46
62
  | Re-exports: `Browser`, `BrowserContext`, `Page`, `Locator` | Playwright types — no direct `playwright` import needed. |
47
63
 
64
+ ### Authenticated specs
65
+
66
+ `@owlmeans/test-ui` also carries the supervisor-login helpers, so a spec can reach a screen that
67
+ sits behind auth without hand-rolling a token:
68
+
69
+ | Helper | Purpose |
70
+ |---|---|
71
+ | `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? }`. |
72
+ | `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? }`. |
73
+ | `loginViaDispatcher(page, 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? }`. |
74
+ | `loginViaSupervisorForm(page, 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? }`. |
75
+ | `answerMarketingConsent(page, 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. |
76
+
77
+ These need a project whose backend trusts the private key they sign with, so they belong to specs
78
+ that run against a real app rather than a mounted component. See `[[supervisor-auth]]` for the
79
+ server and web wiring they assume.
80
+
48
81
  ## Spec shape
49
82
 
50
83
  ```ts
@@ -52,25 +85,35 @@ import { afterAll, describe, expect, test } from 'bun:test'
52
85
  import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
53
86
  import { HARNESS_URL } from './context.js'
54
87
 
88
+ // Browser work does not fit bun's 5s default: a cold harness compiles the app on first request,
89
+ // and the first test to run pays for the Vite boot happening in the same process.
90
+ const TIMEOUT = 30_000
91
+
55
92
  afterAll(async () => { await closeBrowser() })
56
93
 
94
+ // The harness is a real app, so a spec picks its case by opening a path on it.
95
+ const open = async (path: string) =>
96
+ mountComponent({ url: `${HARNESS_URL.replace(/\/$/, '')}${path}` })
97
+
57
98
  describe('<package> — <component>', () => {
58
99
  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
- })
100
+ const { page, close } = await open('/login')
64
101
  try {
65
102
  expect(await page.locator('h1').textContent()).toBe('Sign in')
66
103
  expect(await page.getByRole('button', { name: /sign in/i }).isVisible()).toBe(true)
67
104
  } finally {
68
105
  await close()
69
106
  }
70
- })
107
+ }, TIMEOUT)
71
108
  })
72
109
  ```
73
110
 
111
+ **Every chromium test carries its own timeout** — the third argument to `test`. Bun's default is
112
+ 5s, which a cold harness blows through before the first assertion, and `bunfig.toml`'s
113
+ `[test] timeout` does not raise it (bun 1.4.0 reads the section but ignores that key). 30s covers a
114
+ mounted component against a Vite harness; a suite that drives a login form or a consent gate needs
115
+ 60s. `bun test --timeout=<ms>` raises a whole run from the command line.
116
+
74
117
  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
118
 
76
119
  ## Per-package `tests/context.ts`
@@ -93,7 +136,12 @@ export const getHarnessUrl = async (): Promise<string> => {
93
136
  configFile: false,
94
137
  root: resolve(here, './harness'),
95
138
  plugins: [react()],
139
+ // Pre-bundle the third-party runtime deps the mounted components pull in. A dependency
140
+ // discovered mid-transform makes Vite re-optimize and reload the page underneath the
141
+ // navigation, and the first `goto` of a cold run pays for that as a timeout.
142
+ optimizeDeps: { include: ['<runtime dep the components import>'] },
96
143
  server: { port: 0 },
144
+ logLevel: 'warn',
97
145
  })
98
146
  await server.listen()
99
147
  const local = server.resolvedUrls?.local?.[0]
@@ -105,11 +153,23 @@ export const getHarnessUrl = async (): Promise<string> => {
105
153
  export const HARNESS_URL = await getHarnessUrl()
106
154
  ```
107
155
 
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`.
156
+ List in `optimizeDeps.include` every non-`@owlmeans` runtime package the components reach — the
157
+ toast library, the icon set, a form library. It is the difference between a cold run that passes
158
+ and one that times out on its first navigation for no visible reason.
159
+
160
+ The per-package `tests/harness/mount.tsx` statically imports what the package ships and renders it
161
+ via `react-dom/client` into the `<div id="root">` that `index.html` provides. It builds a real
162
+ application — `@owlmeans/web-panel` wires a context, the framework entrypoints and a `NavLayout`
163
+ over its screens; `@owlmeans/web-router` compiles a route tree behind the browser router plugin —
164
+ so a spec selects its case by the path it opens rather than by naming a component. A harness that
165
+ needs variants of one surface reads its own query string for them, the way `@owlmeans/web-consent`
166
+ picks a locale, a category set or the policy view. `mountComponent`'s `component` and `props`
167
+ options are the ready-made form of that convention, for a harness written to read them.
168
+ 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
169
 
110
170
  ## Smoke-only baseline
111
171
 
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:
172
+ 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
173
 
114
174
  ```ts
115
175
  import { afterAll, describe, expect, test } from 'bun:test'
@@ -124,10 +184,13 @@ describe('chromium smoke', () => {
124
184
  const { page, close } = await mountComponent({ url: harness })
125
185
  try { expect(await page.locator('#t').textContent()).toBe('hello') }
126
186
  finally { await close() }
127
- })
187
+ }, 30_000)
128
188
  })
129
189
  ```
130
190
 
191
+ A smoke spec needs the same explicit timeout as any other: bun runs every spec file of a package
192
+ in one process, so the first test to start pays for whatever harness the sibling files boot.
193
+
131
194
  ## Wiring `bun test`
132
195
 
133
196
  Per-package `package.json`:
@@ -142,7 +205,7 @@ Same line as categories A/B/C. No `playwright.config.ts`, no `bun x playwright t
142
205
 
143
206
  ## Tailwind + shadcn packages
144
207
 
145
- For shadcn-based OwlMeans packages the `tests/context.ts` Vite config needs two additions beyond the standard setup:
208
+ For shadcn-based OwlMeans packages the `tests/context.ts` Vite config needs three additions beyond the standard setup:
146
209
 
147
210
  ```ts
148
211
  import tailwindcss from '@tailwindcss/vite'
@@ -154,11 +217,16 @@ const server = await createServer({
154
217
  plugins: [react(), tailwindcss()], // add Tailwind v4 Vite plugin
155
218
  resolve: {
156
219
  alias: { '@': resolve(here, '../src/@') }, // resolve @/ to the package's local copy
220
+ dedupe: ['react', 'react-dom'], // one React across the workspace links
157
221
  },
222
+ optimizeDeps: { include: ['sonner'] }, // pre-bundle, never discover mid-navigation
158
223
  server: { port: 0 },
224
+ logLevel: 'warn',
159
225
  })
160
226
  ```
161
227
 
228
+ `dedupe` is not optional in a workspace: symlinked sibling packages otherwise resolve their own React copy and every hook throws.
229
+
162
230
  `tests/harness/mount.tsx` imports the package CSS so components render with Tailwind styles:
163
231
 
164
232
  ```tsx
@@ -166,6 +234,15 @@ import '../../src/@/globals.css'
166
234
  // ... dynamic component mounting ...
167
235
  ```
168
236
 
237
+ ### Testing routed UI (navigation, layouts)
238
+
239
+ 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:
240
+
241
+ - **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.
242
+ - **Give every entrypoint that has children one child declared `default: true`**, or the parent's own path renders blank.
243
+
244
+ 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.
245
+
169
246
  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
247
 
171
248
  ```ts
@@ -182,5 +259,108 @@ See `[[shadcn-web]]` for the full `@` alias contract and `tests/context.ts` patt
182
259
  - **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
260
  - **Cover SKILL.md and README.md cases first.** Each component the package documents gets at least one acceptance spec for its happy path.
184
261
  - **Max 3-4 tests per component**: render, primary interaction, one edge case.
262
+ - **Always pass a timeout as `test`'s third argument.** 30s for a mounted component, 60s for a
263
+ suite that drives a login or a consent gate. Bun's 5s default is for in-process work, and
264
+ `bunfig.toml`'s `[test] timeout` is not a way to change it.
185
265
  - **Always `close()` the page and `closeBrowser()` in `afterAll`.** A leaked context blocks the bun process from exiting.
186
266
  - **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.
267
+
268
+ ## `mountComponent` waits for `domcontentloaded`, not `load`
269
+
270
+ Playwright's own default is `load`, and it is wrong for any page that is a real application:
271
+ `load` waits for EVERY subresource, so one analytics beacon, long-poll or third-party pixel that
272
+ never settles holds the navigation open until it times out — with the page fully rendered and
273
+ working the whole time. The failure reads as "the site is down", and the first instinct is to go
274
+ looking at the server.
275
+
276
+ `mountComponent` therefore navigates with `domcontentloaded` and lets the spec wait for the
277
+ selector it actually needs. That wait IS the assertion; the load event never was. Pass
278
+ `waitUntil` explicitly for the rare case that wants otherwise.
279
+
280
+ The rule is not `mountComponent`'s alone — **every** navigation helper here follows it, including
281
+ `loginViaSupervisorForm` and `loginViaDispatcher`. A helper that navigates on playwright's default
282
+ turns "someone added a tag manager" into "the whole login-driven suite times out", with the browser
283
+ showing a working login form the entire time. Any new helper that calls `page.goto` passes
284
+ `domcontentloaded` and takes a `waitUntil` override.
285
+
286
+ `saveScreenshot(page, dir, name)` is the fastest way to see what the browser actually had on
287
+ screen when an assertion timed out; `loginViaSupervisorForm` takes a `screenshotDir` that captures
288
+ the filled form just before submit.
289
+
290
+ ## The consent dialog blocks the login form, and the failure blames the button
291
+
292
+ An OwlMeans app asks for cookie consent **before** it will start an authentication flow, and the
293
+ dialog is a modal with no dismissal — a decision is what the gate is waiting for. Until it is
294
+ answered the overlay intercepts pointer events, so a login form renders, resolves, reports itself
295
+ `visible, enabled and stable`, and still cannot be clicked. Playwright retries for the full timeout
296
+ and then reports `click: Timeout … waiting for getByTestId('supervisor-submit')`, naming the
297
+ button. Nothing is wrong with the button; read the `subtree intercepts pointer events` line, which
298
+ names `[data-consent-dialog]`.
299
+
300
+ `loginViaSupervisorForm` therefore calls `acceptConsent(page)` after navigating. `acceptConsent` is
301
+ exported for specs that drive their own login: it waits for `[data-consent-dialog]` to become
302
+ visible, clicks `[data-consent-accept-all]`, waits for the dialog to detach, and reports whether it
303
+ answered one. It accepts **all** categories deliberately — a spec asserting a narrower decision must
304
+ make that decision itself rather than inherit a silent minimum.
305
+
306
+ **The `false` return costs the full wait.** It is what the visibility wait rejects into, not a cheap
307
+ probe, so a page with no dialog pays the timeout — 5s by `acceptConsent`'s own default, but
308
+ `loginViaSupervisorForm` passes its `timeout` straight through and that defaults to `60_000`. On an
309
+ app that ships no consent widget the login helper therefore sits for a full minute before it fills
310
+ the first field, with nothing on screen to explain it. Drive such an app with `consent: 'ignore'`,
311
+ which skips the call entirely; that is also the switch a spec flips when it wants to answer the
312
+ dialog itself.
313
+
314
+ ## `answerMarketingConsent` — cheap absence detection, unlike the cookie dialog
315
+
316
+ `loginViaSupervisorForm` and `loginViaDispatcher` both call `answerMarketingConsent(page, { accept:
317
+ 'all', timeout })` right after they confirm the app has navigated away from login/dispatch, passing
318
+ their **own** `timeout` through explicitly rather than relying on this helper's internal default —
319
+ both of them already budget more than one round trip (login + terms + items) into the value they
320
+ pick. Both also default `marketingConsent` (their own option, not `answerMarketingConsent`'s) to
321
+ `'save'` — pass `'ignore'` to skip the call in a spec that drives the step itself.
322
+ `@owlmeans/web-marketing-consent` renders a full-page step, when shown, with `[data-marketing-consent]`
323
+ (root, carrying `data-state="loading"` while its status read is outstanding and `"ready"` once it can
324
+ render), `[data-marketing-consent-terms]` (the Terms checkbox, Terms mode only, with `data-version`),
325
+ `[data-marketing-consent-terms-error]` (Terms record failed — the person stays and retries),
326
+ `[data-marketing-consent-all]` (select-all, never ticks Terms), `[data-marketing-consent-item="<key>"]`
327
+ (per-consent), `[data-marketing-consent-save]`, `[data-marketing-consent-skip]` (shown whenever the
328
+ step is optional-only — i.e. `!loading && !terms.needed` — not only after a failed save),
329
+ `[data-marketing-consent-skip-note]`, `[data-marketing-consent-signout]` (shown while a Terms box or
330
+ the loading state is up) and `[data-marketing-consent-error]`.
331
+
332
+ Unlike `acceptConsent`, whose `false` return deliberately costs the full wait (see above),
333
+ `answerMarketingConsent` does NOT pay the full `timeout` (default `30_000`) when the step is absent.
334
+ The screen is opt-in per app and most applications and environments don't have it wired in yet, and this helper
335
+ is called unconditionally by both login helpers — a full-timeout tax on every login, on every app,
336
+ whether or not the screen exists, would be an unacceptable regression to every existing suite. It
337
+ instead races `[data-marketing-consent]` against the generic "the app has already landed" /
338
+ "still on the login screen" markers the login helpers themselves already rely on (`#app-prompt`,
339
+ `[data-login-method]`) via the same `locator('a, b, c').first().waitFor({ state: 'visible' })` idiom
340
+ `loginViaSupervisorForm` uses for its own dialog-vs-form race. By the time a login helper calls this,
341
+ one of those markers has typically already rendered, so on an app without the screen the race
342
+ resolves as soon as it does — no fixed sleep, and no meaningful added latency over what the login
343
+ helper already paid to get there.
344
+
345
+ Once the step's root is present, it may still be `data-state="loading"` (its own status read is
346
+ outstanding) — the helper races that attribute clearing against the whole root detaching on its own
347
+ (nothing was pending, so it auto-continued while this waited), via two `page.waitForSelector` calls
348
+ rather than an in-page `waitForFunction`/`evaluate` (this package's `tsconfig` has no `lib: dom`, so
349
+ no TS runs inside the browser context here). An older `web-marketing-consent` that predates
350
+ `data-state` entirely satisfies the "not loading" selector immediately, so this costs nothing there.
351
+
352
+ When a Terms box is present, `opts.terms` (default `'accept'`) ticks it before saving; `'leave'`
353
+ keeps it unticked, for a spec that means to exercise the blocked state itself — the confirm click
354
+ still fires (with `{ force: true }`, since the control is never natively `disabled`), and the helper
355
+ then *expects* the box to still be up afterward. `opts.accept` (default `'all'`) additionally clicks
356
+ select-all when present, `'none'` saves with every item unchecked, and `'skip'` clicks
357
+ `[data-marketing-consent-skip]` instead of saving — valid only when no Terms box is on screen.
358
+
359
+ Returns `false` when the step never appeared. Otherwise it **throws** in two cases rather than
360
+ silently swallowing them: a `[data-marketing-consent-terms-error]` after attempting to confirm
361
+ Terms, and a Terms box still on screen after a failed/blocked confirm (Skip is never offered there
362
+ on purpose, so there is no fallback and no honest way to report success). Short of those, it returns
363
+ `true` whether the save succeeded outright or fell back to clicking
364
+ `[data-marketing-consent-skip]` after a save error on an optional-only screen (logged as a console
365
+ warning, never thrown) — the step is "answered" either way, so a spec is never permanently blocked
366
+ behind a flaky save.
package/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './browser.js';
2
+ export * from './marketing-consent.js';
2
3
  export * from './mount.js';
3
4
  export * from './supervisor.js';
4
5
  export type { Browser, BrowserContext, Locator, Page } from 'playwright';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA"}
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './browser.js';
2
+ export * from './marketing-consent.js';
2
3
  export * from './mount.js';
3
4
  export * from './supervisor.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,wBAAwB,CAAA;AACtC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA"}
@@ -0,0 +1,43 @@
1
+ import type { Page } from 'playwright';
2
+ export interface AnswerMarketingConsentOptions {
3
+ /**
4
+ * Which choice to save when the step is shown. `'all'` (the default) ticks the "select all"
5
+ * checkbox (every consent, and the Terms row while it is up — see {@link terms} for what is left
6
+ * of that) before saving; `'none'` saves with every item left unchecked, which is a valid,
7
+ * fully-compliant state on its own; `'skip'` clicks "Skip for now" instead of saving anything —
8
+ * only valid when a Terms box is not on screen (nothing calls it there; see {@link terms}).
9
+ */
10
+ accept?: 'none' | 'all' | 'skip';
11
+ /**
12
+ * When a Terms row is on screen (`appendMarketingConsent({ terms: 'step' })`), `'accept'` (the
13
+ * default) ends with it ticked. `'leave'` ends with it unticked, even after a select-all — for a
14
+ * spec that means to drive the blocked state itself; the confirm click below still fires (with `{ force: true }`, since
15
+ * the control is never truly `disabled`), and this helper then EXPECTS the box to still be up
16
+ * afterward rather than treating that as a failure.
17
+ */
18
+ terms?: 'accept' | 'leave';
19
+ timeout?: number;
20
+ }
21
+ /**
22
+ * Answer the marketing-consent full-page step (`@owlmeans/web-marketing-consent`) if it is shown
23
+ * right after login, so a spec driving {@link loginViaSupervisorForm} or {@link loginViaDispatcher}
24
+ * is never blocked behind it.
25
+ *
26
+ * The screen is opt-in per app, and most apps/environments will not have it wired in — that makes
27
+ * detecting its ABSENCE cheaply the whole point. A plain `waitFor` on `[data-marketing-consent]`
28
+ * alone would cost the full `timeout` on every login of every app that never renders it (the same
29
+ * caveat `acceptConsent` documents for the cookie dialog). Instead this races the step's own root
30
+ * against the generic OwlMeans "the app has already landed" / "still on the login screen" markers
31
+ * this file's own login helpers already wait on (`#app-prompt`, `[data-login-method]`) — by the
32
+ * time a login helper calls this, one of those has typically already rendered, so the race resolves
33
+ * as soon as it does: no fixed sleep, and in the common "step absent" case, no material added
34
+ * latency over what the login helper already paid to get here.
35
+ *
36
+ * @returns whether the step was actually shown and answered (including the error/skip fallback
37
+ * below); `false` when it never appeared within `timeout`, which is the expected result everywhere
38
+ * the screen is not yet configured or the visitor already has saved consents.
39
+ * @throws if a Terms box could not be confirmed (a `[data-marketing-consent-terms-error]`, or the
40
+ * box is still up with nowhere left to go — Skip is never offered there on purpose).
41
+ */
42
+ export declare const answerMarketingConsent: (page: Page, opts?: AnswerMarketingConsentOptions) => Promise<boolean>;
43
+ //# sourceMappingURL=marketing-consent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marketing-consent.d.ts","sourceRoot":"","sources":["../src/marketing-consent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,MAAM,WAAW,6BAA6B;IAC5C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;IAChC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,SAC3B,IAAI,SAAS,6BAA6B,KAC/C,OAAO,CAAC,OAAO,CA2FjB,CAAA"}
@@ -0,0 +1,107 @@
1
+ const DEFAULT_MARKETING_CONSENT_TIMEOUT = 30_000;
2
+ /**
3
+ * Answer the marketing-consent full-page step (`@owlmeans/web-marketing-consent`) if it is shown
4
+ * right after login, so a spec driving {@link loginViaSupervisorForm} or {@link loginViaDispatcher}
5
+ * is never blocked behind it.
6
+ *
7
+ * The screen is opt-in per app, and most apps/environments will not have it wired in — that makes
8
+ * detecting its ABSENCE cheaply the whole point. A plain `waitFor` on `[data-marketing-consent]`
9
+ * alone would cost the full `timeout` on every login of every app that never renders it (the same
10
+ * caveat `acceptConsent` documents for the cookie dialog). Instead this races the step's own root
11
+ * against the generic OwlMeans "the app has already landed" / "still on the login screen" markers
12
+ * this file's own login helpers already wait on (`#app-prompt`, `[data-login-method]`) — by the
13
+ * time a login helper calls this, one of those has typically already rendered, so the race resolves
14
+ * as soon as it does: no fixed sleep, and in the common "step absent" case, no material added
15
+ * latency over what the login helper already paid to get here.
16
+ *
17
+ * @returns whether the step was actually shown and answered (including the error/skip fallback
18
+ * below); `false` when it never appeared within `timeout`, which is the expected result everywhere
19
+ * the screen is not yet configured or the visitor already has saved consents.
20
+ * @throws if a Terms box could not be confirmed (a `[data-marketing-consent-terms-error]`, or the
21
+ * box is still up with nowhere left to go — Skip is never offered there on purpose).
22
+ */
23
+ export const answerMarketingConsent = async (page, opts) => {
24
+ const timeout = opts?.timeout ?? DEFAULT_MARKETING_CONSENT_TIMEOUT;
25
+ const step = page.locator('[data-marketing-consent]');
26
+ try {
27
+ await page.locator('[data-marketing-consent], #app-prompt, [data-login-method]')
28
+ .first().waitFor({ state: 'visible', timeout });
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ if (await step.count() === 0)
34
+ return false;
35
+ // The step's own status read may still be outstanding (`data-state="loading"`) — wait past it,
36
+ // OR notice it already auto-continued (nothing was pending) and detached on its own. An older
37
+ // `web-marketing-consent` that predates `data-state` entirely satisfies the first selector
38
+ // immediately (a missing attribute is never `"loading"`), so this costs nothing there.
39
+ await Promise.race([
40
+ page.waitForSelector('[data-marketing-consent]:not([data-state="loading"])', { timeout }),
41
+ page.waitForSelector('[data-marketing-consent]', { state: 'detached', timeout }),
42
+ ]).catch(() => undefined);
43
+ // Nothing was pending — it already auto-continued while this waited.
44
+ if (await step.count() === 0)
45
+ return true;
46
+ const terms = page.locator('[data-marketing-consent-terms]');
47
+ const hasTerms = await terms.count() > 0;
48
+ if (!hasTerms && opts?.accept === 'skip') {
49
+ await page.locator('[data-marketing-consent-skip]').click();
50
+ await step.waitFor({ state: 'detached', timeout });
51
+ return true;
52
+ }
53
+ if ((opts?.accept ?? 'all') === 'all') {
54
+ // The step draws "Select all" only when there is more than one row to select; a lone consent
55
+ // is ticked itself. Select all speaks for the Terms row too while it is up, which the choice
56
+ // below then settles either way.
57
+ const all = page.locator('[data-marketing-consent-all]');
58
+ if (await all.count() > 0) {
59
+ await all.click();
60
+ }
61
+ else {
62
+ for (const item of await page.locator('[data-marketing-consent-item]').all()) {
63
+ await item.check();
64
+ }
65
+ }
66
+ }
67
+ // 'none' leaves every item unchecked and clicks nothing else before saving.
68
+ if (hasTerms) {
69
+ const wanted = (opts?.terms ?? 'accept') === 'accept';
70
+ if (wanted && !(await terms.isChecked())) {
71
+ await terms.check();
72
+ }
73
+ else if (!wanted && await terms.isChecked()) {
74
+ await terms.uncheck();
75
+ }
76
+ }
77
+ // `{ force: true }`: the confirm is never the native `disabled` — only `aria-disabled` while a
78
+ // Terms box is unticked, or merely styled muted with nothing changed yet — and Playwright's own
79
+ // actionability check honours `aria-disabled`, which this helper deliberately does not.
80
+ await page.locator('[data-marketing-consent-save]').click({ force: true });
81
+ const outcome = await Promise.race([
82
+ step.waitFor({ state: 'detached', timeout }).then(() => 'saved'),
83
+ page.locator('[data-marketing-consent-error]').waitFor({ state: 'visible', timeout })
84
+ .then(() => 'error'),
85
+ page.locator('[data-marketing-consent-terms-error]').waitFor({ state: 'visible', timeout })
86
+ .then(() => 'terms-error'),
87
+ ]).catch(() => 'error');
88
+ if (outcome === 'terms-error') {
89
+ throw new Error('[@owlmeans/test-ui] the marketing-consent step could not confirm the Terms');
90
+ }
91
+ if (outcome !== 'saved') {
92
+ const skip = page.locator('[data-marketing-consent-skip]');
93
+ if (await skip.count() > 0) {
94
+ console.warn('[@owlmeans/test-ui] marketing consent save did not complete; using the skip link instead');
95
+ await skip.click();
96
+ await step.waitFor({ state: 'detached', timeout }).catch(() => undefined);
97
+ }
98
+ else if (await terms.count() > 0) {
99
+ // No Skip is ever offered while the Terms box is up — a failed/blocked confirm here has no
100
+ // fallback, and silently returning would hide a caller stuck behind a real requirement.
101
+ throw new Error('[@owlmeans/test-ui] the marketing-consent step is still showing its Terms box');
102
+ }
103
+ }
104
+ // Answered either way: saved, or skipped past a flaky save. Never leaves the caller blocked.
105
+ return true;
106
+ };
107
+ //# sourceMappingURL=marketing-consent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marketing-consent.js","sourceRoot":"","sources":["../src/marketing-consent.ts"],"names":[],"mappings":"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"}
package/build/mount.d.ts CHANGED
@@ -18,6 +18,19 @@ export interface MountOptions {
18
18
  * URL as `?props=<json>` for the harness's `mount.tsx` to read.
19
19
  */
20
20
  props?: Record<string, unknown>;
21
+ /**
22
+ * What counts as "arrived". Defaults to `domcontentloaded`.
23
+ *
24
+ * NOT `load`, which is playwright's own default and is wrong for any page that is a real
25
+ * application: `load` waits for EVERY subresource, and an analytics beacon, a long-poll or a
26
+ * third-party pixel that never settles holds it open until the navigation times out — with the
27
+ * page fully rendered and working the whole time. The failure reads as "the site is down".
28
+ *
29
+ * A spec waits for the selector it actually needs; that is the assertion, not the load event.
30
+ */
31
+ waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle';
32
+ /** Navigation timeout in ms. */
33
+ timeout?: number;
21
34
  }
22
35
  export interface Mounted {
23
36
  page: Page;
@@ -1 +1 @@
1
- {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAGtC,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAA;IACV,0EAA0E;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAYD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,SAAgB,YAAY,KAAG,OAAO,CAAC,OAAO,CASxE,CAAA"}
1
+ {"version":3,"file":"mount.d.ts","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAGtC,MAAM,WAAW,YAAY;IAC3B;;;;;;OAMG;IACH,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,kBAAkB,GAAG,MAAM,GAAG,aAAa,CAAA;IAClE,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAA;IACV,0EAA0E;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAYD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,SAAgB,YAAY,KAAG,OAAO,CAAC,OAAO,CAYxE,CAAA"}
package/build/mount.js CHANGED
@@ -27,7 +27,10 @@ export const mountComponent = async (opts) => {
27
27
  const browser = await launchBrowser();
28
28
  const context = await browser.newContext();
29
29
  const page = await context.newPage();
30
- await page.goto(buildUrl(opts));
30
+ await page.goto(buildUrl(opts), {
31
+ waitUntil: opts.waitUntil ?? 'domcontentloaded',
32
+ ...(opts.timeout != null ? { timeout: opts.timeout } : {}),
33
+ });
31
34
  return {
32
35
  page,
33
36
  close: async () => { await context.close(); },
@@ -1 +1 @@
1
- {"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AA6B5C,MAAM,QAAQ,GAAG,CAAC,IAAkB,EAAU,EAAE;IAC9C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IACpC,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACnE,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACvE,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;IAC5B,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,GAAG,CAAA;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;IAC9C,OAAO,GAAG,IAAI,CAAC,GAAG,GAAG,GAAG,GAAG,EAAE,EAAE,CAAA;AACjC,CAAC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,IAAkB,EAAoB,EAAE;IAC3E,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,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAA;IAC/B,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA,CAAC,CAAC;KAC7C,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AA0C5C,MAAM,QAAQ,GAAG,CAAC,IAAkB,EAAU,EAAE;IAC9C,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;IACpC,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;IACnE,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IACvE,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAA;IAC5B,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC,GAAG,CAAA;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;IAC9C,OAAO,GAAG,IAAI,CAAC,GAAG,GAAG,GAAG,GAAG,EAAE,EAAE,CAAA;AACjC,CAAC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,IAAkB,EAAoB,EAAE;IAC3E,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,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;QAC9B,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,kBAAkB;QAC/C,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3D,CAAC,CAAA;IACF,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,KAAK,IAAI,EAAE,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA,CAAC,CAAC;KAC7C,CAAA;AACH,CAAC,CAAA"}
@@ -34,6 +34,13 @@ export declare const pregenerateAuthToken: (opts: PregenerateAuthTokenOptions) =
34
34
  */
35
35
  export declare const loginViaDispatcher: (page: Page, baseUrl: string, token: string, opts?: {
36
36
  dispatcherPath?: string;
37
+ waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle';
38
+ /**
39
+ * Answer the marketing-consent step if it is shown right after the dispatcher navigates away.
40
+ * Defaults to `'save'` (select-all and save) — see {@link answerMarketingConsent}. Pass
41
+ * `'ignore'` to skip the call entirely, e.g. in a spec that answers the step itself.
42
+ */
43
+ marketingConsent?: 'save' | 'ignore';
37
44
  }) => Promise<void>;
38
45
  export interface SupervisorApiAuthOptions {
39
46
  /** Auth-manager API base URL (the backend serving /authentication/* and /authenticate). */
@@ -78,9 +85,49 @@ export interface SupervisorFormLoginOptions {
78
85
  /** Predicate/URL fragment that signals a successful landing (default: leaving the login path). */
79
86
  expectPath?: string;
80
87
  timeout?: number;
88
+ /**
89
+ * What counts as "arrived" for the initial navigation. Defaults to `domcontentloaded`.
90
+ *
91
+ * See the note on `MountOptions.waitUntil`: `load` is playwright's default and is wrong for a
92
+ * real application, because it waits for every subresource — a tag manager, an analytics beacon
93
+ * or any pixel that never settles holds it open until the navigation times out, with the login
94
+ * form rendered and fillable the whole time.
95
+ */
96
+ waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle';
97
+ /**
98
+ * What to do about a cookie-consent dialog covering the form. Defaults to `accept`, because an
99
+ * OwlMeans app will not start an authentication flow until consent is answered — see
100
+ * {@link acceptConsent}. Pass `ignore` only in a spec that answers the dialog itself.
101
+ */
102
+ consent?: 'accept' | 'ignore';
81
103
  /** When set, capture the filled login form (`supervisor-login-form.png`) before submit. */
82
104
  screenshotDir?: string;
105
+ /**
106
+ * Answer the marketing-consent step if it is shown right after landing. Defaults to `'save'`
107
+ * (select-all and save) — see {@link answerMarketingConsent}. Pass `'ignore'` to skip the call
108
+ * entirely, e.g. in a spec that answers the step itself.
109
+ */
110
+ marketingConsent?: 'save' | 'ignore';
83
111
  }
112
+ /**
113
+ * Answer the cookie-consent dialog if one is up, so the page underneath can be driven.
114
+ *
115
+ * An OwlMeans app asks for consent BEFORE it will start an authentication flow, and the dialog is
116
+ * a modal with no dismissal — by design, since a decision is what the gate is waiting for. For a
117
+ * test that means the login form renders, resolves, reports itself "visible, enabled and stable"
118
+ * and still cannot be clicked, because the overlay intercepts the pointer. Playwright retries for
119
+ * the full timeout and then blames the button, which is the most misleading failure in this file:
120
+ * nothing is wrong with the form.
121
+ *
122
+ * Accepting is the honest default for a login helper — the flow it is about to drive is exactly
123
+ * what the dialog is gating — but it is best-effort: an app without the widget, or one where the
124
+ * visitor already decided, simply has no dialog and this returns at once.
125
+ *
126
+ * @returns whether a dialog was actually answered.
127
+ */
128
+ export declare const acceptConsent: (page: Page, opts?: {
129
+ timeout?: number;
130
+ }) => Promise<boolean>;
84
131
  /**
85
132
  * Drive the PK-based supervisor login FORM end-to-end (the faithful path that
86
133
  * exercises the real supervisor plugin + registration): navigate to the
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,KAAK,EAAyB,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AASrE,MAAM,WAAW,2BAA2B;IAC1C,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,SAAgB,2BAA2B,KAAG,OAAO,CAAC,MAAM,CAe5F,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,SACvB,IAAI,WAAW,MAAM,SAAS,MAAM,SAAS;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAE,KAC7E,OAAO,CAAC,IAAI,CAOd,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAA;IACd,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAA;IACV,kDAAkD;IAClD,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAgBD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,SACjC,wBAAwB,KAC7B,OAAO,CAAC,MAAM,CAkChB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,SAAgB,IAAI,OAAO,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,MAAM,CAM1F,CAAA;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAA;IACV,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2FAA2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAID;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,SAC3B,IAAI,QAAQ,0BAA0B,KAC3C,OAAO,CAAC,IAAI,CAmBd,CAAA"}
1
+ {"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,KAAK,EAAyB,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAUrE,MAAM,WAAW,2BAA2B;IAC1C,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,SAAgB,2BAA2B,KAAG,OAAO,CAAC,MAAM,CAe5F,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,SACvB,IAAI,WAAW,MAAM,SAAS,MAAM,SACnC;IACL,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,QAAQ,GAAG,kBAAkB,GAAG,MAAM,GAAG,aAAa,CAAA;IAC3F;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;CACrC,KACA,OAAO,CAAC,IAAI,CAYd,CAAA;AAED,MAAM,WAAW,wBAAwB;IACvC,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAA;IAClB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAA;IACd,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAA;IACV,kDAAkD;IAClD,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACnE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAgBD;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,SACjC,wBAAwB,KAC7B,OAAO,CAAC,MAAM,CAkChB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,SAAgB,IAAI,OAAO,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,MAAM,CAM1F,CAAA;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAA;IACV,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,kBAAkB,GAAG,MAAM,GAAG,aAAa,CAAA;IAClE;;;;OAIG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC7B,2FAA2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;CACrC;AAID;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,SAClB,IAAI,SAAS;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,KACtC,OAAO,CAAC,OAAO,CAajB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,SAC3B,IAAI,QAAQ,0BAA0B,KAC3C,OAAO,CAAC,IAAI,CAuCd,CAAA"}
@@ -5,6 +5,7 @@ import { makeKeyPairModel } from '@owlmeans/basic-keys';
5
5
  import { createIdOfLength } from '@owlmeans/basic-ids';
6
6
  import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope';
7
7
  import { makeBearer } from '@owlmeans/test-auth';
8
+ import { answerMarketingConsent } from './marketing-consent.js';
8
9
  /**
9
10
  * Mint a ready-to-use `ED25519-BASIC-TOKEN ...` bearer offline, reusing
10
11
  * `@owlmeans/test-auth`'s `makeBearer`. This is the "set a token directly"
@@ -19,7 +20,7 @@ export const pregenerateAuthToken = async (opts) => {
19
20
  token: createIdOfLength(32),
20
21
  userId: opts.userId,
21
22
  profileId: opts.profileId ?? opts.userId,
22
- entityId: opts.entityId,
23
+ entitySlug: opts.entityId,
23
24
  scopes: opts.scopes ?? ['*'],
24
25
  role: opts.role ?? Role.User,
25
26
  type: AuthroizationType.Ed25519BasicToken,
@@ -39,9 +40,13 @@ export const loginViaDispatcher = async (page, baseUrl, token, opts) => {
39
40
  const path = opts?.dispatcherPath ?? '/dispatcher';
40
41
  const url = new URL(path, baseUrl);
41
42
  url.searchParams.set('token', token);
42
- await page.goto(url.toString());
43
+ // `domcontentloaded`, not playwright's `load` default — see the note on `MountOptions.waitUntil`.
44
+ await page.goto(url.toString(), { waitUntil: opts?.waitUntil ?? 'domcontentloaded' });
43
45
  // Wait until the dispatcher has navigated away (token consumed).
44
46
  await page.waitForURL(u => !u.pathname.startsWith(path), { timeout: 30_000 });
47
+ if (opts?.marketingConsent !== 'ignore') {
48
+ await answerMarketingConsent(page, { accept: 'all', timeout: 30_000 });
49
+ }
45
50
  };
46
51
  const postJson = async (fetchImpl, url, body) => {
47
52
  const res = await fetchImpl(url, {
@@ -106,6 +111,36 @@ export const saveScreenshot = async (page, dir, name) => {
106
111
  return file;
107
112
  };
108
113
  const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor';
114
+ /**
115
+ * Answer the cookie-consent dialog if one is up, so the page underneath can be driven.
116
+ *
117
+ * An OwlMeans app asks for consent BEFORE it will start an authentication flow, and the dialog is
118
+ * a modal with no dismissal — by design, since a decision is what the gate is waiting for. For a
119
+ * test that means the login form renders, resolves, reports itself "visible, enabled and stable"
120
+ * and still cannot be clicked, because the overlay intercepts the pointer. Playwright retries for
121
+ * the full timeout and then blames the button, which is the most misleading failure in this file:
122
+ * nothing is wrong with the form.
123
+ *
124
+ * Accepting is the honest default for a login helper — the flow it is about to drive is exactly
125
+ * what the dialog is gating — but it is best-effort: an app without the widget, or one where the
126
+ * visitor already decided, simply has no dialog and this returns at once.
127
+ *
128
+ * @returns whether a dialog was actually answered.
129
+ */
130
+ export const acceptConsent = async (page, opts) => {
131
+ const dialog = page.locator('[data-consent-dialog]');
132
+ try {
133
+ await dialog.waitFor({ state: 'visible', timeout: opts?.timeout ?? 5_000 });
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ // Accept-all rather than essential-only: a test asserting a narrower decision should make that
139
+ // decision itself, and silently choosing the minimum here would hide it from the spec.
140
+ await page.locator('[data-consent-accept-all]').click();
141
+ await dialog.waitFor({ state: 'detached', timeout: opts?.timeout ?? 5_000 });
142
+ return true;
143
+ };
109
144
  /**
110
145
  * Drive the PK-based supervisor login FORM end-to-end (the faithful path that
111
146
  * exercises the real supervisor plugin + registration): navigate to the
@@ -115,18 +150,37 @@ const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor';
115
150
  export const loginViaSupervisorForm = async (page, opts) => {
116
151
  const path = opts.path ?? DEFAULT_SUPERVISOR_PATH;
117
152
  const timeout = opts.timeout ?? 60_000;
118
- await page.goto(new URL(path, opts.baseUrl).toString());
119
- await page.getByTestId('supervisor-user-id').fill(opts.userId);
120
- await page.getByTestId('supervisor-pk').fill(opts.pk);
153
+ await page.goto(new URL(path, opts.baseUrl).toString(), {
154
+ waitUntil: opts.waitUntil ?? 'domcontentloaded', timeout
155
+ });
156
+ // Wait for the app to MOUNT before deciding whether there is a dialog to answer. `domcontentloaded`
157
+ // returns while the SPA is still booting, and on a dev server that boot is occasionally tens of
158
+ // seconds — so a short consent probe reports "no dialog", the dialog then mounts over the form,
159
+ // and the fill below waits out its own timeout on a field that is present but covered. Either
160
+ // marker proves the app is up: the dialog, or the form itself when consent was already given.
161
+ await page.locator('[data-consent-dialog], [data-testid="supervisor-user-id"]')
162
+ .first().waitFor({ state: 'visible', timeout });
163
+ if (opts.consent !== 'ignore')
164
+ await acceptConsent(page, { timeout });
165
+ // The form renders only AFTER the dialog is dismissed, so these carry the caller's budget rather
166
+ // than playwright's 30s default — the whole point of accepting an explicit `timeout` here.
167
+ await page.getByTestId('supervisor-user-id').fill(opts.userId, { timeout });
168
+ await page.getByTestId('supervisor-pk').fill(opts.pk, { timeout });
121
169
  if (opts.screenshotDir != null) {
122
170
  await saveScreenshot(page, opts.screenshotDir, 'supervisor-login-form');
123
171
  }
124
- await page.getByTestId('supervisor-submit').click();
172
+ await page.getByTestId('supervisor-submit').click({ timeout });
125
173
  if (opts.expectPath != null) {
126
174
  await page.waitForURL(url => url.toString().includes(opts.expectPath), { timeout });
127
175
  }
128
176
  else {
129
177
  await page.waitForURL(url => !url.pathname.startsWith(path), { timeout });
130
178
  }
179
+ // Carries the CALLER's own budget rather than the helper's smaller internal default — a slow
180
+ // cold environment (vite re-optimizing, a stage pod still warming) can easily spend most of a
181
+ // short fixed timeout just getting here, leaving too little for the two extra POSTs (terms, then
182
+ // items) a Terms-mode step now makes.
183
+ if (opts.marketingConsent !== 'ignore')
184
+ await answerMarketingConsent(page, { accept: 'all', timeout });
131
185
  };
132
186
  //# sourceMappingURL=supervisor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/test-ui",
3
- "version": "0.1.18-rc.4",
3
+ "version": "0.1.18-rc.41",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,12 +22,12 @@
22
22
  "./harness": "./harness/index.html"
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/auth": "^0.1.18-rc.4",
26
- "@owlmeans/basic-envelope": "^0.1.18-rc.4",
27
- "@owlmeans/basic-ids": "^0.1.18-rc.4",
28
- "@owlmeans/basic-keys": "^0.1.18-rc.4",
29
- "@owlmeans/test": "^0.1.18-rc.4",
30
- "@owlmeans/test-auth": "^0.1.18-rc.4",
25
+ "@owlmeans/auth": "^0.1.18-rc.36",
26
+ "@owlmeans/basic-envelope": "^0.1.18-rc.38",
27
+ "@owlmeans/basic-ids": "^0.1.18-rc.33",
28
+ "@owlmeans/basic-keys": "^0.1.18-rc.40",
29
+ "@owlmeans/test": "^0.1.18-rc.32",
30
+ "@owlmeans/test-auth": "^0.1.18-rc.38",
31
31
  "playwright": "^1.61.1"
32
32
  },
33
33
  "peerDependencies": {
@@ -43,8 +43,8 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
- "@owlmeans/dep-config": "^0.1.18-rc.4",
47
- "@types/bun": "^1.3.14",
46
+ "@owlmeans/dep-config": "^0.1.18-rc.6",
47
+ "@types/bun": "^1.4.0",
48
48
  "@types/react": "^19.2.17",
49
49
  "nodemon": "^3.1.14",
50
50
  "typescript": "^7.0.2"
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './browser.js'
2
+ export * from './marketing-consent.js'
2
3
  export * from './mount.js'
3
4
  export * from './supervisor.js'
4
5
  export type { Browser, BrowserContext, Locator, Page } from 'playwright'
@@ -0,0 +1,139 @@
1
+ import type { Page } from 'playwright'
2
+
3
+ export interface AnswerMarketingConsentOptions {
4
+ /**
5
+ * Which choice to save when the step is shown. `'all'` (the default) ticks the "select all"
6
+ * checkbox (every consent, and the Terms row while it is up — see {@link terms} for what is left
7
+ * of that) before saving; `'none'` saves with every item left unchecked, which is a valid,
8
+ * fully-compliant state on its own; `'skip'` clicks "Skip for now" instead of saving anything —
9
+ * only valid when a Terms box is not on screen (nothing calls it there; see {@link terms}).
10
+ */
11
+ accept?: 'none' | 'all' | 'skip'
12
+ /**
13
+ * When a Terms row is on screen (`appendMarketingConsent({ terms: 'step' })`), `'accept'` (the
14
+ * default) ends with it ticked. `'leave'` ends with it unticked, even after a select-all — for a
15
+ * spec that means to drive the blocked state itself; the confirm click below still fires (with `{ force: true }`, since
16
+ * the control is never truly `disabled`), and this helper then EXPECTS the box to still be up
17
+ * afterward rather than treating that as a failure.
18
+ */
19
+ terms?: 'accept' | 'leave'
20
+ timeout?: number
21
+ }
22
+
23
+ const DEFAULT_MARKETING_CONSENT_TIMEOUT = 30_000
24
+
25
+ /**
26
+ * Answer the marketing-consent full-page step (`@owlmeans/web-marketing-consent`) if it is shown
27
+ * right after login, so a spec driving {@link loginViaSupervisorForm} or {@link loginViaDispatcher}
28
+ * is never blocked behind it.
29
+ *
30
+ * The screen is opt-in per app, and most apps/environments will not have it wired in — that makes
31
+ * detecting its ABSENCE cheaply the whole point. A plain `waitFor` on `[data-marketing-consent]`
32
+ * alone would cost the full `timeout` on every login of every app that never renders it (the same
33
+ * caveat `acceptConsent` documents for the cookie dialog). Instead this races the step's own root
34
+ * against the generic OwlMeans "the app has already landed" / "still on the login screen" markers
35
+ * this file's own login helpers already wait on (`#app-prompt`, `[data-login-method]`) — by the
36
+ * time a login helper calls this, one of those has typically already rendered, so the race resolves
37
+ * as soon as it does: no fixed sleep, and in the common "step absent" case, no material added
38
+ * latency over what the login helper already paid to get here.
39
+ *
40
+ * @returns whether the step was actually shown and answered (including the error/skip fallback
41
+ * below); `false` when it never appeared within `timeout`, which is the expected result everywhere
42
+ * the screen is not yet configured or the visitor already has saved consents.
43
+ * @throws if a Terms box could not be confirmed (a `[data-marketing-consent-terms-error]`, or the
44
+ * box is still up with nowhere left to go — Skip is never offered there on purpose).
45
+ */
46
+ export const answerMarketingConsent = async (
47
+ page: Page, opts?: AnswerMarketingConsentOptions
48
+ ): Promise<boolean> => {
49
+ const timeout = opts?.timeout ?? DEFAULT_MARKETING_CONSENT_TIMEOUT
50
+ const step = page.locator('[data-marketing-consent]')
51
+
52
+ try {
53
+ await page.locator('[data-marketing-consent], #app-prompt, [data-login-method]')
54
+ .first().waitFor({ state: 'visible', timeout })
55
+ } catch {
56
+ return false
57
+ }
58
+
59
+ if (await step.count() === 0) return false
60
+
61
+ // The step's own status read may still be outstanding (`data-state="loading"`) — wait past it,
62
+ // OR notice it already auto-continued (nothing was pending) and detached on its own. An older
63
+ // `web-marketing-consent` that predates `data-state` entirely satisfies the first selector
64
+ // immediately (a missing attribute is never `"loading"`), so this costs nothing there.
65
+ await Promise.race([
66
+ page.waitForSelector('[data-marketing-consent]:not([data-state="loading"])', { timeout }),
67
+ page.waitForSelector('[data-marketing-consent]', { state: 'detached', timeout }),
68
+ ]).catch(() => undefined)
69
+
70
+ // Nothing was pending — it already auto-continued while this waited.
71
+ if (await step.count() === 0) return true
72
+
73
+ const terms = page.locator('[data-marketing-consent-terms]')
74
+ const hasTerms = await terms.count() > 0
75
+
76
+ if (!hasTerms && opts?.accept === 'skip') {
77
+ await page.locator('[data-marketing-consent-skip]').click()
78
+ await step.waitFor({ state: 'detached', timeout })
79
+
80
+ return true
81
+ }
82
+
83
+ if ((opts?.accept ?? 'all') === 'all') {
84
+ // The step draws "Select all" only when there is more than one row to select; a lone consent
85
+ // is ticked itself. Select all speaks for the Terms row too while it is up, which the choice
86
+ // below then settles either way.
87
+ const all = page.locator('[data-marketing-consent-all]')
88
+ if (await all.count() > 0) {
89
+ await all.click()
90
+ } else {
91
+ for (const item of await page.locator('[data-marketing-consent-item]').all()) {
92
+ await item.check()
93
+ }
94
+ }
95
+ }
96
+ // 'none' leaves every item unchecked and clicks nothing else before saving.
97
+
98
+ if (hasTerms) {
99
+ const wanted = (opts?.terms ?? 'accept') === 'accept'
100
+ if (wanted && !(await terms.isChecked())) {
101
+ await terms.check()
102
+ } else if (!wanted && await terms.isChecked()) {
103
+ await terms.uncheck()
104
+ }
105
+ }
106
+
107
+ // `{ force: true }`: the confirm is never the native `disabled` — only `aria-disabled` while a
108
+ // Terms box is unticked, or merely styled muted with nothing changed yet — and Playwright's own
109
+ // actionability check honours `aria-disabled`, which this helper deliberately does not.
110
+ await page.locator('[data-marketing-consent-save]').click({ force: true })
111
+
112
+ const outcome = await Promise.race([
113
+ step.waitFor({ state: 'detached', timeout }).then(() => 'saved' as const),
114
+ page.locator('[data-marketing-consent-error]').waitFor({ state: 'visible', timeout })
115
+ .then(() => 'error' as const),
116
+ page.locator('[data-marketing-consent-terms-error]').waitFor({ state: 'visible', timeout })
117
+ .then(() => 'terms-error' as const),
118
+ ]).catch(() => 'error' as const)
119
+
120
+ if (outcome === 'terms-error') {
121
+ throw new Error('[@owlmeans/test-ui] the marketing-consent step could not confirm the Terms')
122
+ }
123
+
124
+ if (outcome !== 'saved') {
125
+ const skip = page.locator('[data-marketing-consent-skip]')
126
+ if (await skip.count() > 0) {
127
+ console.warn('[@owlmeans/test-ui] marketing consent save did not complete; using the skip link instead')
128
+ await skip.click()
129
+ await step.waitFor({ state: 'detached', timeout }).catch(() => undefined)
130
+ } else if (await terms.count() > 0) {
131
+ // No Skip is ever offered while the Terms box is up — a failed/blocked confirm here has no
132
+ // fallback, and silently returning would hide a caller stuck behind a real requirement.
133
+ throw new Error('[@owlmeans/test-ui] the marketing-consent step is still showing its Terms box')
134
+ }
135
+ }
136
+
137
+ // Answered either way: saved, or skipped past a flaky save. Never leaves the caller blocked.
138
+ return true
139
+ }
package/src/mount.ts CHANGED
@@ -20,6 +20,19 @@ export interface MountOptions {
20
20
  * URL as `?props=<json>` for the harness's `mount.tsx` to read.
21
21
  */
22
22
  props?: Record<string, unknown>
23
+ /**
24
+ * What counts as "arrived". Defaults to `domcontentloaded`.
25
+ *
26
+ * NOT `load`, which is playwright's own default and is wrong for any page that is a real
27
+ * application: `load` waits for EVERY subresource, and an analytics beacon, a long-poll or a
28
+ * third-party pixel that never settles holds it open until the navigation times out — with the
29
+ * page fully rendered and working the whole time. The failure reads as "the site is down".
30
+ *
31
+ * A spec waits for the selector it actually needs; that is the assertion, not the load event.
32
+ */
33
+ waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle'
34
+ /** Navigation timeout in ms. */
35
+ timeout?: number
23
36
  }
24
37
 
25
38
  export interface Mounted {
@@ -54,7 +67,10 @@ export const mountComponent = async (opts: MountOptions): Promise<Mounted> => {
54
67
  const browser = await launchBrowser()
55
68
  const context = await browser.newContext()
56
69
  const page = await context.newPage()
57
- await page.goto(buildUrl(opts))
70
+ await page.goto(buildUrl(opts), {
71
+ waitUntil: opts.waitUntil ?? 'domcontentloaded',
72
+ ...(opts.timeout != null ? { timeout: opts.timeout } : {}),
73
+ })
58
74
  return {
59
75
  page,
60
76
  close: async () => { await context.close() },
package/src/supervisor.ts CHANGED
@@ -9,6 +9,7 @@ import { makeKeyPairModel } from '@owlmeans/basic-keys'
9
9
  import { createIdOfLength } from '@owlmeans/basic-ids'
10
10
  import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
11
11
  import { makeBearer } from '@owlmeans/test-auth'
12
+ import { answerMarketingConsent } from './marketing-consent.js'
12
13
 
13
14
  export interface PregenerateAuthTokenOptions {
14
15
  /** Target user id / email the token represents. */
@@ -41,7 +42,7 @@ export const pregenerateAuthToken = async (opts: PregenerateAuthTokenOptions): P
41
42
  token: createIdOfLength(32),
42
43
  userId: opts.userId,
43
44
  profileId: opts.profileId ?? opts.userId,
44
- entityId: opts.entityId,
45
+ entitySlug: opts.entityId,
45
46
  scopes: opts.scopes ?? ['*'],
46
47
  role: opts.role ?? Role.User,
47
48
  type: AuthroizationType.Ed25519BasicToken,
@@ -60,14 +61,28 @@ export const pregenerateAuthToken = async (opts: PregenerateAuthTokenOptions): P
60
61
  * (e.g. to force an external IdP) should use {@link loginViaSupervisorForm}.
61
62
  */
62
63
  export const loginViaDispatcher = async (
63
- page: Page, baseUrl: string, token: string, opts?: { dispatcherPath?: string }
64
+ page: Page, baseUrl: string, token: string,
65
+ opts?: {
66
+ dispatcherPath?: string, waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle'
67
+ /**
68
+ * Answer the marketing-consent step if it is shown right after the dispatcher navigates away.
69
+ * Defaults to `'save'` (select-all and save) — see {@link answerMarketingConsent}. Pass
70
+ * `'ignore'` to skip the call entirely, e.g. in a spec that answers the step itself.
71
+ */
72
+ marketingConsent?: 'save' | 'ignore'
73
+ }
64
74
  ): Promise<void> => {
65
75
  const path = opts?.dispatcherPath ?? '/dispatcher'
66
76
  const url = new URL(path, baseUrl)
67
77
  url.searchParams.set('token', token)
68
- await page.goto(url.toString())
78
+ // `domcontentloaded`, not playwright's `load` default — see the note on `MountOptions.waitUntil`.
79
+ await page.goto(url.toString(), { waitUntil: opts?.waitUntil ?? 'domcontentloaded' })
69
80
  // Wait until the dispatcher has navigated away (token consumed).
70
81
  await page.waitForURL(u => !u.pathname.startsWith(path), { timeout: 30_000 })
82
+
83
+ if (opts?.marketingConsent !== 'ignore') {
84
+ await answerMarketingConsent(page, { accept: 'all', timeout: 30_000 })
85
+ }
71
86
  }
72
87
 
73
88
  export interface SupervisorApiAuthOptions {
@@ -168,12 +183,66 @@ export interface SupervisorFormLoginOptions {
168
183
  /** Predicate/URL fragment that signals a successful landing (default: leaving the login path). */
169
184
  expectPath?: string
170
185
  timeout?: number
186
+ /**
187
+ * What counts as "arrived" for the initial navigation. Defaults to `domcontentloaded`.
188
+ *
189
+ * See the note on `MountOptions.waitUntil`: `load` is playwright's default and is wrong for a
190
+ * real application, because it waits for every subresource — a tag manager, an analytics beacon
191
+ * or any pixel that never settles holds it open until the navigation times out, with the login
192
+ * form rendered and fillable the whole time.
193
+ */
194
+ waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle'
195
+ /**
196
+ * What to do about a cookie-consent dialog covering the form. Defaults to `accept`, because an
197
+ * OwlMeans app will not start an authentication flow until consent is answered — see
198
+ * {@link acceptConsent}. Pass `ignore` only in a spec that answers the dialog itself.
199
+ */
200
+ consent?: 'accept' | 'ignore'
171
201
  /** When set, capture the filled login form (`supervisor-login-form.png`) before submit. */
172
202
  screenshotDir?: string
203
+ /**
204
+ * Answer the marketing-consent step if it is shown right after landing. Defaults to `'save'`
205
+ * (select-all and save) — see {@link answerMarketingConsent}. Pass `'ignore'` to skip the call
206
+ * entirely, e.g. in a spec that answers the step itself.
207
+ */
208
+ marketingConsent?: 'save' | 'ignore'
173
209
  }
174
210
 
175
211
  const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor'
176
212
 
213
+ /**
214
+ * Answer the cookie-consent dialog if one is up, so the page underneath can be driven.
215
+ *
216
+ * An OwlMeans app asks for consent BEFORE it will start an authentication flow, and the dialog is
217
+ * a modal with no dismissal — by design, since a decision is what the gate is waiting for. For a
218
+ * test that means the login form renders, resolves, reports itself "visible, enabled and stable"
219
+ * and still cannot be clicked, because the overlay intercepts the pointer. Playwright retries for
220
+ * the full timeout and then blames the button, which is the most misleading failure in this file:
221
+ * nothing is wrong with the form.
222
+ *
223
+ * Accepting is the honest default for a login helper — the flow it is about to drive is exactly
224
+ * what the dialog is gating — but it is best-effort: an app without the widget, or one where the
225
+ * visitor already decided, simply has no dialog and this returns at once.
226
+ *
227
+ * @returns whether a dialog was actually answered.
228
+ */
229
+ export const acceptConsent = async (
230
+ page: Page, opts?: { timeout?: number }
231
+ ): Promise<boolean> => {
232
+ const dialog = page.locator('[data-consent-dialog]')
233
+ try {
234
+ await dialog.waitFor({ state: 'visible', timeout: opts?.timeout ?? 5_000 })
235
+ } catch {
236
+ return false
237
+ }
238
+ // Accept-all rather than essential-only: a test asserting a narrower decision should make that
239
+ // decision itself, and silently choosing the minimum here would hide it from the spec.
240
+ await page.locator('[data-consent-accept-all]').click()
241
+ await dialog.waitFor({ state: 'detached', timeout: opts?.timeout ?? 5_000 })
242
+
243
+ return true
244
+ }
245
+
177
246
  /**
178
247
  * Drive the PK-based supervisor login FORM end-to-end (the faithful path that
179
248
  * exercises the real supervisor plugin + registration): navigate to the
@@ -185,20 +254,40 @@ export const loginViaSupervisorForm = async (
185
254
  ): Promise<void> => {
186
255
  const path = opts.path ?? DEFAULT_SUPERVISOR_PATH
187
256
  const timeout = opts.timeout ?? 60_000
188
- await page.goto(new URL(path, opts.baseUrl).toString())
257
+ await page.goto(new URL(path, opts.baseUrl).toString(), {
258
+ waitUntil: opts.waitUntil ?? 'domcontentloaded', timeout
259
+ })
260
+
261
+ // Wait for the app to MOUNT before deciding whether there is a dialog to answer. `domcontentloaded`
262
+ // returns while the SPA is still booting, and on a dev server that boot is occasionally tens of
263
+ // seconds — so a short consent probe reports "no dialog", the dialog then mounts over the form,
264
+ // and the fill below waits out its own timeout on a field that is present but covered. Either
265
+ // marker proves the app is up: the dialog, or the form itself when consent was already given.
266
+ await page.locator('[data-consent-dialog], [data-testid="supervisor-user-id"]')
267
+ .first().waitFor({ state: 'visible', timeout })
189
268
 
190
- await page.getByTestId('supervisor-user-id').fill(opts.userId)
191
- await page.getByTestId('supervisor-pk').fill(opts.pk)
269
+ if (opts.consent !== 'ignore') await acceptConsent(page, { timeout })
270
+
271
+ // The form renders only AFTER the dialog is dismissed, so these carry the caller's budget rather
272
+ // than playwright's 30s default — the whole point of accepting an explicit `timeout` here.
273
+ await page.getByTestId('supervisor-user-id').fill(opts.userId, { timeout })
274
+ await page.getByTestId('supervisor-pk').fill(opts.pk, { timeout })
192
275
 
193
276
  if (opts.screenshotDir != null) {
194
277
  await saveScreenshot(page, opts.screenshotDir, 'supervisor-login-form')
195
278
  }
196
279
 
197
- await page.getByTestId('supervisor-submit').click()
280
+ await page.getByTestId('supervisor-submit').click({ timeout })
198
281
 
199
282
  if (opts.expectPath != null) {
200
283
  await page.waitForURL(url => url.toString().includes(opts.expectPath!), { timeout })
201
284
  } else {
202
285
  await page.waitForURL(url => !url.pathname.startsWith(path), { timeout })
203
286
  }
287
+
288
+ // Carries the CALLER's own budget rather than the helper's smaller internal default — a slow
289
+ // cold environment (vite re-optimizing, a stage pod still warming) can easily spend most of a
290
+ // short fixed timeout just getting here, leaving too little for the two extra POSTs (terms, then
291
+ // items) a Terms-mode step now makes.
292
+ if (opts.marketingConsent !== 'ignore') await answerMarketingConsent(page, { accept: 'all', timeout })
204
293
  }