@owlmeans/test-ui 0.1.18-rc.2 → 0.1.18-rc.21
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 +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/testing-ui/SKILL.md +141 -16
- package/build/mount.d.ts +13 -0
- package/build/mount.d.ts.map +1 -1
- package/build/mount.js +4 -1
- package/build/mount.js.map +1 -1
- package/build/supervisor.d.ts +35 -0
- package/build/supervisor.d.ts.map +1 -1
- package/build/supervisor.js +50 -6
- package/build/supervisor.js.map +1 -1
- package/package.json +9 -9
- package/src/mount.ts +17 -1
- package/src/supervisor.ts +71 -7
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.18
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/test-ui",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.21",
|
|
5
|
+
"generatedAt": "2026-09-12T13:35:38.904Z",
|
|
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
|
|
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
|
-
|
|
9
|
+
**Install:** `"@owlmeans/test-ui": "^0.1.18-rc.21"` 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-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
|
-
|
|
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:
|
|
36
|
+
├── harness/ # per-package: the app the specs drive
|
|
24
37
|
│ ├── index.html # copy of @owlmeans/test-ui/harness/index.html
|
|
25
|
-
│ └── mount.tsx #
|
|
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,28 @@ 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(
|
|
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 and wait for the app to navigate away. |
|
|
74
|
+
| `loginViaSupervisorForm(page, opts)` | Drive the real login form end-to-end — navigate, answer consent, fill user id + key, submit, wait for the landing. Options: `{ baseUrl, userId, pk, path?, expectPath?, timeout?, waitUntil?, consent?, screenshotDir? }`. |
|
|
75
|
+
|
|
76
|
+
These need a project whose backend trusts the private key they sign with, so they belong to specs
|
|
77
|
+
that run against a real app rather than a mounted component. See `[[supervisor-auth]]` for the
|
|
78
|
+
server and web wiring they assume.
|
|
79
|
+
|
|
48
80
|
## Spec shape
|
|
49
81
|
|
|
50
82
|
```ts
|
|
@@ -52,25 +84,35 @@ import { afterAll, describe, expect, test } from 'bun:test'
|
|
|
52
84
|
import { closeBrowser, mountComponent } from '@owlmeans/test-ui'
|
|
53
85
|
import { HARNESS_URL } from './context.js'
|
|
54
86
|
|
|
87
|
+
// Browser work does not fit bun's 5s default: a cold harness compiles the app on first request,
|
|
88
|
+
// and the first test to run pays for the Vite boot happening in the same process.
|
|
89
|
+
const TIMEOUT = 30_000
|
|
90
|
+
|
|
55
91
|
afterAll(async () => { await closeBrowser() })
|
|
56
92
|
|
|
93
|
+
// The harness is a real app, so a spec picks its case by opening a path on it.
|
|
94
|
+
const open = async (path: string) =>
|
|
95
|
+
mountComponent({ url: `${HARNESS_URL.replace(/\/$/, '')}${path}` })
|
|
96
|
+
|
|
57
97
|
describe('<package> — <component>', () => {
|
|
58
98
|
test('renders the SKILL.md happy-path example', async () => {
|
|
59
|
-
const { page, close } = await
|
|
60
|
-
url: HARNESS_URL,
|
|
61
|
-
component: 'LoginForm',
|
|
62
|
-
props: { redirect: '/' },
|
|
63
|
-
})
|
|
99
|
+
const { page, close } = await open('/login')
|
|
64
100
|
try {
|
|
65
101
|
expect(await page.locator('h1').textContent()).toBe('Sign in')
|
|
66
102
|
expect(await page.getByRole('button', { name: /sign in/i }).isVisible()).toBe(true)
|
|
67
103
|
} finally {
|
|
68
104
|
await close()
|
|
69
105
|
}
|
|
70
|
-
})
|
|
106
|
+
}, TIMEOUT)
|
|
71
107
|
})
|
|
72
108
|
```
|
|
73
109
|
|
|
110
|
+
**Every chromium test carries its own timeout** — the third argument to `test`. Bun's default is
|
|
111
|
+
5s, which a cold harness blows through before the first assertion, and `bunfig.toml`'s
|
|
112
|
+
`[test] timeout` does not raise it (bun 1.4.0 reads the section but ignores that key). 30s covers a
|
|
113
|
+
mounted component against a Vite harness; a suite that drives a login form or a consent gate needs
|
|
114
|
+
60s. `bun test --timeout=<ms>` raises a whole run from the command line.
|
|
115
|
+
|
|
74
116
|
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
117
|
|
|
76
118
|
## Per-package `tests/context.ts`
|
|
@@ -93,7 +135,12 @@ export const getHarnessUrl = async (): Promise<string> => {
|
|
|
93
135
|
configFile: false,
|
|
94
136
|
root: resolve(here, './harness'),
|
|
95
137
|
plugins: [react()],
|
|
138
|
+
// Pre-bundle the third-party runtime deps the mounted components pull in. A dependency
|
|
139
|
+
// discovered mid-transform makes Vite re-optimize and reload the page underneath the
|
|
140
|
+
// navigation, and the first `goto` of a cold run pays for that as a timeout.
|
|
141
|
+
optimizeDeps: { include: ['<runtime dep the components import>'] },
|
|
96
142
|
server: { port: 0 },
|
|
143
|
+
logLevel: 'warn',
|
|
97
144
|
})
|
|
98
145
|
await server.listen()
|
|
99
146
|
const local = server.resolvedUrls?.local?.[0]
|
|
@@ -105,11 +152,23 @@ export const getHarnessUrl = async (): Promise<string> => {
|
|
|
105
152
|
export const HARNESS_URL = await getHarnessUrl()
|
|
106
153
|
```
|
|
107
154
|
|
|
108
|
-
|
|
155
|
+
List in `optimizeDeps.include` every non-`@owlmeans` runtime package the components reach — the
|
|
156
|
+
toast library, the icon set, a form library. It is the difference between a cold run that passes
|
|
157
|
+
and one that times out on its first navigation for no visible reason.
|
|
158
|
+
|
|
159
|
+
The per-package `tests/harness/mount.tsx` statically imports what the package ships and renders it
|
|
160
|
+
via `react-dom/client` into the `<div id="root">` that `index.html` provides. It builds a real
|
|
161
|
+
application — `@owlmeans/web-panel` wires a context, the framework entrypoints and a `NavLayout`
|
|
162
|
+
over its screens; `@owlmeans/web-router` compiles a route tree behind the browser router plugin —
|
|
163
|
+
so a spec selects its case by the path it opens rather than by naming a component. A harness that
|
|
164
|
+
needs variants of one surface reads its own query string for them, the way `@owlmeans/web-consent`
|
|
165
|
+
picks a locale, a category set or the policy view. `mountComponent`'s `component` and `props`
|
|
166
|
+
options are the ready-made form of that convention, for a harness written to read them.
|
|
167
|
+
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
168
|
|
|
110
169
|
## Smoke-only baseline
|
|
111
170
|
|
|
112
|
-
For a package that only needs to prove the bun-test + chromium pipeline works
|
|
171
|
+
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
172
|
|
|
114
173
|
```ts
|
|
115
174
|
import { afterAll, describe, expect, test } from 'bun:test'
|
|
@@ -124,10 +183,13 @@ describe('chromium smoke', () => {
|
|
|
124
183
|
const { page, close } = await mountComponent({ url: harness })
|
|
125
184
|
try { expect(await page.locator('#t').textContent()).toBe('hello') }
|
|
126
185
|
finally { await close() }
|
|
127
|
-
})
|
|
186
|
+
}, 30_000)
|
|
128
187
|
})
|
|
129
188
|
```
|
|
130
189
|
|
|
190
|
+
A smoke spec needs the same explicit timeout as any other: bun runs every spec file of a package
|
|
191
|
+
in one process, so the first test to start pays for whatever harness the sibling files boot.
|
|
192
|
+
|
|
131
193
|
## Wiring `bun test`
|
|
132
194
|
|
|
133
195
|
Per-package `package.json`:
|
|
@@ -142,7 +204,7 @@ Same line as categories A/B/C. No `playwright.config.ts`, no `bun x playwright t
|
|
|
142
204
|
|
|
143
205
|
## Tailwind + shadcn packages
|
|
144
206
|
|
|
145
|
-
For shadcn-based OwlMeans packages the `tests/context.ts` Vite config needs
|
|
207
|
+
For shadcn-based OwlMeans packages the `tests/context.ts` Vite config needs three additions beyond the standard setup:
|
|
146
208
|
|
|
147
209
|
```ts
|
|
148
210
|
import tailwindcss from '@tailwindcss/vite'
|
|
@@ -154,11 +216,16 @@ const server = await createServer({
|
|
|
154
216
|
plugins: [react(), tailwindcss()], // add Tailwind v4 Vite plugin
|
|
155
217
|
resolve: {
|
|
156
218
|
alias: { '@': resolve(here, '../src/@') }, // resolve @/ to the package's local copy
|
|
219
|
+
dedupe: ['react', 'react-dom'], // one React across the workspace links
|
|
157
220
|
},
|
|
221
|
+
optimizeDeps: { include: ['sonner'] }, // pre-bundle, never discover mid-navigation
|
|
158
222
|
server: { port: 0 },
|
|
223
|
+
logLevel: 'warn',
|
|
159
224
|
})
|
|
160
225
|
```
|
|
161
226
|
|
|
227
|
+
`dedupe` is not optional in a workspace: symlinked sibling packages otherwise resolve their own React copy and every hook throws.
|
|
228
|
+
|
|
162
229
|
`tests/harness/mount.tsx` imports the package CSS so components render with Tailwind styles:
|
|
163
230
|
|
|
164
231
|
```tsx
|
|
@@ -166,6 +233,15 @@ import '../../src/@/globals.css'
|
|
|
166
233
|
// ... dynamic component mounting ...
|
|
167
234
|
```
|
|
168
235
|
|
|
236
|
+
### Testing routed UI (navigation, layouts)
|
|
237
|
+
|
|
238
|
+
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:
|
|
239
|
+
|
|
240
|
+
- **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.
|
|
241
|
+
- **Give every entrypoint that has children one child declared `default: true`**, or the parent's own path renders blank.
|
|
242
|
+
|
|
243
|
+
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.
|
|
244
|
+
|
|
169
245
|
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
246
|
|
|
171
247
|
```ts
|
|
@@ -182,5 +258,54 @@ See `[[shadcn-web]]` for the full `@` alias contract and `tests/context.ts` patt
|
|
|
182
258
|
- **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
259
|
- **Cover SKILL.md and README.md cases first.** Each component the package documents gets at least one acceptance spec for its happy path.
|
|
184
260
|
- **Max 3-4 tests per component**: render, primary interaction, one edge case.
|
|
261
|
+
- **Always pass a timeout as `test`'s third argument.** 30s for a mounted component, 60s for a
|
|
262
|
+
suite that drives a login or a consent gate. Bun's 5s default is for in-process work, and
|
|
263
|
+
`bunfig.toml`'s `[test] timeout` is not a way to change it.
|
|
185
264
|
- **Always `close()` the page and `closeBrowser()` in `afterAll`.** A leaked context blocks the bun process from exiting.
|
|
186
265
|
- **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.
|
|
266
|
+
|
|
267
|
+
## `mountComponent` waits for `domcontentloaded`, not `load`
|
|
268
|
+
|
|
269
|
+
Playwright's own default is `load`, and it is wrong for any page that is a real application:
|
|
270
|
+
`load` waits for EVERY subresource, so one analytics beacon, long-poll or third-party pixel that
|
|
271
|
+
never settles holds the navigation open until it times out — with the page fully rendered and
|
|
272
|
+
working the whole time. The failure reads as "the site is down", and the first instinct is to go
|
|
273
|
+
looking at the server.
|
|
274
|
+
|
|
275
|
+
`mountComponent` therefore navigates with `domcontentloaded` and lets the spec wait for the
|
|
276
|
+
selector it actually needs. That wait IS the assertion; the load event never was. Pass
|
|
277
|
+
`waitUntil` explicitly for the rare case that wants otherwise.
|
|
278
|
+
|
|
279
|
+
The rule is not `mountComponent`'s alone — **every** navigation helper here follows it, including
|
|
280
|
+
`loginViaSupervisorForm` and `loginViaDispatcher`. A helper that navigates on playwright's default
|
|
281
|
+
turns "someone added a tag manager" into "the whole login-driven suite times out", with the browser
|
|
282
|
+
showing a working login form the entire time. Any new helper that calls `page.goto` passes
|
|
283
|
+
`domcontentloaded` and takes a `waitUntil` override.
|
|
284
|
+
|
|
285
|
+
`saveScreenshot(page, dir, name)` is the fastest way to see what the browser actually had on
|
|
286
|
+
screen when an assertion timed out; `loginViaSupervisorForm` takes a `screenshotDir` that captures
|
|
287
|
+
the filled form just before submit.
|
|
288
|
+
|
|
289
|
+
## The consent dialog blocks the login form, and the failure blames the button
|
|
290
|
+
|
|
291
|
+
An OwlMeans app asks for cookie consent **before** it will start an authentication flow, and the
|
|
292
|
+
dialog is a modal with no dismissal — a decision is what the gate is waiting for. Until it is
|
|
293
|
+
answered the overlay intercepts pointer events, so a login form renders, resolves, reports itself
|
|
294
|
+
`visible, enabled and stable`, and still cannot be clicked. Playwright retries for the full timeout
|
|
295
|
+
and then reports `click: Timeout … waiting for getByTestId('supervisor-submit')`, naming the
|
|
296
|
+
button. Nothing is wrong with the button; read the `subtree intercepts pointer events` line, which
|
|
297
|
+
names `[data-consent-dialog]`.
|
|
298
|
+
|
|
299
|
+
`loginViaSupervisorForm` therefore calls `acceptConsent(page)` after navigating. `acceptConsent` is
|
|
300
|
+
exported for specs that drive their own login: it waits for `[data-consent-dialog]` to become
|
|
301
|
+
visible, clicks `[data-consent-accept-all]`, waits for the dialog to detach, and reports whether it
|
|
302
|
+
answered one. It accepts **all** categories deliberately — a spec asserting a narrower decision must
|
|
303
|
+
make that decision itself rather than inherit a silent minimum.
|
|
304
|
+
|
|
305
|
+
**The `false` return costs the full wait.** It is what the visibility wait rejects into, not a cheap
|
|
306
|
+
probe, so a page with no dialog pays the timeout — 5s by `acceptConsent`'s own default, but
|
|
307
|
+
`loginViaSupervisorForm` passes its `timeout` straight through and that defaults to `60_000`. On an
|
|
308
|
+
app that ships no consent widget the login helper therefore sits for a full minute before it fills
|
|
309
|
+
the first field, with nothing on screen to explain it. Drive such an app with `consent: 'ignore'`,
|
|
310
|
+
which skips the call entirely; that is also the switch a spec flips when it wants to answer the
|
|
311
|
+
dialog itself.
|
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;
|
package/build/mount.d.ts.map
CHANGED
|
@@ -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;
|
|
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(); },
|
package/build/mount.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,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"}
|
package/build/supervisor.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ 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';
|
|
37
38
|
}) => Promise<void>;
|
|
38
39
|
export interface SupervisorApiAuthOptions {
|
|
39
40
|
/** Auth-manager API base URL (the backend serving /authentication/* and /authenticate). */
|
|
@@ -78,9 +79,43 @@ export interface SupervisorFormLoginOptions {
|
|
|
78
79
|
/** Predicate/URL fragment that signals a successful landing (default: leaving the login path). */
|
|
79
80
|
expectPath?: string;
|
|
80
81
|
timeout?: number;
|
|
82
|
+
/**
|
|
83
|
+
* What counts as "arrived" for the initial navigation. Defaults to `domcontentloaded`.
|
|
84
|
+
*
|
|
85
|
+
* See the note on `MountOptions.waitUntil`: `load` is playwright's default and is wrong for a
|
|
86
|
+
* real application, because it waits for every subresource — a tag manager, an analytics beacon
|
|
87
|
+
* or any pixel that never settles holds it open until the navigation times out, with the login
|
|
88
|
+
* form rendered and fillable the whole time.
|
|
89
|
+
*/
|
|
90
|
+
waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle';
|
|
91
|
+
/**
|
|
92
|
+
* What to do about a cookie-consent dialog covering the form. Defaults to `accept`, because an
|
|
93
|
+
* OwlMeans app will not start an authentication flow until consent is answered — see
|
|
94
|
+
* {@link acceptConsent}. Pass `ignore` only in a spec that answers the dialog itself.
|
|
95
|
+
*/
|
|
96
|
+
consent?: 'accept' | 'ignore';
|
|
81
97
|
/** When set, capture the filled login form (`supervisor-login-form.png`) before submit. */
|
|
82
98
|
screenshotDir?: string;
|
|
83
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Answer the cookie-consent dialog if one is up, so the page underneath can be driven.
|
|
102
|
+
*
|
|
103
|
+
* An OwlMeans app asks for consent BEFORE it will start an authentication flow, and the dialog is
|
|
104
|
+
* a modal with no dismissal — by design, since a decision is what the gate is waiting for. For a
|
|
105
|
+
* test that means the login form renders, resolves, reports itself "visible, enabled and stable"
|
|
106
|
+
* and still cannot be clicked, because the overlay intercepts the pointer. Playwright retries for
|
|
107
|
+
* the full timeout and then blames the button, which is the most misleading failure in this file:
|
|
108
|
+
* nothing is wrong with the form.
|
|
109
|
+
*
|
|
110
|
+
* Accepting is the honest default for a login helper — the flow it is about to drive is exactly
|
|
111
|
+
* what the dialog is gating — but it is best-effort: an app without the widget, or one where the
|
|
112
|
+
* visitor already decided, simply has no dialog and this returns at once.
|
|
113
|
+
*
|
|
114
|
+
* @returns whether a dialog was actually answered.
|
|
115
|
+
*/
|
|
116
|
+
export declare const acceptConsent: (page: Page, opts?: {
|
|
117
|
+
timeout?: number;
|
|
118
|
+
}) => Promise<boolean>;
|
|
84
119
|
/**
|
|
85
120
|
* Drive the PK-based supervisor login FORM end-to-end (the faithful path that
|
|
86
121
|
* 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,
|
|
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,SACnC;IAAE,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,QAAQ,GAAG,kBAAkB,GAAG,MAAM,GAAG,aAAa,CAAA;CAAE,KACrG,OAAO,CAAC,IAAI,CAQd,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;CACvB;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,CAiCd,CAAA"}
|
package/build/supervisor.js
CHANGED
|
@@ -19,7 +19,7 @@ export const pregenerateAuthToken = async (opts) => {
|
|
|
19
19
|
token: createIdOfLength(32),
|
|
20
20
|
userId: opts.userId,
|
|
21
21
|
profileId: opts.profileId ?? opts.userId,
|
|
22
|
-
|
|
22
|
+
entitySlug: opts.entityId,
|
|
23
23
|
scopes: opts.scopes ?? ['*'],
|
|
24
24
|
role: opts.role ?? Role.User,
|
|
25
25
|
type: AuthroizationType.Ed25519BasicToken,
|
|
@@ -39,7 +39,8 @@ export const loginViaDispatcher = async (page, baseUrl, token, opts) => {
|
|
|
39
39
|
const path = opts?.dispatcherPath ?? '/dispatcher';
|
|
40
40
|
const url = new URL(path, baseUrl);
|
|
41
41
|
url.searchParams.set('token', token);
|
|
42
|
-
|
|
42
|
+
// `domcontentloaded`, not playwright's `load` default — see the note on `MountOptions.waitUntil`.
|
|
43
|
+
await page.goto(url.toString(), { waitUntil: opts?.waitUntil ?? 'domcontentloaded' });
|
|
43
44
|
// Wait until the dispatcher has navigated away (token consumed).
|
|
44
45
|
await page.waitForURL(u => !u.pathname.startsWith(path), { timeout: 30_000 });
|
|
45
46
|
};
|
|
@@ -106,6 +107,36 @@ export const saveScreenshot = async (page, dir, name) => {
|
|
|
106
107
|
return file;
|
|
107
108
|
};
|
|
108
109
|
const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor';
|
|
110
|
+
/**
|
|
111
|
+
* Answer the cookie-consent dialog if one is up, so the page underneath can be driven.
|
|
112
|
+
*
|
|
113
|
+
* An OwlMeans app asks for consent BEFORE it will start an authentication flow, and the dialog is
|
|
114
|
+
* a modal with no dismissal — by design, since a decision is what the gate is waiting for. For a
|
|
115
|
+
* test that means the login form renders, resolves, reports itself "visible, enabled and stable"
|
|
116
|
+
* and still cannot be clicked, because the overlay intercepts the pointer. Playwright retries for
|
|
117
|
+
* the full timeout and then blames the button, which is the most misleading failure in this file:
|
|
118
|
+
* nothing is wrong with the form.
|
|
119
|
+
*
|
|
120
|
+
* Accepting is the honest default for a login helper — the flow it is about to drive is exactly
|
|
121
|
+
* what the dialog is gating — but it is best-effort: an app without the widget, or one where the
|
|
122
|
+
* visitor already decided, simply has no dialog and this returns at once.
|
|
123
|
+
*
|
|
124
|
+
* @returns whether a dialog was actually answered.
|
|
125
|
+
*/
|
|
126
|
+
export const acceptConsent = async (page, opts) => {
|
|
127
|
+
const dialog = page.locator('[data-consent-dialog]');
|
|
128
|
+
try {
|
|
129
|
+
await dialog.waitFor({ state: 'visible', timeout: opts?.timeout ?? 5_000 });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
// Accept-all rather than essential-only: a test asserting a narrower decision should make that
|
|
135
|
+
// decision itself, and silently choosing the minimum here would hide it from the spec.
|
|
136
|
+
await page.locator('[data-consent-accept-all]').click();
|
|
137
|
+
await dialog.waitFor({ state: 'detached', timeout: opts?.timeout ?? 5_000 });
|
|
138
|
+
return true;
|
|
139
|
+
};
|
|
109
140
|
/**
|
|
110
141
|
* Drive the PK-based supervisor login FORM end-to-end (the faithful path that
|
|
111
142
|
* exercises the real supervisor plugin + registration): navigate to the
|
|
@@ -115,13 +146,26 @@ const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor';
|
|
|
115
146
|
export const loginViaSupervisorForm = async (page, opts) => {
|
|
116
147
|
const path = opts.path ?? DEFAULT_SUPERVISOR_PATH;
|
|
117
148
|
const timeout = opts.timeout ?? 60_000;
|
|
118
|
-
await page.goto(new URL(path, opts.baseUrl).toString()
|
|
119
|
-
|
|
120
|
-
|
|
149
|
+
await page.goto(new URL(path, opts.baseUrl).toString(), {
|
|
150
|
+
waitUntil: opts.waitUntil ?? 'domcontentloaded', timeout
|
|
151
|
+
});
|
|
152
|
+
// Wait for the app to MOUNT before deciding whether there is a dialog to answer. `domcontentloaded`
|
|
153
|
+
// returns while the SPA is still booting, and on a dev server that boot is occasionally tens of
|
|
154
|
+
// seconds — so a short consent probe reports "no dialog", the dialog then mounts over the form,
|
|
155
|
+
// and the fill below waits out its own timeout on a field that is present but covered. Either
|
|
156
|
+
// marker proves the app is up: the dialog, or the form itself when consent was already given.
|
|
157
|
+
await page.locator('[data-consent-dialog], [data-testid="supervisor-user-id"]')
|
|
158
|
+
.first().waitFor({ state: 'visible', timeout });
|
|
159
|
+
if (opts.consent !== 'ignore')
|
|
160
|
+
await acceptConsent(page, { timeout });
|
|
161
|
+
// The form renders only AFTER the dialog is dismissed, so these carry the caller's budget rather
|
|
162
|
+
// than playwright's 30s default — the whole point of accepting an explicit `timeout` here.
|
|
163
|
+
await page.getByTestId('supervisor-user-id').fill(opts.userId, { timeout });
|
|
164
|
+
await page.getByTestId('supervisor-pk').fill(opts.pk, { timeout });
|
|
121
165
|
if (opts.screenshotDir != null) {
|
|
122
166
|
await saveScreenshot(page, opts.screenshotDir, 'supervisor-login-form');
|
|
123
167
|
}
|
|
124
|
-
await page.getByTestId('supervisor-submit').click();
|
|
168
|
+
await page.getByTestId('supervisor-submit').click({ timeout });
|
|
125
169
|
if (opts.expectPath != null) {
|
|
126
170
|
await page.waitForURL(url => url.toString().includes(opts.expectPath), { timeout });
|
|
127
171
|
}
|
package/build/supervisor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGzC,OAAO,EACL,iBAAiB,EAAE,QAAQ,IAAI,IAAI,EAAE,kBAAkB,EAAE,sBAAsB,EAChF,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAA;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAmBhD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EAAE,IAAiC,EAAmB,EAAE;IAC/F,MAAM,IAAI,GAAS;QACjB,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC;QAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,MAAM;QACxC,
|
|
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.
|
|
3
|
+
"version": "0.1.18-rc.21",
|
|
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.
|
|
26
|
-
"@owlmeans/basic-envelope": "^0.1.18-rc.
|
|
27
|
-
"@owlmeans/basic-ids": "^0.1.18-rc.
|
|
28
|
-
"@owlmeans/basic-keys": "^0.1.18-rc.
|
|
29
|
-
"@owlmeans/test": "^0.1.18-rc.
|
|
30
|
-
"@owlmeans/test-auth": "^0.1.18-rc.
|
|
25
|
+
"@owlmeans/auth": "^0.1.18-rc.15",
|
|
26
|
+
"@owlmeans/basic-envelope": "^0.1.18-rc.18",
|
|
27
|
+
"@owlmeans/basic-ids": "^0.1.18-rc.15",
|
|
28
|
+
"@owlmeans/basic-keys": "^0.1.18-rc.18",
|
|
29
|
+
"@owlmeans/test": "^0.1.18-rc.14",
|
|
30
|
+
"@owlmeans/test-auth": "^0.1.18-rc.19",
|
|
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.
|
|
47
|
-
"@types/bun": "^1.
|
|
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/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
|
@@ -41,7 +41,7 @@ export const pregenerateAuthToken = async (opts: PregenerateAuthTokenOptions): P
|
|
|
41
41
|
token: createIdOfLength(32),
|
|
42
42
|
userId: opts.userId,
|
|
43
43
|
profileId: opts.profileId ?? opts.userId,
|
|
44
|
-
|
|
44
|
+
entitySlug: opts.entityId,
|
|
45
45
|
scopes: opts.scopes ?? ['*'],
|
|
46
46
|
role: opts.role ?? Role.User,
|
|
47
47
|
type: AuthroizationType.Ed25519BasicToken,
|
|
@@ -60,12 +60,14 @@ export const pregenerateAuthToken = async (opts: PregenerateAuthTokenOptions): P
|
|
|
60
60
|
* (e.g. to force an external IdP) should use {@link loginViaSupervisorForm}.
|
|
61
61
|
*/
|
|
62
62
|
export const loginViaDispatcher = async (
|
|
63
|
-
page: Page, baseUrl: string, token: string,
|
|
63
|
+
page: Page, baseUrl: string, token: string,
|
|
64
|
+
opts?: { dispatcherPath?: string, waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle' }
|
|
64
65
|
): Promise<void> => {
|
|
65
66
|
const path = opts?.dispatcherPath ?? '/dispatcher'
|
|
66
67
|
const url = new URL(path, baseUrl)
|
|
67
68
|
url.searchParams.set('token', token)
|
|
68
|
-
|
|
69
|
+
// `domcontentloaded`, not playwright's `load` default — see the note on `MountOptions.waitUntil`.
|
|
70
|
+
await page.goto(url.toString(), { waitUntil: opts?.waitUntil ?? 'domcontentloaded' })
|
|
69
71
|
// Wait until the dispatcher has navigated away (token consumed).
|
|
70
72
|
await page.waitForURL(u => !u.pathname.startsWith(path), { timeout: 30_000 })
|
|
71
73
|
}
|
|
@@ -168,12 +170,60 @@ export interface SupervisorFormLoginOptions {
|
|
|
168
170
|
/** Predicate/URL fragment that signals a successful landing (default: leaving the login path). */
|
|
169
171
|
expectPath?: string
|
|
170
172
|
timeout?: number
|
|
173
|
+
/**
|
|
174
|
+
* What counts as "arrived" for the initial navigation. Defaults to `domcontentloaded`.
|
|
175
|
+
*
|
|
176
|
+
* See the note on `MountOptions.waitUntil`: `load` is playwright's default and is wrong for a
|
|
177
|
+
* real application, because it waits for every subresource — a tag manager, an analytics beacon
|
|
178
|
+
* or any pixel that never settles holds it open until the navigation times out, with the login
|
|
179
|
+
* form rendered and fillable the whole time.
|
|
180
|
+
*/
|
|
181
|
+
waitUntil?: 'commit' | 'domcontentloaded' | 'load' | 'networkidle'
|
|
182
|
+
/**
|
|
183
|
+
* What to do about a cookie-consent dialog covering the form. Defaults to `accept`, because an
|
|
184
|
+
* OwlMeans app will not start an authentication flow until consent is answered — see
|
|
185
|
+
* {@link acceptConsent}. Pass `ignore` only in a spec that answers the dialog itself.
|
|
186
|
+
*/
|
|
187
|
+
consent?: 'accept' | 'ignore'
|
|
171
188
|
/** When set, capture the filled login form (`supervisor-login-form.png`) before submit. */
|
|
172
189
|
screenshotDir?: string
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
const DEFAULT_SUPERVISOR_PATH = '/authentication/login/pk-supervisor'
|
|
176
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Answer the cookie-consent dialog if one is up, so the page underneath can be driven.
|
|
196
|
+
*
|
|
197
|
+
* An OwlMeans app asks for consent BEFORE it will start an authentication flow, and the dialog is
|
|
198
|
+
* a modal with no dismissal — by design, since a decision is what the gate is waiting for. For a
|
|
199
|
+
* test that means the login form renders, resolves, reports itself "visible, enabled and stable"
|
|
200
|
+
* and still cannot be clicked, because the overlay intercepts the pointer. Playwright retries for
|
|
201
|
+
* the full timeout and then blames the button, which is the most misleading failure in this file:
|
|
202
|
+
* nothing is wrong with the form.
|
|
203
|
+
*
|
|
204
|
+
* Accepting is the honest default for a login helper — the flow it is about to drive is exactly
|
|
205
|
+
* what the dialog is gating — but it is best-effort: an app without the widget, or one where the
|
|
206
|
+
* visitor already decided, simply has no dialog and this returns at once.
|
|
207
|
+
*
|
|
208
|
+
* @returns whether a dialog was actually answered.
|
|
209
|
+
*/
|
|
210
|
+
export const acceptConsent = async (
|
|
211
|
+
page: Page, opts?: { timeout?: number }
|
|
212
|
+
): Promise<boolean> => {
|
|
213
|
+
const dialog = page.locator('[data-consent-dialog]')
|
|
214
|
+
try {
|
|
215
|
+
await dialog.waitFor({ state: 'visible', timeout: opts?.timeout ?? 5_000 })
|
|
216
|
+
} catch {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
// Accept-all rather than essential-only: a test asserting a narrower decision should make that
|
|
220
|
+
// decision itself, and silently choosing the minimum here would hide it from the spec.
|
|
221
|
+
await page.locator('[data-consent-accept-all]').click()
|
|
222
|
+
await dialog.waitFor({ state: 'detached', timeout: opts?.timeout ?? 5_000 })
|
|
223
|
+
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
|
|
177
227
|
/**
|
|
178
228
|
* Drive the PK-based supervisor login FORM end-to-end (the faithful path that
|
|
179
229
|
* exercises the real supervisor plugin + registration): navigate to the
|
|
@@ -185,16 +235,30 @@ export const loginViaSupervisorForm = async (
|
|
|
185
235
|
): Promise<void> => {
|
|
186
236
|
const path = opts.path ?? DEFAULT_SUPERVISOR_PATH
|
|
187
237
|
const timeout = opts.timeout ?? 60_000
|
|
188
|
-
await page.goto(new URL(path, opts.baseUrl).toString()
|
|
238
|
+
await page.goto(new URL(path, opts.baseUrl).toString(), {
|
|
239
|
+
waitUntil: opts.waitUntil ?? 'domcontentloaded', timeout
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// Wait for the app to MOUNT before deciding whether there is a dialog to answer. `domcontentloaded`
|
|
243
|
+
// returns while the SPA is still booting, and on a dev server that boot is occasionally tens of
|
|
244
|
+
// seconds — so a short consent probe reports "no dialog", the dialog then mounts over the form,
|
|
245
|
+
// and the fill below waits out its own timeout on a field that is present but covered. Either
|
|
246
|
+
// marker proves the app is up: the dialog, or the form itself when consent was already given.
|
|
247
|
+
await page.locator('[data-consent-dialog], [data-testid="supervisor-user-id"]')
|
|
248
|
+
.first().waitFor({ state: 'visible', timeout })
|
|
249
|
+
|
|
250
|
+
if (opts.consent !== 'ignore') await acceptConsent(page, { timeout })
|
|
189
251
|
|
|
190
|
-
|
|
191
|
-
|
|
252
|
+
// The form renders only AFTER the dialog is dismissed, so these carry the caller's budget rather
|
|
253
|
+
// than playwright's 30s default — the whole point of accepting an explicit `timeout` here.
|
|
254
|
+
await page.getByTestId('supervisor-user-id').fill(opts.userId, { timeout })
|
|
255
|
+
await page.getByTestId('supervisor-pk').fill(opts.pk, { timeout })
|
|
192
256
|
|
|
193
257
|
if (opts.screenshotDir != null) {
|
|
194
258
|
await saveScreenshot(page, opts.screenshotDir, 'supervisor-login-form')
|
|
195
259
|
}
|
|
196
260
|
|
|
197
|
-
await page.getByTestId('supervisor-submit').click()
|
|
261
|
+
await page.getByTestId('supervisor-submit').click({ timeout })
|
|
198
262
|
|
|
199
263
|
if (opts.expectPath != null) {
|
|
200
264
|
await page.waitForURL(url => url.toString().includes(opts.expectPath!), { timeout })
|