@qoretechnologies/qlip 0.1.3-pr.30.gc00bad8 → 0.1.3-pr.30.gc7b05dc
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/dist/runtime/fullPage.d.ts +49 -22
- package/dist/runtime/fullPage.d.ts.map +1 -1
- package/dist/runtime/fullPage.js +112 -38
- package/dist/runtime/screenshot.d.ts.map +1 -1
- package/dist/runtime/screenshot.js +7 -3
- package/dist/stories/TallPage.d.ts +6 -0
- package/dist/stories/TallPage.d.ts.map +1 -1
- package/dist/stories/TallPage.js +6 -0
- package/dist/stories/TallPage.stories.d.ts +7 -0
- package/dist/stories/TallPage.stories.d.ts.map +1 -1
- package/dist/stories/TallPage.stories.js +20 -1
- package/dist/test-runner/index.d.ts.map +1 -1
- package/dist/test-runner/index.js +10 -3
- package/dist/types.d.ts +9 -5
- package/dist/types.d.ts.map +1 -1
- package/dist-cjs/runtime/fullPage.js +114 -39
- package/dist-cjs/test-runner/index.js +9 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ qlipVitestPlugin({
|
|
|
231
231
|
});
|
|
232
232
|
```
|
|
233
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
|
|
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 by what they hide, and re-measures after each growth until nothing scrolls. Two kinds of box are not chased. A box that keeps its height when the viewport grows (a fixed-height code block, a capped list) stays scrolled, and the growth it asked for is given back, so the capture is not padded with blank space. Content that grows with the viewport (a tile sized to its column, a `vh` hero) would ask for more every round; it stops at the height its real content asked for. The height is capped at `fullPageMaxHeight` (default 10000); a page taller than that is captured at the cap with a `[qlip] fullPage:` warning naming the story. 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
235
|
|
|
236
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.
|
|
237
237
|
|
|
@@ -22,48 +22,75 @@ export interface FullPageSize {
|
|
|
22
22
|
height: number;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* How tall the page
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* How tall the page needs the viewport to be for nothing on it to scroll:
|
|
26
|
+
* the document, and every box that scrolls its own content. A scrolling box
|
|
27
|
+
* asks for the viewport plus whatever it hides — a box that fills the
|
|
28
|
+
* viewport grows with it one for one, whatever sits above or below it — and
|
|
29
|
+
* a box that already fits asks for the viewport minus its spare room.
|
|
30
|
+
* Answers 0 when nothing scrolls.
|
|
31
|
+
*
|
|
32
|
+
* A box only counts while growing the viewport can still reveal it, which
|
|
33
|
+
* only a re-measure after a growth can tell, so the rounds of one capture
|
|
34
|
+
* share a memory (`reset` starts a new one):
|
|
35
|
+
* - A box that kept its height when the viewport grew has a height of its
|
|
36
|
+
* own (a 200px code block, a capped list). More viewport never reveals its
|
|
37
|
+
* content, so it stops counting, and growth it asked for is given back.
|
|
38
|
+
* - A box whose content grew as much as the box did is sized to it (a tile
|
|
39
|
+
* that follows its column). Chasing it only stretches the tile, so it keeps
|
|
40
|
+
* asking for what it asked before it scaled. The document gets the same
|
|
41
|
+
* test (a `vh` hero).
|
|
42
|
+
* - A box whose hidden content shrank was revealed and keeps counting, even
|
|
43
|
+
* when a growth reveals less than it added.
|
|
44
|
+
*
|
|
45
|
+
* The document's own `scrollHeight` is the viewport again once the viewport
|
|
46
|
+
* outgrows it, so its tallest height while it still scrolled is remembered.
|
|
29
47
|
*
|
|
30
48
|
* Closure-free, so the test-runner can hand it to Playwright's
|
|
31
49
|
* `page.evaluate()`, which re-evaluates the function's source inside the
|
|
32
50
|
* page; there it reads the real `document` and `window`. `env` exists for
|
|
33
51
|
* the unit test, which has neither.
|
|
34
52
|
*/
|
|
35
|
-
export declare const measureContentHeight: (
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
export declare const measureContentHeight: (options?: {
|
|
54
|
+
reset?: boolean;
|
|
55
|
+
env?: {
|
|
56
|
+
doc: Pick<Document, "documentElement" | "body" | "querySelectorAll">;
|
|
57
|
+
win: Pick<Window, "getComputedStyle" | "innerHeight">;
|
|
58
|
+
};
|
|
38
59
|
}) => number;
|
|
60
|
+
/** Where `growViewportToContent` left the viewport, and why. */
|
|
61
|
+
export interface FullPageGrowth {
|
|
62
|
+
/** The height the viewport ended at: the pinned height when nothing grew. */
|
|
63
|
+
height: number;
|
|
64
|
+
/** What the content last asked for, before the `maxHeight` cap. */
|
|
65
|
+
wanted: number;
|
|
66
|
+
}
|
|
39
67
|
/**
|
|
40
68
|
* Grows the viewport until nothing on the page scrolls. Growing the viewport
|
|
41
69
|
* re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
|
|
42
70
|
* and may reveal another box below it that scrolls in turn — so the height is
|
|
43
71
|
* measured again after every growth, until a round asks for nothing more or
|
|
44
|
-
* `maxRounds` is spent.
|
|
72
|
+
* `maxRounds` is spent. Content that does not grow with the viewport, or
|
|
73
|
+
* grows with it, is `measure`'s call (see `measureContentHeight`); when a
|
|
74
|
+
* round asks for less than the viewport already is, that growth went to such
|
|
75
|
+
* a box, and it is given back.
|
|
45
76
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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).
|
|
77
|
+
* `measure` is told when it is the capture's first round, so it can start a
|
|
78
|
+
* new memory.
|
|
58
79
|
*/
|
|
59
80
|
export declare const growViewportToContent: ({ viewport, maxHeight, setViewport, measure, settle, maxRounds, }: {
|
|
60
81
|
viewport: FullPageSize;
|
|
61
82
|
maxHeight: number;
|
|
62
83
|
setViewport: (width: number, height: number) => Promise<void>;
|
|
63
|
-
measure: () => number | Promise<number>;
|
|
84
|
+
measure: (first: boolean) => number | Promise<number>;
|
|
64
85
|
settle: () => Promise<void>;
|
|
65
86
|
maxRounds?: number;
|
|
66
|
-
}) => Promise<
|
|
87
|
+
}) => Promise<FullPageGrowth>;
|
|
88
|
+
/**
|
|
89
|
+
* The warning for a `fullPage` capture the height cap cut short, or `null`:
|
|
90
|
+
* a page taller than `fullPageMaxHeight` is captured at the cap, and a cut-off
|
|
91
|
+
* page must not pass for the whole one.
|
|
92
|
+
*/
|
|
93
|
+
export declare const fullPageCapWarning: (storyId: string, growth: FullPageGrowth, maxHeight: number) => string | null;
|
|
67
94
|
/**
|
|
68
95
|
* The browser-window half of the viewport, as the qlip plugin's browser
|
|
69
96
|
* command exposes it: called without a size it answers the window's current
|
|
@@ -1 +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;
|
|
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;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,oBAAoB,GAAI,UAAU;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE;QACJ,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,GAAG,kBAAkB,CAAC,CAAC;QACrE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,kBAAkB,GAAG,aAAa,CAAC,CAAC;KACvD,CAAC;CACH,KAAG,MAsEH,CAAC;AAEF,gEAAgE;AAChE,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;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,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,KAAG,OAAO,CAAC,cAAc,CAmBzB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,EACf,QAAQ,cAAc,EACtB,WAAW,MAAM,KAChB,MAAM,GAAG,IAKF,CAAC;AAEX;;;;;;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"}
|
package/dist/runtime/fullPage.js
CHANGED
|
@@ -18,32 +18,103 @@
|
|
|
18
18
|
* project (which has no DOM and feeds it fakes) all share it.
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
|
-
* How tall the page
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* How tall the page needs the viewport to be for nothing on it to scroll:
|
|
22
|
+
* the document, and every box that scrolls its own content. A scrolling box
|
|
23
|
+
* asks for the viewport plus whatever it hides — a box that fills the
|
|
24
|
+
* viewport grows with it one for one, whatever sits above or below it — and
|
|
25
|
+
* a box that already fits asks for the viewport minus its spare room.
|
|
26
|
+
* Answers 0 when nothing scrolls.
|
|
27
|
+
*
|
|
28
|
+
* A box only counts while growing the viewport can still reveal it, which
|
|
29
|
+
* only a re-measure after a growth can tell, so the rounds of one capture
|
|
30
|
+
* share a memory (`reset` starts a new one):
|
|
31
|
+
* - A box that kept its height when the viewport grew has a height of its
|
|
32
|
+
* own (a 200px code block, a capped list). More viewport never reveals its
|
|
33
|
+
* content, so it stops counting, and growth it asked for is given back.
|
|
34
|
+
* - A box whose content grew as much as the box did is sized to it (a tile
|
|
35
|
+
* that follows its column). Chasing it only stretches the tile, so it keeps
|
|
36
|
+
* asking for what it asked before it scaled. The document gets the same
|
|
37
|
+
* test (a `vh` hero).
|
|
38
|
+
* - A box whose hidden content shrank was revealed and keeps counting, even
|
|
39
|
+
* when a growth reveals less than it added.
|
|
40
|
+
*
|
|
41
|
+
* The document's own `scrollHeight` is the viewport again once the viewport
|
|
42
|
+
* outgrows it, so its tallest height while it still scrolled is remembered.
|
|
25
43
|
*
|
|
26
44
|
* Closure-free, so the test-runner can hand it to Playwright's
|
|
27
45
|
* `page.evaluate()`, which re-evaluates the function's source inside the
|
|
28
46
|
* page; there it reads the real `document` and `window`. `env` exists for
|
|
29
47
|
* the unit test, which has neither.
|
|
30
48
|
*/
|
|
31
|
-
export const measureContentHeight = (
|
|
32
|
-
const doc = env?.doc ?? document;
|
|
33
|
-
const win = env?.win ?? window;
|
|
34
|
-
|
|
49
|
+
export const measureContentHeight = (options) => {
|
|
50
|
+
const doc = options?.env?.doc ?? document;
|
|
51
|
+
const win = options?.env?.win ?? window;
|
|
52
|
+
const holder = win;
|
|
53
|
+
if (options?.reset || !holder.__qlipFullPage) {
|
|
54
|
+
holder.__qlipFullPage = {
|
|
55
|
+
viewport: 0,
|
|
56
|
+
docHeight: 0,
|
|
57
|
+
docOverflow: 0,
|
|
58
|
+
docScales: false,
|
|
59
|
+
boxes: new WeakMap(),
|
|
60
|
+
fixed: new WeakSet(),
|
|
61
|
+
scales: new WeakSet(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const memory = holder.__qlipFullPage;
|
|
65
|
+
const viewport = win.innerHeight;
|
|
66
|
+
const grew = memory.viewport > 0 && viewport > memory.viewport + 1;
|
|
67
|
+
const docScroll = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
|
|
68
|
+
const docOverflow = Math.max(0, docScroll - viewport);
|
|
69
|
+
if (grew && memory.docOverflow > 0 && docOverflow >= memory.docOverflow - 1) {
|
|
70
|
+
memory.docScales = true;
|
|
71
|
+
}
|
|
72
|
+
memory.docOverflow = docOverflow;
|
|
73
|
+
if (docOverflow > 1 && !memory.docScales) {
|
|
74
|
+
memory.docHeight = Math.max(memory.docHeight, docScroll);
|
|
75
|
+
}
|
|
76
|
+
let height = memory.docHeight;
|
|
35
77
|
for (const el of Array.from(doc.querySelectorAll('*'))) {
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
const seen = memory.boxes.get(el);
|
|
79
|
+
if (!seen) {
|
|
80
|
+
if (el.scrollHeight <= el.clientHeight + 1) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const overflowY = win.getComputedStyle(el).overflowY;
|
|
84
|
+
if (overflowY !== 'auto' && overflowY !== 'scroll') {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const overflow = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
89
|
+
if (seen && grew) {
|
|
90
|
+
if (el.clientHeight <= seen.clientHeight + 1) {
|
|
91
|
+
memory.fixed.add(el);
|
|
92
|
+
}
|
|
93
|
+
else if (seen.overflow > 0 && overflow >= seen.overflow - 1) {
|
|
94
|
+
memory.scales.add(el);
|
|
95
|
+
}
|
|
38
96
|
}
|
|
39
|
-
|
|
40
|
-
if (overflowY !== 'auto' && overflowY !== 'scroll') {
|
|
97
|
+
if (memory.fixed.has(el)) {
|
|
41
98
|
continue;
|
|
42
99
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
// A box that fits reports its own height as scrollHeight, so its content
|
|
101
|
+
// height is the one it had while it still scrolled.
|
|
102
|
+
const content = overflow > 1 ? Math.max(seen?.content ?? 0, el.scrollHeight) : (seen?.content ?? el.scrollHeight);
|
|
103
|
+
let need;
|
|
104
|
+
if (seen && memory.scales.has(el)) {
|
|
105
|
+
need = seen.need;
|
|
106
|
+
}
|
|
107
|
+
else if (overflow > 1) {
|
|
108
|
+
need = viewport + overflow;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
need = viewport - Math.max(0, el.clientHeight - content);
|
|
112
|
+
}
|
|
113
|
+
need = Math.ceil(need);
|
|
114
|
+
memory.boxes.set(el, { clientHeight: el.clientHeight, overflow, content, need });
|
|
115
|
+
height = Math.max(height, need);
|
|
46
116
|
}
|
|
117
|
+
memory.viewport = viewport;
|
|
47
118
|
return height;
|
|
48
119
|
};
|
|
49
120
|
/**
|
|
@@ -51,41 +122,44 @@ export const measureContentHeight = (env) => {
|
|
|
51
122
|
* re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
|
|
52
123
|
* and may reveal another box below it that scrolls in turn — so the height is
|
|
53
124
|
* measured again after every growth, until a round asks for nothing more or
|
|
54
|
-
* `maxRounds` is spent.
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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.)
|
|
125
|
+
* `maxRounds` is spent. Content that does not grow with the viewport, or
|
|
126
|
+
* grows with it, is `measure`'s call (see `measureContentHeight`); when a
|
|
127
|
+
* round asks for less than the viewport already is, that growth went to such
|
|
128
|
+
* a box, and it is given back.
|
|
65
129
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
130
|
+
* `measure` is told when it is the capture's first round, so it can start a
|
|
131
|
+
* new memory.
|
|
68
132
|
*/
|
|
69
133
|
export const growViewportToContent = async ({ viewport, maxHeight, setViewport, measure, settle, maxRounds = 4, }) => {
|
|
70
134
|
let height = viewport.height;
|
|
71
|
-
let
|
|
72
|
-
let previous = null;
|
|
135
|
+
let wanted = 0;
|
|
73
136
|
for (let round = 0; round < maxRounds; round += 1) {
|
|
74
|
-
|
|
75
|
-
|
|
137
|
+
wanted = Math.ceil(await measure(round === 0));
|
|
138
|
+
const asked = Math.min(Math.max(wanted, viewport.height), maxHeight);
|
|
139
|
+
if (asked === height) {
|
|
76
140
|
break;
|
|
77
141
|
}
|
|
78
|
-
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
previous = { asked, growth: asked - height };
|
|
142
|
+
const givingBack = asked < height;
|
|
82
143
|
height = asked;
|
|
83
|
-
grown = true;
|
|
84
144
|
await setViewport(viewport.width, height);
|
|
85
145
|
await settle();
|
|
146
|
+
if (givingBack) {
|
|
147
|
+
// Every box still counted asked for no more than this, so it fits.
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
86
150
|
}
|
|
87
|
-
return
|
|
151
|
+
return { height, wanted };
|
|
88
152
|
};
|
|
153
|
+
/**
|
|
154
|
+
* The warning for a `fullPage` capture the height cap cut short, or `null`:
|
|
155
|
+
* a page taller than `fullPageMaxHeight` is captured at the cap, and a cut-off
|
|
156
|
+
* page must not pass for the whole one.
|
|
157
|
+
*/
|
|
158
|
+
export const fullPageCapWarning = (storyId, growth, maxHeight) => growth.wanted > maxHeight
|
|
159
|
+
? `[qlip] fullPage: ${storyId} is ${String(growth.wanted)}px tall but was captured at ` +
|
|
160
|
+
`the ${String(maxHeight)}px cap; raise fullPageMaxHeight (Chromium allows up to 16384) ` +
|
|
161
|
+
'to capture all of it.'
|
|
162
|
+
: null;
|
|
89
163
|
/**
|
|
90
164
|
* The two viewports a capture sizes, and how each is put back. `set` grows
|
|
91
165
|
* the window to hold the iframe whenever the iframe would not fit (never
|
|
@@ -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;AAqCrB,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;AA4bF,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,5 +1,5 @@
|
|
|
1
1
|
import { resolveQlipOptions } from '../config/parameters.js';
|
|
2
|
-
import { createCaptureViewport, growViewportToContent, measureContentHeight, } from './fullPage.js';
|
|
2
|
+
import { createCaptureViewport, fullPageCapWarning, growViewportToContent, measureContentHeight, } from './fullPage.js';
|
|
3
3
|
import { monotonicNow, realClearInterval, realSetInterval, realSetTimeout, } from './clock.js';
|
|
4
4
|
import { AUTO_ERROR_SCREENSHOT_BASE, MANIFEST_FRAGMENT_DIR, buildAutoLogPath, buildAutoScreenshotPath, buildManualLogPath, buildManualScreenshotPath, fragmentFileName, joinPath, sanitizeSegment, } from '../fs/output.js';
|
|
5
5
|
import { flushConsoleLogs, initRuntimeState, markStorybookWarning, nextFragmentSeq, nextStepName, pruneStaleErrorEntries, pushEntry, updateStats, } from './context.js';
|
|
@@ -508,13 +508,17 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
|
|
|
508
508
|
});
|
|
509
509
|
await waitForDomIdle(resolved.waitForIdleMs, resolved.maxWaitForIdleMs);
|
|
510
510
|
if (resolved.fullPage) {
|
|
511
|
-
await growViewportToContent({
|
|
511
|
+
const growth = await growViewportToContent({
|
|
512
512
|
viewport: resolved.viewport,
|
|
513
513
|
maxHeight: resolved.fullPageMaxHeight,
|
|
514
514
|
setViewport: captureViewport.set,
|
|
515
|
-
measure: () => measureContentHeight(),
|
|
515
|
+
measure: (first) => measureContentHeight({ reset: first }),
|
|
516
516
|
settle: () => waitForDomIdle(resolved.waitForIdleMs, resolved.maxWaitForIdleMs),
|
|
517
517
|
});
|
|
518
|
+
const warning = fullPageCapWarning(story.id, growth, resolved.fullPageMaxHeight);
|
|
519
|
+
if (warning) {
|
|
520
|
+
console.warn(warning);
|
|
521
|
+
}
|
|
518
522
|
}
|
|
519
523
|
cleanupMasks = applyIgnoreMasks(resolved.ignoreElements);
|
|
520
524
|
await page.screenshot({ path: absolutePath, save: true });
|
|
@@ -11,4 +11,10 @@ export interface ITallPageProps {
|
|
|
11
11
|
* screenshot can see it.
|
|
12
12
|
*/
|
|
13
13
|
export declare const TallPage: ({ sections }: ITallPageProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* An ordinary page, nothing pinned, whose only scroller is a fixed-height
|
|
16
|
+
* box (a code block, a capped list). Growing the viewport never grows that
|
|
17
|
+
* box, so a `fullPage` capture has nothing to reveal and stays one screen.
|
|
18
|
+
*/
|
|
19
|
+
export declare const FixedBoxPage: () => import("react/jsx-runtime").JSX.Element;
|
|
14
20
|
//# sourceMappingURL=TallPage.d.ts.map
|
|
@@ -1 +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"}
|
|
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;AAEF;;;;GAIG;AACH,eAAO,MAAM,YAAY,+CAWxB,CAAC"}
|
package/dist/stories/TallPage.js
CHANGED
|
@@ -22,3 +22,9 @@ export const TallPage = ({ sections = 12 }) => (_jsxs("div", { style: { height:
|
|
|
22
22
|
borderBottom: '1px solid #e5e7eb',
|
|
23
23
|
background: index % 2 ? '#f9fafb' : '#fff',
|
|
24
24
|
}, children: [_jsxs("h2", { style: { margin: 0 }, children: ["Section ", index + 1] }), _jsxs("p", { children: ["Message ", index + 1, " of ", sections, "."] })] }, index))) })] }));
|
|
25
|
+
/**
|
|
26
|
+
* An ordinary page, nothing pinned, whose only scroller is a fixed-height
|
|
27
|
+
* box (a code block, a capped list). Growing the viewport never grows that
|
|
28
|
+
* box, so a `fullPage` capture has nothing to reveal and stays one screen.
|
|
29
|
+
*/
|
|
30
|
+
export const FixedBoxPage = () => (_jsxs("div", { style: { padding: 16, fontFamily: 'sans-serif' }, children: [_jsx("h1", { style: { margin: 0 }, children: "Release notes" }), _jsx("pre", { "data-testid": "fixed-box", style: { height: 200, margin: '16px 0', overflowY: 'auto', border: '1px solid #d1d5db' }, children: Array.from({ length: 100 }, (_, index) => `line ${String(index + 1)}`).join('\n') }), _jsx("p", { style: { margin: 0 }, children: "The page ends here." })] }));
|
|
@@ -17,4 +17,11 @@ type Story = StoryObj<typeof meta>;
|
|
|
17
17
|
export declare const ScrollsAnInnerBox: Story;
|
|
18
18
|
/** The same page without `fullPage`: one screen, the rest scrolled away. */
|
|
19
19
|
export declare const OneScreen: Story;
|
|
20
|
+
/**
|
|
21
|
+
* `fullPage` on an ordinary page whose only scroller is a fixed-height box:
|
|
22
|
+
* more viewport never reveals it, so qlip gives the growth back and the
|
|
23
|
+
* capture stays one screen — not a screen of page over a tall blank strip.
|
|
24
|
+
* The e2e test reads the PNG's height.
|
|
25
|
+
*/
|
|
26
|
+
export declare const FixedHeightBox: Story;
|
|
20
27
|
//# sourceMappingURL=TallPage.stories.d.ts.map
|
|
@@ -1 +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"}
|
|
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;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,EAAE,KAU5B,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
2
|
import { expect, within } from 'storybook/test';
|
|
2
|
-
import { TallPage } from './TallPage.js';
|
|
3
|
+
import { FixedBoxPage, TallPage } from './TallPage.js';
|
|
3
4
|
const meta = {
|
|
4
5
|
title: 'Example/TallPage',
|
|
5
6
|
component: TallPage,
|
|
@@ -39,3 +40,21 @@ export const OneScreen = {
|
|
|
39
40
|
},
|
|
40
41
|
play: expectsAnInnerScroller,
|
|
41
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* `fullPage` on an ordinary page whose only scroller is a fixed-height box:
|
|
45
|
+
* more viewport never reveals it, so qlip gives the growth back and the
|
|
46
|
+
* capture stays one screen — not a screen of page over a tall blank strip.
|
|
47
|
+
* The e2e test reads the PNG's height.
|
|
48
|
+
*/
|
|
49
|
+
export const FixedHeightBox = {
|
|
50
|
+
render: () => _jsx(FixedBoxPage, {}),
|
|
51
|
+
parameters: {
|
|
52
|
+
qlip: { viewport: PHONE, fullPage: true },
|
|
53
|
+
},
|
|
54
|
+
play: async ({ canvasElement }) => {
|
|
55
|
+
const box = canvasElement.querySelector('[data-testid="fixed-box"]');
|
|
56
|
+
if (!box)
|
|
57
|
+
throw new Error('box not rendered');
|
|
58
|
+
await expect(box.scrollHeight).toBeGreaterThan(box.clientHeight);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -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;AAkBH,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;AA0JF,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -44,7 +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
|
+
import { fullPageCapWarning, growViewportToContent, measureContentHeight, } from '../runtime/fullPage.js';
|
|
48
48
|
import { DEFAULT_OUTPUT_DIR, DEFAULT_VIEWPORT, MANIFEST_FRAGMENT_DIR, TOOL_NAME, buildAutoScreenshotPath, generateBuildId, } from '../fs/output.js';
|
|
49
49
|
import { writeEntryFragment, getOrInitStore, nextStepName, peekStore, pushEntry, } from './manifest-store.js';
|
|
50
50
|
import { applyAnimationControlInPage, applyIgnoreMasksInPage, removeIgnoreMasksInPage, waitForDomIdleInPage, } from './prep-scripts.js';
|
|
@@ -243,16 +243,23 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
243
243
|
if (resolved.fullPage) {
|
|
244
244
|
// Here the viewport IS the browser window, so growing it is one call;
|
|
245
245
|
// the measure runs inside the page (see ../runtime/fullPage.ts).
|
|
246
|
-
|
|
246
|
+
const growth = await growViewportToContent({
|
|
247
247
|
viewport: resolved.viewport,
|
|
248
248
|
maxHeight: resolved.fullPageMaxHeight,
|
|
249
249
|
setViewport: (width, height) => page.setViewportSize({ width, height }),
|
|
250
|
-
measure: () => page.evaluate(measureContentHeight,
|
|
250
|
+
measure: (first) => page.evaluate(measureContentHeight, { reset: first }),
|
|
251
251
|
settle: () => page.evaluate(waitForDomIdleInPage, {
|
|
252
252
|
idleMs: resolved.waitForIdleMs,
|
|
253
253
|
maxWaitMs: resolved.maxWaitForIdleMs,
|
|
254
254
|
}),
|
|
255
255
|
});
|
|
256
|
+
if (growth.height !== resolved.viewport.height) {
|
|
257
|
+
grownTo = growth.height;
|
|
258
|
+
}
|
|
259
|
+
const warning = fullPageCapWarning(context.id, growth, resolved.fullPageMaxHeight);
|
|
260
|
+
if (warning) {
|
|
261
|
+
console.warn(warning);
|
|
262
|
+
}
|
|
256
263
|
}
|
|
257
264
|
if (resolved.ignoreElements.length > 0) {
|
|
258
265
|
maskIds = await page.evaluate(applyIgnoreMasksInPage, {
|
package/dist/types.d.ts
CHANGED
|
@@ -32,10 +32,13 @@ export interface QlipCaptureOptions {
|
|
|
32
32
|
* and a Playwright "full page" shot would not help, because an app shell
|
|
33
33
|
* usually scrolls an inner box, not the document. The height is measured
|
|
34
34
|
* from the document AND every scrolling element (an `overflow-y: auto |
|
|
35
|
-
* scroll` box whose `scrollHeight` exceeds its `clientHeight
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* scroll` box whose `scrollHeight` exceeds its `clientHeight`), and
|
|
36
|
+
* re-measured after each growth until nothing scrolls. A box that keeps
|
|
37
|
+
* its height when the viewport grows (a fixed-height code block) stays
|
|
38
|
+
* scrolled and gives its growth back, and content that grows with the
|
|
39
|
+
* viewport is not chased. Capped at `fullPageMaxHeight`. Under Vitest
|
|
40
|
+
* browser mode the browser window (the Playwright context viewport) grows
|
|
41
|
+
* with the test iframe,
|
|
39
42
|
* or the capture would be scaled down to fit the window; both are put
|
|
40
43
|
* back afterwards. The manifest still reports the pinned `viewport` —
|
|
41
44
|
* that is the baseline key; the picture is simply taller. A capture whose
|
|
@@ -48,7 +51,8 @@ export interface QlipCaptureOptions {
|
|
|
48
51
|
/**
|
|
49
52
|
* The tallest a `fullPage` capture may grow to, in CSS pixels. Chromium
|
|
50
53
|
* refuses viewports past 16384; the default leaves room for very long
|
|
51
|
-
* pages without inviting a runaway layout.
|
|
54
|
+
* pages without inviting a runaway layout. A page taller than this is
|
|
55
|
+
* captured at the cap, with a `[qlip] fullPage:` warning naming the story.
|
|
52
56
|
*
|
|
53
57
|
* @default 10000
|
|
54
58
|
*/
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;OASG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;OASG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oBAAoB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yBAAyB,CAAC,EAAE,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACxD,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,kBAAkB,CAAC,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,2CAA2C;IAC3C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,qBAAsB,SAAQ,kBAAkB;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,cAAc,CAAC;QACtB;;;;;;;WAOG;QACH,IAAI,CAAC,EAAE;YAAE,WAAW,CAAC,EAAE;gBAAE,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,CAAC;KAC7C,CAAC;CACH;AAED,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,gBAAgB,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;IACvB,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,KAAK,EAAE;QACL,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,OAAO,EAAE,iBAAiB,EAAE,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,aAAa,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACxD,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACtC"}
|
|
@@ -19,34 +19,105 @@
|
|
|
19
19
|
* project (which has no DOM and feeds it fakes) all share it.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.createCaptureViewport = exports.growViewportToContent = exports.measureContentHeight = void 0;
|
|
22
|
+
exports.createCaptureViewport = exports.fullPageCapWarning = exports.growViewportToContent = exports.measureContentHeight = void 0;
|
|
23
23
|
/**
|
|
24
|
-
* How tall the page
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* How tall the page needs the viewport to be for nothing on it to scroll:
|
|
25
|
+
* the document, and every box that scrolls its own content. A scrolling box
|
|
26
|
+
* asks for the viewport plus whatever it hides — a box that fills the
|
|
27
|
+
* viewport grows with it one for one, whatever sits above or below it — and
|
|
28
|
+
* a box that already fits asks for the viewport minus its spare room.
|
|
29
|
+
* Answers 0 when nothing scrolls.
|
|
30
|
+
*
|
|
31
|
+
* A box only counts while growing the viewport can still reveal it, which
|
|
32
|
+
* only a re-measure after a growth can tell, so the rounds of one capture
|
|
33
|
+
* share a memory (`reset` starts a new one):
|
|
34
|
+
* - A box that kept its height when the viewport grew has a height of its
|
|
35
|
+
* own (a 200px code block, a capped list). More viewport never reveals its
|
|
36
|
+
* content, so it stops counting, and growth it asked for is given back.
|
|
37
|
+
* - A box whose content grew as much as the box did is sized to it (a tile
|
|
38
|
+
* that follows its column). Chasing it only stretches the tile, so it keeps
|
|
39
|
+
* asking for what it asked before it scaled. The document gets the same
|
|
40
|
+
* test (a `vh` hero).
|
|
41
|
+
* - A box whose hidden content shrank was revealed and keeps counting, even
|
|
42
|
+
* when a growth reveals less than it added.
|
|
43
|
+
*
|
|
44
|
+
* The document's own `scrollHeight` is the viewport again once the viewport
|
|
45
|
+
* outgrows it, so its tallest height while it still scrolled is remembered.
|
|
28
46
|
*
|
|
29
47
|
* Closure-free, so the test-runner can hand it to Playwright's
|
|
30
48
|
* `page.evaluate()`, which re-evaluates the function's source inside the
|
|
31
49
|
* page; there it reads the real `document` and `window`. `env` exists for
|
|
32
50
|
* the unit test, which has neither.
|
|
33
51
|
*/
|
|
34
|
-
const measureContentHeight = (
|
|
35
|
-
const doc = env?.doc ?? document;
|
|
36
|
-
const win = env?.win ?? window;
|
|
37
|
-
|
|
52
|
+
const measureContentHeight = (options) => {
|
|
53
|
+
const doc = options?.env?.doc ?? document;
|
|
54
|
+
const win = options?.env?.win ?? window;
|
|
55
|
+
const holder = win;
|
|
56
|
+
if (options?.reset || !holder.__qlipFullPage) {
|
|
57
|
+
holder.__qlipFullPage = {
|
|
58
|
+
viewport: 0,
|
|
59
|
+
docHeight: 0,
|
|
60
|
+
docOverflow: 0,
|
|
61
|
+
docScales: false,
|
|
62
|
+
boxes: new WeakMap(),
|
|
63
|
+
fixed: new WeakSet(),
|
|
64
|
+
scales: new WeakSet(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const memory = holder.__qlipFullPage;
|
|
68
|
+
const viewport = win.innerHeight;
|
|
69
|
+
const grew = memory.viewport > 0 && viewport > memory.viewport + 1;
|
|
70
|
+
const docScroll = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
|
|
71
|
+
const docOverflow = Math.max(0, docScroll - viewport);
|
|
72
|
+
if (grew && memory.docOverflow > 0 && docOverflow >= memory.docOverflow - 1) {
|
|
73
|
+
memory.docScales = true;
|
|
74
|
+
}
|
|
75
|
+
memory.docOverflow = docOverflow;
|
|
76
|
+
if (docOverflow > 1 && !memory.docScales) {
|
|
77
|
+
memory.docHeight = Math.max(memory.docHeight, docScroll);
|
|
78
|
+
}
|
|
79
|
+
let height = memory.docHeight;
|
|
38
80
|
for (const el of Array.from(doc.querySelectorAll('*'))) {
|
|
39
|
-
|
|
40
|
-
|
|
81
|
+
const seen = memory.boxes.get(el);
|
|
82
|
+
if (!seen) {
|
|
83
|
+
if (el.scrollHeight <= el.clientHeight + 1) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const overflowY = win.getComputedStyle(el).overflowY;
|
|
87
|
+
if (overflowY !== 'auto' && overflowY !== 'scroll') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const overflow = Math.max(0, el.scrollHeight - el.clientHeight);
|
|
92
|
+
if (seen && grew) {
|
|
93
|
+
if (el.clientHeight <= seen.clientHeight + 1) {
|
|
94
|
+
memory.fixed.add(el);
|
|
95
|
+
}
|
|
96
|
+
else if (seen.overflow > 0 && overflow >= seen.overflow - 1) {
|
|
97
|
+
memory.scales.add(el);
|
|
98
|
+
}
|
|
41
99
|
}
|
|
42
|
-
|
|
43
|
-
if (overflowY !== 'auto' && overflowY !== 'scroll') {
|
|
100
|
+
if (memory.fixed.has(el)) {
|
|
44
101
|
continue;
|
|
45
102
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
103
|
+
// A box that fits reports its own height as scrollHeight, so its content
|
|
104
|
+
// height is the one it had while it still scrolled.
|
|
105
|
+
const content = overflow > 1 ? Math.max(seen?.content ?? 0, el.scrollHeight) : (seen?.content ?? el.scrollHeight);
|
|
106
|
+
let need;
|
|
107
|
+
if (seen && memory.scales.has(el)) {
|
|
108
|
+
need = seen.need;
|
|
109
|
+
}
|
|
110
|
+
else if (overflow > 1) {
|
|
111
|
+
need = viewport + overflow;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
need = viewport - Math.max(0, el.clientHeight - content);
|
|
115
|
+
}
|
|
116
|
+
need = Math.ceil(need);
|
|
117
|
+
memory.boxes.set(el, { clientHeight: el.clientHeight, overflow, content, need });
|
|
118
|
+
height = Math.max(height, need);
|
|
49
119
|
}
|
|
120
|
+
memory.viewport = viewport;
|
|
50
121
|
return height;
|
|
51
122
|
};
|
|
52
123
|
exports.measureContentHeight = measureContentHeight;
|
|
@@ -55,42 +126,46 @@ exports.measureContentHeight = measureContentHeight;
|
|
|
55
126
|
* re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
|
|
56
127
|
* and may reveal another box below it that scrolls in turn — so the height is
|
|
57
128
|
* measured again after every growth, until a round asks for nothing more or
|
|
58
|
-
* `maxRounds` is spent.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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.)
|
|
129
|
+
* `maxRounds` is spent. Content that does not grow with the viewport, or
|
|
130
|
+
* grows with it, is `measure`'s call (see `measureContentHeight`); when a
|
|
131
|
+
* round asks for less than the viewport already is, that growth went to such
|
|
132
|
+
* a box, and it is given back.
|
|
69
133
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
134
|
+
* `measure` is told when it is the capture's first round, so it can start a
|
|
135
|
+
* new memory.
|
|
72
136
|
*/
|
|
73
137
|
const growViewportToContent = async ({ viewport, maxHeight, setViewport, measure, settle, maxRounds = 4, }) => {
|
|
74
138
|
let height = viewport.height;
|
|
75
|
-
let
|
|
76
|
-
let previous = null;
|
|
139
|
+
let wanted = 0;
|
|
77
140
|
for (let round = 0; round < maxRounds; round += 1) {
|
|
78
|
-
|
|
79
|
-
|
|
141
|
+
wanted = Math.ceil(await measure(round === 0));
|
|
142
|
+
const asked = Math.min(Math.max(wanted, viewport.height), maxHeight);
|
|
143
|
+
if (asked === height) {
|
|
80
144
|
break;
|
|
81
145
|
}
|
|
82
|
-
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
previous = { asked, growth: asked - height };
|
|
146
|
+
const givingBack = asked < height;
|
|
86
147
|
height = asked;
|
|
87
|
-
grown = true;
|
|
88
148
|
await setViewport(viewport.width, height);
|
|
89
149
|
await settle();
|
|
150
|
+
if (givingBack) {
|
|
151
|
+
// Every box still counted asked for no more than this, so it fits.
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
90
154
|
}
|
|
91
|
-
return
|
|
155
|
+
return { height, wanted };
|
|
92
156
|
};
|
|
93
157
|
exports.growViewportToContent = growViewportToContent;
|
|
158
|
+
/**
|
|
159
|
+
* The warning for a `fullPage` capture the height cap cut short, or `null`:
|
|
160
|
+
* a page taller than `fullPageMaxHeight` is captured at the cap, and a cut-off
|
|
161
|
+
* page must not pass for the whole one.
|
|
162
|
+
*/
|
|
163
|
+
const fullPageCapWarning = (storyId, growth, maxHeight) => growth.wanted > maxHeight
|
|
164
|
+
? `[qlip] fullPage: ${storyId} is ${String(growth.wanted)}px tall but was captured at ` +
|
|
165
|
+
`the ${String(maxHeight)}px cap; raise fullPageMaxHeight (Chromium allows up to 16384) ` +
|
|
166
|
+
'to capture all of it.'
|
|
167
|
+
: null;
|
|
168
|
+
exports.fullPageCapWarning = fullPageCapWarning;
|
|
94
169
|
/**
|
|
95
170
|
* The two viewports a capture sizes, and how each is put back. `set` grows
|
|
96
171
|
* the window to hold the iframe whenever the iframe would not fit (never
|
|
@@ -251,16 +251,23 @@ const qlipCaptureInner = async (page, context, options) => {
|
|
|
251
251
|
if (resolved.fullPage) {
|
|
252
252
|
// Here the viewport IS the browser window, so growing it is one call;
|
|
253
253
|
// the measure runs inside the page (see ../runtime/fullPage.ts).
|
|
254
|
-
|
|
254
|
+
const growth = await (0, fullPage_js_1.growViewportToContent)({
|
|
255
255
|
viewport: resolved.viewport,
|
|
256
256
|
maxHeight: resolved.fullPageMaxHeight,
|
|
257
257
|
setViewport: (width, height) => page.setViewportSize({ width, height }),
|
|
258
|
-
measure: () => page.evaluate(fullPage_js_1.measureContentHeight,
|
|
258
|
+
measure: (first) => page.evaluate(fullPage_js_1.measureContentHeight, { reset: first }),
|
|
259
259
|
settle: () => page.evaluate(prep_scripts_js_1.waitForDomIdleInPage, {
|
|
260
260
|
idleMs: resolved.waitForIdleMs,
|
|
261
261
|
maxWaitMs: resolved.maxWaitForIdleMs,
|
|
262
262
|
}),
|
|
263
263
|
});
|
|
264
|
+
if (growth.height !== resolved.viewport.height) {
|
|
265
|
+
grownTo = growth.height;
|
|
266
|
+
}
|
|
267
|
+
const warning = (0, fullPage_js_1.fullPageCapWarning)(context.id, growth, resolved.fullPageMaxHeight);
|
|
268
|
+
if (warning) {
|
|
269
|
+
console.warn(warning);
|
|
270
|
+
}
|
|
264
271
|
}
|
|
265
272
|
if (resolved.ignoreElements.length > 0) {
|
|
266
273
|
maskIds = await page.evaluate(prep_scripts_js_1.applyIgnoreMasksInPage, {
|