@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 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, 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.
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 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.
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: (env?: {
36
- doc: Pick<Document, "documentElement" | "body" | "querySelectorAll">;
37
- win: Pick<Window, "getComputedStyle" | "scrollY">;
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
- * 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).
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<number | null>;
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;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"}
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"}
@@ -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 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.
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 = (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);
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
- if (el.scrollHeight <= el.clientHeight + 1) {
37
- continue;
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
- const overflowY = win.getComputedStyle(el).overflowY;
40
- if (overflowY !== 'auto' && overflowY !== 'scroll') {
97
+ if (memory.fixed.has(el)) {
41
98
  continue;
42
99
  }
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));
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
- * 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.)
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
- * Returns the height the viewport ended at, or `null` when it never had to
67
- * grow (the caller restores only in that case).
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 grown = false;
72
- let previous = null;
135
+ let wanted = 0;
73
136
  for (let round = 0; round < maxRounds; round += 1) {
74
- const asked = Math.min(Math.ceil(await measure()), maxHeight);
75
- if (asked <= height) {
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
- if (previous && asked - previous.asked <= previous.growth) {
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 grown ? height : null;
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;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
+ {"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"}
@@ -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;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"}
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
- grownTo = await growViewportToContent({
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, undefined),
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`, 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,
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
  */
@@ -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":"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 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.
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 = (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);
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
- if (el.scrollHeight <= el.clientHeight + 1) {
40
- continue;
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
- const overflowY = win.getComputedStyle(el).overflowY;
43
- if (overflowY !== 'auto' && overflowY !== 'scroll') {
100
+ if (memory.fixed.has(el)) {
44
101
  continue;
45
102
  }
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));
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
- * 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.)
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
- * Returns the height the viewport ended at, or `null` when it never had to
71
- * grow (the caller restores only in that case).
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 grown = false;
76
- let previous = null;
139
+ let wanted = 0;
77
140
  for (let round = 0; round < maxRounds; round += 1) {
78
- const asked = Math.min(Math.ceil(await measure()), maxHeight);
79
- if (asked <= height) {
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
- if (previous && asked - previous.asked <= previous.growth) {
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 grown ? height : null;
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
- grownTo = await (0, fullPage_js_1.growViewportToContent)({
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, undefined),
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, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qoretechnologies/qlip",
3
- "version": "0.1.3-pr.30.gc00bad8",
3
+ "version": "0.1.3-pr.30.gc7b05dc",
4
4
  "description": "Storybook screenshot capture for the Vitest addon",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",