@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 +184 -2
- package/dist/src/analyse.d.ts +63 -0
- package/dist/src/analyse.d.ts.map +1 -0
- package/dist/src/analyse.js +118 -0
- package/dist/src/analyse.js.map +1 -0
- package/dist/src/cdp.d.ts +40 -0
- package/dist/src/cdp.d.ts.map +1 -0
- package/dist/src/cdp.js +136 -0
- package/dist/src/cdp.js.map +1 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +102 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/collect.d.ts +24 -0
- package/dist/src/collect.d.ts.map +1 -0
- package/dist/src/collect.js +95 -0
- package/dist/src/collect.js.map +1 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +5 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/playwright.d.ts +34 -0
- package/dist/src/playwright.d.ts.map +1 -0
- package/dist/src/playwright.js +50 -0
- package/dist/src/playwright.js.map +1 -0
- package/dist/src/sweep.d.ts +58 -0
- package/dist/src/sweep.d.ts.map +1 -0
- package/dist/src/sweep.js +94 -0
- package/dist/src/sweep.js.map +1 -0
- package/package.json +62 -4
package/README.md
CHANGED
|
@@ -1,3 +1,185 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nayaverse/overflow
|
|
2
2
|
|
|
3
|
-
|
|
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"}
|
package/dist/src/cdp.js
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/src/cli.js
ADDED
|
@@ -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 @@
|
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|