@zerotal/testing 1.7.4 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api-surface.md +4 -0
- package/package.json +6 -6
- package/src/browser/FlowBrowser.ts +120 -6
package/api-surface.md
CHANGED
|
@@ -302,8 +302,12 @@ class BrowserPage = {
|
|
|
302
302
|
connection: () => Promise<string | null>
|
|
303
303
|
count: (selector: string) => Promise<number>
|
|
304
304
|
evaluate: <T = unknown>(expression: string) => Promise<T>
|
|
305
|
+
horizontalOverflow: () => Promise<number>
|
|
305
306
|
html: (selector: string) => Promise<string | null>
|
|
307
|
+
overflowTrace: (selector: string) => Promise<string[]>
|
|
308
|
+
overflowingElements: (limit?: number) => Promise<string[]>
|
|
306
309
|
press: (key: string) => Promise<BrowserPage>
|
|
310
|
+
resize: (width: number, height: number) => Promise<BrowserPage>
|
|
307
311
|
socketUpgraded: () => boolean
|
|
308
312
|
text: (selector: string) => Promise<string | null>
|
|
309
313
|
transport: () => TransportReport
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/testing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zerotal/core": "1.
|
|
36
|
-
"@zerotal/orm": "1.
|
|
37
|
-
"@zerotal/queue": "1.
|
|
38
|
-
"@zerotal/notifications": "1.
|
|
35
|
+
"@zerotal/core": "1.8.0",
|
|
36
|
+
"@zerotal/orm": "1.8.0",
|
|
37
|
+
"@zerotal/queue": "1.8.0",
|
|
38
|
+
"@zerotal/notifications": "1.8.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0",
|
|
42
|
-
"@zerotal/session": "1.
|
|
42
|
+
"@zerotal/session": "1.8.0"
|
|
43
43
|
},
|
|
44
44
|
"description": "Testing utilities for Zerotal — an in-process test app, HTTP helpers, and database refresh.",
|
|
45
45
|
"keywords": [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createTestApp, type TestApp } from "../TestApp.ts";
|
|
2
2
|
import { backendOption, browserAvailability } from "./chrome.ts";
|
|
3
3
|
import type { Application } from "@zerotal/core";
|
|
4
|
+
import { Router } from "@zerotal/core";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Read a CDP event's params.
|
|
@@ -88,6 +89,96 @@ export class BrowserPage {
|
|
|
88
89
|
return this._view.evaluate<T>(expression);
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Resize the viewport, for the failures that only exist at a particular width.
|
|
94
|
+
*
|
|
95
|
+
* Horizontal overflow is the obvious one: a page can be flawless at 1280 and
|
|
96
|
+
* scroll sideways at 375 because one element inside a `not-prose` block has no
|
|
97
|
+
* width constraint. Nothing server-side can see it, and neither can a browser
|
|
98
|
+
* test that never leaves the default window size.
|
|
99
|
+
*/
|
|
100
|
+
async resize(width: number, height: number): Promise<this> {
|
|
101
|
+
this._assertOpen();
|
|
102
|
+
await this._view.resize(width, height);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* How far the document can scroll sideways beyond the viewport, in pixels.
|
|
108
|
+
*
|
|
109
|
+
* `0` is the only healthy answer. `documentElement.scrollWidth` rather than
|
|
110
|
+
* `body`'s, because an overflowing child can push the scrollable area wider
|
|
111
|
+
* than the body box without the body itself ever being wide.
|
|
112
|
+
*/
|
|
113
|
+
async horizontalOverflow(): Promise<number> {
|
|
114
|
+
return this.evaluate<number>(
|
|
115
|
+
"Math.max(0, document.documentElement.scrollWidth - window.innerWidth)",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The elements sticking out past the right edge, widest first.
|
|
121
|
+
*
|
|
122
|
+
* A number alone says a page overflows by 204 pixels and leaves whoever reads
|
|
123
|
+
* the failure to work out by what — which on a page of six thousand nodes is
|
|
124
|
+
* the whole of the job. This names them.
|
|
125
|
+
*/
|
|
126
|
+
async overflowingElements(limit = 5): Promise<string[]> {
|
|
127
|
+
return this.evaluate<string[]>(
|
|
128
|
+
"(() => {" +
|
|
129
|
+
"const edge = window.innerWidth; const out = [];" +
|
|
130
|
+
'for (const el of document.querySelectorAll("body *")) {' +
|
|
131
|
+
"const r = el.getBoundingClientRect();" +
|
|
132
|
+
"if (r.right <= edge + 1 || r.width === 0) continue;" +
|
|
133
|
+
'const id = el.id ? "#" + el.id : "";' +
|
|
134
|
+
'const cls = typeof el.className === "string" && el.className' +
|
|
135
|
+
' ? "." + el.className.trim().split(" ").filter(Boolean).slice(0, 3).join(".") : "";' +
|
|
136
|
+
"out.push({ s: el.tagName.toLowerCase() + id + cls, o: Math.round(r.right - edge) });" +
|
|
137
|
+
"}" +
|
|
138
|
+
"return out.sort((a, b) => b.o - a.o).slice(0, " +
|
|
139
|
+
limit +
|
|
140
|
+
")" +
|
|
141
|
+
'.map((e) => e.s + " (+" + e.o + "px)");' +
|
|
142
|
+
"})()",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Why an element overflows: its ancestors, and which of them fails to contain it.
|
|
148
|
+
*
|
|
149
|
+
* An element inside an `overflow-x: auto` box is *allowed* to measure wider than
|
|
150
|
+
* the viewport — the box scrolls and the page does not. So "this element sticks
|
|
151
|
+
* out" is not yet a bug report; the question is which ancestor was supposed to
|
|
152
|
+
* clip it and did not, which is what this walks up and answers.
|
|
153
|
+
*/
|
|
154
|
+
async overflowTrace(selector: string): Promise<string[]> {
|
|
155
|
+
return this.evaluate<string[]>(
|
|
156
|
+
"(() => {" +
|
|
157
|
+
"let el = document.querySelector(" +
|
|
158
|
+
quote(selector) +
|
|
159
|
+
");" +
|
|
160
|
+
'if (!el) return ["not found: " + ' +
|
|
161
|
+
quote(selector) +
|
|
162
|
+
"];" +
|
|
163
|
+
"const out = [];" +
|
|
164
|
+
"while (el && el !== document.documentElement) {" +
|
|
165
|
+
"const cs = getComputedStyle(el);" +
|
|
166
|
+
"out.push(" +
|
|
167
|
+
"el.tagName.toLowerCase() +" +
|
|
168
|
+
'(el.id ? "#" + el.id : "") +' +
|
|
169
|
+
'" client=" + el.clientWidth +' +
|
|
170
|
+
'" scroll=" + el.scrollWidth +' +
|
|
171
|
+
'" overflowX=" + cs.overflowX +' +
|
|
172
|
+
'" minWidth=" + cs.minWidth +' +
|
|
173
|
+
'" display=" + cs.display' +
|
|
174
|
+
");" +
|
|
175
|
+
"el = el.parentElement;" +
|
|
176
|
+
"}" +
|
|
177
|
+
"return out;" +
|
|
178
|
+
"})()",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
91
182
|
/** `textContent` of the first match, trimmed. `null` when nothing matches. */
|
|
92
183
|
async text(selector: string): Promise<string | null> {
|
|
93
184
|
return this.evaluate<string | null>(
|
|
@@ -323,12 +414,35 @@ export class FlowBrowser {
|
|
|
323
414
|
// `boot()`, inside `start()`. So a `setup` that calls `Router.flow(...)`
|
|
324
415
|
// would find it undefined. `boot()` is idempotent and `start()` skips it
|
|
325
416
|
// when already booted, so pulling it forward changes only the ordering.
|
|
326
|
-
const app = await createTestApp(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
417
|
+
const app = await createTestApp(
|
|
418
|
+
async () => {
|
|
419
|
+
const application = await bootstrap();
|
|
420
|
+
application.adoptAsCurrent();
|
|
421
|
+
await application.boot();
|
|
422
|
+
return application;
|
|
423
|
+
},
|
|
424
|
+
() => {
|
|
425
|
+
// Serve `public/`, which the framework mounts only for the `web`
|
|
426
|
+
// environment — and a test app is not one.
|
|
427
|
+
//
|
|
428
|
+
// Without this a browser suite drives an unstyled site: the stylesheet is
|
|
429
|
+
// built and present on disk and simply never served, so every `<pre>`
|
|
430
|
+
// loses its `overflow-x: auto` and the pages scroll sideways. A layout
|
|
431
|
+
// assertion then measures a page no user will ever see. It cost four CI
|
|
432
|
+
// round trips to find, because `zt serve` runs as `web` and every local
|
|
433
|
+
// reproduction served the file correctly.
|
|
434
|
+
//
|
|
435
|
+
// Registered before the caller's own `setup`, so a suite can still
|
|
436
|
+
// override the mount if it means to.
|
|
437
|
+
// `process.cwd()`, matching what the framework's own `_bootConventions`
|
|
438
|
+
// does — which means a suite invoked from the repo root rather than from
|
|
439
|
+
// the app directory mounts a `public/` that is not there, and every page
|
|
440
|
+
// arrives unstyled. It fails as a layout assertion rather than as a
|
|
441
|
+
// missing file, so it is worth knowing before spending an hour on it.
|
|
442
|
+
Router.static("/", `${process.cwd()}/public`);
|
|
443
|
+
options.setup?.();
|
|
444
|
+
},
|
|
445
|
+
);
|
|
332
446
|
return new FlowBrowser(app, options.timeout ?? DEFAULT_TIMEOUT);
|
|
333
447
|
}
|
|
334
448
|
|