@nayaverse/overflow 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,185 @@
1
- # Temporary Holding Version
1
+ # @nayaverse/overflow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Find horizontal overflow at every breakpoint, in CI, and name the element.**
4
+
5
+ Zero dependencies. Node 20+. Works through Playwright, Puppeteer, or on its
6
+ own.
7
+
8
+ ```bash
9
+ npm i -D @nayaverse/overflow
10
+ ```
11
+
12
+ ---
13
+
14
+ ## Why this is not already solved
15
+
16
+ The problem is old and well written up —
17
+ [Smashing Magazine](https://www.smashingmagazine.com/2021/04/css-overflow-issues/),
18
+ [CSS-Tricks](https://css-tricks.com/?p=175021),
19
+ [Polypane](https://polypane.app/blog/strategies-for-dealing-with-horizontal-overflows/) —
20
+ and the technique is agreed: compare `scrollWidth` to `clientWidth`, then find
21
+ the elements whose right edge passes the viewport.
22
+
23
+ What you can actually install is a
24
+ [Chrome extension](https://chrome.google.com/webstore/detail/overflow-detector/gpdnihamialibcjfmdnfbjcljnfbgkbc/reviews),
25
+ a paid browser like [Polypane](https://polypane.app/) or
26
+ [Sizzy](https://sizzy.co/blog/fix-horizontal-scroll), or a console snippet.
27
+ All of them need a person to be looking, at one viewport, at one moment.
28
+
29
+ Nothing packaged **fails a build** across a list of breakpoints and tells you
30
+ which element to change.
31
+
32
+ ---
33
+
34
+ ## Use
35
+
36
+ ### Standalone
37
+
38
+ ```bash
39
+ # any Chrome with remote debugging
40
+ chrome --headless=new --remote-debugging-port=9222 --user-data-dir=/tmp/ovf about:blank
41
+
42
+ npx overflow https://example.com
43
+ npx overflow https://example.com --widths 320,390,768 --tolerance 2
44
+ ```
45
+
46
+ ```
47
+ 320px OVERFLOW 434px — table#wide-table
48
+ 390px OVERFLOW 364px — table#wide-table
49
+ 768px ok
50
+
51
+ 320px viewport scrolls to 754px (434px of overflow)
52
+ table#wide-table
53
+ reaches 434px past the edge; it is 738px wide in a 320px viewport
54
+ html > body > div.frame > table#wide-table
55
+
56
+ Horizontal overflow at 320, 390px.
57
+ ```
58
+
59
+ Exit code is 1 when anything overflowed, so it gates a deploy.
60
+
61
+ ### With Playwright
62
+
63
+ Prefer this if you already have Playwright — it sets the viewport correctly
64
+ by default, which is the part that matters.
65
+
66
+ ```ts
67
+ import { sweepPage } from '@nayaverse/overflow/playwright';
68
+
69
+ test('no horizontal overflow', async ({ page }) => {
70
+ await page.goto('/');
71
+ const result = await sweepPage(page, { widths: [320, 390, 768, 1280] });
72
+ expect(result.ok, result.report()).toBe(true);
73
+ });
74
+ ```
75
+
76
+ `sweepPage` restores the viewport afterwards, so the next assertion in the
77
+ same test does not fail for a reason that is not its own.
78
+
79
+ ### With anything else
80
+
81
+ ```ts
82
+ import { checkViewport } from '@nayaverse/overflow';
83
+
84
+ const analysis = await checkViewport((expr) => page.evaluate(expr));
85
+ if (analysis.overflow > 0) console.log(analysis.causes);
86
+ ```
87
+
88
+ `evaluate` is any function that runs a JavaScript expression in the page and
89
+ returns its value. Puppeteer, Playwright, a raw CDP session, or your own.
90
+
91
+ ---
92
+
93
+ ## What makes the output usable
94
+
95
+ Detection is the easy half. A sweep that reports every element whose right
96
+ edge passes the viewport produces dozens of findings, most of them children
97
+ of one real cause and several of them perfectly fine — and a check that cries
98
+ wolf gets switched off, after which it catches nothing.
99
+
100
+ So three things are dismissed, with the reason recorded in `analysis.dismissed`:
101
+
102
+ **An element inside a horizontal scroller.** A tab strip in
103
+ `overflow-x: auto` has a right edge far past the viewport and is doing
104
+ exactly what it was built to do. An early version of this reported six of
105
+ those while the real cause — a table elsewhere — sat unmentioned.
106
+
107
+ **A fixed or sticky element.** Out of flow, so it does not extend the
108
+ document.
109
+
110
+ **Children carried by a wider parent.** A 738px table in a 320px viewport
111
+ pushes its cells past the edge too. Reporting five 90px `<td>` elements is
112
+ true and useless; the table is the thing you can change. When something is
113
+ genuinely wider than the viewport, the **outermost** such element is blamed.
114
+ When nothing is oversized and an element is merely pushed right by a margin,
115
+ the rule inverts and the **innermost** is blamed, because there is no
116
+ oversized ancestor to point at.
117
+
118
+ And when the page overflows but nothing can be blamed, it says so rather than
119
+ passing — a margin, a transform or a pseudo-element is usually the answer, and
120
+ silence would read as success.
121
+
122
+ ---
123
+
124
+ ## The viewport trap this exists to avoid
125
+
126
+ **On Windows, Chrome cannot open a window narrower than about 494px.**
127
+
128
+ Launch with `--window-size=390,844` and the page lays out at 494px and is then
129
+ clipped to 390 — which looks exactly like a horizontal overflow bug, in a
130
+ layout that is completely fine. It is possible to spend hours fixing something
131
+ that was never broken. (That is not hypothetical; it is why this package
132
+ exists.)
133
+
134
+ The bundled driver always sets the viewport with
135
+ `Emulation.setDeviceMetricsOverride`, never a window size. Playwright does the
136
+ right thing by default, which is why most people never meet this — but anyone
137
+ driving Chrome directly walks straight into it.
138
+
139
+ ---
140
+
141
+ ## API
142
+
143
+ ### `sweep(options)`
144
+
145
+ | Option | Type | Default | |
146
+ |---|---|---|---|
147
+ | `url` | `string` | — | page to check |
148
+ | `widths` | `number[]` | `320,360,390,414,768,1024,1440` | |
149
+ | `height` | `number` | `900` | |
150
+ | `settleMs` | `number` | `600` | wait after navigation for layout |
151
+ | `tolerance` | `number` | `1` | sub-pixel rounding is normal |
152
+ | `maxCauses` | `number` | `10` | |
153
+ | `cdp.endpoint` | `string` | `http://127.0.0.1:9222` | |
154
+
155
+ ### `checkViewport(evaluate, options)` → `Analysis`
156
+
157
+ ```ts
158
+ type Analysis = {
159
+ viewport: number;
160
+ scrollWidth: number;
161
+ overflow: number; // <= 0 means it fits
162
+ causes: Cause[]; // what to change
163
+ dismissed: { selector: string; why: string }[];
164
+ };
165
+ ```
166
+
167
+ ### `sweepPage(page, options)` — Playwright
168
+ ### `analyse(measurements, viewport, scrollWidth, options)` — the pure judgement, if you collect measurements yourself
169
+
170
+ ---
171
+
172
+ ## Tests
173
+
174
+ The judgement is a pure function, so it is tested without a browser: thirteen
175
+ cases covering each dismissal rule, both blame directions, ordering, capping,
176
+ tolerance, and the overflow nobody can be blamed for.
177
+
178
+ The collection half is proven against real Chrome on fixtures built to be
179
+ wrong in known ways — a page that fits, a page with an oversized table, a page
180
+ whose only overflow is a scroller and a sticky header, and a page with both,
181
+ where only the table may be reported.
182
+
183
+ ## Licence
184
+
185
+ MIT.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Deciding which elements actually cause a horizontal overflow.
3
+ *
4
+ * Kept pure, and separate from the browser, for one reason: this is where a
5
+ * tool like this is useful or useless. Reporting every element whose right
6
+ * edge exceeds the viewport produces dozens of findings, most of them
7
+ * children of one real cause and several of them inside a scroller that is
8
+ * supposed to scroll. A check that cries wolf gets switched off, and then it
9
+ * catches nothing at all.
10
+ *
11
+ * Being pure, it is also testable without a browser, which is why the rules
12
+ * below have tests rather than hopes.
13
+ */
14
+ /** One element, as measured in the page. */
15
+ export type Measured = {
16
+ /** A readable selector, for a human to go and look at. */
17
+ selector: string;
18
+ /** Ancestor chain, outermost first. Used to find leaves. */
19
+ path: string;
20
+ left: number;
21
+ right: number;
22
+ width: number;
23
+ /** An ancestor clips horizontally and holds this element inside itself. */
24
+ clipped: boolean;
25
+ /** position: fixed or sticky. */
26
+ fixed: boolean;
27
+ /** Depth from <html>. */
28
+ depth: number;
29
+ };
30
+ export type Cause = {
31
+ selector: string;
32
+ path: string;
33
+ /** How far past the viewport it reaches, in px. */
34
+ exceedsBy: number;
35
+ width: number;
36
+ left: number;
37
+ right: number;
38
+ /** Why this one is reported and others were not. */
39
+ why: string;
40
+ };
41
+ export type Analysis = {
42
+ viewport: number;
43
+ scrollWidth: number;
44
+ /** scrollWidth - viewport. Zero or less means no overflow. */
45
+ overflow: number;
46
+ causes: Cause[];
47
+ /** Flagged, then dismissed, with the reason. Kept so the tool can explain
48
+ * itself when somebody insists there is a bug it did not report. */
49
+ dismissed: {
50
+ selector: string;
51
+ why: string;
52
+ }[];
53
+ };
54
+ export type AnalyseOptions = {
55
+ /** Sub-pixel layout and scrollbar rounding make a 1px "overflow" normal. */
56
+ tolerance?: number;
57
+ /** Most reports are one cause and its children. */
58
+ maxCauses?: number;
59
+ };
60
+ export declare function analyse(measurements: Measured[], viewport: number, scrollWidth: number, options?: AnalyseOptions): Analysis;
61
+ /** A short line per cause, for a terminal or a CI annotation. */
62
+ export declare function explain(a: Analysis): string[];
63
+ //# sourceMappingURL=analyse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyse.d.ts","sourceRoot":"","sources":["../../src/analyse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;IACjB,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB;yEACqE;IACrE,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,OAAO,CACrB,YAAY,EAAE,QAAQ,EAAE,EACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,cAAmB,GAC3B,QAAQ,CA2FV;AAED,iEAAiE;AACjE,wBAAgB,OAAO,CAAC,CAAC,EAAE,QAAQ,GAAG,MAAM,EAAE,CAkB7C"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Deciding which elements actually cause a horizontal overflow.
3
+ *
4
+ * Kept pure, and separate from the browser, for one reason: this is where a
5
+ * tool like this is useful or useless. Reporting every element whose right
6
+ * edge exceeds the viewport produces dozens of findings, most of them
7
+ * children of one real cause and several of them inside a scroller that is
8
+ * supposed to scroll. A check that cries wolf gets switched off, and then it
9
+ * catches nothing at all.
10
+ *
11
+ * Being pure, it is also testable without a browser, which is why the rules
12
+ * below have tests rather than hopes.
13
+ */
14
+ export function analyse(measurements, viewport, scrollWidth, options = {}) {
15
+ const { tolerance = 1, maxCauses = 10 } = options;
16
+ const overflow = scrollWidth - viewport;
17
+ const result = { viewport, scrollWidth, overflow, causes: [], dismissed: [] };
18
+ if (overflow <= tolerance)
19
+ return result;
20
+ const dismiss = (m, why) => result.dismissed.push({ selector: m.selector, why });
21
+ const candidates = [];
22
+ for (const m of measurements) {
23
+ if (m.width <= 0) {
24
+ dismiss(m, 'zero width');
25
+ continue;
26
+ }
27
+ if (m.right <= viewport + tolerance) {
28
+ // Does not reach past the edge at all.
29
+ continue;
30
+ }
31
+ if (m.clipped) {
32
+ // The tab strip inside overflow-x: auto. Its right edge is genuinely
33
+ // past the viewport and it is genuinely not the problem -- an earlier
34
+ // version of this check reported six of these and the real cause was a
35
+ // table somewhere else entirely.
36
+ dismiss(m, 'inside a horizontally scrollable ancestor, so it is clipped, not causing');
37
+ continue;
38
+ }
39
+ if (m.fixed) {
40
+ // A fixed element is out of flow and does not extend the document.
41
+ dismiss(m, 'position is fixed or sticky, so it does not extend the document');
42
+ continue;
43
+ }
44
+ candidates.push(m);
45
+ }
46
+ // Two different shapes of cause, and they want opposite rules.
47
+ //
48
+ // Something genuinely wider than the viewport -- a table, a pre, an image
49
+ // with a fixed width -- is the thing to fix, and its children are merely
50
+ // carried along by it. Reporting those children is useless: a first version
51
+ // of this blamed five 90px <td> cells for a 720px table, which tells you
52
+ // nothing you can act on. So for these, report the OUTERMOST.
53
+ //
54
+ // Something that fits but starts too far right -- a badge pushed by a
55
+ // margin, an absolutely positioned decoration -- has no oversized ancestor
56
+ // to blame, and there the innermost element is the one with the offending
57
+ // geometry.
58
+ const tooWide = candidates.filter((m) => m.width > viewport + tolerance);
59
+ const pushedOut = candidates.filter((m) => m.width <= viewport + tolerance);
60
+ let blamed;
61
+ if (tooWide.length > 0) {
62
+ blamed = tooWide.filter((m) => {
63
+ const outer = tooWide.find((other) => other !== m && m.path.startsWith(other.path + ' > '));
64
+ if (outer) {
65
+ dismiss(m, `carried by ${outer.selector}, which is the wider element`);
66
+ return false;
67
+ }
68
+ return true;
69
+ });
70
+ for (const m of pushedOut) {
71
+ dismiss(m, 'fits the viewport; it is pushed out by a wider ancestor already reported');
72
+ }
73
+ }
74
+ else {
75
+ blamed = pushedOut.filter((m) => {
76
+ const inner = pushedOut.find((other) => other !== m && other.path.startsWith(m.path + ' > '));
77
+ if (inner) {
78
+ dismiss(m, `contains ${inner.selector}, which is the more specific cause`);
79
+ return false;
80
+ }
81
+ return true;
82
+ });
83
+ }
84
+ result.causes = blamed
85
+ .map((m) => ({
86
+ selector: m.selector,
87
+ path: m.path,
88
+ exceedsBy: Math.round(m.right - viewport),
89
+ width: Math.round(m.width),
90
+ left: Math.round(m.left),
91
+ right: Math.round(m.right),
92
+ why: m.width > viewport
93
+ ? `it is ${Math.round(m.width)}px wide in a ${viewport}px viewport`
94
+ : `it starts at ${Math.round(m.left)}px and is ${Math.round(m.width)}px wide`,
95
+ }))
96
+ .sort((a, b) => b.exceedsBy - a.exceedsBy)
97
+ .slice(0, maxCauses);
98
+ return result;
99
+ }
100
+ /** A short line per cause, for a terminal or a CI annotation. */
101
+ export function explain(a) {
102
+ if (a.overflow <= 0)
103
+ return [];
104
+ const lines = [
105
+ `${a.viewport}px viewport scrolls to ${a.scrollWidth}px (${a.overflow}px of overflow)`,
106
+ ];
107
+ if (a.causes.length === 0) {
108
+ lines.push(' no element could be blamed. Every candidate was clipped, fixed, or zero width.', ' A margin, a transform or a pseudo-element is the usual answer here.');
109
+ return lines;
110
+ }
111
+ for (const c of a.causes) {
112
+ lines.push(` ${c.selector}`);
113
+ lines.push(` reaches ${c.exceedsBy}px past the edge; ${c.why}`);
114
+ lines.push(` ${c.path}`);
115
+ }
116
+ return lines;
117
+ }
118
+ //# sourceMappingURL=analyse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyse.js","sourceRoot":"","sources":["../../src/analyse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A minimal Chrome DevTools Protocol client.
3
+ *
4
+ * Zero dependencies, about a hundred lines, and it exists for one reason: the
5
+ * viewport has to be set with `Emulation.setDeviceMetricsOverride`, never with
6
+ * a window size.
7
+ *
8
+ * On Windows a Chrome window cannot be narrower than roughly 494px. Launch
9
+ * with `--window-size=390,844` and the page lays out at 494px and is then
10
+ * clipped to 390 — which looks exactly like a horizontal overflow bug, in a
11
+ * layout that is perfectly fine. Hours go into fixing something that was
12
+ * never broken. Playwright gets this right by default; anyone driving Chrome
13
+ * directly walks straight into it.
14
+ *
15
+ * If you already use Playwright, use the adapter in ./playwright.js instead.
16
+ * This driver is for pipelines that do not want a browser automation
17
+ * dependency to run one check.
18
+ */
19
+ export type CdpOptions = {
20
+ /** A running Chrome with --remote-debugging-port. */
21
+ endpoint?: string;
22
+ /** Per-command timeout. */
23
+ timeoutMs?: number;
24
+ };
25
+ export declare class CdpSession {
26
+ #private;
27
+ private constructor();
28
+ static open(options?: CdpOptions): Promise<CdpSession>;
29
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
30
+ /**
31
+ * Set the viewport the page actually sees.
32
+ *
33
+ * Device metrics, never a window size. See the note at the top of this file.
34
+ */
35
+ setViewport(width: number, height?: number, deviceScaleFactor?: number): Promise<void>;
36
+ goto(url: string, settleMs?: number): Promise<void>;
37
+ evaluate(expression: string): Promise<unknown>;
38
+ close(): Promise<void>;
39
+ }
40
+ //# sourceMappingURL=cdp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp.d.ts","sourceRoot":"","sources":["../../src/cdp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,UAAU,GAAG;IACvB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAIF,qBAAa,UAAU;;IAQrB,OAAO;WAgBM,IAAI,CAAC,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,UAAU,CAAC;IAuChE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAsB5E;;;;OAIG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,SAAM,EAAE,iBAAiB,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9E,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,SAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMhD,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAY9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ7B"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * A minimal Chrome DevTools Protocol client.
3
+ *
4
+ * Zero dependencies, about a hundred lines, and it exists for one reason: the
5
+ * viewport has to be set with `Emulation.setDeviceMetricsOverride`, never with
6
+ * a window size.
7
+ *
8
+ * On Windows a Chrome window cannot be narrower than roughly 494px. Launch
9
+ * with `--window-size=390,844` and the page lays out at 494px and is then
10
+ * clipped to 390 — which looks exactly like a horizontal overflow bug, in a
11
+ * layout that is perfectly fine. Hours go into fixing something that was
12
+ * never broken. Playwright gets this right by default; anyone driving Chrome
13
+ * directly walks straight into it.
14
+ *
15
+ * If you already use Playwright, use the adapter in ./playwright.js instead.
16
+ * This driver is for pipelines that do not want a browser automation
17
+ * dependency to run one check.
18
+ */
19
+ export class CdpSession {
20
+ #ws;
21
+ #id = 0;
22
+ #pending = new Map();
23
+ #timeoutMs;
24
+ #targetId;
25
+ #endpoint;
26
+ constructor(ws, targetId, endpoint, timeoutMs) {
27
+ this.#ws = ws;
28
+ this.#targetId = targetId;
29
+ this.#endpoint = endpoint;
30
+ this.#timeoutMs = timeoutMs;
31
+ ws.addEventListener('message', (ev) => {
32
+ const msg = JSON.parse(String(ev.data));
33
+ if (msg.id === undefined)
34
+ return;
35
+ const p = this.#pending.get(msg.id);
36
+ if (!p)
37
+ return;
38
+ this.#pending.delete(msg.id);
39
+ if (msg.error)
40
+ p.reject(new Error(`CDP: ${JSON.stringify(msg.error)}`));
41
+ else
42
+ p.resolve(msg.result);
43
+ });
44
+ }
45
+ static async open(options = {}) {
46
+ const endpoint = options.endpoint ?? 'http://127.0.0.1:9222';
47
+ const timeoutMs = options.timeoutMs ?? 30_000;
48
+ if (typeof WebSocket === 'undefined') {
49
+ throw new Error('overflow: this Node has no global WebSocket, which the bundled CDP driver needs.\n' +
50
+ 'Use Node 22+, or run the check through Playwright with ' +
51
+ "`import { checkPage } from '@nayaverse/overflow/playwright'`.");
52
+ }
53
+ let target;
54
+ try {
55
+ const res = await fetch(`${endpoint}/json/new?about:blank`, { method: 'PUT' });
56
+ if (!res.ok)
57
+ throw new Error(`HTTP ${res.status}`);
58
+ target = (await res.json());
59
+ }
60
+ catch (err) {
61
+ throw new Error(`overflow: could not reach Chrome at ${endpoint} (${err instanceof Error ? err.message : err}).\n` +
62
+ 'Start one with:\n' +
63
+ ' chrome --headless=new --remote-debugging-port=9222 --user-data-dir=/tmp/overflow-profile about:blank');
64
+ }
65
+ const ws = new WebSocket(target.webSocketDebuggerUrl);
66
+ await new Promise((resolve, reject) => {
67
+ ws.addEventListener('open', () => resolve(), { once: true });
68
+ ws.addEventListener('error', () => reject(new Error('overflow: CDP socket failed to open')), {
69
+ once: true,
70
+ });
71
+ });
72
+ const session = new CdpSession(ws, target.id, endpoint, timeoutMs);
73
+ await session.send('Page.enable');
74
+ await session.send('Runtime.enable');
75
+ return session;
76
+ }
77
+ send(method, params = {}) {
78
+ const id = ++this.#id;
79
+ return new Promise((resolve, reject) => {
80
+ const timer = setTimeout(() => {
81
+ this.#pending.delete(id);
82
+ reject(new Error(`overflow: CDP ${method} timed out after ${this.#timeoutMs}ms`));
83
+ }, this.#timeoutMs);
84
+ this.#pending.set(id, {
85
+ resolve: (v) => {
86
+ clearTimeout(timer);
87
+ resolve(v);
88
+ },
89
+ reject: (e) => {
90
+ clearTimeout(timer);
91
+ reject(e);
92
+ },
93
+ });
94
+ this.#ws.send(JSON.stringify({ id, method, params }));
95
+ });
96
+ }
97
+ /**
98
+ * Set the viewport the page actually sees.
99
+ *
100
+ * Device metrics, never a window size. See the note at the top of this file.
101
+ */
102
+ async setViewport(width, height = 900, deviceScaleFactor = 1) {
103
+ await this.send('Emulation.setDeviceMetricsOverride', {
104
+ width,
105
+ height,
106
+ deviceScaleFactor,
107
+ mobile: width < 768,
108
+ });
109
+ }
110
+ async goto(url, settleMs = 500) {
111
+ await this.send('Page.navigate', { url });
112
+ // Loading is not the same as laid out. Fonts and late CSS move things.
113
+ await new Promise((r) => setTimeout(r, settleMs));
114
+ }
115
+ async evaluate(expression) {
116
+ const res = (await this.send('Runtime.evaluate', {
117
+ expression,
118
+ returnByValue: true,
119
+ awaitPromise: true,
120
+ }));
121
+ if (res.exceptionDetails) {
122
+ throw new Error(`overflow: page threw: ${res.exceptionDetails.text ?? 'unknown'}`);
123
+ }
124
+ return res.result?.value;
125
+ }
126
+ async close() {
127
+ try {
128
+ await fetch(`${this.#endpoint}/json/close/${this.#targetId}`);
129
+ }
130
+ catch {
131
+ // The tab is going away either way.
132
+ }
133
+ this.#ws.close();
134
+ }
135
+ }
136
+ //# sourceMappingURL=cdp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdp.js","sourceRoot":"","sources":["../../src/cdp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * overflow https://example.com
4
+ * overflow https://example.com --widths 320,390,768 --json
5
+ *
6
+ * Exits non-zero when the page scrolls sideways at any width. That is what
7
+ * makes it a gate rather than a report nobody reads.
8
+ */
9
+ import { sweep, DEFAULT_WIDTHS } from './sweep.js';
10
+ import { explain } from './analyse.js';
11
+ const USAGE = [
12
+ 'overflow - find horizontal overflow at every breakpoint, and name the element',
13
+ '',
14
+ ' overflow <url> [options]',
15
+ '',
16
+ 'Options',
17
+ ` --widths a,b,c viewport widths (default ${DEFAULT_WIDTHS.join(',')})`,
18
+ ' --height <n> viewport height (default 900)',
19
+ ' --settle-ms <n> wait after navigation for layout (default 600)',
20
+ ' --tolerance <n> px of overflow to ignore (default 1)',
21
+ ' --endpoint <url> running Chrome (default http://127.0.0.1:9222)',
22
+ ' --json print the full result as JSON',
23
+ '',
24
+ 'Needs a Chrome with remote debugging:',
25
+ ' chrome --headless=new --remote-debugging-port=9222 --user-data-dir=/tmp/ovf about:blank',
26
+ '',
27
+ 'Already using Playwright? Use the adapter instead:',
28
+ " import { sweepPage } from '@nayaverse/overflow/playwright'",
29
+ ].join('\n');
30
+ function main() {
31
+ const argv = process.argv.slice(2);
32
+ const flags = {};
33
+ const rest = [];
34
+ for (let i = 0; i < argv.length; i++) {
35
+ const a = argv[i];
36
+ if (a.startsWith('--')) {
37
+ const [k, inline] = a.slice(2).split('=', 2);
38
+ if (inline !== undefined)
39
+ flags[k] = inline;
40
+ else if (argv[i + 1] && !argv[i + 1].startsWith('--'))
41
+ flags[k] = argv[++i];
42
+ else
43
+ flags[k] = true;
44
+ }
45
+ else
46
+ rest.push(a);
47
+ }
48
+ if (flags.help || flags.h || rest.length === 0) {
49
+ console.log(USAGE);
50
+ process.exit(rest.length === 0 ? 1 : 0);
51
+ }
52
+ const url = rest[0];
53
+ const widths = typeof flags.widths === 'string'
54
+ ? flags.widths
55
+ .split(',')
56
+ .map((w) => Number(w.trim()))
57
+ .filter((n) => Number.isFinite(n) && n > 0)
58
+ : [...DEFAULT_WIDTHS];
59
+ if (widths.length === 0) {
60
+ console.error('overflow: --widths had no usable numbers');
61
+ process.exit(1);
62
+ }
63
+ const num = (k, d) => (flags[k] === undefined ? d : Number(flags[k]));
64
+ sweep({
65
+ url,
66
+ widths,
67
+ height: num('height', 900),
68
+ settleMs: num('settle-ms', 600),
69
+ tolerance: num('tolerance', 1),
70
+ ...(typeof flags.endpoint === 'string' ? { cdp: { endpoint: flags.endpoint } } : {}),
71
+ onProgress: flags.json ? () => { } : (line) => console.log(line),
72
+ })
73
+ .then((result) => {
74
+ if (flags.json) {
75
+ console.log(JSON.stringify(result.results, null, 2));
76
+ }
77
+ else if (!result.ok) {
78
+ // The one-line-per-width summary already streamed through onProgress.
79
+ // What follows is the detail for the widths that failed, in full:
80
+ // the selector, how far it reaches, and where it sits in the tree.
81
+ console.log('');
82
+ for (const r of result.results) {
83
+ if (r.overflow > 0)
84
+ for (const line of explain(r))
85
+ console.log(line);
86
+ }
87
+ console.log('');
88
+ console.log(`Horizontal overflow at ${result.failedAt.join(', ')}px.`);
89
+ }
90
+ else {
91
+ console.log('');
92
+ console.log(`No horizontal overflow at any of ${result.results.length} width(s).`);
93
+ }
94
+ process.exit(result.ok ? 0 : 1);
95
+ })
96
+ .catch((err) => {
97
+ console.error(err instanceof Error ? err.message : String(err));
98
+ process.exit(1);
99
+ });
100
+ }
101
+ main();
102
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The part that has to run inside the page.
3
+ *
4
+ * Shipped as a string so it can be handed to anything that evaluates
5
+ * JavaScript in a browser — Playwright's `page.evaluate`, Puppeteer's, a raw
6
+ * CDP `Runtime.evaluate`, or a `<script>` tag in a console. That is what makes
7
+ * this usable without choosing a browser driver for you.
8
+ *
9
+ * It only measures. Every judgement lives in analyse.ts, where it can be
10
+ * tested without a browser.
11
+ */
12
+ /** Matches the Measured type in analyse.ts. Kept in sync by the round-trip
13
+ * test rather than by a shared import, because this code is serialised. */
14
+ export declare const COLLECT_SOURCE: string;
15
+ export type Collected = {
16
+ viewport: number;
17
+ scrollWidth: number;
18
+ measurements: import('./analyse.js').Measured[];
19
+ };
20
+ /** Anything that can run a JavaScript expression in the page and give back
21
+ * the value. `page.evaluate` in Playwright and Puppeteer both satisfy it. */
22
+ export type Evaluate = (expression: string) => Promise<unknown>;
23
+ export declare function collect(evaluate: Evaluate): Promise<Collected>;
24
+ //# sourceMappingURL=collect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect.d.ts","sourceRoot":"","sources":["../../src/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;4EAC4E;AAC5E,eAAO,MAAM,cAAc,QAwE1B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,cAAc,EAAE,QAAQ,EAAE,CAAC;CACjD,CAAC;AAEF;8EAC8E;AAC9E,MAAM,MAAM,QAAQ,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhE,wBAAsB,OAAO,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,CAAC,CASpE"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The part that has to run inside the page.
3
+ *
4
+ * Shipped as a string so it can be handed to anything that evaluates
5
+ * JavaScript in a browser — Playwright's `page.evaluate`, Puppeteer's, a raw
6
+ * CDP `Runtime.evaluate`, or a `<script>` tag in a console. That is what makes
7
+ * this usable without choosing a browser driver for you.
8
+ *
9
+ * It only measures. Every judgement lives in analyse.ts, where it can be
10
+ * tested without a browser.
11
+ */
12
+ /** Matches the Measured type in analyse.ts. Kept in sync by the round-trip
13
+ * test rather than by a shared import, because this code is serialised. */
14
+ export const COLLECT_SOURCE = String.raw `
15
+ (function () {
16
+ var doc = document.documentElement;
17
+ var viewport = doc.clientWidth;
18
+ var scrollWidth = doc.scrollWidth;
19
+
20
+ function label(el) {
21
+ var name = el.tagName.toLowerCase();
22
+ if (el.id) return name + '#' + el.id;
23
+ var cls = (el.getAttribute('class') || '').trim().split(/\s+/).filter(Boolean);
24
+ if (cls.length) {
25
+ // Three classes is enough to find it; a utility-class soup is not.
26
+ return name + '.' + cls.slice(0, 3).join('.') + (cls.length > 3 ? '…' : '');
27
+ }
28
+ return name;
29
+ }
30
+
31
+ function pathOf(el) {
32
+ var parts = [];
33
+ for (var n = el; n && n.nodeType === 1 && n !== doc.parentNode; n = n.parentElement) {
34
+ parts.unshift(label(n));
35
+ if (parts.length > 6) break;
36
+ }
37
+ return parts.join(' > ');
38
+ }
39
+
40
+ // An ancestor that scrolls or clips horizontally, and whose own right edge
41
+ // is inside the viewport, is holding this element rather than being pushed
42
+ // by it.
43
+ function clippedBy(el) {
44
+ for (var n = el.parentElement; n && n !== doc; n = n.parentElement) {
45
+ var s = getComputedStyle(n);
46
+ var ox = s.overflowX;
47
+ if (ox === 'auto' || ox === 'scroll' || ox === 'hidden' || ox === 'clip') {
48
+ if (n.getBoundingClientRect().right <= viewport + 1) return true;
49
+ }
50
+ }
51
+ return false;
52
+ }
53
+
54
+ var out = [];
55
+ var all = document.body ? document.body.querySelectorAll('*') : [];
56
+ for (var i = 0; i < all.length; i++) {
57
+ var el = all[i];
58
+ var r = el.getBoundingClientRect();
59
+ if (r.width <= 0 || r.height <= 0) continue;
60
+ if (r.right <= viewport + 1) continue;
61
+
62
+ var style = getComputedStyle(el);
63
+ var position = style.position;
64
+
65
+ var depth = 0;
66
+ for (var p = el; p; p = p.parentElement) depth++;
67
+
68
+ out.push({
69
+ selector: label(el),
70
+ path: pathOf(el),
71
+ left: r.left,
72
+ right: r.right,
73
+ width: r.width,
74
+ clipped: clippedBy(el),
75
+ fixed: position === 'fixed' || position === 'sticky',
76
+ depth: depth
77
+ });
78
+
79
+ // A page with thousands of overflowing elements has one cause and a lot
80
+ // of children; the first few hundred are more than enough to find it.
81
+ if (out.length >= 400) break;
82
+ }
83
+
84
+ return { viewport: viewport, scrollWidth: scrollWidth, measurements: out };
85
+ })()
86
+ `;
87
+ export async function collect(evaluate) {
88
+ const raw = (await evaluate(COLLECT_SOURCE));
89
+ if (!raw || typeof raw.viewport !== 'number') {
90
+ throw new Error('overflow: the page returned no measurements. The evaluate function must ' +
91
+ 'return the expression value, not a handle to it.');
92
+ }
93
+ return raw;
94
+ }
95
+ //# sourceMappingURL=collect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect.js","sourceRoot":"","sources":["../../src/collect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;4EAC4E;AAC5E,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwEvC,CAAC;AAYF,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,QAAkB;IAC9C,MAAM,GAAG,GAAG,CAAC,MAAM,QAAQ,CAAC,cAAc,CAAC,CAA0B,CAAC;IACtE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACb,0EAA0E;YACxE,kDAAkD,CACrD,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { analyse, explain } from './analyse.js';
2
+ export type { Measured, Cause, Analysis, AnalyseOptions } from './analyse.js';
3
+ export { collect, COLLECT_SOURCE } from './collect.js';
4
+ export type { Collected, Evaluate } from './collect.js';
5
+ export { checkViewport, sweep, sweepWith, DEFAULT_WIDTHS } from './sweep.js';
6
+ export type { SweepOptions, SweepResult } from './sweep.js';
7
+ export { CdpSession } from './cdp.js';
8
+ export type { CdpOptions } from './cdp.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { analyse, explain } from './analyse.js';
2
+ export { collect, COLLECT_SOURCE } from './collect.js';
3
+ export { checkViewport, sweep, sweepWith, DEFAULT_WIDTHS } from './sweep.js';
4
+ export { CdpSession } from './cdp.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEhD,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE7E,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { type Analysis, type AnalyseOptions } from './analyse.js';
2
+ /** The shape this needs from a Playwright Page. */
3
+ export type PlaywrightPage = {
4
+ setViewportSize(size: {
5
+ width: number;
6
+ height: number;
7
+ }): Promise<void>;
8
+ evaluate<R>(fn: string): Promise<R>;
9
+ viewportSize(): {
10
+ width: number;
11
+ height: number;
12
+ } | null;
13
+ };
14
+ /** Check the page as it is, at its current viewport. */
15
+ export declare function checkPage(page: PlaywrightPage, options?: AnalyseOptions): Promise<Analysis>;
16
+ export type PlaywrightSweepOptions = AnalyseOptions & {
17
+ widths?: readonly number[];
18
+ height?: number;
19
+ onProgress?: (line: string) => void;
20
+ };
21
+ export type PlaywrightSweepResult = {
22
+ ok: boolean;
23
+ results: Analysis[];
24
+ failedAt: number[];
25
+ report(): string;
26
+ };
27
+ /**
28
+ * Check every width, then put the viewport back where it was.
29
+ *
30
+ * Restoring matters: a check that silently leaves the page at 320px makes the
31
+ * next assertion in the same test fail for a reason that is not its own.
32
+ */
33
+ export declare function sweepPage(page: PlaywrightPage, options?: PlaywrightSweepOptions): Promise<PlaywrightSweepResult>;
34
+ //# sourceMappingURL=playwright.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright.d.ts","sourceRoot":"","sources":["../../src/playwright.ts"],"names":[],"mappings":"AAUA,OAAO,EAAW,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAE3E,mDAAmD;AACnD,MAAM,MAAM,cAAc,GAAG;IAC3B,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,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACpC,YAAY,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1D,CAAC;AAEF,wDAAwD;AACxD,wBAAsB,SAAS,CAC7B,IAAI,EAAE,cAAc,EACpB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAEnB;AAED,MAAM,MAAM,sBAAsB,GAAG,cAAc,GAAG;IACpD,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,IAAI,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE,cAAc,EACpB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC,CA8BhC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Playwright adapter.
3
+ *
4
+ * No dependency on Playwright: the page is accepted structurally, so this
5
+ * compiles and ships whether or not Playwright is installed. If you already
6
+ * use Playwright, prefer this over the bundled CDP driver — Playwright
7
+ * already sets the viewport with device metrics, which is the part that
8
+ * matters.
9
+ */
10
+ import { checkViewport, DEFAULT_WIDTHS } from './sweep.js';
11
+ import { explain } from './analyse.js';
12
+ /** Check the page as it is, at its current viewport. */
13
+ export async function checkPage(page, options = {}) {
14
+ return checkViewport((expr) => page.evaluate(expr), options);
15
+ }
16
+ /**
17
+ * Check every width, then put the viewport back where it was.
18
+ *
19
+ * Restoring matters: a check that silently leaves the page at 320px makes the
20
+ * next assertion in the same test fail for a reason that is not its own.
21
+ */
22
+ export async function sweepPage(page, options = {}) {
23
+ const { widths = DEFAULT_WIDTHS, height = 900, onProgress = () => { }, ...rest } = options;
24
+ const original = page.viewportSize();
25
+ const results = [];
26
+ try {
27
+ for (const width of widths) {
28
+ await page.setViewportSize({ width, height });
29
+ const analysis = await checkPage(page, rest);
30
+ results.push(analysis);
31
+ onProgress(analysis.overflow > 0
32
+ ? `${String(width).padStart(5)}px OVERFLOW ${analysis.overflow}px`
33
+ : `${String(width).padStart(5)}px ok`);
34
+ }
35
+ }
36
+ finally {
37
+ if (original)
38
+ await page.setViewportSize(original);
39
+ }
40
+ const failedAt = results.filter((r) => r.overflow > 0).map((r) => r.viewport);
41
+ return {
42
+ ok: failedAt.length === 0,
43
+ results,
44
+ failedAt,
45
+ report: () => results
46
+ .flatMap((r) => (r.overflow > 0 ? explain(r) : [`${String(r.viewport).padStart(5)}px ok`]))
47
+ .join('\n'),
48
+ };
49
+ }
50
+ //# sourceMappingURL=playwright.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright.js","sourceRoot":"","sources":["../../src/playwright.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAsC,MAAM,cAAc,CAAC;AAS3E,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAAoB,EACpB,UAA0B,EAAE;IAE5B,OAAO,aAAa,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;AAC/D,CAAC;AAeD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAAoB,EACpB,UAAkC,EAAE;IAEpC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,MAAM,GAAG,GAAG,EAAE,UAAU,GAAG,GAAG,EAAE,GAAE,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAC1F,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;IACrC,MAAM,OAAO,GAAe,EAAE,CAAC;IAE/B,IAAI,CAAC;QACH,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YAC9C,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC7C,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACvB,UAAU,CACR,QAAQ,CAAC,QAAQ,GAAG,CAAC;gBACnB,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,QAAQ,CAAC,QAAQ,IAAI;gBACnE,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CACzC,CAAC;QACJ,CAAC;IACH,CAAC;YAAS,CAAC;QACT,IAAI,QAAQ;YAAE,MAAM,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9E,OAAO;QACL,EAAE,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC;QACzB,OAAO;QACP,QAAQ;QACR,MAAM,EAAE,GAAG,EAAE,CACX,OAAO;aACJ,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;aAC3F,IAAI,CAAC,IAAI,CAAC;KAChB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Walk a set of viewport widths and report, for each, whether the page
3
+ * scrolls sideways and which element is responsible.
4
+ */
5
+ import { type Analysis, type AnalyseOptions } from './analyse.js';
6
+ import { type Evaluate } from './collect.js';
7
+ import { type CdpOptions } from './cdp.js';
8
+ /** The widths worth checking by default: the narrowest phone still in use,
9
+ * the common Android and iPhone sizes, the large phone, the tablet, the
10
+ * small laptop, and a desktop. */
11
+ export declare const DEFAULT_WIDTHS: readonly [320, 360, 390, 414, 768, 1024, 1440];
12
+ export type SweepResult = {
13
+ ok: boolean;
14
+ url?: string;
15
+ /** One per width, in the order checked. */
16
+ results: Analysis[];
17
+ /** Widths at which the page overflowed. */
18
+ failedAt: number[];
19
+ report(): string;
20
+ };
21
+ /**
22
+ * Check one already-loaded page at its current viewport.
23
+ *
24
+ * Use this when you own the browser: Playwright, Puppeteer, or your own CDP
25
+ * session. `evaluate` is anything that runs an expression in the page and
26
+ * returns its value.
27
+ */
28
+ export declare function checkViewport(evaluate: Evaluate, options?: AnalyseOptions): Promise<Analysis>;
29
+ export type SweepOptions = AnalyseOptions & {
30
+ url: string;
31
+ widths?: readonly number[];
32
+ /** Height for each check. Only matters for lazy content. */
33
+ height?: number;
34
+ /** Milliseconds to wait after navigation for layout to settle. */
35
+ settleMs?: number;
36
+ cdp?: CdpOptions;
37
+ onProgress?: (line: string) => void;
38
+ };
39
+ /**
40
+ * Open a page at each width and check it, using the bundled CDP driver.
41
+ *
42
+ * The viewport is set with device metrics rather than a window size, which is
43
+ * the only way to get an honest narrow viewport. See cdp.ts.
44
+ */
45
+ export declare function sweep(options: SweepOptions): Promise<SweepResult>;
46
+ /**
47
+ * Sweep using a browser you already drive.
48
+ *
49
+ * `setViewport` and `evaluate` are yours, so this works with Playwright,
50
+ * Puppeteer, or anything else, without this package depending on any of them.
51
+ */
52
+ export declare function sweepWith(page: {
53
+ setViewport(width: number, height: number): Promise<void>;
54
+ evaluate: Evaluate;
55
+ }, options?: Omit<SweepOptions, 'url' | 'cdp'> & {
56
+ url?: string;
57
+ }): Promise<SweepResult>;
58
+ //# sourceMappingURL=sweep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweep.d.ts","sourceRoot":"","sources":["../../src/sweep.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAoB,KAAK,QAAQ,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AACpF,OAAO,EAAW,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAEvD;;mCAEmC;AACnC,eAAO,MAAM,cAAc,gDAAiD,CAAC;AAE7E,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,IAAI,MAAM,CAAC;CAClB,CAAC;AA6BF;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAGnB;AAED,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAC7B,IAAI,EAAE;IAAE,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,EACvF,OAAO,GAAE,IAAI,CAAC,YAAY,EAAE,KAAK,GAAG,KAAK,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GACjE,OAAO,CAAC,WAAW,CAAC,CAuBtB"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Walk a set of viewport widths and report, for each, whether the page
3
+ * scrolls sideways and which element is responsible.
4
+ */
5
+ import { analyse, explain } from './analyse.js';
6
+ import { collect } from './collect.js';
7
+ import { CdpSession } from './cdp.js';
8
+ /** The widths worth checking by default: the narrowest phone still in use,
9
+ * the common Android and iPhone sizes, the large phone, the tablet, the
10
+ * small laptop, and a desktop. */
11
+ export const DEFAULT_WIDTHS = [320, 360, 390, 414, 768, 1024, 1440];
12
+ function makeResult(results, url) {
13
+ const failedAt = results.filter((r) => r.overflow > 0).map((r) => r.viewport);
14
+ return {
15
+ ok: failedAt.length === 0,
16
+ ...(url ? { url } : {}),
17
+ results,
18
+ failedAt,
19
+ report() {
20
+ const lines = [];
21
+ for (const r of results) {
22
+ if (r.overflow > 0) {
23
+ lines.push(...explain(r));
24
+ }
25
+ else {
26
+ lines.push(`${String(r.viewport).padStart(5)}px ok`);
27
+ }
28
+ }
29
+ lines.push('');
30
+ lines.push(failedAt.length === 0
31
+ ? `No horizontal overflow at any of ${results.length} width(s).`
32
+ : `Horizontal overflow at ${failedAt.join(', ')}px.`);
33
+ return lines.join('\n');
34
+ },
35
+ };
36
+ }
37
+ /**
38
+ * Check one already-loaded page at its current viewport.
39
+ *
40
+ * Use this when you own the browser: Playwright, Puppeteer, or your own CDP
41
+ * session. `evaluate` is anything that runs an expression in the page and
42
+ * returns its value.
43
+ */
44
+ export async function checkViewport(evaluate, options = {}) {
45
+ const { viewport, scrollWidth, measurements } = await collect(evaluate);
46
+ return analyse(measurements, viewport, scrollWidth, options);
47
+ }
48
+ /**
49
+ * Open a page at each width and check it, using the bundled CDP driver.
50
+ *
51
+ * The viewport is set with device metrics rather than a window size, which is
52
+ * the only way to get an honest narrow viewport. See cdp.ts.
53
+ */
54
+ export async function sweep(options) {
55
+ const { url, widths = DEFAULT_WIDTHS, height = 900, settleMs = 600, cdp, onProgress = () => { }, ...analyseOptions } = options;
56
+ const session = await CdpSession.open(cdp ?? {});
57
+ const results = [];
58
+ try {
59
+ for (const width of widths) {
60
+ await session.setViewport(width, height);
61
+ await session.goto(url, settleMs);
62
+ const analysis = await checkViewport((e) => session.evaluate(e), analyseOptions);
63
+ results.push(analysis);
64
+ onProgress(analysis.overflow > 0
65
+ ? `${String(width).padStart(5)}px OVERFLOW ${analysis.overflow}px — ${analysis.causes[0]?.selector ?? 'no element could be blamed'}`
66
+ : `${String(width).padStart(5)}px ok`);
67
+ }
68
+ }
69
+ finally {
70
+ await session.close();
71
+ }
72
+ return makeResult(results, url);
73
+ }
74
+ /**
75
+ * Sweep using a browser you already drive.
76
+ *
77
+ * `setViewport` and `evaluate` are yours, so this works with Playwright,
78
+ * Puppeteer, or anything else, without this package depending on any of them.
79
+ */
80
+ export async function sweepWith(page, options = {}) {
81
+ const { widths = DEFAULT_WIDTHS, height = 900, onProgress = () => { }, url, settleMs, ...analyseOptions } = options;
82
+ void settleMs;
83
+ const results = [];
84
+ for (const width of widths) {
85
+ await page.setViewport(width, height);
86
+ const analysis = await checkViewport(page.evaluate, analyseOptions);
87
+ results.push(analysis);
88
+ onProgress(analysis.overflow > 0
89
+ ? `${String(width).padStart(5)}px OVERFLOW ${analysis.overflow}px`
90
+ : `${String(width).padStart(5)}px ok`);
91
+ }
92
+ return makeResult(results, url);
93
+ }
94
+ //# sourceMappingURL=sweep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sweep.js","sourceRoot":"","sources":["../../src/sweep.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@nayaverse/overflow",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Find horizontal overflow at every breakpoint in CI, and name the element causing it.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "main": "./dist/src/index.js",
11
+ "types": "./dist/src/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/src/index.d.ts",
15
+ "default": "./dist/src/index.js"
16
+ },
17
+ "./playwright": {
18
+ "types": "./dist/src/playwright.d.ts",
19
+ "default": "./dist/src/playwright.js"
20
+ }
21
+ },
22
+ "bin": {
23
+ "overflow": "dist/src/cli.js"
24
+ },
25
+ "files": [
26
+ "dist/src",
27
+ "README.md"
28
+ ],
29
+ "keywords": [
30
+ "responsive",
31
+ "overflow",
32
+ "horizontal-scroll",
33
+ "ci",
34
+ "breakpoints",
35
+ "css",
36
+ "testing",
37
+ "playwright",
38
+ "cdp"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "node --test dist/test/*.test.js",
44
+ "pretest": "npm run build",
45
+ "prepublishOnly": "npm test"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.10.2"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/suhelali14/keel.git",
56
+ "directory": "packages/overflow"
57
+ },
58
+ "homepage": "https://github.com/suhelali14/keel/tree/main/packages/overflow#readme",
59
+ "bugs": {
60
+ "url": "https://github.com/suhelali14/keel/issues"
61
+ },
62
+ "author": "Suhelali Pakjade",
63
+ "sideEffects": false
64
+ }