rpc-agent 0.4.0 → 0.6.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/CHANGELOG.md +100 -0
- package/README.md +449 -5
- package/dist/args.js +197 -31
- package/dist/browser.js +57 -3
- package/dist/capture.js +45 -42
- package/dist/check-format.js +120 -0
- package/dist/check-page.js +351 -0
- package/dist/check.js +685 -0
- package/dist/cli.js +88 -1
- package/dist/errors.js +8 -0
- package/dist/gif.js +212 -0
- package/dist/help.js +56 -2
- package/dist/mcp.js +218 -16
- package/dist/motion.js +487 -0
- package/dist/options.js +33 -0
- package/dist/png.js +47 -0
- package/dist/record.js +333 -0
- package/dist/ui.js +199 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
Motion: captures that match what a person scrolling sees, a Motion check,
|
|
6
|
+
and `rpc record`.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `rpc record <url>` writes a few seconds of video (VP9 webm) of the page
|
|
11
|
+
or of one element (`--selector`, scrolled into view and cropped to its
|
|
12
|
+
box), plus a poster PNG of the first frame. `--duration` (default 5, up
|
|
13
|
+
to 30 s), `-d` for one device (default `desktop-1440`), `--gif`, `--mp4`
|
|
14
|
+
(needs an ffmpeg with H.264 on PATH; skipped with a warning otherwise),
|
|
15
|
+
and the same auth flags as `rpc shot`. Frames come from a screenshot loop
|
|
16
|
+
at device pixels, encoded at a constant 25 fps by Playwright's own ffmpeg
|
|
17
|
+
build (already downloaded by `rpc install`), with a system ffmpeg as the
|
|
18
|
+
fallback. GIFs are encoded in Node with no new dependency.
|
|
19
|
+
- MCP `record` tool with the same options; it returns the paths, a summary
|
|
20
|
+
and the poster inline.
|
|
21
|
+
- `rpc shot --finish-animations` (MCP `finishAnimations`) jumps GSAP tweens
|
|
22
|
+
and non-scrubbed ScrollTriggers to their end when `gsap` / `ScrollTrigger`
|
|
23
|
+
are on `window`, and finite Web Animations, CSS animations and transitions
|
|
24
|
+
too. Counts are in `motion.finished`.
|
|
25
|
+
- `rpc shot --no-unpin` (MCP `unpin: false`) keeps pinned sections as they
|
|
26
|
+
are in full-page shots.
|
|
27
|
+
- Each shot has `warnings` when motion made the capture differ from the
|
|
28
|
+
page in use: elements still animating, a pinned section left in place,
|
|
29
|
+
content still hidden by a reveal, and carousels ("use `rpc record` for
|
|
30
|
+
motion"). The terminal prints them under the device and the MCP headline
|
|
31
|
+
lists them with a fix.
|
|
32
|
+
- `rpc check` has a Motion group: `motion-running` and
|
|
33
|
+
`motion-reveal-hidden` (warn), `motion-loop`, `motion-pinned` and
|
|
34
|
+
`motion-carousel` (info). Never an error.
|
|
35
|
+
- `rpc doctor` reports the ffmpeg `rpc record` would use and whether mp4 is
|
|
36
|
+
available. Its exit code still depends on Chromium only.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- `rpc shot` waits for the visible page to settle before capturing, and at
|
|
41
|
+
each warm-up scroll step: no style or class change on a visible element
|
|
42
|
+
for a short quiet period (and at the warm-up steps no finite animation
|
|
43
|
+
running on one; the screenshot ends CSS and Web Animations itself),
|
|
44
|
+
capped (2 s before capture, 800 ms per step, 8 s for the warm-up).
|
|
45
|
+
The result is in `motion.settle`.
|
|
46
|
+
- Full-page shots collapse pinned sections (GSAP-style `.pin-spacer`
|
|
47
|
+
wrappers, pins `ScrollTrigger.getAll()` reports, and sticky or fixed
|
|
48
|
+
sections that are the only child of a much taller parent), so each
|
|
49
|
+
section shows once instead of over a blank spacer. Each one is listed in
|
|
50
|
+
`motion.unpinned` with the CSS px removed.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- `rpc check`: the tap-target check skips inline links in a sentence (WCAG
|
|
55
|
+
2.5.8 inline exception). A link is skipped when its computed `display` is
|
|
56
|
+
`inline` and its block of text has letters or digits outside any
|
|
57
|
+
control. Buttons, `[role=button]`, links laid out as boxes, nav links and
|
|
58
|
+
icon links still count, and the threshold stays 44x44.
|
|
59
|
+
- Test servers answer 500 with the error when a fixture file is missing
|
|
60
|
+
instead of hanging, and the check suite stops early with "run pnpm
|
|
61
|
+
install" when `@fontsource/ibm-plex-mono` isn't installed.
|
|
62
|
+
|
|
63
|
+
## 0.5.0
|
|
64
|
+
|
|
65
|
+
Ship-check: `rpc check <url>` audits a page before launch, from the CLI, in
|
|
66
|
+
CI or through MCP.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- `rpc check <url>` runs on a phone and a desktop width by default (`-d`
|
|
71
|
+
to change) and reports findings with an id, severity (`error`, `warn`,
|
|
72
|
+
`info`), message, selector or URL, devices and a fix. Checks:
|
|
73
|
+
- Basics: HTTP status, mixed content, uncaught exceptions, console errors,
|
|
74
|
+
`theme-color`.
|
|
75
|
+
- Meta and social: title, description, canonical, `lang`, viewport (and
|
|
76
|
+
zoom blocking), favicon, apple-touch-icon, `og:title`,
|
|
77
|
+
`og:description`, `og:image`, `og:url`, `twitter:card`. `og:image` is
|
|
78
|
+
fetched for its status, content type and pixel size.
|
|
79
|
+
- Layout shift: CLS (largest session window) after load and one scroll,
|
|
80
|
+
with the elements that moved. Warn above 0.1, error above 0.25.
|
|
81
|
+
- Unlabelled controls: buttons, links and `[role=button]` with no
|
|
82
|
+
accessible name, images without `alt`, form fields without a label.
|
|
83
|
+
- Fonts: missing or `block` `font-display`, above-the-fold fonts that
|
|
84
|
+
aren't preloaded, preloads without `crossorigin`, unused preloads,
|
|
85
|
+
font files that fail.
|
|
86
|
+
- Mobile viewport (phones): `100vh`, sideways scroll with the elements
|
|
87
|
+
that cause it, tap targets under 44x44, text fields under 16px.
|
|
88
|
+
- Exit code 1 on errors; `--fail-on warn` fails on warnings too.
|
|
89
|
+
`--json` prints a versioned report (`schema: 1`, documented in the
|
|
90
|
+
README). `--shots` saves PNGs of the elements behind layout-shift and
|
|
91
|
+
overflow findings.
|
|
92
|
+
- Auth works as for `rpc shot`: `--cookie`, `--header`,
|
|
93
|
+
`--storage-state`, `--allow-origin`, saved `rpc login` sessions and
|
|
94
|
+
`AUTH_REQUIRED` on a redirect to sign-in.
|
|
95
|
+
- MCP `check` tool with the same options and the `screenshot` tool's auth
|
|
96
|
+
params. It returns a short text summary and the JSON report.
|
|
97
|
+
|
|
98
|
+
### Unchanged
|
|
99
|
+
|
|
100
|
+
- `rpc shot`, its output and the MCP `screenshot` tool. The MCP tool list
|
|
101
|
+
gains `check`.
|
|
102
|
+
|
|
3
103
|
## 0.4.0
|
|
4
104
|
|
|
5
105
|
Authenticated captures: screenshot logged-in pages without a hand-written
|
package/README.md
CHANGED
|
@@ -63,6 +63,8 @@ these docs say `rpc`, or `pnpm rpc` from inside the repo.
|
|
|
63
63
|
```sh
|
|
64
64
|
rpc # interactive: pick a URL, devices and options
|
|
65
65
|
rpc shot <url> [options]
|
|
66
|
+
rpc check <url> [options] # pre-launch audit, see Ship-check below
|
|
67
|
+
rpc record <url> [options] # a few seconds of video of the page or one element
|
|
66
68
|
rpc login <url> [--form] # sign in once; later shots of that origin use the session
|
|
67
69
|
rpc sessions [list|clear [origin]]
|
|
68
70
|
rpc devices [--json]
|
|
@@ -93,6 +95,8 @@ the files it wrote:
|
|
|
93
95
|
| `--slice-width <px>` | Maximum slice width. Default 768 |
|
|
94
96
|
| `--max-height <px>` | Full-page cap in CSS px. Default 60000 |
|
|
95
97
|
| `--reduced-motion` | Emulate `prefers-reduced-motion: reduce` |
|
|
98
|
+
| `--finish-animations` | Jump animations to their end state before capturing. See [Animation-aware capture](#animation-aware-capture) |
|
|
99
|
+
| `--no-unpin` | With `--full-page`, keep pinned sections as they are instead of collapsing them |
|
|
96
100
|
| `-o, --out <dir>` | Output directory. Default `./.rpc-shots` |
|
|
97
101
|
| `--wait-for <selector>` | Wait for a visible element before capturing |
|
|
98
102
|
| `--wait <ms>` | Extra settle delay after load |
|
|
@@ -148,6 +152,90 @@ and `100vh` sections keep their real height.
|
|
|
148
152
|
Bezels frame the above-the-fold viewport. With `--bezels --full-page` you get
|
|
149
153
|
both: the full page raw, and the first screen framed.
|
|
150
154
|
|
|
155
|
+
### Animation-aware capture
|
|
156
|
+
|
|
157
|
+
A stitched shot of a page with scroll animations used to come out wrong in
|
|
158
|
+
three ways: pinned scroll sections left tall blank bands, text was caught
|
|
159
|
+
halfway through its reveal, and carousels froze on whatever slide was up.
|
|
160
|
+
|
|
161
|
+
#### Settle
|
|
162
|
+
|
|
163
|
+
Before each capture, and at every step of the full-page warm-up
|
|
164
|
+
scroll, rpc waits until the visible part of the page stops moving: no
|
|
165
|
+
`style` or `class` change on a visible element for 150 ms (120 ms per
|
|
166
|
+
warm-up step), and, at the warm-up steps, no finite animation running on
|
|
167
|
+
one. The screenshot itself ends finite CSS and Web Animations (Playwright's
|
|
168
|
+
`animations: 'disabled'`), so the wait just before it only watches for
|
|
169
|
+
JS-driven changes, the kind a GSAP tween makes. It gives up after 2 s
|
|
170
|
+
(800 ms per step, 8 s for the whole warm-up), so a page that never stops
|
|
171
|
+
can't hang it. Anything still changing or animating when a step runs out
|
|
172
|
+
of time is treated as a loop (a JS marquee, a ticker, a long CSS
|
|
173
|
+
animation), and later steps stop waiting for it. A static page only pays the quiet period. The result is on each shot as
|
|
174
|
+
`motion.settle`: `settled`, `ms`, and the selectors still active.
|
|
175
|
+
|
|
176
|
+
#### Pinned sections
|
|
177
|
+
|
|
178
|
+
GSAP ScrollTrigger pins a section by wrapping it in a
|
|
179
|
+
`.pin-spacer` with padding below it, then fixing the section in place while
|
|
180
|
+
you scroll through that padding. A full-page capture renders the page at
|
|
181
|
+
scroll 0, where the section sits at the top of its spacer and the padding
|
|
182
|
+
is empty, so the shot gets a blank band as tall as the pin's scroll
|
|
183
|
+
distance. With `--full-page`, rpc collapses each pin it finds. The spacer
|
|
184
|
+
loses its padding, the section goes back into normal flow, and the shot
|
|
185
|
+
shows it once with the rest of the page right after it. A sticky section
|
|
186
|
+
that is the only child of a much taller parent gets the same treatment. The
|
|
187
|
+
rules go in an injected stylesheet as `!important`, so when the library
|
|
188
|
+
rewrites its inline styles during the capture, they still win. Each
|
|
189
|
+
collapsed pin is listed in `motion.unpinned` with the CSS px removed, and
|
|
190
|
+
the stitched height is measured after the collapse. `--no-unpin` turns it
|
|
191
|
+
off.
|
|
192
|
+
|
|
193
|
+
| `--no-unpin` (before) | default (after) |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| <img alt="A full-page shot of a fixture with a pinned orange section: a 2100px white band sits between the section and the green outro" src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/motion/pinned-before.png" width="240"> | <img alt="The same page with the pin collapsed: blue intro, orange section, green outro, no gap" src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/motion/pinned-after.png" width="240"> |
|
|
196
|
+
|
|
197
|
+
#### Finish animations
|
|
198
|
+
|
|
199
|
+
`--finish-animations` is for JS-driven motion, which the screenshot can't
|
|
200
|
+
end on its own. It calls `.finish()` on every finite Web Animation (CSS
|
|
201
|
+
animations and transitions too, so layout measured before the capture is
|
|
202
|
+
the end state). If `window.gsap` exists,
|
|
203
|
+
tweens and timelines on its global timeline that don't repeat forever go to
|
|
204
|
+
`progress(1)`. If `window.ScrollTrigger` exists, triggers that aren't
|
|
205
|
+
scrubbed or pinned get their animation set to `progress(1)`. Scrubbed and
|
|
206
|
+
pinned ones follow the scroll position, so they keep their scroll-0 state.
|
|
207
|
+
Each library step runs in its own try/catch and does nothing when the
|
|
208
|
+
library isn't there. Infinite loops are left alone. The counts are in
|
|
209
|
+
`motion.finished`.
|
|
210
|
+
|
|
211
|
+
#### Reduced motion
|
|
212
|
+
|
|
213
|
+
`--reduced-motion` still emulates
|
|
214
|
+
`prefers-reduced-motion: reduce`. On a site that respects it, that's the
|
|
215
|
+
cleanest fix, because the animations never run.
|
|
216
|
+
|
|
217
|
+
#### Warnings
|
|
218
|
+
|
|
219
|
+
Each shot gets a `warnings` list in the JSON, and the terminal
|
|
220
|
+
prints them under the device:
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
ok desktop-1440 (1440x900 @1x)
|
|
224
|
+
.rpc-shots/localhost-3000-desktop-1440.png 1440x900
|
|
225
|
+
warn: 3 elements were still animating when captured: div.reveal:nth-of-type(1), div.reveal:nth-of-type(2), div.reveal:nth-of-type(3)
|
|
226
|
+
warn: carousel: use `rpc record` for motion: #carousel
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
| id | When |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `motion-running` | Elements were still changing (style or class writes, as a JS tween makes) when the settle ran out of time: "N elements were still animating when captured". CSS and Web Animations don't count here, because the screenshot ends them |
|
|
232
|
+
| `motion-pinned` | Full page only. A pin was left in place (`--no-unpin`, or one that couldn't be collapsed): "pinned section: the stitched shot may show blank space" |
|
|
233
|
+
| `motion-reveal-hidden` | Content still at opacity 0, or offset by 24px or more, that looks like a reveal (see [Motion](#motion)) |
|
|
234
|
+
| `motion-carousel` | A carousel or marquee is in the shot: "carousel: use `rpc record` for motion" |
|
|
235
|
+
|
|
236
|
+
Each warning has a `fix` naming the flag to try. Warnings never change the
|
|
237
|
+
exit code.
|
|
238
|
+
|
|
151
239
|
### Terminal, pipes and CI
|
|
152
240
|
|
|
153
241
|
The banner, spinners, prompts and boxes only appear when stdout is an
|
|
@@ -299,9 +387,299 @@ rpc shot localhost:3000/app --storage-state auth.json
|
|
|
299
387
|
- **Use a test account.** Sign in as a user whose data you're happy for
|
|
300
388
|
screenshots (and the agent reading them) to see.
|
|
301
389
|
|
|
390
|
+
## Ship-check
|
|
391
|
+
|
|
392
|
+
`rpc check <url>` audits one page before you call it done. It loads the page
|
|
393
|
+
on a phone and a desktop width, the way `rpc shot` does, and lists what's
|
|
394
|
+
wrong: missing or broken social tags, layout shift, buttons a screen reader
|
|
395
|
+
can't name, fonts that hide text while they load, the mobile viewport bugs
|
|
396
|
+
that only show up on a phone, and motion a screenshot will get wrong.
|
|
397
|
+
|
|
398
|
+
```sh
|
|
399
|
+
rpc check localhost:3000
|
|
400
|
+
rpc check localhost:3000 --fail-on warn --json # CI
|
|
401
|
+
rpc check example.com/pricing -d pixel,iphone-se,desktop --shots
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
<img alt="rpc check in a terminal on turbo-start-paystack.vercel.app: both devices load with HTTP 200 and CLS 0.000, then a Meta and social box (no canonical link, no og:url) and a Mobile viewport box (16 of 54 tap targets under 44x44 px, with selectors and sizes), each finding with a fix, and the line 0 errors, 3 warnings, 0 info" src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/terminal-check.png" width="720">
|
|
405
|
+
|
|
406
|
+
URLs, devices and auth work as in `rpc shot`: `localhost:3000` needs no
|
|
407
|
+
scheme, `-d` takes ids or aliases (default `iphone-15-pro,desktop-1440`),
|
|
408
|
+
and `--cookie`, `--header`, `--storage-state` and the session saved by
|
|
409
|
+
`rpc login` all apply. A page that redirects to a login URL fails with
|
|
410
|
+
`AUTH_REQUIRED`. Each run checks one URL. It isn't Lighthouse: CLS is the
|
|
411
|
+
only performance number it reports.
|
|
412
|
+
|
|
413
|
+
| Option | Meaning |
|
|
414
|
+
| --- | --- |
|
|
415
|
+
| `-d, --devices <ids>` | Devices to check on. The mobile checks run on phones only |
|
|
416
|
+
| `--fail-on <level>` | `error` (default) exits 1 on errors; `warn` exits 1 on warnings too |
|
|
417
|
+
| `--shots` | Save PNGs of the elements behind layout-shift and overflow findings into `--out` |
|
|
418
|
+
| `-o, --out <dir>` | Where `--shots` writes. Default `./.rpc-shots` |
|
|
419
|
+
| `--json` | Print the report as JSON (see below) |
|
|
420
|
+
| `--wait-for`, `--wait`, `--timeout`, `--color-scheme` | As in `rpc shot` |
|
|
421
|
+
| `--cookie`, `--header`, `--storage-state`, `--no-session`, `--allow-origin` | As in `rpc shot` |
|
|
422
|
+
|
|
423
|
+
The exit code is 0 when there are no errors (with `--fail-on warn`, no
|
|
424
|
+
warnings either), 1 otherwise and 2 for a usage error.
|
|
425
|
+
|
|
426
|
+
### How a check runs
|
|
427
|
+
|
|
428
|
+
Each device gets a fresh browser context, the same one `rpc shot` uses.
|
|
429
|
+
Before the page's own scripts run, rpc installs a `PerformanceObserver` for
|
|
430
|
+
`layout-shift`. It loads the page and waits for network idle (up to 10 s),
|
|
431
|
+
web fonts and pending images. Then it waits a fixed 500 ms, scrolls down one
|
|
432
|
+
viewport and back with a 300 ms pause each way, and reads the page. Because
|
|
433
|
+
the sequence is fixed, CLS comes out the same on every run; the tests check
|
|
434
|
+
the same page twice and allow 0.005 of difference.
|
|
435
|
+
|
|
436
|
+
When a finding is identical on several devices it's reported once, with
|
|
437
|
+
every device listed, so a missing `lang` appears one time, not two.
|
|
438
|
+
|
|
439
|
+
### What it checks
|
|
440
|
+
|
|
441
|
+
Severity decides the exit code. `error` fails the run, `warn` fails it only
|
|
442
|
+
with `--fail-on warn`, and `info` never does.
|
|
443
|
+
|
|
444
|
+
#### Basics
|
|
445
|
+
|
|
446
|
+
| id | Severity | When | Fix |
|
|
447
|
+
| --- | --- | --- | --- |
|
|
448
|
+
| `http-status` | error | The page answered 400 or above | Serve it with a 200 |
|
|
449
|
+
| `page-load-failed`, `auth-required` | error | The page didn't load on a device (DNS, refused, timeout) or landed on a login page | The error's own hint, e.g. ``run `rpc login <origin>` `` |
|
|
450
|
+
| `mixed-content` | error (warn if the browser upgraded every request) | `http://` requests on an https page | Load everything over https |
|
|
451
|
+
| `uncaught-exception` | error | Exceptions thrown during load, first three listed | Fix the first one; the rest often follow from it |
|
|
452
|
+
| `console-errors` | warn | `console.error` during load: the count and the first three | Fix or silence them |
|
|
453
|
+
| `theme-color-missing` | info | No `<meta name="theme-color">` | Add one to tint the mobile browser bar |
|
|
454
|
+
|
|
455
|
+
#### Meta and social
|
|
456
|
+
|
|
457
|
+
| id | Severity | When | Fix |
|
|
458
|
+
| --- | --- | --- | --- |
|
|
459
|
+
| `title-missing`, `title-long` | error, warn | No `<title>`, or one over 60 characters | 50 to 60 characters, key words first |
|
|
460
|
+
| `description-missing`, `description-long` | warn, info | No meta description, or one over 160 characters | One or two sentences |
|
|
461
|
+
| `canonical-missing` | warn | No `<link rel="canonical">` | The absolute URL to index |
|
|
462
|
+
| `lang-missing` | error | No `lang` on `<html>` | `<html lang="en">` |
|
|
463
|
+
| `viewport-missing`, `viewport-no-zoom` | error | No viewport meta, or one with `user-scalable=no` or `maximum-scale` under 2 | `width=device-width, initial-scale=1` and nothing that blocks zoom |
|
|
464
|
+
| `favicon-missing` | warn | No `<link rel="icon">`, and `/favicon.ico` doesn't load | Add an icon link |
|
|
465
|
+
| `apple-touch-icon-missing` | info | No `apple-touch-icon` | A 180x180 PNG |
|
|
466
|
+
| `og-title-missing`, `og-description-missing`, `og-image-missing`, `og-url-missing` | warn | That Open Graph tag is absent | Add it; link previews read it |
|
|
467
|
+
| `twitter-card-missing` | warn | No `twitter:card` | `summary_large_image` |
|
|
468
|
+
| `og-image-relative` | warn | `og:image` isn't an absolute URL | Make it absolute; most scrapers don't resolve relative URLs |
|
|
469
|
+
| `og-image-broken`, `og-image-not-image` | error | `og:image` doesn't return 200, or isn't served as an image | Point it at the image file |
|
|
470
|
+
| `og-image-small`, `og-image-ratio` | warn, info | Smaller than 600x315, or not the 1.91:1 shape of 1200x630 | Export it at 1200x630 |
|
|
471
|
+
|
|
472
|
+
rpc fetches `og:image` once per run and reads its pixel size from the file
|
|
473
|
+
header (PNG, JPEG, GIF or WebP). The result is in the report's `ogImage`.
|
|
474
|
+
|
|
475
|
+
#### Layout shift
|
|
476
|
+
|
|
477
|
+
| id | Severity | When | Fix |
|
|
478
|
+
| --- | --- | --- | --- |
|
|
479
|
+
| `layout-shift` | error above 0.25, warn above 0.1, info above 0 | CLS for the load and the scroll, with the three elements that moved most and each one's share | Reserve space for late content: `width`/`height` or `aspect-ratio` on media, `min-height` on async sections, nothing inserted above what's on screen |
|
|
480
|
+
|
|
481
|
+
The score is computed the way Chrome computes it: the largest session
|
|
482
|
+
window, meaning shifts less than 1 s apart and at most 5 s in total. Chrome
|
|
483
|
+
marks shifts that follow input as `hadRecentInput`, including any in the
|
|
484
|
+
first 500 ms after navigation, and those don't count. Totals under 0.001
|
|
485
|
+
count as zero. Each device's score is also in `devices[].cls`.
|
|
486
|
+
|
|
487
|
+
#### Unlabelled controls
|
|
488
|
+
|
|
489
|
+
| id | Severity | When | Fix |
|
|
490
|
+
| --- | --- | --- | --- |
|
|
491
|
+
| `button-no-name`, `link-no-name` | error | A button, link or `[role=button]` with no accessible name, such as an icon-only button | `aria-label`, visually hidden text, or alt text on the image inside |
|
|
492
|
+
| `img-no-alt` | error | An `<img>` with no `alt` attribute (`alt=""` is fine) | Describe the image, or `alt=""` if it's decorative |
|
|
493
|
+
| `field-no-label` | error | An input, select or textarea with no label | `<label for>`, a wrapping `<label>`, or `aria-label` |
|
|
494
|
+
| `field-placeholder-only` | warn | A field labelled only by its placeholder | Add a visible label |
|
|
495
|
+
|
|
496
|
+
rpc works out accessible names in the page from `aria-labelledby`,
|
|
497
|
+
`aria-label`, `<label>`, `alt`, the text inside (minus anything
|
|
498
|
+
`aria-hidden`), an SVG `<title>` and `title`. It skips elements assistive
|
|
499
|
+
tech can't reach: `display: none`, `visibility: hidden`, `aria-hidden` and
|
|
500
|
+
`inert`.
|
|
501
|
+
|
|
502
|
+
#### Fonts
|
|
503
|
+
|
|
504
|
+
| id | Severity | When | Fix |
|
|
505
|
+
| --- | --- | --- | --- |
|
|
506
|
+
| `font-display-missing`, `font-display-block` | warn | A loaded `@font-face` has no `font-display` (Chrome then hides its text for up to 3 s) or uses `block`. The message says when it sets text above the fold (FOIT) | `font-display: swap` or `optional`; `&display=swap` for Google Fonts |
|
|
507
|
+
| `font-not-preloaded` | warn | A font file that sets text in the first viewport isn't preloaded | `<link rel="preload" as="font" type="font/woff2" crossorigin>` |
|
|
508
|
+
| `font-preload-no-crossorigin` | warn | A font preload without `crossorigin`, so the file downloads twice | Add `crossorigin` |
|
|
509
|
+
| `font-preload-unused` | info | A preloaded font that no `@font-face` uses | Remove it, or match the URL exactly |
|
|
510
|
+
| `font-failed` | error | A font file returned 400 or above | Fix the `src` URL |
|
|
511
|
+
|
|
512
|
+
Every font finding names the file URL. The `@font-face` rules come from the
|
|
513
|
+
page's stylesheets; the page can't read cross-origin ones (Google Fonts, a
|
|
514
|
+
CDN), so rpc downloads and parses those itself. Faces with only `local()`
|
|
515
|
+
sources, like next/font's size-adjusted fallbacks, never block text and are
|
|
516
|
+
skipped.
|
|
517
|
+
|
|
518
|
+
#### Mobile viewport
|
|
519
|
+
|
|
520
|
+
These run on phone devices only.
|
|
521
|
+
|
|
522
|
+
| id | Severity | When | Fix |
|
|
523
|
+
| --- | --- | --- | --- |
|
|
524
|
+
| `horizontal-overflow` | error | `scrollWidth` is wider than the layout viewport. Lists the outermost elements that stick out, furthest first | `max-width: 100%` on the element, or `overflow-x: auto` on its wrapper |
|
|
525
|
+
| `vh-units` | warn | A rendered element sized with `100vh` in `height`, `min-height` or `max-height`, from a stylesheet rule that applies or an inline style | `100dvh` or `100svh`, with `100vh` before it as a fallback |
|
|
526
|
+
| `tap-target-small` | warn | Links, buttons and form controls smaller than 44x44 CSS px. Inline links in a sentence are exempt, per the WCAG 2.5.8 inline exception: a link with `display: inline` and other text in the same block. Buttons and links laid out as boxes (`inline-block`, `flex`, `block`) still count | Padding, `min-height`, or a bigger hit area |
|
|
527
|
+
| `input-zoom` | warn | A text field with `font-size` under 16px; iOS Safari zooms the page when it gets focus | `font-size: 16px` or more |
|
|
528
|
+
|
|
529
|
+
A headless phone has no browser bars, so `100vh` and `100dvh` measure the
|
|
530
|
+
same there. That's why `vh-units` reads the CSS instead of measuring
|
|
531
|
+
elements. The `height: 100vh; height: 100dvh` fallback pattern isn't
|
|
532
|
+
flagged: the browser keeps only the last value it understands.
|
|
533
|
+
|
|
534
|
+
#### Motion
|
|
535
|
+
|
|
536
|
+
After the scroll, rpc gives running animations up to 1 s to stop, then
|
|
537
|
+
looks at the viewport plus half a viewport above and below. These findings
|
|
538
|
+
are about whether a screenshot comes out right, and a page can ship with
|
|
539
|
+
them, so they are `warn` or `info` and never `error`.
|
|
540
|
+
|
|
541
|
+
| id | Severity | When | Fix |
|
|
542
|
+
| --- | --- | --- | --- |
|
|
543
|
+
| `motion-running` | warn | Finite animations (from `document.getAnimations()`) or style changes still going on visible elements | `rpc shot --finish-animations`, `--wait`, or `--reduced-motion` |
|
|
544
|
+
| `motion-reveal-hidden` | warn | Elements with text or media at opacity under 0.1, `visibility: hidden`, offset by 24px or more, or scaled under 0.85, that also carry a reveal class (`reveal`, `fade`, `aos`, `in-view`, `appear`, `animated` and similar; utility words such as `scroll`, `visible` or `animate-` don't count), a reveal attribute (`data-aos`, `data-sal`, `data-scroll` and similar) or inline opacity or transform written by a library. Carousel tracks, elements inside a carousel library root, and `translate(-50%)` centring are not offsets. Only the outermost element is listed | Make sure the reveal fires, and that the content shows without JavaScript or animation |
|
|
545
|
+
| `motion-loop` | info | Infinite animations: spinners, pulses, CSS marquees | Stop them under `prefers-reduced-motion: reduce` |
|
|
546
|
+
| `motion-pinned` | info | A `.pin-spacer` taller than its section by a quarter of the viewport, a pin that `ScrollTrigger.getAll()` reports as its parent's only child, or a sticky or fixed section (half a viewport tall or more) that is the only child of a parent at least half a viewport taller | `rpc shot --full-page` collapses it; `rpc record --selector` shows it moving |
|
|
547
|
+
| `motion-carousel` | info | Swiper, Embla, Splide, Keen, Flickity, Slick or Glide roots, `aria-roledescription="carousel"`, horizontal scrollers with `scroll-snap-type` on x, and tracks whose scroll position or transform changes during a 300 ms sample | `rpc record <url> --selector '<the carousel>'` |
|
|
548
|
+
|
|
549
|
+
### JSON report
|
|
550
|
+
|
|
551
|
+
`--json` prints this object, and the MCP `check` tool returns the same one.
|
|
552
|
+
`schema` goes up only when a field changes meaning or is removed; new fields
|
|
553
|
+
can appear without it.
|
|
554
|
+
|
|
555
|
+
```jsonc
|
|
556
|
+
{
|
|
557
|
+
"schema": 1,
|
|
558
|
+
"url": "https://example.com/",
|
|
559
|
+
"devices": [
|
|
560
|
+
{
|
|
561
|
+
"device": "iphone-15-pro",
|
|
562
|
+
"name": "iPhone 15 Pro",
|
|
563
|
+
"viewport": { "width": 393, "height": 852 },
|
|
564
|
+
"dpr": 3,
|
|
565
|
+
"mobile": true, // the phone-only checks ran here
|
|
566
|
+
"status": 200,
|
|
567
|
+
"finalUrl": "https://example.com/",
|
|
568
|
+
"title": "Example",
|
|
569
|
+
"cls": 0.012,
|
|
570
|
+
"layoutShifts": 2,
|
|
571
|
+
// only when the page didn't load on this device:
|
|
572
|
+
"error": "…", "code": "ERR_NAME_NOT_RESOLVED", "hint": "…"
|
|
573
|
+
}
|
|
574
|
+
],
|
|
575
|
+
"summary": { "errors": 1, "warnings": 4, "info": 2 },
|
|
576
|
+
"findings": [
|
|
577
|
+
{
|
|
578
|
+
"id": "horizontal-overflow", // stable; see the tables above
|
|
579
|
+
"check": "mobile", // basics | meta | layout-shift | labels | fonts | mobile
|
|
580
|
+
"severity": "error", // error | warn | info
|
|
581
|
+
"message": "The page scrolls sideways: 601px wide in a 393px viewport",
|
|
582
|
+
"fix": "Find the element that sticks out …",
|
|
583
|
+
"devices": ["iphone-15-pro"],
|
|
584
|
+
"selector": "div.pricing-table", // when one element is the answer
|
|
585
|
+
"url": "…", // when a file is (a font, og:image)
|
|
586
|
+
"value": 208, // the number behind it, if there is one
|
|
587
|
+
"items": [ // elements, files or log lines, when there are several
|
|
588
|
+
{
|
|
589
|
+
"selector": "div.pricing-table",
|
|
590
|
+
"text": "600px wide, 207px past the edge",
|
|
591
|
+
"value": 207,
|
|
592
|
+
"screenshot": "/abs/.rpc-shots/…-check-horizontal-overflow-1.png" // with --shots
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
],
|
|
597
|
+
"ogImage": { "url": "…", "status": 200, "contentType": "image/png", "width": 1200, "height": 630 },
|
|
598
|
+
"auth": { … } // as in rpc shot, values masked
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Findings are sorted by check, in the order of the tables above, then by
|
|
603
|
+
severity. `id`, `check`, `severity`, `message`, `fix` and `devices` are
|
|
604
|
+
always there; the other fields appear when they apply.
|
|
605
|
+
|
|
606
|
+
### In CI
|
|
607
|
+
|
|
608
|
+
```sh
|
|
609
|
+
npx rpc-agent install
|
|
610
|
+
npx rpc-agent check https://staging.example.com --fail-on warn --json > ship-check.json
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
The exit code fails the job, and the JSON can go into a PR comment or a
|
|
614
|
+
build artifact. For a page behind a login, sign in first with
|
|
615
|
+
`rpc login <url> --form` (see [CI and agents](#ci-and-agents-rpc-login---form)).
|
|
616
|
+
|
|
617
|
+
### With a coding agent
|
|
618
|
+
|
|
619
|
+
Add the MCP server (below) and tell the agent to run `check` before it
|
|
620
|
+
reports UI work as finished, for example in `CLAUDE.md` or `AGENTS.md`:
|
|
621
|
+
|
|
622
|
+
```md
|
|
623
|
+
Before you say UI work is done, run the rpc `check` tool on each page you
|
|
624
|
+
changed and fix every error. Fix the warnings too, or say why one doesn't apply.
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## Recording motion: `rpc record`
|
|
628
|
+
|
|
629
|
+
A still can't show a carousel, a marquee, or a pinned section whose panels
|
|
630
|
+
slide as you scroll. `rpc record` takes a few seconds of video of the page,
|
|
631
|
+
or of one element on it.
|
|
632
|
+
|
|
633
|
+
```sh
|
|
634
|
+
rpc record localhost:3000 --selector '.hero-carousel' # 5 s of one element
|
|
635
|
+
rpc record localhost:3000 -d iphone --duration 10 --gif # whole phone viewport, plus a GIF
|
|
636
|
+
rpc record example.com --selector '#logos' --mp4 # mp4 too, if ffmpeg is on PATH
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
| Option | Meaning |
|
|
640
|
+
| --- | --- |
|
|
641
|
+
| `--selector <css>` | Record this element. rpc scrolls it into view, waits for reveals around it to settle, and crops the video to its box. If it's bigger than the viewport, the visible part is recorded and the result says so |
|
|
642
|
+
| `--duration <s>` | Seconds of video, 0.5 to 30. Default 5 |
|
|
643
|
+
| `-d, --device <id>` | One device id or alias. Default `desktop-1440` |
|
|
644
|
+
| `--gif` | Also write a GIF: 10 fps, at most 640 px wide |
|
|
645
|
+
| `--mp4` | Also write an mp4. This needs an ffmpeg with an H.264 encoder on PATH; without one, rpc skips the mp4 and says so |
|
|
646
|
+
| `-o, --out <dir>`, `--json`, `--open` | As in `rpc shot` |
|
|
647
|
+
| `--wait-for`, `--wait`, `--timeout`, `--color-scheme`, `--reduced-motion`, `--no-emulate` | As in `rpc shot` |
|
|
648
|
+
| `--cookie`, `--header`, `--storage-state`, `--no-session`, `--allow-origin` | As in `rpc shot` |
|
|
649
|
+
|
|
650
|
+
It writes `<host>[-<path>]-<device>[-<selector>].webm` (VP9) and a
|
|
651
|
+
`-poster.png` of the first frame, plus the `.gif` and `.mp4` when asked:
|
|
652
|
+
|
|
653
|
+
```
|
|
654
|
+
ok iphone-15-pro (393x852 @3x) #carousel
|
|
655
|
+
.rpc-shots/localhost-3000-iphone-15-pro-carousel.webm 1080x600, 3s at 25 fps (90 frames captured, playwright ffmpeg)
|
|
656
|
+
.rpc-shots/localhost-3000-iphone-15-pro-carousel-poster.png 1080x600 poster
|
|
657
|
+
.rpc-shots/localhost-3000-iphone-15-pro-carousel.gif 640x356, 32 frames
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
<img alt="A GIF recorded from a fixture's autoplaying carousel: four coloured slides advancing in turn" src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/motion/record-carousel.gif" width="360">
|
|
661
|
+
|
|
662
|
+
Frames come from a loop of `page.screenshot()` calls clipped to the
|
|
663
|
+
element's box. Headless Chromium returns those at device pixels and cropped
|
|
664
|
+
exactly, at about 30 fps on a DPR 2 or 3 phone and 60 fps at DPR 1. The two
|
|
665
|
+
obvious alternatives do worse. CDP screencast frames arrive at CSS pixel
|
|
666
|
+
size in headless mode (393x852 for a DPR 3 phone), so they lose the
|
|
667
|
+
pixel density and need a scaled crop. Playwright's `recordVideo` records the
|
|
668
|
+
whole viewport at CSS size with a fixed bitrate, and it can't crop.
|
|
669
|
+
|
|
670
|
+
The frames go to ffmpeg as MJPEG at a constant 25 fps. Each one is repeated
|
|
671
|
+
until the next one's timestamp, so the video is as long as you asked for,
|
|
672
|
+
whatever rate the screenshots came in at. The ffmpeg is Playwright's own
|
|
673
|
+
build, which `rpc install` already downloads next to Chromium. It writes
|
|
674
|
+
VP9 webm and PNG but has no H.264 encoder, which is why webm is the default
|
|
675
|
+
and mp4 needs a system ffmpeg. If Playwright's build is missing, rpc falls
|
|
676
|
+
back to `ffmpeg` on PATH. GIFs are encoded in Node (`src/gif.ts`) from PNG
|
|
677
|
+
frames that ffmpeg decodes out of the webm, so they add no dependency.
|
|
678
|
+
`rpc doctor` says which ffmpeg it found and whether mp4 will work.
|
|
679
|
+
|
|
302
680
|
## MCP server
|
|
303
681
|
|
|
304
|
-
`rpc mcp` runs an MCP server on stdio with
|
|
682
|
+
`rpc mcp` runs an MCP server on stdio with five tools.
|
|
305
683
|
|
|
306
684
|
### `screenshot`
|
|
307
685
|
|
|
@@ -316,6 +694,8 @@ rpc shot localhost:3000/app --storage-state auth.json
|
|
|
316
694
|
| `sliceWidth` | 768 | full page: max slice width in px (raise it for small text on desktop) |
|
|
317
695
|
| `maxHeight` | 60000 | full page: CSS px cap |
|
|
318
696
|
| `reducedMotion` | false | emulate `prefers-reduced-motion: reduce` |
|
|
697
|
+
| `finishAnimations` | false | jump animations (Web Animations, GSAP on `window`) to their end first |
|
|
698
|
+
| `unpin` | true | full page: collapse pinned sections so each shows once |
|
|
319
699
|
| `waitFor`, `waitMs`, `colorScheme`, `outDir`, `timeoutMs` | | as in the CLI |
|
|
320
700
|
| `inline` | true | return images, not just paths |
|
|
321
701
|
| `cookies` | | `[{ name, value, domain?, path? }]`, set on the target origin |
|
|
@@ -327,7 +707,10 @@ rpc shot localhost:3000/app --storage-state auth.json
|
|
|
327
707
|
The result starts with a plain-text block: one line per device (or the error
|
|
328
708
|
and hint), followed by the JSON summary (file paths, pixel sizes,
|
|
329
709
|
`scrollHeight`, `capturedHeight`, `segments`, `clipped`, `slices`, HTTP status,
|
|
330
|
-
final URL, title, console errors, `error`/`code`/`hint`).
|
|
710
|
+
final URL, title, console errors, `motion`, `warnings`, `error`/`code`/`hint`).
|
|
711
|
+
Each motion warning also gets its own line under its device, followed by
|
|
712
|
+
the fix, like ``WARN [motion-carousel] carousel: use `rpc record` for motion
|
|
713
|
+
at #carousel``.
|
|
331
714
|
|
|
332
715
|
Then the images:
|
|
333
716
|
|
|
@@ -355,6 +738,46 @@ session, cookies and headers (values masked), and the JSON summary gains an
|
|
|
355
738
|
`AUTH_REQUIRED` and the `rpc login` hint. Signing in is something the user
|
|
356
739
|
does in a terminal (`rpc login`); the agent then just calls `screenshot`.
|
|
357
740
|
|
|
741
|
+
### `check`
|
|
742
|
+
|
|
743
|
+
Runs [Ship-check](#ship-check) on one URL.
|
|
744
|
+
|
|
745
|
+
| Param | Default | |
|
|
746
|
+
| --- | --- | --- |
|
|
747
|
+
| `url` | required | http(s), file, bare host or local path |
|
|
748
|
+
| `devices` | `["iphone-15-pro", "desktop-1440"]` | ids or aliases; the mobile checks run on phones |
|
|
749
|
+
| `shots` | false | save PNGs of the elements behind layout-shift and overflow findings |
|
|
750
|
+
| `outDir`, `waitFor`, `waitMs`, `colorScheme`, `timeoutMs` | | as for `screenshot` |
|
|
751
|
+
| `cookies`, `headers`, `storageState`, `useSession`, `allowOrigins` | | as for `screenshot` |
|
|
752
|
+
|
|
753
|
+
The result is two text blocks. The first is a short summary: the counts,
|
|
754
|
+
then one line per error and warning with its id and selector, like
|
|
755
|
+
`ERROR [button-no-name] 2 buttons with no accessible name at button.icon-menu`.
|
|
756
|
+
The second is the JSON report described under [JSON report](#json-report).
|
|
757
|
+
The call is a tool error only when the page failed to load on every device;
|
|
758
|
+
findings, even errors, come back as a normal result for the agent to act on.
|
|
759
|
+
|
|
760
|
+
### `record`
|
|
761
|
+
|
|
762
|
+
Runs [`rpc record`](#recording-motion-rpc-record) on one URL.
|
|
763
|
+
|
|
764
|
+
| Param | Default | |
|
|
765
|
+
| --- | --- | --- |
|
|
766
|
+
| `url` | required | http(s), file, bare host or local path |
|
|
767
|
+
| `device` | `desktop-1440` | one id or alias |
|
|
768
|
+
| `selector` | whole viewport | CSS selector of the element to record |
|
|
769
|
+
| `duration` | 5 | seconds, 0.5 to 30 |
|
|
770
|
+
| `gif` | false | also write a GIF |
|
|
771
|
+
| `mp4` | false | also write an mp4, when a system ffmpeg has H.264 |
|
|
772
|
+
| `reducedMotion`, `waitFor`, `waitMs`, `colorScheme`, `outDir`, `timeoutMs` | | as for `screenshot` |
|
|
773
|
+
| `cookies`, `headers`, `storageState`, `useSession`, `allowOrigins` | | as for `screenshot` |
|
|
774
|
+
|
|
775
|
+
The result starts with a summary line (`ok iphone-15 #carousel: 3s webm
|
|
776
|
+
1080x600 at 25 fps <path>`, then the poster, GIF and mp4 paths) and the
|
|
777
|
+
JSON result. The poster comes back inline as a JPEG, so the agent can see
|
|
778
|
+
what it recorded. An element that isn't there is a tool error with the code
|
|
779
|
+
`SELECTOR_NOT_FOUND`.
|
|
780
|
+
|
|
358
781
|
### `list_devices`
|
|
359
782
|
|
|
360
783
|
Takes an optional `category` (`apple-phone`, `android-phone`, `tablet`,
|
|
@@ -445,6 +868,13 @@ appear once, that the IntersectionObserver section rendered, and the slice
|
|
|
445
868
|
count and labels, including through the MCP server. The missing-browser tests
|
|
446
869
|
point `PLAYWRIGHT_BROWSERS_PATH` at an empty temp dir.
|
|
447
870
|
|
|
871
|
+
The Ship-check tests serve `test/fixtures/check`: one page per check
|
|
872
|
+
(`meta.html`, `shift.html`, `labels.html`, `fonts.html`, `mobile.html`,
|
|
873
|
+
`basics.html`) and `clean.html`, which must produce no errors or warnings.
|
|
874
|
+
`shift.html` inserts a 300 px banner 800 ms after load, so CLS is known in
|
|
875
|
+
advance (300/852 on the phone, 300/1440 on desktop); the test runs it twice
|
|
876
|
+
and compares.
|
|
877
|
+
|
|
448
878
|
The README media are built from real captures by `scripts/media/build.ts`;
|
|
449
879
|
`docs/media.md` explains how. The CLI's design notes, and what it borrows
|
|
450
880
|
from the `skills` CLI, are in `docs/cli-design.md`.
|
|
@@ -454,10 +884,19 @@ from the `skills` CLI, are in `docs/cli-design.md`.
|
|
|
454
884
|
- Chromium only, portrait only. There is no landscape mode, and none of RPC's
|
|
455
885
|
fake browser chrome bars.
|
|
456
886
|
- Scroll-driven animations are captured in their scroll-top state. Entrance
|
|
457
|
-
animations that run once are
|
|
887
|
+
animations that run once are fine after the warm-up scroll and settle. Ones
|
|
458
888
|
that reverse when scrolled back (GSAP `toggleActions` with reverse, scrubbed
|
|
459
|
-
timelines) may show their initial state
|
|
460
|
-
|
|
889
|
+
timelines) may show their initial state, and a collapsed pin shows its
|
|
890
|
+
section's first frame. `--finish-animations` only reaches GSAP when it's on
|
|
891
|
+
`window`; GSAP bundled into the app's JavaScript isn't, so its tweens stay
|
|
892
|
+
as they are. `--reduced-motion` helps on sites that respect it.
|
|
893
|
+
- Pins are found by GSAP's `.pin-spacer` class, `ScrollTrigger.getAll()`, and
|
|
894
|
+
sticky or fixed sections that are the only child of a tall parent. A pin
|
|
895
|
+
built some other way is still captured with its blank spacer.
|
|
896
|
+
- `rpc record` records what the page does on its own. It doesn't scroll or
|
|
897
|
+
click, so a scrubbed section only moves if something else moves it. Frames
|
|
898
|
+
come at the screenshot rate (about 30 fps on high-DPR phones), and the
|
|
899
|
+
webm repeats frames to hold 25 fps.
|
|
461
900
|
- Very tall stitched PNGs are big files; review them through the slices.
|
|
462
901
|
- The bezel geometry is RPC's, unchanged. On kind-default phones the page
|
|
463
902
|
covers the dynamic island or notch, and generic monitors are drawn edge to
|
|
@@ -466,6 +905,11 @@ from the `skills` CLI, are in `docs/cli-design.md`.
|
|
|
466
905
|
- Login-page detection goes by the URL path. An app that shows its sign-in
|
|
467
906
|
form at the requested URL without redirecting is captured as is.
|
|
468
907
|
- Every MCP call launches a fresh Chromium, which adds about a second.
|
|
908
|
+
- Ship-check reads `100vh` only from stylesheets the page can read and from
|
|
909
|
+
inline styles; a `100vh` rule in a cross-origin stylesheet is missed. Tap
|
|
910
|
+
targets are measured by their box, so a hit area enlarged with a
|
|
911
|
+
pseudo-element still counts as small. CLS covers the load and one scroll,
|
|
912
|
+
not shifts caused by clicking around.
|
|
469
913
|
|
|
470
914
|
## License
|
|
471
915
|
|