@qoretechnologies/qlip 0.1.3-pr.30.gc00bad8 → 0.1.3-pr.30.gffaaa15

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
@@ -194,6 +194,7 @@ export const LoggedIn = {
194
194
  qlip: {
195
195
  skip: false,
196
196
  viewport: { width: 1280, height: 720 },
197
+ fullSizeCaptures: false,
197
198
  disableAnimations: true,
198
199
  pauseAnimationsAtEnd: false,
199
200
  fullPage: false,
@@ -219,6 +220,7 @@ Options precedence:
219
220
  qlipVitestPlugin({
220
221
  outputDir: './qlip/screenshots',
221
222
  viewport: { width: 1280, height: 720 },
223
+ fullSizeCaptures: false,
222
224
  disableAnimations: false,
223
225
  pauseAnimationsAtEnd: false,
224
226
  fullPage: false,
@@ -231,7 +233,11 @@ qlipVitestPlugin({
231
233
  });
232
234
  ```
233
235
 
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.
236
+ `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 a `fullPage` capture also grows the window — the Playwright context viewport — through a browser command the plugin registers (`qlipBrowserViewport`), and restores it first; the capture is 1:1, not a shrunk picture of a tall page. `fullSizeCaptures` does the same for every capture (below). 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.
237
+
238
+ `fullSizeCaptures` captures every story at the full size of its `viewport`. Under Vitest browser mode the browser window stays at the Playwright context viewport — 1280×720 headless, unless you configure it — whatever the story's `viewport` says, and Vitest scales the test iframe down to fit the window. The layout is the story's, but the pixels are shrunk: a 1920×1080 story comes back 1280×720, a 390×844 phone story 333×720, with its text and any fine difference shrunk with it. With `fullSizeCaptures: true` the window grows to hold the viewport for each capture and is put back afterwards, so the PNG is exactly the viewport. The runner path always captures at full size, and `fullPage` always grows the window.
239
+
240
+ It is off by default, so upgrading qlip changes no baselines, and it will become the default in a future minor release. Turning it on changes the size of every capture whose viewport is larger than the window (stories that fit it are unaffected), and the server reports each of those as changed against its baseline, a dimensions change. Turn it on in one change on your default branch and accept those captures once.
235
241
 
236
242
  `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
243
 
@@ -5,6 +5,7 @@ export interface QlipResolvedOptions {
5
5
  width: number;
6
6
  height: number;
7
7
  };
8
+ fullSizeCaptures: boolean;
8
9
  disableAnimations: boolean;
9
10
  pauseAnimationsAtEnd: boolean;
10
11
  disableBackdropFilter: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/config/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE5G,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,oBAAoB,EAAE,gBAAgB,EAAE,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,yBAAyB,EAAE,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,kBAAkB,GAAI,gCAIhC;IACD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC,KAAG,mBAqDH,CAAC"}
1
+ {"version":3,"file":"parameters.d.ts","sourceRoot":"","sources":["../../src/config/parameters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAE5G,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,oBAAoB,EAAE,gBAAgB,EAAE,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,yBAAyB,EAAE,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,kBAAkB,GAAI,gCAIhC;IACD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CAClC,KAAG,mBAyDH,CAAC"}
@@ -2,6 +2,9 @@ export const resolveQlipOptions = ({ defaults, story, override, }) => {
2
2
  return {
3
3
  skip: override?.skip ?? story?.skip ?? defaults.skip,
4
4
  viewport: override?.viewport ?? story?.viewport ?? defaults.viewport,
5
+ fullSizeCaptures: override?.fullSizeCaptures ??
6
+ story?.fullSizeCaptures ??
7
+ defaults.fullSizeCaptures,
5
8
  disableAnimations: override?.disableAnimations ??
6
9
  story?.disableAnimations ??
7
10
  defaults.disableAnimations,
@@ -1 +1 @@
1
- {"version":3,"file":"vitestPlugin.d.ts","sourceRoot":"","sources":["../../src/plugin/vitestPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAU5C,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;AAanE,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,MA8QlE,CAAC"}
1
+ {"version":3,"file":"vitestPlugin.d.ts","sourceRoot":"","sources":["../../src/plugin/vitestPlugin.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAU5C,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;AAanE,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,MAkRlE,CAAC"}
@@ -33,6 +33,9 @@ export const qlipVitestPlugin = (options = {}) => {
33
33
  defaults: {
34
34
  outputDir,
35
35
  viewport: options.viewport ?? DEFAULT_VIEWPORT,
36
+ // Off by default — turning it on resizes every capture that was
37
+ // being scaled to fit the window. See QlipCaptureOptions.
38
+ fullSizeCaptures: options.fullSizeCaptures ?? false,
36
39
  skip: false,
37
40
  disableAnimations: options.disableAnimations ?? false,
38
41
  pauseAnimationsAtEnd: options.pauseAnimationsAtEnd ?? false,
@@ -190,9 +193,10 @@ export const qlipVitestPlugin = (options = {}) => {
190
193
  setupFiles: [setupFile],
191
194
  globalSetup: [globalSetupFile],
192
195
  reporters: reportersList,
193
- // The runtime grows the browser window for a `fullPage` capture
194
- // through this command; it runs here, where the Playwright page
195
- // is. `mergeConfig` merges it beside any commands the consumer
196
+ // The runtime grows the browser window for a `fullPage` capture,
197
+ // and for every capture with `fullSizeCaptures`, through this
198
+ // command; it runs here, where the Playwright page is.
199
+ // `mergeConfig` merges it beside any commands the consumer
196
200
  // declares.
197
201
  browser: {
198
202
  commands: {
@@ -10,9 +10,11 @@
10
10
  * viewport) stays as configured, and Vitest scales the iframe down to fit it.
11
11
  * A 3,600px-tall iframe in a 1,440px window is photographed at 40% — the
12
12
  * whole page, unreadable — and so is a plain 844px-tall phone story in a
13
- * 720px window, at 85%. `createCaptureViewport` therefore grows the window
14
- * to hold whatever viewport a capture asks for, pinned or grown, and puts
15
- * both back afterwards, the window first, so the iframe lands at scale 1.
13
+ * 720px window, at 85%. `createCaptureViewport` therefore grows the window,
14
+ * when it is given one, to hold whatever viewport a capture asks for, pinned
15
+ * or grown, and puts both back afterwards, the window first, so the iframe
16
+ * lands at scale 1. The runtime gives it the window for a `fullPage` capture
17
+ * and, with `fullSizeCaptures` on, for every capture.
16
18
  *
17
19
  * This module has no imports: the runtime, the test-runner and the unit
18
20
  * project (which has no DOM and feeds it fakes) all share it.
@@ -22,48 +24,75 @@ export interface FullPageSize {
22
24
  height: number;
23
25
  }
24
26
  /**
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.
27
+ * How tall the page needs the viewport to be for nothing on it to scroll:
28
+ * the document, and every box that scrolls its own content. A scrolling box
29
+ * asks for the viewport plus whatever it hides — a box that fills the
30
+ * viewport grows with it one for one, whatever sits above or below it — and
31
+ * a box that already fits asks for the viewport minus its spare room.
32
+ * Answers 0 when nothing scrolls.
33
+ *
34
+ * A box only counts while growing the viewport can still reveal it, which
35
+ * only a re-measure after a growth can tell, so the rounds of one capture
36
+ * share a memory (`reset` starts a new one):
37
+ * - A box that kept its height when the viewport grew has a height of its
38
+ * own (a 200px code block, a capped list). More viewport never reveals its
39
+ * content, so it stops counting, and growth it asked for is given back.
40
+ * - A box whose content grew as much as the box did is sized to it (a tile
41
+ * that follows its column). Chasing it only stretches the tile, so it keeps
42
+ * asking for what it asked before it scaled. The document gets the same
43
+ * test (a `vh` hero).
44
+ * - A box whose hidden content shrank was revealed and keeps counting, even
45
+ * when a growth reveals less than it added.
46
+ *
47
+ * The document's own `scrollHeight` is the viewport again once the viewport
48
+ * outgrows it, so its tallest height while it still scrolled is remembered.
29
49
  *
30
50
  * Closure-free, so the test-runner can hand it to Playwright's
31
51
  * `page.evaluate()`, which re-evaluates the function's source inside the
32
52
  * page; there it reads the real `document` and `window`. `env` exists for
33
53
  * the unit test, which has neither.
34
54
  */
35
- export declare const measureContentHeight: (env?: {
36
- doc: Pick<Document, "documentElement" | "body" | "querySelectorAll">;
37
- win: Pick<Window, "getComputedStyle" | "scrollY">;
55
+ export declare const measureContentHeight: (options?: {
56
+ reset?: boolean;
57
+ env?: {
58
+ doc: Pick<Document, "documentElement" | "body" | "querySelectorAll">;
59
+ win: Pick<Window, "getComputedStyle" | "innerHeight">;
60
+ };
38
61
  }) => number;
62
+ /** Where `growViewportToContent` left the viewport, and why. */
63
+ export interface FullPageGrowth {
64
+ /** The height the viewport ended at: the pinned height when nothing grew. */
65
+ height: number;
66
+ /** What the content last asked for, before the `maxHeight` cap. */
67
+ wanted: number;
68
+ }
39
69
  /**
40
70
  * Grows the viewport until nothing on the page scrolls. Growing the viewport
41
71
  * re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
42
72
  * and may reveal another box below it that scrolls in turn — so the height is
43
73
  * measured again after every growth, until a round asks for nothing more or
44
- * `maxRounds` is spent.
74
+ * `maxRounds` is spent. Content that does not grow with the viewport, or
75
+ * grows with it, is `measure`'s call (see `measureContentHeight`); when a
76
+ * round asks for less than the viewport already is, that growth went to such
77
+ * a box, and it is given back.
45
78
  *
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).
79
+ * `measure` is told when it is the capture's first round, so it can start a
80
+ * new memory.
58
81
  */
59
82
  export declare const growViewportToContent: ({ viewport, maxHeight, setViewport, measure, settle, maxRounds, }: {
60
83
  viewport: FullPageSize;
61
84
  maxHeight: number;
62
85
  setViewport: (width: number, height: number) => Promise<void>;
63
- measure: () => number | Promise<number>;
86
+ measure: (first: boolean) => number | Promise<number>;
64
87
  settle: () => Promise<void>;
65
88
  maxRounds?: number;
66
- }) => Promise<number | null>;
89
+ }) => Promise<FullPageGrowth>;
90
+ /**
91
+ * The warning for a `fullPage` capture the height cap cut short, or `null`:
92
+ * a page taller than `fullPageMaxHeight` is captured at the cap, and a cut-off
93
+ * page must not pass for the whole one.
94
+ */
95
+ export declare const fullPageCapWarning: (storyId: string, growth: FullPageGrowth, maxHeight: number) => string | null;
67
96
  /**
68
97
  * The browser-window half of the viewport, as the qlip plugin's browser
69
98
  * 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;;;;;;;;;;;;;;;;;;;;GAoBG;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"}
@@ -10,40 +10,113 @@
10
10
  * viewport) stays as configured, and Vitest scales the iframe down to fit it.
11
11
  * A 3,600px-tall iframe in a 1,440px window is photographed at 40% — the
12
12
  * whole page, unreadable — and so is a plain 844px-tall phone story in a
13
- * 720px window, at 85%. `createCaptureViewport` therefore grows the window
14
- * to hold whatever viewport a capture asks for, pinned or grown, and puts
15
- * both back afterwards, the window first, so the iframe lands at scale 1.
13
+ * 720px window, at 85%. `createCaptureViewport` therefore grows the window,
14
+ * when it is given one, to hold whatever viewport a capture asks for, pinned
15
+ * or grown, and puts both back afterwards, the window first, so the iframe
16
+ * lands at scale 1. The runtime gives it the window for a `fullPage` capture
17
+ * and, with `fullSizeCaptures` on, for every capture.
16
18
  *
17
19
  * This module has no imports: the runtime, the test-runner and the unit
18
20
  * project (which has no DOM and feeds it fakes) all share it.
19
21
  */
20
22
  /**
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.
23
+ * How tall the page needs the viewport to be for nothing on it to scroll:
24
+ * the document, and every box that scrolls its own content. A scrolling box
25
+ * asks for the viewport plus whatever it hides — a box that fills the
26
+ * viewport grows with it one for one, whatever sits above or below it — and
27
+ * a box that already fits asks for the viewport minus its spare room.
28
+ * Answers 0 when nothing scrolls.
29
+ *
30
+ * A box only counts while growing the viewport can still reveal it, which
31
+ * only a re-measure after a growth can tell, so the rounds of one capture
32
+ * share a memory (`reset` starts a new one):
33
+ * - A box that kept its height when the viewport grew has a height of its
34
+ * own (a 200px code block, a capped list). More viewport never reveals its
35
+ * content, so it stops counting, and growth it asked for is given back.
36
+ * - A box whose content grew as much as the box did is sized to it (a tile
37
+ * that follows its column). Chasing it only stretches the tile, so it keeps
38
+ * asking for what it asked before it scaled. The document gets the same
39
+ * test (a `vh` hero).
40
+ * - A box whose hidden content shrank was revealed and keeps counting, even
41
+ * when a growth reveals less than it added.
42
+ *
43
+ * The document's own `scrollHeight` is the viewport again once the viewport
44
+ * outgrows it, so its tallest height while it still scrolled is remembered.
25
45
  *
26
46
  * Closure-free, so the test-runner can hand it to Playwright's
27
47
  * `page.evaluate()`, which re-evaluates the function's source inside the
28
48
  * page; there it reads the real `document` and `window`. `env` exists for
29
49
  * the unit test, which has neither.
30
50
  */
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);
51
+ export const measureContentHeight = (options) => {
52
+ const doc = options?.env?.doc ?? document;
53
+ const win = options?.env?.win ?? window;
54
+ const holder = win;
55
+ if (options?.reset || !holder.__qlipFullPage) {
56
+ holder.__qlipFullPage = {
57
+ viewport: 0,
58
+ docHeight: 0,
59
+ docOverflow: 0,
60
+ docScales: false,
61
+ boxes: new WeakMap(),
62
+ fixed: new WeakSet(),
63
+ scales: new WeakSet(),
64
+ };
65
+ }
66
+ const memory = holder.__qlipFullPage;
67
+ const viewport = win.innerHeight;
68
+ const grew = memory.viewport > 0 && viewport > memory.viewport + 1;
69
+ const docScroll = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
70
+ const docOverflow = Math.max(0, docScroll - viewport);
71
+ if (grew && memory.docOverflow > 0 && docOverflow >= memory.docOverflow - 1) {
72
+ memory.docScales = true;
73
+ }
74
+ memory.docOverflow = docOverflow;
75
+ if (docOverflow > 1 && !memory.docScales) {
76
+ memory.docHeight = Math.max(memory.docHeight, docScroll);
77
+ }
78
+ let height = memory.docHeight;
35
79
  for (const el of Array.from(doc.querySelectorAll('*'))) {
36
- if (el.scrollHeight <= el.clientHeight + 1) {
37
- continue;
80
+ const seen = memory.boxes.get(el);
81
+ if (!seen) {
82
+ if (el.scrollHeight <= el.clientHeight + 1) {
83
+ continue;
84
+ }
85
+ const overflowY = win.getComputedStyle(el).overflowY;
86
+ if (overflowY !== 'auto' && overflowY !== 'scroll') {
87
+ continue;
88
+ }
89
+ }
90
+ const overflow = Math.max(0, el.scrollHeight - el.clientHeight);
91
+ if (seen && grew) {
92
+ if (el.clientHeight <= seen.clientHeight + 1) {
93
+ memory.fixed.add(el);
94
+ }
95
+ else if (seen.overflow > 0 && overflow >= seen.overflow - 1) {
96
+ memory.scales.add(el);
97
+ }
38
98
  }
39
- const overflowY = win.getComputedStyle(el).overflowY;
40
- if (overflowY !== 'auto' && overflowY !== 'scroll') {
99
+ if (memory.fixed.has(el)) {
41
100
  continue;
42
101
  }
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));
102
+ // A box that fits reports its own height as scrollHeight, so its content
103
+ // height is the one it had while it still scrolled.
104
+ const content = overflow > 1 ? Math.max(seen?.content ?? 0, el.scrollHeight) : (seen?.content ?? el.scrollHeight);
105
+ let need;
106
+ if (seen && memory.scales.has(el)) {
107
+ need = seen.need;
108
+ }
109
+ else if (overflow > 1) {
110
+ need = viewport + overflow;
111
+ }
112
+ else {
113
+ need = viewport - Math.max(0, el.clientHeight - content);
114
+ }
115
+ need = Math.ceil(need);
116
+ memory.boxes.set(el, { clientHeight: el.clientHeight, overflow, content, need });
117
+ height = Math.max(height, need);
46
118
  }
119
+ memory.viewport = viewport;
47
120
  return height;
48
121
  };
49
122
  /**
@@ -51,41 +124,44 @@ export const measureContentHeight = (env) => {
51
124
  * re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
52
125
  * and may reveal another box below it that scrolls in turn — so the height is
53
126
  * 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.)
127
+ * `maxRounds` is spent. Content that does not grow with the viewport, or
128
+ * grows with it, is `measure`'s call (see `measureContentHeight`); when a
129
+ * round asks for less than the viewport already is, that growth went to such
130
+ * a box, and it is given back.
65
131
  *
66
- * Returns the height the viewport ended at, or `null` when it never had to
67
- * grow (the caller restores only in that case).
132
+ * `measure` is told when it is the capture's first round, so it can start a
133
+ * new memory.
68
134
  */
69
135
  export const growViewportToContent = async ({ viewport, maxHeight, setViewport, measure, settle, maxRounds = 4, }) => {
70
136
  let height = viewport.height;
71
- let grown = false;
72
- let previous = null;
137
+ let wanted = 0;
73
138
  for (let round = 0; round < maxRounds; round += 1) {
74
- const asked = Math.min(Math.ceil(await measure()), maxHeight);
75
- if (asked <= height) {
139
+ wanted = Math.ceil(await measure(round === 0));
140
+ const asked = Math.min(Math.max(wanted, viewport.height), maxHeight);
141
+ if (asked === height) {
76
142
  break;
77
143
  }
78
- if (previous && asked - previous.asked <= previous.growth) {
79
- break;
80
- }
81
- previous = { asked, growth: asked - height };
144
+ const givingBack = asked < height;
82
145
  height = asked;
83
- grown = true;
84
146
  await setViewport(viewport.width, height);
85
147
  await settle();
148
+ if (givingBack) {
149
+ // Every box still counted asked for no more than this, so it fits.
150
+ break;
151
+ }
86
152
  }
87
- return grown ? height : null;
153
+ return { height, wanted };
88
154
  };
155
+ /**
156
+ * The warning for a `fullPage` capture the height cap cut short, or `null`:
157
+ * a page taller than `fullPageMaxHeight` is captured at the cap, and a cut-off
158
+ * page must not pass for the whole one.
159
+ */
160
+ export const fullPageCapWarning = (storyId, growth, maxHeight) => growth.wanted > maxHeight
161
+ ? `[qlip] fullPage: ${storyId} is ${String(growth.wanted)}px tall but was captured at ` +
162
+ `the ${String(maxHeight)}px cap; raise fullPageMaxHeight (Chromium allows up to 16384) ` +
163
+ 'to capture all of it.'
164
+ : null;
89
165
  /**
90
166
  * The two viewports a capture sizes, and how each is put back. `set` grows
91
167
  * 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;AAicF,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';
@@ -491,13 +491,17 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
491
491
  // throws.
492
492
  let error = testError ?? null;
493
493
  let cleanupMasks = null;
494
- // The browser window grows to hold whatever viewport the capture asks for,
495
- // pinned or full-page, or the capture comes back scaled down to fit the
496
- // window (see ./fullPage.ts).
494
+ // Unless the browser window grows with it, a viewport larger than the
495
+ // window is captured scaled down to fit it (see ./fullPage.ts). Growing it
496
+ // resizes captures that already have baselines, so it is opt-in
497
+ // (`fullSizeCaptures`); a `fullPage` capture always grows it, since scaled
498
+ // it is unreadable.
497
499
  const captureViewport = createCaptureViewport({
498
500
  pinned: resolved.viewport,
499
501
  iframe: (width, height) => page.viewport(width, height),
500
- window: commands.qlipBrowserViewport,
502
+ window: resolved.fullSizeCaptures || resolved.fullPage
503
+ ? commands.qlipBrowserViewport
504
+ : undefined,
501
505
  });
502
506
  try {
503
507
  await captureViewport.set(resolved.viewport.width, resolved.viewport.height);
@@ -508,13 +512,17 @@ const captureScreenshot = async ({ kind, story, screenshotName, options, testErr
508
512
  });
509
513
  await waitForDomIdle(resolved.waitForIdleMs, resolved.maxWaitForIdleMs);
510
514
  if (resolved.fullPage) {
511
- await growViewportToContent({
515
+ const growth = await growViewportToContent({
512
516
  viewport: resolved.viewport,
513
517
  maxHeight: resolved.fullPageMaxHeight,
514
518
  setViewport: captureViewport.set,
515
- measure: () => measureContentHeight(),
519
+ measure: (first) => measureContentHeight({ reset: first }),
516
520
  settle: () => waitForDomIdle(resolved.waitForIdleMs, resolved.maxWaitForIdleMs),
517
521
  });
522
+ const warning = fullPageCapWarning(story.id, growth, resolved.fullPageMaxHeight);
523
+ if (warning) {
524
+ console.warn(warning);
525
+ }
518
526
  }
519
527
  cleanupMasks = applyIgnoreMasks(resolved.ignoreElements);
520
528
  await page.screenshot({ path: absolutePath, save: true });
@@ -9,6 +9,13 @@ declare const meta: {
9
9
  export default meta;
10
10
  type Story = StoryObj<typeof meta>;
11
11
  export declare const LoggedOut: Story;
12
+ /**
13
+ * A desktop viewport wider and taller than the 1280×720 headless browser
14
+ * window. `fullSizeCaptures` grows the window to hold it, so the capture is
15
+ * 1920×1080; without it Vitest would scale the story down to 1280×720. The
16
+ * e2e test reads the PNG.
17
+ */
18
+ export declare const DesktopFullSize: Story;
12
19
  export declare const LoggedIn: Story;
13
20
  export declare const ElementWithFullHeight: Story;
14
21
  export declare const WithConsoleError: Story;
@@ -1 +1 @@
1
- {"version":3,"file":"Page.stories.d.ts","sourceRoot":"","sources":["../../src/stories/Page.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAO5D,QAAA,MAAM,IAAI;;;;;;CAOmB,CAAC;AAE9B,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAGnC,eAAO,MAAM,SAAS,EAAE,KAAU,CAAC;AAGnC,eAAO,MAAM,QAAQ,EAAE,KAatB,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,KAQnC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAS9B,CAAC"}
1
+ {"version":3,"file":"Page.stories.d.ts","sourceRoot":"","sources":["../../src/stories/Page.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAO5D,QAAA,MAAM,IAAI;;;;;;CAOmB,CAAC;AAE9B,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAGnC,eAAO,MAAM,SAAS,EAAE,KAAU,CAAC;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,KAI7B,CAAC;AAGF,eAAO,MAAM,QAAQ,EAAE,KAatB,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,KAQnC,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAS9B,CAAC"}
@@ -12,6 +12,17 @@ const meta = {
12
12
  };
13
13
  export default meta;
14
14
  export const LoggedOut = {};
15
+ /**
16
+ * A desktop viewport wider and taller than the 1280×720 headless browser
17
+ * window. `fullSizeCaptures` grows the window to hold it, so the capture is
18
+ * 1920×1080; without it Vitest would scale the story down to 1280×720. The
19
+ * e2e test reads the PNG.
20
+ */
21
+ export const DesktopFullSize = {
22
+ parameters: {
23
+ qlip: { viewport: { width: 1920, height: 1080 }, fullSizeCaptures: true },
24
+ },
25
+ };
15
26
  // More on component testing: https://storybook.js.org/docs/writing-tests/interaction-testing
16
27
  export const LoggedIn = {
17
28
  play: async (ctx) => {
@@ -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." })] }));
@@ -15,6 +15,23 @@ type Story = StoryObj<typeof meta>;
15
15
  * The manifest keeps the pinned viewport as the baseline key.
16
16
  */
17
17
  export declare const ScrollsAnInnerBox: Story;
18
- /** The same page without `fullPage`: one screen, the rest scrolled away. */
18
+ /**
19
+ * The same page without `fullPage`: one screen, the rest scrolled away. The
20
+ * 844px-tall viewport does not fit the 720px headless browser window, so
21
+ * Vitest scales the story down to fit it and the capture comes back 333×720
22
+ * — the size such a capture has always had. The e2e test reads it.
23
+ */
19
24
  export declare const OneScreen: Story;
25
+ /**
26
+ * `OneScreen` with `fullSizeCaptures`: qlip grows the browser window to hold
27
+ * the viewport, so the capture is exactly 390×844.
28
+ */
29
+ export declare const OneScreenFullSize: Story;
30
+ /**
31
+ * `fullPage` on an ordinary page whose only scroller is a fixed-height box:
32
+ * more viewport never reveals it, so qlip gives the growth back and the
33
+ * capture stays one screen — not a screen of page over a tall blank strip.
34
+ * The e2e test reads the PNG's height.
35
+ */
36
+ export declare const FixedHeightBox: Story;
20
37
  //# 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;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,KAKvB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAK/B,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,
@@ -32,10 +33,43 @@ export const ScrollsAnInnerBox = {
32
33
  },
33
34
  play: expectsAnInnerScroller,
34
35
  };
35
- /** The same page without `fullPage`: one screen, the rest scrolled away. */
36
+ /**
37
+ * The same page without `fullPage`: one screen, the rest scrolled away. The
38
+ * 844px-tall viewport does not fit the 720px headless browser window, so
39
+ * Vitest scales the story down to fit it and the capture comes back 333×720
40
+ * — the size such a capture has always had. The e2e test reads it.
41
+ */
36
42
  export const OneScreen = {
37
43
  parameters: {
38
44
  qlip: { viewport: PHONE },
39
45
  },
40
46
  play: expectsAnInnerScroller,
41
47
  };
48
+ /**
49
+ * `OneScreen` with `fullSizeCaptures`: qlip grows the browser window to hold
50
+ * the viewport, so the capture is exactly 390×844.
51
+ */
52
+ export const OneScreenFullSize = {
53
+ parameters: {
54
+ qlip: { viewport: PHONE, fullSizeCaptures: true },
55
+ },
56
+ play: expectsAnInnerScroller,
57
+ };
58
+ /**
59
+ * `fullPage` on an ordinary page whose only scroller is a fixed-height box:
60
+ * more viewport never reveals it, so qlip gives the growth back and the
61
+ * capture stays one screen — not a screen of page over a tall blank strip.
62
+ * The e2e test reads the PNG's height.
63
+ */
64
+ export const FixedHeightBox = {
65
+ render: () => _jsx(FixedBoxPage, {}),
66
+ parameters: {
67
+ qlip: { viewport: PHONE, fullPage: true },
68
+ },
69
+ play: async ({ canvasElement }) => {
70
+ const box = canvasElement.querySelector('[data-testid="fixed-box"]');
71
+ if (!box)
72
+ throw new Error('box not rendered');
73
+ await expect(box.scrollHeight).toBeGreaterThan(box.clientHeight);
74
+ },
75
+ };
@@ -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;AAkID;;;;;;;;;;;;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';
@@ -60,6 +60,9 @@ const buildResolvedDefaults = (override) => {
60
60
  return {
61
61
  outputDir: DEFAULT_OUTPUT_DIR,
62
62
  viewport: fallbackViewport,
63
+ // Not read on this path: `page.setViewportSize` sizes the page itself,
64
+ // so every capture here is full size already.
65
+ fullSizeCaptures: override?.fullSizeCaptures ?? false,
63
66
  skip: false,
64
67
  disableAnimations: override?.disableAnimations ?? false,
65
68
  pauseAnimationsAtEnd: override?.pauseAnimationsAtEnd ?? false,
@@ -243,16 +246,23 @@ const qlipCaptureInner = async (page, context, options) => {
243
246
  if (resolved.fullPage) {
244
247
  // Here the viewport IS the browser window, so growing it is one call;
245
248
  // the measure runs inside the page (see ../runtime/fullPage.ts).
246
- grownTo = await growViewportToContent({
249
+ const growth = await growViewportToContent({
247
250
  viewport: resolved.viewport,
248
251
  maxHeight: resolved.fullPageMaxHeight,
249
252
  setViewport: (width, height) => page.setViewportSize({ width, height }),
250
- measure: () => page.evaluate(measureContentHeight, undefined),
253
+ measure: (first) => page.evaluate(measureContentHeight, { reset: first }),
251
254
  settle: () => page.evaluate(waitForDomIdleInPage, {
252
255
  idleMs: resolved.waitForIdleMs,
253
256
  maxWaitMs: resolved.maxWaitForIdleMs,
254
257
  }),
255
258
  });
259
+ if (growth.height !== resolved.viewport.height) {
260
+ grownTo = growth.height;
261
+ }
262
+ const warning = fullPageCapWarning(context.id, growth, resolved.fullPageMaxHeight);
263
+ if (warning) {
264
+ console.warn(warning);
265
+ }
256
266
  }
257
267
  if (resolved.ignoreElements.length > 0) {
258
268
  maskIds = await page.evaluate(applyIgnoreMasksInPage, {
package/dist/types.d.ts CHANGED
@@ -6,12 +6,29 @@ export interface QlipCaptureOptions {
6
6
  skip?: boolean;
7
7
  /**
8
8
  * The capture's viewport, and the baseline key on the server. Under
9
- * Vitest browser mode a viewport the browser window cannot hold grows the
10
- * window for the capture (and puts it back), because Vitest otherwise
11
- * scales the test iframe down to fit and the capture comes back smaller
12
- * than the viewport it names.
9
+ * Vitest browser mode a viewport larger than the browser window is
10
+ * captured scaled down to fit it, unless `fullSizeCaptures` is on.
13
11
  */
14
12
  viewport?: QlipViewport;
13
+ /**
14
+ * Capture at the full size of `viewport`, pixel for pixel. Under Vitest
15
+ * browser mode the browser window (the Playwright context viewport,
16
+ * 1280×720 headless unless configured) does not follow `viewport`: Vitest
17
+ * sizes the test iframe to the viewport and scales the iframe down to fit
18
+ * the window. The layout is the viewport's, but the pixels are shrunk: a
19
+ * 1920×1080 story is captured at 1280×720, a 390×844 phone story at
20
+ * 333×720. With this on, the window grows to hold the viewport for the
21
+ * capture and is put back afterwards, so the capture is exactly `viewport`.
22
+ *
23
+ * Off by default: turning it on changes the size of every capture that was
24
+ * being scaled, and the server reports each one as changed against its
25
+ * baseline, so accept them once. A `fullPage` capture grows the window
26
+ * either way. The Storybook test-runner path sizes the page's own viewport
27
+ * and always captures at full size.
28
+ *
29
+ * @default false
30
+ */
31
+ fullSizeCaptures?: boolean;
15
32
  disableAnimations?: boolean;
16
33
  pauseAnimationsAtEnd?: boolean;
17
34
  /**
@@ -32,15 +49,18 @@ export interface QlipCaptureOptions {
32
49
  * and a Playwright "full page" shot would not help, because an app shell
33
50
  * usually scrolls an inner box, not the document. The height is measured
34
51
  * from the document AND every scrolling element (an `overflow-y: auto |
35
- * scroll` box whose `scrollHeight` exceeds its `clientHeight`, placed at
36
- * its offset), re-measured after each growth until nothing scrolls, and
37
- * capped at `fullPageMaxHeight`. Under Vitest browser mode the browser
38
- * window (the Playwright context viewport) grows with the test iframe,
39
- * or the capture would be scaled down to fit the window; both are put
40
- * back afterwards. The manifest still reports the pinned `viewport` —
41
- * that is the baseline key; the picture is simply taller. A capture whose
42
- * content height changed shows on the server as a dimensions change
43
- * rather than a pixel diff.
52
+ * scroll` box whose `scrollHeight` exceeds its `clientHeight`), and
53
+ * re-measured after each growth until nothing scrolls. A box that keeps
54
+ * its height when the viewport grows (a fixed-height code block) stays
55
+ * scrolled and gives its growth back, and content that grows with the
56
+ * viewport is not chased. Capped at `fullPageMaxHeight`. Under Vitest
57
+ * browser mode the browser window (the Playwright context viewport) grows
58
+ * with the test iframe whatever `fullSizeCaptures` says, or the capture
59
+ * would be scaled down to fit the window; both are put back afterwards.
60
+ * The manifest still reports the pinned `viewport` — that is the baseline
61
+ * key; the picture is simply taller. A capture whose content height
62
+ * changed shows on the server as a dimensions change rather than a pixel
63
+ * diff.
44
64
  *
45
65
  * @default false
46
66
  */
@@ -48,7 +68,8 @@ export interface QlipCaptureOptions {
48
68
  /**
49
69
  * The tallest a `fullPage` capture may grow to, in CSS pixels. Chromium
50
70
  * refuses viewports past 16384; the default leaves room for very long
51
- * pages without inviting a runaway layout.
71
+ * pages without inviting a runaway layout. A page taller than this is
72
+ * captured at the cap, with a `[qlip] fullPage:` warning naming the story.
52
73
  *
53
74
  * @default 10000
54
75
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,9 @@ const resolveQlipOptions = ({ defaults, story, override, }) => {
5
5
  return {
6
6
  skip: override?.skip ?? story?.skip ?? defaults.skip,
7
7
  viewport: override?.viewport ?? story?.viewport ?? defaults.viewport,
8
+ fullSizeCaptures: override?.fullSizeCaptures ??
9
+ story?.fullSizeCaptures ??
10
+ defaults.fullSizeCaptures,
8
11
  disableAnimations: override?.disableAnimations ??
9
12
  story?.disableAnimations ??
10
13
  defaults.disableAnimations,
@@ -11,42 +11,115 @@
11
11
  * viewport) stays as configured, and Vitest scales the iframe down to fit it.
12
12
  * A 3,600px-tall iframe in a 1,440px window is photographed at 40% — the
13
13
  * whole page, unreadable — and so is a plain 844px-tall phone story in a
14
- * 720px window, at 85%. `createCaptureViewport` therefore grows the window
15
- * to hold whatever viewport a capture asks for, pinned or grown, and puts
16
- * both back afterwards, the window first, so the iframe lands at scale 1.
14
+ * 720px window, at 85%. `createCaptureViewport` therefore grows the window,
15
+ * when it is given one, to hold whatever viewport a capture asks for, pinned
16
+ * or grown, and puts both back afterwards, the window first, so the iframe
17
+ * lands at scale 1. The runtime gives it the window for a `fullPage` capture
18
+ * and, with `fullSizeCaptures` on, for every capture.
17
19
  *
18
20
  * This module has no imports: the runtime, the test-runner and the unit
19
21
  * project (which has no DOM and feeds it fakes) all share it.
20
22
  */
21
23
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.createCaptureViewport = exports.growViewportToContent = exports.measureContentHeight = void 0;
24
+ exports.createCaptureViewport = exports.fullPageCapWarning = exports.growViewportToContent = exports.measureContentHeight = void 0;
23
25
  /**
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.
26
+ * How tall the page needs the viewport to be for nothing on it to scroll:
27
+ * the document, and every box that scrolls its own content. A scrolling box
28
+ * asks for the viewport plus whatever it hides — a box that fills the
29
+ * viewport grows with it one for one, whatever sits above or below it — and
30
+ * a box that already fits asks for the viewport minus its spare room.
31
+ * Answers 0 when nothing scrolls.
32
+ *
33
+ * A box only counts while growing the viewport can still reveal it, which
34
+ * only a re-measure after a growth can tell, so the rounds of one capture
35
+ * share a memory (`reset` starts a new one):
36
+ * - A box that kept its height when the viewport grew has a height of its
37
+ * own (a 200px code block, a capped list). More viewport never reveals its
38
+ * content, so it stops counting, and growth it asked for is given back.
39
+ * - A box whose content grew as much as the box did is sized to it (a tile
40
+ * that follows its column). Chasing it only stretches the tile, so it keeps
41
+ * asking for what it asked before it scaled. The document gets the same
42
+ * test (a `vh` hero).
43
+ * - A box whose hidden content shrank was revealed and keeps counting, even
44
+ * when a growth reveals less than it added.
45
+ *
46
+ * The document's own `scrollHeight` is the viewport again once the viewport
47
+ * outgrows it, so its tallest height while it still scrolled is remembered.
28
48
  *
29
49
  * Closure-free, so the test-runner can hand it to Playwright's
30
50
  * `page.evaluate()`, which re-evaluates the function's source inside the
31
51
  * page; there it reads the real `document` and `window`. `env` exists for
32
52
  * the unit test, which has neither.
33
53
  */
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);
54
+ const measureContentHeight = (options) => {
55
+ const doc = options?.env?.doc ?? document;
56
+ const win = options?.env?.win ?? window;
57
+ const holder = win;
58
+ if (options?.reset || !holder.__qlipFullPage) {
59
+ holder.__qlipFullPage = {
60
+ viewport: 0,
61
+ docHeight: 0,
62
+ docOverflow: 0,
63
+ docScales: false,
64
+ boxes: new WeakMap(),
65
+ fixed: new WeakSet(),
66
+ scales: new WeakSet(),
67
+ };
68
+ }
69
+ const memory = holder.__qlipFullPage;
70
+ const viewport = win.innerHeight;
71
+ const grew = memory.viewport > 0 && viewport > memory.viewport + 1;
72
+ const docScroll = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
73
+ const docOverflow = Math.max(0, docScroll - viewport);
74
+ if (grew && memory.docOverflow > 0 && docOverflow >= memory.docOverflow - 1) {
75
+ memory.docScales = true;
76
+ }
77
+ memory.docOverflow = docOverflow;
78
+ if (docOverflow > 1 && !memory.docScales) {
79
+ memory.docHeight = Math.max(memory.docHeight, docScroll);
80
+ }
81
+ let height = memory.docHeight;
38
82
  for (const el of Array.from(doc.querySelectorAll('*'))) {
39
- if (el.scrollHeight <= el.clientHeight + 1) {
40
- continue;
83
+ const seen = memory.boxes.get(el);
84
+ if (!seen) {
85
+ if (el.scrollHeight <= el.clientHeight + 1) {
86
+ continue;
87
+ }
88
+ const overflowY = win.getComputedStyle(el).overflowY;
89
+ if (overflowY !== 'auto' && overflowY !== 'scroll') {
90
+ continue;
91
+ }
92
+ }
93
+ const overflow = Math.max(0, el.scrollHeight - el.clientHeight);
94
+ if (seen && grew) {
95
+ if (el.clientHeight <= seen.clientHeight + 1) {
96
+ memory.fixed.add(el);
97
+ }
98
+ else if (seen.overflow > 0 && overflow >= seen.overflow - 1) {
99
+ memory.scales.add(el);
100
+ }
41
101
  }
42
- const overflowY = win.getComputedStyle(el).overflowY;
43
- if (overflowY !== 'auto' && overflowY !== 'scroll') {
102
+ if (memory.fixed.has(el)) {
44
103
  continue;
45
104
  }
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));
105
+ // A box that fits reports its own height as scrollHeight, so its content
106
+ // height is the one it had while it still scrolled.
107
+ const content = overflow > 1 ? Math.max(seen?.content ?? 0, el.scrollHeight) : (seen?.content ?? el.scrollHeight);
108
+ let need;
109
+ if (seen && memory.scales.has(el)) {
110
+ need = seen.need;
111
+ }
112
+ else if (overflow > 1) {
113
+ need = viewport + overflow;
114
+ }
115
+ else {
116
+ need = viewport - Math.max(0, el.clientHeight - content);
117
+ }
118
+ need = Math.ceil(need);
119
+ memory.boxes.set(el, { clientHeight: el.clientHeight, overflow, content, need });
120
+ height = Math.max(height, need);
49
121
  }
122
+ memory.viewport = viewport;
50
123
  return height;
51
124
  };
52
125
  exports.measureContentHeight = measureContentHeight;
@@ -55,42 +128,46 @@ exports.measureContentHeight = measureContentHeight;
55
128
  * re-lays the page out — a scrolling box that was 700px tall becomes 3,500px
56
129
  * and may reveal another box below it that scrolls in turn — so the height is
57
130
  * 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.)
131
+ * `maxRounds` is spent. Content that does not grow with the viewport, or
132
+ * grows with it, is `measure`'s call (see `measureContentHeight`); when a
133
+ * round asks for less than the viewport already is, that growth went to such
134
+ * a box, and it is given back.
69
135
  *
70
- * Returns the height the viewport ended at, or `null` when it never had to
71
- * grow (the caller restores only in that case).
136
+ * `measure` is told when it is the capture's first round, so it can start a
137
+ * new memory.
72
138
  */
73
139
  const growViewportToContent = async ({ viewport, maxHeight, setViewport, measure, settle, maxRounds = 4, }) => {
74
140
  let height = viewport.height;
75
- let grown = false;
76
- let previous = null;
141
+ let wanted = 0;
77
142
  for (let round = 0; round < maxRounds; round += 1) {
78
- const asked = Math.min(Math.ceil(await measure()), maxHeight);
79
- if (asked <= height) {
143
+ wanted = Math.ceil(await measure(round === 0));
144
+ const asked = Math.min(Math.max(wanted, viewport.height), maxHeight);
145
+ if (asked === height) {
80
146
  break;
81
147
  }
82
- if (previous && asked - previous.asked <= previous.growth) {
83
- break;
84
- }
85
- previous = { asked, growth: asked - height };
148
+ const givingBack = asked < height;
86
149
  height = asked;
87
- grown = true;
88
150
  await setViewport(viewport.width, height);
89
151
  await settle();
152
+ if (givingBack) {
153
+ // Every box still counted asked for no more than this, so it fits.
154
+ break;
155
+ }
90
156
  }
91
- return grown ? height : null;
157
+ return { height, wanted };
92
158
  };
93
159
  exports.growViewportToContent = growViewportToContent;
160
+ /**
161
+ * The warning for a `fullPage` capture the height cap cut short, or `null`:
162
+ * a page taller than `fullPageMaxHeight` is captured at the cap, and a cut-off
163
+ * page must not pass for the whole one.
164
+ */
165
+ const fullPageCapWarning = (storyId, growth, maxHeight) => growth.wanted > maxHeight
166
+ ? `[qlip] fullPage: ${storyId} is ${String(growth.wanted)}px tall but was captured at ` +
167
+ `the ${String(maxHeight)}px cap; raise fullPageMaxHeight (Chromium allows up to 16384) ` +
168
+ 'to capture all of it.'
169
+ : null;
170
+ exports.fullPageCapWarning = fullPageCapWarning;
94
171
  /**
95
172
  * The two viewports a capture sizes, and how each is put back. `set` grows
96
173
  * the window to hold the iframe whenever the iframe would not fit (never
@@ -67,6 +67,9 @@ const buildResolvedDefaults = (override) => {
67
67
  return {
68
68
  outputDir: output_js_1.DEFAULT_OUTPUT_DIR,
69
69
  viewport: fallbackViewport,
70
+ // Not read on this path: `page.setViewportSize` sizes the page itself,
71
+ // so every capture here is full size already.
72
+ fullSizeCaptures: override?.fullSizeCaptures ?? false,
70
73
  skip: false,
71
74
  disableAnimations: override?.disableAnimations ?? false,
72
75
  pauseAnimationsAtEnd: override?.pauseAnimationsAtEnd ?? false,
@@ -251,16 +254,23 @@ const qlipCaptureInner = async (page, context, options) => {
251
254
  if (resolved.fullPage) {
252
255
  // Here the viewport IS the browser window, so growing it is one call;
253
256
  // the measure runs inside the page (see ../runtime/fullPage.ts).
254
- grownTo = await (0, fullPage_js_1.growViewportToContent)({
257
+ const growth = await (0, fullPage_js_1.growViewportToContent)({
255
258
  viewport: resolved.viewport,
256
259
  maxHeight: resolved.fullPageMaxHeight,
257
260
  setViewport: (width, height) => page.setViewportSize({ width, height }),
258
- measure: () => page.evaluate(fullPage_js_1.measureContentHeight, undefined),
261
+ measure: (first) => page.evaluate(fullPage_js_1.measureContentHeight, { reset: first }),
259
262
  settle: () => page.evaluate(prep_scripts_js_1.waitForDomIdleInPage, {
260
263
  idleMs: resolved.waitForIdleMs,
261
264
  maxWaitMs: resolved.maxWaitForIdleMs,
262
265
  }),
263
266
  });
267
+ if (growth.height !== resolved.viewport.height) {
268
+ grownTo = growth.height;
269
+ }
270
+ const warning = (0, fullPage_js_1.fullPageCapWarning)(context.id, growth, resolved.fullPageMaxHeight);
271
+ if (warning) {
272
+ console.warn(warning);
273
+ }
264
274
  }
265
275
  if (resolved.ignoreElements.length > 0) {
266
276
  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.gffaaa15",
4
4
  "description": "Storybook screenshot capture for the Vitest addon",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",