@qoretechnologies/qlip 0.1.3-pr.28.g211f2a4 → 0.1.3-pr.30.gc00bad8
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 +6 -0
- package/dist/config/parameters.d.ts +2 -0
- package/dist/config/parameters.d.ts.map +1 -1
- package/dist/config/parameters.js +4 -0
- package/dist/plugin/browserViewport.d.ts +21 -0
- package/dist/plugin/browserViewport.d.ts.map +1 -0
- package/dist/plugin/browserViewport.js +28 -0
- package/dist/plugin/vitestPlugin.d.ts.map +1 -1
- package/dist/plugin/vitestPlugin.js +14 -0
- package/dist/runtime/fullPage.d.ts +91 -0
- package/dist/runtime/fullPage.d.ts.map +1 -0
- package/dist/runtime/fullPage.js +140 -0
- package/dist/runtime/screenshot.d.ts.map +1 -1
- package/dist/runtime/screenshot.js +28 -1
- package/dist/runtime/setup.d.ts +4 -1
- package/dist/runtime/setup.d.ts.map +1 -1
- package/dist/runtime/setup.js +13 -3
- package/dist/stories/BodyPortal.d.ts +12 -0
- package/dist/stories/BodyPortal.d.ts.map +1 -0
- package/dist/stories/BodyPortal.js +18 -0
- package/dist/stories/BodyPortal.stories.d.ts +17 -0
- package/dist/stories/BodyPortal.stories.d.ts.map +1 -0
- package/dist/stories/BodyPortal.stories.js +33 -0
- package/dist/stories/TallPage.d.ts +14 -0
- package/dist/stories/TallPage.d.ts.map +1 -0
- package/dist/stories/TallPage.js +24 -0
- package/dist/stories/TallPage.stories.d.ts +20 -0
- package/dist/stories/TallPage.stories.d.ts.map +1 -0
- package/dist/stories/TallPage.stories.js +41 -0
- package/dist/test-runner/index.d.ts.map +1 -1
- package/dist/test-runner/index.js +31 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -1
- package/dist-cjs/config/parameters.js +4 -0
- package/dist-cjs/runtime/fullPage.js +146 -0
- package/dist-cjs/test-runner/index.js +31 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -196,6 +196,8 @@ export const LoggedIn = {
|
|
|
196
196
|
viewport: { width: 1280, height: 720 },
|
|
197
197
|
disableAnimations: true,
|
|
198
198
|
pauseAnimationsAtEnd: false,
|
|
199
|
+
fullPage: false,
|
|
200
|
+
fullPageMaxHeight: 10000,
|
|
199
201
|
captureOnError: false,
|
|
200
202
|
waitForIdleMs: 300,
|
|
201
203
|
maxWaitForIdleMs: 2000,
|
|
@@ -219,6 +221,8 @@ qlipVitestPlugin({
|
|
|
219
221
|
viewport: { width: 1280, height: 720 },
|
|
220
222
|
disableAnimations: false,
|
|
221
223
|
pauseAnimationsAtEnd: false,
|
|
224
|
+
fullPage: false,
|
|
225
|
+
fullPageMaxHeight: 10000,
|
|
222
226
|
captureOnError: false,
|
|
223
227
|
waitForIdleMs: 300,
|
|
224
228
|
maxWaitForIdleMs: 2000,
|
|
@@ -227,6 +231,8 @@ qlipVitestPlugin({
|
|
|
227
231
|
});
|
|
228
232
|
```
|
|
229
233
|
|
|
234
|
+
`fullPage` grows the viewport to the page's full content height for the capture and puts it back afterwards. A story that pins a phone viewport is otherwise photographed one screen tall; a browser "full page" screenshot would not help, because an app shell usually scrolls an inner box rather than the document. qlip measures the document and every scrolling element, grows the viewport, re-measures until nothing scrolls, and caps the height at `fullPageMaxHeight` (default 10000). A page whose content grows with the viewport (a tile sized to its column, a `vh` box) would ask for more every round; a round that asks for no more than the previous growth gave is taken as that, and the capture stays at the height the real content asked for. Under Vitest browser mode the test iframe is scaled down to fit the browser window, so the plugin also grows the window — the Playwright context viewport — through a browser command it registers (`qlipBrowserViewport`), and restores it first; the capture is 1:1, not a shrunk picture of a tall page. The same holds for any pinned `viewport` the window cannot hold: an 844px-tall phone story in a 720px window used to come back at 85%, and now grows the window too. The test-runner path grows its Playwright viewport the same way. The manifest keeps reporting the pinned `viewport` — that is the baseline key; the image is simply taller, and a capture whose content height changed shows on the server as a dimensions change rather than a pixel diff.
|
|
235
|
+
|
|
230
236
|
`waitForIdleMs` waits for DOM mutations to settle before taking a screenshot. This is especially useful for animation libraries like `react-spring` that update inline styles via `requestAnimationFrame`, which bypasses CSS-based animation disabling. Increase it if you still catch mid-transition frames, or lower it for faster runs when your UI is static. `maxWaitForIdleMs` caps the wait so stories with continuously changing UI still complete.
|
|
231
237
|
|
|
232
238
|
`ignoreElements` lets you provide CSS selectors to mask before capture. Qlip draws solid overlays on matching elements so layout stays intact while visual diffs ignore those regions.
|
|
@@ -8,6 +8,8 @@ export interface QlipResolvedOptions {
|
|
|
8
8
|
disableAnimations: boolean;
|
|
9
9
|
pauseAnimationsAtEnd: boolean;
|
|
10
10
|
disableBackdropFilter: boolean;
|
|
11
|
+
fullPage: boolean;
|
|
12
|
+
fullPageMaxHeight: number;
|
|
11
13
|
waitForIdleMs: number;
|
|
12
14
|
maxWaitForIdleMs: number;
|
|
13
15
|
ignoreElements: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/config/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE5G,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,oBAAoB,EAAE,gBAAgB,EAAE,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,yBAAyB,EAAE,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,kBAAkB,GAAI,gCAIhC;IACD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC,KAAG,
|
|
1
|
+
{"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/config/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE5G,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,oBAAoB,EAAE,gBAAgB,EAAE,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,yBAAyB,EAAE,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,kBAAkB,GAAI,gCAIhC;IACD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC,KAAG,mBAqDH,CAAC"}
|
|
@@ -11,6 +11,10 @@ export const resolveQlipOptions = ({ defaults, story, override, }) => {
|
|
|
11
11
|
disableBackdropFilter: override?.disableBackdropFilter ??
|
|
12
12
|
story?.disableBackdropFilter ??
|
|
13
13
|
defaults.disableBackdropFilter,
|
|
14
|
+
fullPage: override?.fullPage ?? story?.fullPage ?? defaults.fullPage,
|
|
15
|
+
fullPageMaxHeight: override?.fullPageMaxHeight ??
|
|
16
|
+
story?.fullPageMaxHeight ??
|
|
17
|
+
defaults.fullPageMaxHeight,
|
|
14
18
|
waitForIdleMs: override?.waitForIdleMs ??
|
|
15
19
|
story?.waitForIdleMs ??
|
|
16
20
|
defaults.waitForIdleMs,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BrowserCommand } from 'vitest/node';
|
|
2
|
+
import type { FullPageSize } from '../runtime/fullPage.js';
|
|
3
|
+
/** The name the runtime calls the command by (`commands.qlipBrowserViewport`). */
|
|
4
|
+
export declare const QLIP_BROWSER_VIEWPORT_COMMAND = "qlipBrowserViewport";
|
|
5
|
+
/**
|
|
6
|
+
* Reads — and, given a size, sets — the browser window's viewport: the
|
|
7
|
+
* Playwright context viewport that Vitest scales the test iframe to fit into.
|
|
8
|
+
* A capture whose viewport is taller or wider than the window comes back
|
|
9
|
+
* shrunk to fit unless the window grows with it (a 844px phone story in a
|
|
10
|
+
* 720px window is an 85% picture of it; a 3,600px full-page capture in a
|
|
11
|
+
* 1,440px window, a 40% one). Runs in the Vitest server process where the
|
|
12
|
+
* Playwright page lives; the runtime reaches it through
|
|
13
|
+
* `commands.qlipBrowserViewport`.
|
|
14
|
+
*
|
|
15
|
+
* Answers the size the window had before the call, or `null` when there is
|
|
16
|
+
* no such viewport to grow: another provider (webdriverio resizes the real
|
|
17
|
+
* window from `page.viewport()` and scales nothing), or a context launched
|
|
18
|
+
* with `viewport: null`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const qlipBrowserViewport: BrowserCommand<[size?: FullPageSize | null]>;
|
|
21
|
+
//# sourceMappingURL=browserViewport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserViewport.d.ts","sourceRoot":"","sources":["../../src/plugin/browserViewport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,kFAAkF;AAClF,eAAO,MAAM,6BAA6B,wBAAwB,CAAC;AAQnE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAAc,CAAC,CAAC,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC,CAa5E,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The name the runtime calls the command by (`commands.qlipBrowserViewport`). */
|
|
2
|
+
export const QLIP_BROWSER_VIEWPORT_COMMAND = 'qlipBrowserViewport';
|
|
3
|
+
/**
|
|
4
|
+
* Reads — and, given a size, sets — the browser window's viewport: the
|
|
5
|
+
* Playwright context viewport that Vitest scales the test iframe to fit into.
|
|
6
|
+
* A capture whose viewport is taller or wider than the window comes back
|
|
7
|
+
* shrunk to fit unless the window grows with it (a 844px phone story in a
|
|
8
|
+
* 720px window is an 85% picture of it; a 3,600px full-page capture in a
|
|
9
|
+
* 1,440px window, a 40% one). Runs in the Vitest server process where the
|
|
10
|
+
* Playwright page lives; the runtime reaches it through
|
|
11
|
+
* `commands.qlipBrowserViewport`.
|
|
12
|
+
*
|
|
13
|
+
* Answers the size the window had before the call, or `null` when there is
|
|
14
|
+
* no such viewport to grow: another provider (webdriverio resizes the real
|
|
15
|
+
* window from `page.viewport()` and scales nothing), or a context launched
|
|
16
|
+
* with `viewport: null`.
|
|
17
|
+
*/
|
|
18
|
+
export const qlipBrowserViewport = async (context, size) => {
|
|
19
|
+
const page = context.page;
|
|
20
|
+
if (context.provider.name !== 'playwright' || !page) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const current = page.viewportSize();
|
|
24
|
+
if (size && current && (size.width !== current.width || size.height !== current.height)) {
|
|
25
|
+
await page.setViewportSize(size);
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitestPlugin.d.ts","sourceRoot":"","sources":["../../src/plugin/vitestPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAU5C,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"vitestPlugin.d.ts","sourceRoot":"","sources":["../../src/plugin/vitestPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAU5C,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;AAanE,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,MA8QlE,CAAC"}
|
|
@@ -6,6 +6,9 @@ import pkg from '../../package.json' with { type: 'json' };
|
|
|
6
6
|
import { DEFAULT_OUTPUT_DIR, DEFAULT_VIEWPORT, MANIFEST_FRAGMENT_DIR, TOOL_NAME, generateBuildId, } from '../fs/output.js';
|
|
7
7
|
import { QlipUploadReporter } from '../upload/reporter.js';
|
|
8
8
|
import { stashFinalizeConfig } from '../runtime/global-setup.js';
|
|
9
|
+
import { QLIP_BROWSER_VIEWPORT_COMMAND, qlipBrowserViewport, } from './browserViewport.js';
|
|
10
|
+
/** See `QlipCaptureOptions.fullPageMaxHeight`. */
|
|
11
|
+
const DEFAULT_FULL_PAGE_MAX_HEIGHT = 10000;
|
|
9
12
|
const normalizePath = (value) => value.replace(/\\/g, '/');
|
|
10
13
|
export const qlipVitestPlugin = (options = {}) => {
|
|
11
14
|
const buildId = options.buildId ?? generateBuildId();
|
|
@@ -37,6 +40,8 @@ export const qlipVitestPlugin = (options = {}) => {
|
|
|
37
40
|
// never a wanted baseline. See QlipCaptureOptions.
|
|
38
41
|
disableBackdropFilter: options.disableBackdropFilter ?? true,
|
|
39
42
|
captureOnError: options.captureOnError ?? false,
|
|
43
|
+
fullPage: options.fullPage ?? false,
|
|
44
|
+
fullPageMaxHeight: options.fullPageMaxHeight ?? DEFAULT_FULL_PAGE_MAX_HEIGHT,
|
|
40
45
|
waitForIdleMs: options.waitForIdleMs ?? 300,
|
|
41
46
|
maxWaitForIdleMs: options.maxWaitForIdleMs ?? 2000,
|
|
42
47
|
ignoreElements: options.ignoreElements ?? [],
|
|
@@ -185,6 +190,15 @@ export const qlipVitestPlugin = (options = {}) => {
|
|
|
185
190
|
setupFiles: [setupFile],
|
|
186
191
|
globalSetup: [globalSetupFile],
|
|
187
192
|
reporters: reportersList,
|
|
193
|
+
// The runtime grows the browser window for a `fullPage` capture
|
|
194
|
+
// through this command; it runs here, where the Playwright page
|
|
195
|
+
// is. `mergeConfig` merges it beside any commands the consumer
|
|
196
|
+
// declares.
|
|
197
|
+
browser: {
|
|
198
|
+
commands: {
|
|
199
|
+
[QLIP_BROWSER_VIEWPORT_COMMAND]: qlipBrowserViewport,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
188
202
|
},
|
|
189
203
|
};
|
|
190
204
|
},
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `fullPage` capture grows the viewport until nothing on the page scrolls,
|
|
3
|
+
* photographs it, and puts the viewport back. A story that pins a phone
|
|
4
|
+
* viewport is otherwise one screen tall, and a browser "full page" screenshot
|
|
5
|
+
* would not help: an app shell pins the document to the viewport and scrolls
|
|
6
|
+
* an inner box, so the document is never taller than one screen.
|
|
7
|
+
*
|
|
8
|
+
* Under Vitest browser mode two viewports are involved. `page.viewport()`
|
|
9
|
+
* sizes the test iframe; the real browser window (the Playwright context
|
|
10
|
+
* viewport) stays as configured, and Vitest scales the iframe down to fit it.
|
|
11
|
+
* A 3,600px-tall iframe in a 1,440px window is photographed at 40% — the
|
|
12
|
+
* whole page, unreadable — and so is a plain 844px-tall phone story in a
|
|
13
|
+
* 720px window, at 85%. `createCaptureViewport` therefore grows the window
|
|
14
|
+
* to hold whatever viewport a capture asks for, pinned or grown, and puts
|
|
15
|
+
* both back afterwards, the window first, so the iframe lands at scale 1.
|
|
16
|
+
*
|
|
17
|
+
* This module has no imports: the runtime, the test-runner and the unit
|
|
18
|
+
* project (which has no DOM and feeds it fakes) all share it.
|
|
19
|
+
*/
|
|
20
|
+
export interface FullPageSize {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* How tall the page really is: the document, and every box that scrolls
|
|
26
|
+
* its own content. Each scrolling box counts from its own offset, so a box
|
|
27
|
+
* that starts 105px down and scrolls 3,484px asks for 3,589px plus its own
|
|
28
|
+
* border and horizontal scrollbar.
|
|
29
|
+
*
|
|
30
|
+
* Closure-free, so the test-runner can hand it to Playwright's
|
|
31
|
+
* `page.evaluate()`, which re-evaluates the function's source inside the
|
|
32
|
+
* page; there it reads the real `document` and `window`. `env` exists for
|
|
33
|
+
* the unit test, which has neither.
|
|
34
|
+
*/
|
|
35
|
+
export declare const measureContentHeight: (env?: {
|
|
36
|
+
doc: Pick<Document, "documentElement" | "body" | "querySelectorAll">;
|
|
37
|
+
win: Pick<Window, "getComputedStyle" | "scrollY">;
|
|
38
|
+
}) => number;
|
|
39
|
+
/**
|
|
40
|
+
* Grows the viewport until nothing on the page scrolls. Growing the viewport
|
|
41
|
+
* re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
|
|
42
|
+
* and may reveal another box below it that scrolls in turn — so the height is
|
|
43
|
+
* measured again after every growth, until a round asks for nothing more or
|
|
44
|
+
* `maxRounds` is spent.
|
|
45
|
+
*
|
|
46
|
+
* A page can also ask for more because it GREW WITH the viewport: a tile
|
|
47
|
+
* sized to its column, a `vh` box. Such content asks for about as much again
|
|
48
|
+
* as the last growth gave it, every round, and would be chased to the cap —
|
|
49
|
+
* a capture that is one screen of content over thousands of pixels of
|
|
50
|
+
* stretched tile. So a round that asks for no more than the previous growth
|
|
51
|
+
* is taken as content that scales, not content that was revealed, and the
|
|
52
|
+
* viewport stays where it is. (Revealed content asks for its own height,
|
|
53
|
+
* unrelated to the growth; it is only missed when it happens to be shorter
|
|
54
|
+
* than the growth that revealed it.)
|
|
55
|
+
*
|
|
56
|
+
* Returns the height the viewport ended at, or `null` when it never had to
|
|
57
|
+
* grow (the caller restores only in that case).
|
|
58
|
+
*/
|
|
59
|
+
export declare const growViewportToContent: ({ viewport, maxHeight, setViewport, measure, settle, maxRounds, }: {
|
|
60
|
+
viewport: FullPageSize;
|
|
61
|
+
maxHeight: number;
|
|
62
|
+
setViewport: (width: number, height: number) => Promise<void>;
|
|
63
|
+
measure: () => number | Promise<number>;
|
|
64
|
+
settle: () => Promise<void>;
|
|
65
|
+
maxRounds?: number;
|
|
66
|
+
}) => Promise<number | null>;
|
|
67
|
+
/**
|
|
68
|
+
* The browser-window half of the viewport, as the qlip plugin's browser
|
|
69
|
+
* command exposes it: called without a size it answers the window's current
|
|
70
|
+
* viewport, with one it resizes the window and answers the previous size.
|
|
71
|
+
* `null` means the provider has no window viewport to grow — webdriverio
|
|
72
|
+
* resizes the real window from `page.viewport()` itself and scales nothing.
|
|
73
|
+
*/
|
|
74
|
+
export type BrowserViewportCommand = (size?: FullPageSize | null) => Promise<FullPageSize | null>;
|
|
75
|
+
/**
|
|
76
|
+
* The two viewports a capture sizes, and how each is put back. `set` grows
|
|
77
|
+
* the window to hold the iframe whenever the iframe would not fit (never
|
|
78
|
+
* shrinking it below what it was), then sizes the iframe; the window is
|
|
79
|
+
* asked for its size once. `restore` puts the window back first, so the
|
|
80
|
+
* iframe's pinned size is computed against the original window again, then
|
|
81
|
+
* the iframe — and touches neither when neither moved.
|
|
82
|
+
*/
|
|
83
|
+
export declare const createCaptureViewport: ({ pinned, iframe, window: windowViewport, }: {
|
|
84
|
+
pinned: FullPageSize;
|
|
85
|
+
iframe: (width: number, height: number) => Promise<void>;
|
|
86
|
+
window?: BrowserViewportCommand | undefined;
|
|
87
|
+
}) => {
|
|
88
|
+
set: (width: number, height: number) => Promise<void>;
|
|
89
|
+
restore: () => Promise<void>;
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=fullPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fullPage.d.ts","sourceRoot":"","sources":["../../src/runtime/fullPage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM;IACzC,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,GAAG,kBAAkB,CAAC,CAAC;IACrE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,kBAAkB,GAAG,SAAS,CAAC,CAAC;CACnD,KAAG,MAiBH,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,qBAAqB,GAAU,mEAOzC;IACD,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAmBxB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,sBAAsB,GAAG,CACnC,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI,KACvB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;AAElC;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,6CAInC;IACD,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,MAAM,CAAC,EAAE,sBAAsB,GAAG,SAAS,CAAC;CAC7C,KAAG;IACF,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CA4C9B,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `fullPage` capture grows the viewport until nothing on the page scrolls,
|
|
3
|
+
* photographs it, and puts the viewport back. A story that pins a phone
|
|
4
|
+
* viewport is otherwise one screen tall, and a browser "full page" screenshot
|
|
5
|
+
* would not help: an app shell pins the document to the viewport and scrolls
|
|
6
|
+
* an inner box, so the document is never taller than one screen.
|
|
7
|
+
*
|
|
8
|
+
* Under Vitest browser mode two viewports are involved. `page.viewport()`
|
|
9
|
+
* sizes the test iframe; the real browser window (the Playwright context
|
|
10
|
+
* viewport) stays as configured, and Vitest scales the iframe down to fit it.
|
|
11
|
+
* A 3,600px-tall iframe in a 1,440px window is photographed at 40% — the
|
|
12
|
+
* whole page, unreadable — and so is a plain 844px-tall phone story in a
|
|
13
|
+
* 720px window, at 85%. `createCaptureViewport` therefore grows the window
|
|
14
|
+
* to hold whatever viewport a capture asks for, pinned or grown, and puts
|
|
15
|
+
* both back afterwards, the window first, so the iframe lands at scale 1.
|
|
16
|
+
*
|
|
17
|
+
* This module has no imports: the runtime, the test-runner and the unit
|
|
18
|
+
* project (which has no DOM and feeds it fakes) all share it.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* How tall the page really is: the document, and every box that scrolls
|
|
22
|
+
* its own content. Each scrolling box counts from its own offset, so a box
|
|
23
|
+
* that starts 105px down and scrolls 3,484px asks for 3,589px plus its own
|
|
24
|
+
* border and horizontal scrollbar.
|
|
25
|
+
*
|
|
26
|
+
* Closure-free, so the test-runner can hand it to Playwright's
|
|
27
|
+
* `page.evaluate()`, which re-evaluates the function's source inside the
|
|
28
|
+
* page; there it reads the real `document` and `window`. `env` exists for
|
|
29
|
+
* the unit test, which has neither.
|
|
30
|
+
*/
|
|
31
|
+
export const measureContentHeight = (env) => {
|
|
32
|
+
const doc = env?.doc ?? document;
|
|
33
|
+
const win = env?.win ?? window;
|
|
34
|
+
let height = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
|
|
35
|
+
for (const el of Array.from(doc.querySelectorAll('*'))) {
|
|
36
|
+
if (el.scrollHeight <= el.clientHeight + 1) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const overflowY = win.getComputedStyle(el).overflowY;
|
|
40
|
+
if (overflowY !== 'auto' && overflowY !== 'scroll') {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const top = el.getBoundingClientRect().top + win.scrollY;
|
|
44
|
+
const chrome = el.offsetHeight - el.clientHeight;
|
|
45
|
+
height = Math.max(height, Math.ceil(top + el.scrollHeight + chrome));
|
|
46
|
+
}
|
|
47
|
+
return height;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Grows the viewport until nothing on the page scrolls. Growing the viewport
|
|
51
|
+
* re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
|
|
52
|
+
* and may reveal another box below it that scrolls in turn — so the height is
|
|
53
|
+
* measured again after every growth, until a round asks for nothing more or
|
|
54
|
+
* `maxRounds` is spent.
|
|
55
|
+
*
|
|
56
|
+
* A page can also ask for more because it GREW WITH the viewport: a tile
|
|
57
|
+
* sized to its column, a `vh` box. Such content asks for about as much again
|
|
58
|
+
* as the last growth gave it, every round, and would be chased to the cap —
|
|
59
|
+
* a capture that is one screen of content over thousands of pixels of
|
|
60
|
+
* stretched tile. So a round that asks for no more than the previous growth
|
|
61
|
+
* is taken as content that scales, not content that was revealed, and the
|
|
62
|
+
* viewport stays where it is. (Revealed content asks for its own height,
|
|
63
|
+
* unrelated to the growth; it is only missed when it happens to be shorter
|
|
64
|
+
* than the growth that revealed it.)
|
|
65
|
+
*
|
|
66
|
+
* Returns the height the viewport ended at, or `null` when it never had to
|
|
67
|
+
* grow (the caller restores only in that case).
|
|
68
|
+
*/
|
|
69
|
+
export const growViewportToContent = async ({ viewport, maxHeight, setViewport, measure, settle, maxRounds = 4, }) => {
|
|
70
|
+
let height = viewport.height;
|
|
71
|
+
let grown = false;
|
|
72
|
+
let previous = null;
|
|
73
|
+
for (let round = 0; round < maxRounds; round += 1) {
|
|
74
|
+
const asked = Math.min(Math.ceil(await measure()), maxHeight);
|
|
75
|
+
if (asked <= height) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (previous && asked - previous.asked <= previous.growth) {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
previous = { asked, growth: asked - height };
|
|
82
|
+
height = asked;
|
|
83
|
+
grown = true;
|
|
84
|
+
await setViewport(viewport.width, height);
|
|
85
|
+
await settle();
|
|
86
|
+
}
|
|
87
|
+
return grown ? height : null;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The two viewports a capture sizes, and how each is put back. `set` grows
|
|
91
|
+
* the window to hold the iframe whenever the iframe would not fit (never
|
|
92
|
+
* shrinking it below what it was), then sizes the iframe; the window is
|
|
93
|
+
* asked for its size once. `restore` puts the window back first, so the
|
|
94
|
+
* iframe's pinned size is computed against the original window again, then
|
|
95
|
+
* the iframe — and touches neither when neither moved.
|
|
96
|
+
*/
|
|
97
|
+
export const createCaptureViewport = ({ pinned, iframe, window: windowViewport, }) => {
|
|
98
|
+
// undefined: not asked yet; null: asked, and there is no window to grow
|
|
99
|
+
let base;
|
|
100
|
+
let windowAt = null;
|
|
101
|
+
let iframeAt = null;
|
|
102
|
+
const same = (a, b) => a.width === b.width && a.height === b.height;
|
|
103
|
+
const growWindow = async (width, height) => {
|
|
104
|
+
if (!windowViewport) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (base === undefined) {
|
|
108
|
+
base = await windowViewport();
|
|
109
|
+
}
|
|
110
|
+
if (!base) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const target = {
|
|
114
|
+
width: Math.max(base.width, width),
|
|
115
|
+
height: Math.max(base.height, height),
|
|
116
|
+
};
|
|
117
|
+
if (same(target, windowAt ?? base)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await windowViewport(target);
|
|
121
|
+
windowAt = target;
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
set: async (width, height) => {
|
|
125
|
+
await growWindow(width, height);
|
|
126
|
+
await iframe(width, height);
|
|
127
|
+
iframeAt = { width, height };
|
|
128
|
+
},
|
|
129
|
+
restore: async () => {
|
|
130
|
+
if (base && windowAt && windowViewport) {
|
|
131
|
+
await windowViewport(base);
|
|
132
|
+
windowAt = null;
|
|
133
|
+
}
|
|
134
|
+
if (iframeAt && !same(iframeAt, pinned)) {
|
|
135
|
+
await iframe(pinned.width, pinned.height);
|
|
136
|
+
iframeAt = pinned;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/runtime/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAEjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../src/runtime/screenshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,qBAAqB,EACrB,gBAAgB,EAEjB,MAAM,aAAa,CAAC;AAoCrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AA8L1C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,0BAA0B,GACrC,SAAS,MAAM;AACf;;;;;;;GAOG;AACH,gBAAgB,MAAM,KACrB;IAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAiCvD,CAAC;AAwDF;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,GAAI,qEAIxC;IACD,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;CAChC,SA+DA,CAAC;AAwbF,eAAO,MAAM,UAAU,GACrB,KAAK,gBAAgB,EACrB,gBAAgB,MAAM,GAAG,qBAAqB,EAC9C,UAAU,qBAAqB,kBA0BhC,CAAC;AAEF,KAAK,eAAe,GAAG,WAAW,GAAG;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC;AA8ClE,eAAO,MAAM,qBAAqB,GAAU,KAAK,eAAe,kBAiG/D,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAU,KAAK,eAAe,kBAkEhE,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveQlipOptions } from '../config/parameters.js';
|
|
2
|
+
import { createCaptureViewport, growViewportToContent, measureContentHeight, } from './fullPage.js';
|
|
2
3
|
import { monotonicNow, realClearInterval, realSetInterval, realSetTimeout, } from './clock.js';
|
|
3
4
|
import { AUTO_ERROR_SCREENSHOT_BASE, MANIFEST_FRAGMENT_DIR, buildAutoLogPath, buildAutoScreenshotPath, buildManualLogPath, buildManualScreenshotPath, fragmentFileName, joinPath, sanitizeSegment, } from '../fs/output.js';
|
|
4
5
|
import { flushConsoleLogs, initRuntimeState, markStorybookWarning, nextFragmentSeq, nextStepName, pruneStaleErrorEntries, pushEntry, updateStats, } from './context.js';
|
|
@@ -490,14 +491,31 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
|
|
|
490
491
|
// throws.
|
|
491
492
|
let error = testError ?? null;
|
|
492
493
|
let cleanupMasks = null;
|
|
494
|
+
// The browser window grows to hold whatever viewport the capture asks for,
|
|
495
|
+
// pinned or full-page, or the capture comes back scaled down to fit the
|
|
496
|
+
// window (see ./fullPage.ts).
|
|
497
|
+
const captureViewport = createCaptureViewport({
|
|
498
|
+
pinned: resolved.viewport,
|
|
499
|
+
iframe: (width, height) => page.viewport(width, height),
|
|
500
|
+
window: commands.qlipBrowserViewport,
|
|
501
|
+
});
|
|
493
502
|
try {
|
|
494
|
-
await
|
|
503
|
+
await captureViewport.set(resolved.viewport.width, resolved.viewport.height);
|
|
495
504
|
applyCaptureStyleOverrides({
|
|
496
505
|
disableAnimations: resolved.disableAnimations,
|
|
497
506
|
pauseAnimationsAtEnd: resolved.pauseAnimationsAtEnd,
|
|
498
507
|
disableBackdropFilter: resolved.disableBackdropFilter,
|
|
499
508
|
});
|
|
500
509
|
await waitForDomIdle(resolved.waitForIdleMs, resolved.maxWaitForIdleMs);
|
|
510
|
+
if (resolved.fullPage) {
|
|
511
|
+
await growViewportToContent({
|
|
512
|
+
viewport: resolved.viewport,
|
|
513
|
+
maxHeight: resolved.fullPageMaxHeight,
|
|
514
|
+
setViewport: captureViewport.set,
|
|
515
|
+
measure: () => measureContentHeight(),
|
|
516
|
+
settle: () => waitForDomIdle(resolved.waitForIdleMs, resolved.maxWaitForIdleMs),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
501
519
|
cleanupMasks = applyIgnoreMasks(resolved.ignoreElements);
|
|
502
520
|
await page.screenshot({ path: absolutePath, save: true });
|
|
503
521
|
}
|
|
@@ -513,6 +531,15 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
|
|
|
513
531
|
if (cleanupMasks) {
|
|
514
532
|
cleanupMasks();
|
|
515
533
|
}
|
|
534
|
+
// The pinned viewport is the baseline key and what the next capture
|
|
535
|
+
// inherits; a failure to put it back must not hide the capture's own
|
|
536
|
+
// outcome.
|
|
537
|
+
try {
|
|
538
|
+
await captureViewport.restore();
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// The suite's own viewport reset runs before the next story.
|
|
542
|
+
}
|
|
516
543
|
}
|
|
517
544
|
const flushed = flushConsoleLogs(runtime);
|
|
518
545
|
let logsPath;
|
package/dist/runtime/setup.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const VIEWPORT_RESET_STYLE_ID = "__qlip-viewport-reset";
|
|
2
|
+
export declare const VIEWPORT_RESET_CSS = "html, body, #storybook-root, body > div:not([class]):not([id]) { width: 100%; height: 100%; margin: 0; padding: 0; }";
|
|
3
|
+
/** Exported for unit testing. */
|
|
4
|
+
export declare const injectViewportStyles: () => void;
|
|
2
5
|
//# sourceMappingURL=setup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/runtime/setup.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/runtime/setup.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAS/D,eAAO,MAAM,kBAAkB,yHACyF,CAAC;AAEzH,iCAAiC;AACjC,eAAO,MAAM,oBAAoB,YAOhC,CAAC"}
|
package/dist/runtime/setup.js
CHANGED
|
@@ -3,13 +3,23 @@ import { captureAutoScreenshot, captureErrorScreenshot } from './screenshot.js';
|
|
|
3
3
|
import { flushConsoleLogs, getRuntimeState, initRuntimeState, pushConsoleLog } from './context.js';
|
|
4
4
|
import { realDateNow } from './clock.js';
|
|
5
5
|
const isBrowser = () => typeof globalThis.__vitest_browser__ !== 'undefined';
|
|
6
|
-
const
|
|
6
|
+
export const VIEWPORT_RESET_STYLE_ID = '__qlip-viewport-reset';
|
|
7
|
+
// Storybook's portable stories mount every story into an anonymous <div>
|
|
8
|
+
// (no id, no class) appended to <body>; it needs the full viewport for
|
|
9
|
+
// height-filling stories. A bare `body > div` also matched everything React
|
|
10
|
+
// portals into <body> (reqore's fixed-position floating-actions bar, popovers)
|
|
11
|
+
// and stretched it to 100% x 100%, which captured as opaque black rectangles.
|
|
12
|
+
// `:first-of-type` is no alternative: Storybook's hidden `sb-wrapper` divs
|
|
13
|
+
// precede the canvas.
|
|
14
|
+
export const VIEWPORT_RESET_CSS = 'html, body, #storybook-root, body > div:not([class]):not([id]) { width: 100%; height: 100%; margin: 0; padding: 0; }';
|
|
15
|
+
/** Exported for unit testing. */
|
|
16
|
+
export const injectViewportStyles = () => {
|
|
7
17
|
const doc = globalThis.document;
|
|
8
18
|
if (!doc)
|
|
9
19
|
return;
|
|
10
20
|
const style = doc.createElement('style');
|
|
11
|
-
style.id =
|
|
12
|
-
style.textContent =
|
|
21
|
+
style.id = VIEWPORT_RESET_STYLE_ID;
|
|
22
|
+
style.textContent = VIEWPORT_RESET_CSS;
|
|
13
23
|
doc.head.appendChild(style);
|
|
14
24
|
};
|
|
15
25
|
const serializeArg = (arg) => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface IBodyPortalProps {
|
|
2
|
+
/** Label of the portalled toolbar; the toolbar sizes itself to it. */
|
|
3
|
+
label?: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Renders a fixed-position toolbar straight into <body>, the way UI
|
|
7
|
+
* libraries portal floating actions, popovers and dropdowns. Like those, it
|
|
8
|
+
* carries no explicit width/height and sizes itself to its content, so a
|
|
9
|
+
* capture-time rule that stretches <body> children would balloon it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BodyPortal: ({ label }: IBodyPortalProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=BodyPortal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BodyPortal.d.ts","sourceRoot":"","sources":["../../src/stories/BodyPortal.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,WAAgC,gBAAgB,4CAuB1E,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
/**
|
|
4
|
+
* Renders a fixed-position toolbar straight into <body>, the way UI
|
|
5
|
+
* libraries portal floating actions, popovers and dropdowns. Like those, it
|
|
6
|
+
* carries no explicit width/height and sizes itself to its content, so a
|
|
7
|
+
* capture-time rule that stretches <body> children would balloon it.
|
|
8
|
+
*/
|
|
9
|
+
export const BodyPortal = ({ label = 'floating actions' }) => (_jsxs("div", { style: { height: '100%', padding: 16, boxSizing: 'border-box' }, children: [_jsx("h1", { children: "A toolbar portalled into body sits at the bottom right" }), createPortal(_jsx("div", { className: "body-portal-toolbar", "data-testid": "body-portal", style: {
|
|
10
|
+
position: 'fixed',
|
|
11
|
+
right: 16,
|
|
12
|
+
bottom: 16,
|
|
13
|
+
padding: '6px 12px',
|
|
14
|
+
background: '#1d4ed8',
|
|
15
|
+
color: '#fff',
|
|
16
|
+
borderRadius: 4,
|
|
17
|
+
whiteSpace: 'nowrap',
|
|
18
|
+
}, children: label }), document.body)] }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/react-vite';
|
|
2
|
+
declare const meta: {
|
|
3
|
+
title: string;
|
|
4
|
+
component: ({ label }: import("./BodyPortal.js").IBodyPortalProps) => import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
parameters: {
|
|
6
|
+
layout: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
11
|
+
/**
|
|
12
|
+
* The capture-time viewport reset sizes the story canvas to the viewport but
|
|
13
|
+
* must leave elements portalled into <body> alone. Before the fix the
|
|
14
|
+
* content-sized portal was stretched to 100% x 100% of the viewport.
|
|
15
|
+
*/
|
|
16
|
+
export declare const KeepsItsOwnSize: Story;
|
|
17
|
+
//# sourceMappingURL=BodyPortal.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BodyPortal.stories.d.ts","sourceRoot":"","sources":["../../src/stories/BodyPortal.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAK5D,QAAA,MAAM,IAAI;;;;;;CAMyB,CAAC;AAEpC,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAGnC;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,KAkB7B,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { expect, within } from 'storybook/test';
|
|
2
|
+
import { BodyPortal } from './BodyPortal.js';
|
|
3
|
+
const meta = {
|
|
4
|
+
title: 'Example/BodyPortal',
|
|
5
|
+
component: BodyPortal,
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: 'fullscreen',
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
/**
|
|
12
|
+
* The capture-time viewport reset sizes the story canvas to the viewport but
|
|
13
|
+
* must leave elements portalled into <body> alone. Before the fix the
|
|
14
|
+
* content-sized portal was stretched to 100% x 100% of the viewport.
|
|
15
|
+
*/
|
|
16
|
+
export const KeepsItsOwnSize = {
|
|
17
|
+
args: { label: 'floating actions' },
|
|
18
|
+
play: async ({ canvasElement }) => {
|
|
19
|
+
const canvas = within(canvasElement);
|
|
20
|
+
await expect(canvas.getByText(/portalled into body/i)).toBeInTheDocument();
|
|
21
|
+
const portal = document.querySelector('[data-testid="body-portal"]');
|
|
22
|
+
if (!portal)
|
|
23
|
+
throw new Error('portal not rendered');
|
|
24
|
+
const rect = portal.getBoundingClientRect();
|
|
25
|
+
// Content-sized: a short label, one line. Stretched: the whole viewport.
|
|
26
|
+
await expect(rect.width).toBeLessThan(window.innerWidth / 2);
|
|
27
|
+
await expect(rect.height).toBeLessThan(60);
|
|
28
|
+
await expect(rect.right).toBeLessThanOrEqual(window.innerWidth);
|
|
29
|
+
await expect(rect.bottom).toBeLessThanOrEqual(window.innerHeight);
|
|
30
|
+
// The anonymous story canvas itself still fills the viewport.
|
|
31
|
+
await expect(canvasElement.getBoundingClientRect().height).toBe(window.innerHeight);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ITallPageProps {
|
|
2
|
+
/** Sections in the scrolling column; each is 240px tall. */
|
|
3
|
+
sections?: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* An app shell the way real ones are built: the document is pinned to the
|
|
7
|
+
* viewport and never scrolls, a header stays put, and a column below it
|
|
8
|
+
* scrolls its own content. A one-screen capture of this page shows the
|
|
9
|
+
* header and the first sections; everything else is inside the column's
|
|
10
|
+
* scroll, where neither the document height nor a browser "full page"
|
|
11
|
+
* screenshot can see it.
|
|
12
|
+
*/
|
|
13
|
+
export declare const TallPage: ({ sections }: ITallPageProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
//# sourceMappingURL=TallPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TallPage.d.ts","sourceRoot":"","sources":["../../src/stories/TallPage.tsx"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,GAAI,cAAmB,cAAc,4CAiCzD,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* An app shell the way real ones are built: the document is pinned to the
|
|
4
|
+
* viewport and never scrolls, a header stays put, and a column below it
|
|
5
|
+
* scrolls its own content. A one-screen capture of this page shows the
|
|
6
|
+
* header and the first sections; everything else is inside the column's
|
|
7
|
+
* scroll, where neither the document height nor a browser "full page"
|
|
8
|
+
* screenshot can see it.
|
|
9
|
+
*/
|
|
10
|
+
export const TallPage = ({ sections = 12 }) => (_jsxs("div", { style: { height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [_jsx("header", { style: {
|
|
11
|
+
flex: '0 0 56px',
|
|
12
|
+
display: 'flex',
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
padding: '0 16px',
|
|
15
|
+
background: '#1d4ed8',
|
|
16
|
+
color: '#fff',
|
|
17
|
+
fontWeight: 600,
|
|
18
|
+
}, children: "Inbox" }), _jsx("main", { "data-testid": "tall-page-scroll", style: { flex: 1, minHeight: 0, overflowY: 'auto' }, children: Array.from({ length: sections }, (_, index) => (_jsxs("section", { style: {
|
|
19
|
+
height: 240,
|
|
20
|
+
boxSizing: 'border-box',
|
|
21
|
+
padding: 16,
|
|
22
|
+
borderBottom: '1px solid #e5e7eb',
|
|
23
|
+
background: index % 2 ? '#f9fafb' : '#fff',
|
|
24
|
+
}, children: [_jsxs("h2", { style: { margin: 0 }, children: ["Section ", index + 1] }), _jsxs("p", { children: ["Message ", index + 1, " of ", sections, "."] })] }, index))) })] }));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/react-vite';
|
|
2
|
+
declare const meta: {
|
|
3
|
+
title: string;
|
|
4
|
+
component: ({ sections }: import("./TallPage.js").ITallPageProps) => import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
parameters: {
|
|
6
|
+
layout: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
11
|
+
/**
|
|
12
|
+
* `fullPage`: qlip grows the viewport (and, under Vitest browser mode, the
|
|
13
|
+
* browser window behind it) until the column no longer scrolls, so the
|
|
14
|
+
* capture is the whole column at 1:1 — the e2e test reads the PNG's height.
|
|
15
|
+
* The manifest keeps the pinned viewport as the baseline key.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ScrollsAnInnerBox: Story;
|
|
18
|
+
/** The same page without `fullPage`: one screen, the rest scrolled away. */
|
|
19
|
+
export declare const OneScreen: Story;
|
|
20
|
+
//# sourceMappingURL=TallPage.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TallPage.stories.d.ts","sourceRoot":"","sources":["../../src/stories/TallPage.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAK5D,QAAA,MAAM,IAAI;;;;;;CAMuB,CAAC;AAElC,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAgBnC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAK/B,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,SAAS,EAAE,KAKvB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, within } from 'storybook/test';
|
|
2
|
+
import { TallPage } from './TallPage.js';
|
|
3
|
+
const meta = {
|
|
4
|
+
title: 'Example/TallPage',
|
|
5
|
+
component: TallPage,
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: 'fullscreen',
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
const PHONE = { width: 390, height: 844 };
|
|
12
|
+
/** The precondition that makes the two captures below differ: at the pinned
|
|
13
|
+
* viewport the column really scrolls, and the document does not. */
|
|
14
|
+
const expectsAnInnerScroller = async ({ canvasElement }) => {
|
|
15
|
+
const canvas = within(canvasElement);
|
|
16
|
+
await expect(canvas.getByText('Section 1')).toBeInTheDocument();
|
|
17
|
+
const column = canvasElement.querySelector('[data-testid="tall-page-scroll"]');
|
|
18
|
+
if (!column)
|
|
19
|
+
throw new Error('column not rendered');
|
|
20
|
+
await expect(column.scrollHeight).toBeGreaterThan(column.clientHeight);
|
|
21
|
+
await expect(document.documentElement.scrollHeight).toBe(window.innerHeight);
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* `fullPage`: qlip grows the viewport (and, under Vitest browser mode, the
|
|
25
|
+
* browser window behind it) until the column no longer scrolls, so the
|
|
26
|
+
* capture is the whole column at 1:1 — the e2e test reads the PNG's height.
|
|
27
|
+
* The manifest keeps the pinned viewport as the baseline key.
|
|
28
|
+
*/
|
|
29
|
+
export const ScrollsAnInnerBox = {
|
|
30
|
+
parameters: {
|
|
31
|
+
qlip: { viewport: PHONE, fullPage: true },
|
|
32
|
+
},
|
|
33
|
+
play: expectsAnInnerScroller,
|
|
34
|
+
};
|
|
35
|
+
/** The same page without `fullPage`: one screen, the rest scrolled away. */
|
|
36
|
+
export const OneScreen = {
|
|
37
|
+
parameters: {
|
|
38
|
+
qlip: { viewport: PHONE },
|
|
39
|
+
},
|
|
40
|
+
play: expectsAnInnerScroller,
|
|
41
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-runner/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-runner/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAcH,OAAO,KAAK,EACV,kBAAkB,EAGlB,iBAAiB,EACjB,cAAc,EAGf,MAAM,aAAa,CAAC;AACrB,OAAO,EAGL,YAAY,EAIb,MAAM,qBAAqB,CAAC;AAS7B;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACnE,UAAU,CAAC,IAAI,EAAE;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,cAAc,CAAC,EAAE,OAAO,CAAC;QACzB,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;KACvB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACtE;;;;;;;;;;;;;;OAcG;IACH,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE,gBAAgB,KACtB,OAAO,CAAC;QAAE,UAAU,CAAC,EAAE;YAAE,IAAI,CAAC,EAAE,cAAc,CAAA;SAAE,CAAA;KAAE,CAAC,CAAC;CAC1D;AA+HD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,GACtB,MAAM,mBAAmB,EACzB,SAAS,gBAAgB,EACzB,UAAS,mBAAwB,KAChC,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAelC,CAAC;AAmJF,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
import { promises as fs } from 'node:fs';
|
|
45
45
|
import path from 'node:path';
|
|
46
46
|
import { resolveQlipOptions } from '../config/parameters.js';
|
|
47
|
+
import { growViewportToContent, measureContentHeight } from '../runtime/fullPage.js';
|
|
47
48
|
import { DEFAULT_OUTPUT_DIR, DEFAULT_VIEWPORT, MANIFEST_FRAGMENT_DIR, TOOL_NAME, buildAutoScreenshotPath, generateBuildId, } from '../fs/output.js';
|
|
48
49
|
import { writeEntryFragment, getOrInitStore, nextStepName, peekStore, pushEntry, } from './manifest-store.js';
|
|
49
50
|
import { applyAnimationControlInPage, applyIgnoreMasksInPage, removeIgnoreMasksInPage, waitForDomIdleInPage, } from './prep-scripts.js';
|
|
@@ -64,6 +65,8 @@ const buildResolvedDefaults = (override) => {
|
|
|
64
65
|
pauseAnimationsAtEnd: override?.pauseAnimationsAtEnd ?? false,
|
|
65
66
|
disableBackdropFilter: override?.disableBackdropFilter ?? true,
|
|
66
67
|
captureOnError: override?.captureOnError ?? false,
|
|
68
|
+
fullPage: override?.fullPage ?? false,
|
|
69
|
+
fullPageMaxHeight: override?.fullPageMaxHeight ?? 10000,
|
|
67
70
|
waitForIdleMs: override?.waitForIdleMs ?? 300,
|
|
68
71
|
maxWaitForIdleMs: override?.maxWaitForIdleMs ?? 2000,
|
|
69
72
|
ignoreElements: override?.ignoreElements ?? [],
|
|
@@ -223,6 +226,7 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
223
226
|
let status = 'captured';
|
|
224
227
|
let error = null;
|
|
225
228
|
let maskIds = [];
|
|
229
|
+
let grownTo = null;
|
|
226
230
|
try {
|
|
227
231
|
await page.setViewportSize({
|
|
228
232
|
width: resolved.viewport.width,
|
|
@@ -236,6 +240,20 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
236
240
|
idleMs: resolved.waitForIdleMs,
|
|
237
241
|
maxWaitMs: resolved.maxWaitForIdleMs,
|
|
238
242
|
});
|
|
243
|
+
if (resolved.fullPage) {
|
|
244
|
+
// Here the viewport IS the browser window, so growing it is one call;
|
|
245
|
+
// the measure runs inside the page (see ../runtime/fullPage.ts).
|
|
246
|
+
grownTo = await growViewportToContent({
|
|
247
|
+
viewport: resolved.viewport,
|
|
248
|
+
maxHeight: resolved.fullPageMaxHeight,
|
|
249
|
+
setViewport: (width, height) => page.setViewportSize({ width, height }),
|
|
250
|
+
measure: () => page.evaluate(measureContentHeight, undefined),
|
|
251
|
+
settle: () => page.evaluate(waitForDomIdleInPage, {
|
|
252
|
+
idleMs: resolved.waitForIdleMs,
|
|
253
|
+
maxWaitMs: resolved.maxWaitForIdleMs,
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
239
257
|
if (resolved.ignoreElements.length > 0) {
|
|
240
258
|
maskIds = await page.evaluate(applyIgnoreMasksInPage, {
|
|
241
259
|
selectors: resolved.ignoreElements,
|
|
@@ -260,6 +278,19 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
260
278
|
/* page may have navigated away; masks die with it */
|
|
261
279
|
}
|
|
262
280
|
}
|
|
281
|
+
if (grownTo !== null) {
|
|
282
|
+
// The pinned viewport is the baseline key; whatever the test-runner
|
|
283
|
+
// does with the page next should see the story's own size again.
|
|
284
|
+
try {
|
|
285
|
+
await page.setViewportSize({
|
|
286
|
+
width: resolved.viewport.width,
|
|
287
|
+
height: resolved.viewport.height,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
/* the next capture sets its own viewport anyway */
|
|
292
|
+
}
|
|
293
|
+
}
|
|
263
294
|
}
|
|
264
295
|
const entry = buildEntry({
|
|
265
296
|
kind: 'auto',
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ export interface QlipViewport {
|
|
|
4
4
|
}
|
|
5
5
|
export interface QlipCaptureOptions {
|
|
6
6
|
skip?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The capture's viewport, and the baseline key on the server. Under
|
|
9
|
+
* Vitest browser mode a viewport the browser window cannot hold grows the
|
|
10
|
+
* window for the capture (and puts it back), because Vitest otherwise
|
|
11
|
+
* scales the test iframe down to fit and the capture comes back smaller
|
|
12
|
+
* than the viewport it names.
|
|
13
|
+
*/
|
|
7
14
|
viewport?: QlipViewport;
|
|
8
15
|
disableAnimations?: boolean;
|
|
9
16
|
pauseAnimationsAtEnd?: boolean;
|
|
@@ -18,6 +25,34 @@ export interface QlipCaptureOptions {
|
|
|
18
25
|
* @default true
|
|
19
26
|
*/
|
|
20
27
|
disableBackdropFilter?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Grow the viewport to the page's full content height for the capture,
|
|
30
|
+
* then put it back. A story that pins a phone viewport is otherwise
|
|
31
|
+
* photographed one screen tall with the rest of the page scrolled away —
|
|
32
|
+
* and a Playwright "full page" shot would not help, because an app shell
|
|
33
|
+
* usually scrolls an inner box, not the document. The height is measured
|
|
34
|
+
* from the document AND every scrolling element (an `overflow-y: auto |
|
|
35
|
+
* scroll` box whose `scrollHeight` exceeds its `clientHeight`, placed at
|
|
36
|
+
* its offset), re-measured after each growth until nothing scrolls, and
|
|
37
|
+
* capped at `fullPageMaxHeight`. Under Vitest browser mode the browser
|
|
38
|
+
* window (the Playwright context viewport) grows with the test iframe,
|
|
39
|
+
* or the capture would be scaled down to fit the window; both are put
|
|
40
|
+
* back afterwards. The manifest still reports the pinned `viewport` —
|
|
41
|
+
* that is the baseline key; the picture is simply taller. A capture whose
|
|
42
|
+
* content height changed shows on the server as a dimensions change
|
|
43
|
+
* rather than a pixel diff.
|
|
44
|
+
*
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
fullPage?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The tallest a `fullPage` capture may grow to, in CSS pixels. Chromium
|
|
50
|
+
* refuses viewports past 16384; the default leaves room for very long
|
|
51
|
+
* pages without inviting a runaway layout.
|
|
52
|
+
*
|
|
53
|
+
* @default 10000
|
|
54
|
+
*/
|
|
55
|
+
fullPageMaxHeight?: number;
|
|
21
56
|
waitForIdleMs?: number;
|
|
22
57
|
maxWaitForIdleMs?: number;
|
|
23
58
|
ignoreElements?: string[];
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,6 +14,10 @@ const resolveQlipOptions = ({ defaults, story, override, }) => {
|
|
|
14
14
|
disableBackdropFilter: override?.disableBackdropFilter ??
|
|
15
15
|
story?.disableBackdropFilter ??
|
|
16
16
|
defaults.disableBackdropFilter,
|
|
17
|
+
fullPage: override?.fullPage ?? story?.fullPage ?? defaults.fullPage,
|
|
18
|
+
fullPageMaxHeight: override?.fullPageMaxHeight ??
|
|
19
|
+
story?.fullPageMaxHeight ??
|
|
20
|
+
defaults.fullPageMaxHeight,
|
|
17
21
|
waitForIdleMs: override?.waitForIdleMs ??
|
|
18
22
|
story?.waitForIdleMs ??
|
|
19
23
|
defaults.waitForIdleMs,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A `fullPage` capture grows the viewport until nothing on the page scrolls,
|
|
4
|
+
* photographs it, and puts the viewport back. A story that pins a phone
|
|
5
|
+
* viewport is otherwise one screen tall, and a browser "full page" screenshot
|
|
6
|
+
* would not help: an app shell pins the document to the viewport and scrolls
|
|
7
|
+
* an inner box, so the document is never taller than one screen.
|
|
8
|
+
*
|
|
9
|
+
* Under Vitest browser mode two viewports are involved. `page.viewport()`
|
|
10
|
+
* sizes the test iframe; the real browser window (the Playwright context
|
|
11
|
+
* viewport) stays as configured, and Vitest scales the iframe down to fit it.
|
|
12
|
+
* A 3,600px-tall iframe in a 1,440px window is photographed at 40% — the
|
|
13
|
+
* whole page, unreadable — and so is a plain 844px-tall phone story in a
|
|
14
|
+
* 720px window, at 85%. `createCaptureViewport` therefore grows the window
|
|
15
|
+
* to hold whatever viewport a capture asks for, pinned or grown, and puts
|
|
16
|
+
* both back afterwards, the window first, so the iframe lands at scale 1.
|
|
17
|
+
*
|
|
18
|
+
* This module has no imports: the runtime, the test-runner and the unit
|
|
19
|
+
* project (which has no DOM and feeds it fakes) all share it.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.createCaptureViewport = exports.growViewportToContent = exports.measureContentHeight = void 0;
|
|
23
|
+
/**
|
|
24
|
+
* How tall the page really is: the document, and every box that scrolls
|
|
25
|
+
* its own content. Each scrolling box counts from its own offset, so a box
|
|
26
|
+
* that starts 105px down and scrolls 3,484px asks for 3,589px plus its own
|
|
27
|
+
* border and horizontal scrollbar.
|
|
28
|
+
*
|
|
29
|
+
* Closure-free, so the test-runner can hand it to Playwright's
|
|
30
|
+
* `page.evaluate()`, which re-evaluates the function's source inside the
|
|
31
|
+
* page; there it reads the real `document` and `window`. `env` exists for
|
|
32
|
+
* the unit test, which has neither.
|
|
33
|
+
*/
|
|
34
|
+
const measureContentHeight = (env) => {
|
|
35
|
+
const doc = env?.doc ?? document;
|
|
36
|
+
const win = env?.win ?? window;
|
|
37
|
+
let height = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
|
|
38
|
+
for (const el of Array.from(doc.querySelectorAll('*'))) {
|
|
39
|
+
if (el.scrollHeight <= el.clientHeight + 1) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const overflowY = win.getComputedStyle(el).overflowY;
|
|
43
|
+
if (overflowY !== 'auto' && overflowY !== 'scroll') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const top = el.getBoundingClientRect().top + win.scrollY;
|
|
47
|
+
const chrome = el.offsetHeight - el.clientHeight;
|
|
48
|
+
height = Math.max(height, Math.ceil(top + el.scrollHeight + chrome));
|
|
49
|
+
}
|
|
50
|
+
return height;
|
|
51
|
+
};
|
|
52
|
+
exports.measureContentHeight = measureContentHeight;
|
|
53
|
+
/**
|
|
54
|
+
* Grows the viewport until nothing on the page scrolls. Growing the viewport
|
|
55
|
+
* re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
|
|
56
|
+
* and may reveal another box below it that scrolls in turn — so the height is
|
|
57
|
+
* measured again after every growth, until a round asks for nothing more or
|
|
58
|
+
* `maxRounds` is spent.
|
|
59
|
+
*
|
|
60
|
+
* A page can also ask for more because it GREW WITH the viewport: a tile
|
|
61
|
+
* sized to its column, a `vh` box. Such content asks for about as much again
|
|
62
|
+
* as the last growth gave it, every round, and would be chased to the cap —
|
|
63
|
+
* a capture that is one screen of content over thousands of pixels of
|
|
64
|
+
* stretched tile. So a round that asks for no more than the previous growth
|
|
65
|
+
* is taken as content that scales, not content that was revealed, and the
|
|
66
|
+
* viewport stays where it is. (Revealed content asks for its own height,
|
|
67
|
+
* unrelated to the growth; it is only missed when it happens to be shorter
|
|
68
|
+
* than the growth that revealed it.)
|
|
69
|
+
*
|
|
70
|
+
* Returns the height the viewport ended at, or `null` when it never had to
|
|
71
|
+
* grow (the caller restores only in that case).
|
|
72
|
+
*/
|
|
73
|
+
const growViewportToContent = async ({ viewport, maxHeight, setViewport, measure, settle, maxRounds = 4, }) => {
|
|
74
|
+
let height = viewport.height;
|
|
75
|
+
let grown = false;
|
|
76
|
+
let previous = null;
|
|
77
|
+
for (let round = 0; round < maxRounds; round += 1) {
|
|
78
|
+
const asked = Math.min(Math.ceil(await measure()), maxHeight);
|
|
79
|
+
if (asked <= height) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
if (previous && asked - previous.asked <= previous.growth) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
previous = { asked, growth: asked - height };
|
|
86
|
+
height = asked;
|
|
87
|
+
grown = true;
|
|
88
|
+
await setViewport(viewport.width, height);
|
|
89
|
+
await settle();
|
|
90
|
+
}
|
|
91
|
+
return grown ? height : null;
|
|
92
|
+
};
|
|
93
|
+
exports.growViewportToContent = growViewportToContent;
|
|
94
|
+
/**
|
|
95
|
+
* The two viewports a capture sizes, and how each is put back. `set` grows
|
|
96
|
+
* the window to hold the iframe whenever the iframe would not fit (never
|
|
97
|
+
* shrinking it below what it was), then sizes the iframe; the window is
|
|
98
|
+
* asked for its size once. `restore` puts the window back first, so the
|
|
99
|
+
* iframe's pinned size is computed against the original window again, then
|
|
100
|
+
* the iframe — and touches neither when neither moved.
|
|
101
|
+
*/
|
|
102
|
+
const createCaptureViewport = ({ pinned, iframe, window: windowViewport, }) => {
|
|
103
|
+
// undefined: not asked yet; null: asked, and there is no window to grow
|
|
104
|
+
let base;
|
|
105
|
+
let windowAt = null;
|
|
106
|
+
let iframeAt = null;
|
|
107
|
+
const same = (a, b) => a.width === b.width && a.height === b.height;
|
|
108
|
+
const growWindow = async (width, height) => {
|
|
109
|
+
if (!windowViewport) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (base === undefined) {
|
|
113
|
+
base = await windowViewport();
|
|
114
|
+
}
|
|
115
|
+
if (!base) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const target = {
|
|
119
|
+
width: Math.max(base.width, width),
|
|
120
|
+
height: Math.max(base.height, height),
|
|
121
|
+
};
|
|
122
|
+
if (same(target, windowAt ?? base)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await windowViewport(target);
|
|
126
|
+
windowAt = target;
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
set: async (width, height) => {
|
|
130
|
+
await growWindow(width, height);
|
|
131
|
+
await iframe(width, height);
|
|
132
|
+
iframeAt = { width, height };
|
|
133
|
+
},
|
|
134
|
+
restore: async () => {
|
|
135
|
+
if (base && windowAt && windowViewport) {
|
|
136
|
+
await windowViewport(base);
|
|
137
|
+
windowAt = null;
|
|
138
|
+
}
|
|
139
|
+
if (iframeAt && !same(iframeAt, pinned)) {
|
|
140
|
+
await iframe(pinned.width, pinned.height);
|
|
141
|
+
iframeAt = pinned;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
exports.createCaptureViewport = createCaptureViewport;
|
|
@@ -50,6 +50,7 @@ exports.nextStepName = exports.qlipCapture = void 0;
|
|
|
50
50
|
const node_fs_1 = require("node:fs");
|
|
51
51
|
const node_path_1 = __importDefault(require("node:path"));
|
|
52
52
|
const parameters_js_1 = require("../config/parameters.js");
|
|
53
|
+
const fullPage_js_1 = require("../runtime/fullPage.js");
|
|
53
54
|
const output_js_1 = require("../fs/output.js");
|
|
54
55
|
const manifest_store_js_1 = require("./manifest-store.js");
|
|
55
56
|
Object.defineProperty(exports, "nextStepName", { enumerable: true, get: function () { return manifest_store_js_1.nextStepName; } });
|
|
@@ -71,6 +72,8 @@ const buildResolvedDefaults = (override) => {
|
|
|
71
72
|
pauseAnimationsAtEnd: override?.pauseAnimationsAtEnd ?? false,
|
|
72
73
|
disableBackdropFilter: override?.disableBackdropFilter ?? true,
|
|
73
74
|
captureOnError: override?.captureOnError ?? false,
|
|
75
|
+
fullPage: override?.fullPage ?? false,
|
|
76
|
+
fullPageMaxHeight: override?.fullPageMaxHeight ?? 10000,
|
|
74
77
|
waitForIdleMs: override?.waitForIdleMs ?? 300,
|
|
75
78
|
maxWaitForIdleMs: override?.maxWaitForIdleMs ?? 2000,
|
|
76
79
|
ignoreElements: override?.ignoreElements ?? [],
|
|
@@ -231,6 +234,7 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
231
234
|
let status = 'captured';
|
|
232
235
|
let error = null;
|
|
233
236
|
let maskIds = [];
|
|
237
|
+
let grownTo = null;
|
|
234
238
|
try {
|
|
235
239
|
await page.setViewportSize({
|
|
236
240
|
width: resolved.viewport.width,
|
|
@@ -244,6 +248,20 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
244
248
|
idleMs: resolved.waitForIdleMs,
|
|
245
249
|
maxWaitMs: resolved.maxWaitForIdleMs,
|
|
246
250
|
});
|
|
251
|
+
if (resolved.fullPage) {
|
|
252
|
+
// Here the viewport IS the browser window, so growing it is one call;
|
|
253
|
+
// the measure runs inside the page (see ../runtime/fullPage.ts).
|
|
254
|
+
grownTo = await (0, fullPage_js_1.growViewportToContent)({
|
|
255
|
+
viewport: resolved.viewport,
|
|
256
|
+
maxHeight: resolved.fullPageMaxHeight,
|
|
257
|
+
setViewport: (width, height) => page.setViewportSize({ width, height }),
|
|
258
|
+
measure: () => page.evaluate(fullPage_js_1.measureContentHeight, undefined),
|
|
259
|
+
settle: () => page.evaluate(prep_scripts_js_1.waitForDomIdleInPage, {
|
|
260
|
+
idleMs: resolved.waitForIdleMs,
|
|
261
|
+
maxWaitMs: resolved.maxWaitForIdleMs,
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
247
265
|
if (resolved.ignoreElements.length > 0) {
|
|
248
266
|
maskIds = await page.evaluate(prep_scripts_js_1.applyIgnoreMasksInPage, {
|
|
249
267
|
selectors: resolved.ignoreElements,
|
|
@@ -268,6 +286,19 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
268
286
|
/* page may have navigated away; masks die with it */
|
|
269
287
|
}
|
|
270
288
|
}
|
|
289
|
+
if (grownTo !== null) {
|
|
290
|
+
// The pinned viewport is the baseline key; whatever the test-runner
|
|
291
|
+
// does with the page next should see the story's own size again.
|
|
292
|
+
try {
|
|
293
|
+
await page.setViewportSize({
|
|
294
|
+
width: resolved.viewport.width,
|
|
295
|
+
height: resolved.viewport.height,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* the next capture sets its own viewport anyway */
|
|
300
|
+
}
|
|
301
|
+
}
|
|
271
302
|
}
|
|
272
303
|
const entry = buildEntry({
|
|
273
304
|
kind: 'auto',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qoretechnologies/qlip",
|
|
3
|
-
"version": "0.1.3-pr.
|
|
3
|
+
"version": "0.1.3-pr.30.gc00bad8",
|
|
4
4
|
"description": "Storybook screenshot capture for the Vitest addon",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"@storybook/addon-vitest": "^10.2.0",
|
|
77
77
|
"@storybook/react-vite": "^10.2.0",
|
|
78
78
|
"@types/node": "^20.10.0",
|
|
79
|
+
"@types/react-dom": "^19.2.7",
|
|
79
80
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
|
80
81
|
"@typescript-eslint/parser": "^6.10.0",
|
|
81
82
|
"@vitest/browser-playwright": "^4.0.17",
|