webview-preview 0.0.0-stage → 1.0.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/AGENTS.md +208 -0
- package/LICENSE +21 -0
- package/README.md +657 -2
- package/bin/webview-preview.mjs +161 -0
- package/package.json +54 -5
- package/src/core/config.mjs +16 -0
- package/src/core/server.mjs +57 -0
- package/src/core/steps.mjs +182 -0
- package/src/engines/index.mjs +17 -0
- package/src/engines/playwright.mjs +104 -0
- package/src/engines/webkitgtk.mjs +67 -0
- package/src/engines/webkitgtk.py +318 -0
- package/src/index.d.ts +101 -0
- package/src/index.mjs +101 -0
- package/src/page/globals.mjs +37 -0
- package/src/page/init.mjs +32 -0
- package/src/page/mock.mjs +117 -0
- package/src/page/source.mjs +55 -0
- package/src/page/tauri.mjs +95 -0
- package/src/snapshots/clean.mjs +36 -0
- package/src/snapshots/compare.mjs +72 -0
- package/src/snapshots/png.mjs +119 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# webview-preview — guide for AI agents
|
|
2
|
+
|
|
3
|
+
You changed UI code and want to know whether it really works in the engine the app ships on. This guide is how to get
|
|
4
|
+
a trustworthy answer with `webview-preview`, quickly, and the traps that cost hours if you do not know them.
|
|
5
|
+
|
|
6
|
+
Read the [README](./README.md) for install and options. Everything here assumes the project has a
|
|
7
|
+
`webview-preview.config.mjs` (base URL, dev server) so a run is just a route and steps.
|
|
8
|
+
|
|
9
|
+
## The loop
|
|
10
|
+
|
|
11
|
+
1. Decide what "works" means as **numbers or text**: an element exists, a width stays 418, a gap is 16, a label
|
|
12
|
+
reads "Fabric". Screenshots are for what numbers cannot say (colours, clipping, overlap).
|
|
13
|
+
2. Run the steps that reach the state, then `print:` the facts and `snap:` once.
|
|
14
|
+
3. Read the output: one line per `print:`, `js failed:` / `print failed:` on stderr, snapshot paths at the end, exit
|
|
15
|
+
code 1 if a run failed.
|
|
16
|
+
4. Open the PNG only after the numbers make sense. Crop it to the area you care about before looking.
|
|
17
|
+
5. Change, run again, compare the same numbers.
|
|
18
|
+
|
|
19
|
+
When you report back, say which engine you checked and what you could **not** check (see the limits below).
|
|
20
|
+
|
|
21
|
+
## Writing steps
|
|
22
|
+
|
|
23
|
+
**Wrap every multi-statement `js:` in a block.** In WebKitGTK, `const` / `let` at the top level stay alive for the
|
|
24
|
+
whole page; the second step that declares the same name fails with `Can't create duplicate variable`. Chrome does
|
|
25
|
+
not complain, so the bug only shows on Linux. Always:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
"js:{ const button = document.querySelector('[data-test=save]'); button.click() }"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**`print:` takes an expression.** For several statements use an arrow function that returns:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
"print:(() => { const rect = document.querySelector('form').getBoundingClientRect(); return rect.width + 'x' + rect.height })()"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Wait for things, not for time.** `waitfor:<selector>` returns as soon as the element is visible *and* has stopped
|
|
38
|
+
moving and fading; `waitfor:!<selector>` when a spinner or skeleton is gone. A fixed `wait:5000` is slow when the app
|
|
39
|
+
is fast and flaky when it is slow — keep `wait:` for catching the middle of an animation on purpose (`jsnow:` +
|
|
40
|
+
`wait:<ms>` + `snap:`). `js:` waits 700 ms after the code, `jsnow:` does not.
|
|
41
|
+
|
|
42
|
+
**Select by meaning, not by position.** In steps, `text=Save` finds an element by its exact text, and every
|
|
43
|
+
selector step picks the visible element on top, skipping copies hidden in closed menus or behind dialogs. Inside your
|
|
44
|
+
own `js:` / `print:` code you have to do that yourself: closed menus, inactive tabs and lists rendered for later often
|
|
45
|
+
stay in the DOM (hidden by opacity), so `document.querySelector('[role=option]')` can return an invisible one. Prefer
|
|
46
|
+
text and visibility:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
[...document.querySelectorAll('[role=option]')].find(option => option.textContent.trim() === 'Fabric')
|
|
50
|
+
element.getClientRects().length > 0
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**CSS modules hash class names.** Match the stable part: `[class*=dropdown__menu]`.
|
|
54
|
+
|
|
55
|
+
**Typing into React inputs** needs the native setter and an `input` event:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
{ const input = document.querySelector('input[name=email]');
|
|
59
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(input, 'a@b.c');
|
|
60
|
+
input.dispatchEvent(new Event('input', { bubbles: true })) }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If the project's config has macros (`macros` in `webview-preview.config.mjs` — signing in, opening a dialog), use them
|
|
64
|
+
(`macro:signIn`): they are already correct. A flow you repeat more than twice belongs there too.
|
|
65
|
+
|
|
66
|
+
## Measuring
|
|
67
|
+
|
|
68
|
+
Start with `measure:<selector>` — one line with position, size, box, visibility, font, colours and text:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
measure [role=dialog]: 1 match · x 370 y 133 w 460 h 420 · margin 0 padding 0 border 1 · visible display flex opacity 1 · font 16px/400 color rgb(255, 255, 255) bg rgb(17, 17, 17) · text "New project Fill in the fields — you can "
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`waitfor:` first, so you measure the final state and not a frame of the opening animation. For anything else, write
|
|
75
|
+
it out with `print:`. Positions and sizes from `getBoundingClientRect()` are CSS pixels and do not depend on the
|
|
76
|
+
screenshot scale.
|
|
77
|
+
|
|
78
|
+
**Compare two states in one run** — before and after an action:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
"print:document.querySelector('form').getBoundingClientRect().width"
|
|
82
|
+
"js:document.querySelector('[data-test=add-cover]').click()"
|
|
83
|
+
"print:document.querySelector('form').getBoundingClientRect().width"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Frame by frame.** To see whether something animates smoothly, jumps, or flickers, record every animation frame into
|
|
87
|
+
`window` and print the log afterwards. Keep only the frames where the value changed:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
"jsnow:{ window.__log = []; const start = performance.now(); const tick = () => { const height = Math.round(document.querySelector('[role=dialog]').getBoundingClientRect().height); window.__log.push(Math.round(performance.now() - start) + ':' + height); if (performance.now() - start < 1500) requestAnimationFrame(tick) }; requestAnimationFrame(tick) }"
|
|
91
|
+
"jsnow:document.querySelector('[data-test=toggle]').click()"
|
|
92
|
+
"wait:1600"
|
|
93
|
+
"print:window.__log.filter((entry, index, all) => index === 0 || entry.split(':')[1] !== all[index - 1].split(':')[1]).join(' ')"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`0:420 225:433 233:444 … 391:512` is a smooth grow; `0:420 16:512` is a jump; a value that goes up, down and up again
|
|
97
|
+
is a flicker. The same trick with a `MutationObserver` catches state that exists for only one render (for example a
|
|
98
|
+
label that is "up" for a single frame).
|
|
99
|
+
|
|
100
|
+
**Counting pixels.** When the question is "is there a 1 px line" or "is the ring clipped", read pixel rows of the
|
|
101
|
+
snapshot (Python + Pillow, or any image library) instead of squinting:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from PIL import Image
|
|
105
|
+
image = Image.open("webkitgtk-dialog.png").convert("RGB")
|
|
106
|
+
scale = image.width // 1200 # window width you used
|
|
107
|
+
for y in range(320 * scale, 340 * scale):
|
|
108
|
+
print(y, image.getpixel((415 * scale, y)))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Getting past login and the backend
|
|
112
|
+
|
|
113
|
+
The screen you need is often behind a login, or empty because the backend is not running. **Never change the app's
|
|
114
|
+
code to skip a check** (no `if (PREVIEW) skipAuth`, no commented-out guards): such switches end up in releases. All of
|
|
115
|
+
it goes into the preview config: `init`, `mock`, `tauri`, `globals`, `macros`. The app's code stays as it is.
|
|
116
|
+
|
|
117
|
+
1. **Find out what stands in the way.** Run with `--log-requests` and print the list:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
npx webview-preview /target-route --log-requests wait:2000 "print:location.href" "print:__webviewPreviewRequests.join('\n')"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- `location.href` is not your route → a guard redirected you; the app wants a session.
|
|
124
|
+
- The list shows which backend calls the screen makes.
|
|
125
|
+
|
|
126
|
+
2. **Read the code that decides** — only to learn what it expects, not to change it:
|
|
127
|
+
- route guards and redirects (`Navigate`, `redirect`, `beforeEach`, `useEffect` that calls `navigate`);
|
|
128
|
+
- where the session comes from (`localStorage.getItem(...)`, cookies, a context filled after a request like
|
|
129
|
+
`GET /api/me`);
|
|
130
|
+
- the API layer (`api/*.ts`, an `axios` instance, interceptors that add a token or react to `401`).
|
|
131
|
+
|
|
132
|
+
3. **Give it what it expects:**
|
|
133
|
+
- session in storage → `init`: `localStorage.setItem('<key>', '<value in the exact format the code parses>')`;
|
|
134
|
+
- session from a request → `mock` that request (`"GET /api/me": { json: { … } }`) and anything else the screen
|
|
135
|
+
loads; take field names from the types / interfaces in the code;
|
|
136
|
+
- desktop bridge → `tauri: { commands: { … } }` for Tauri, `globals` with `anyApi({ … })` for Electron's
|
|
137
|
+
preload API or Wails bindings; the request list shows `invoke <command> -> null` / `call <path> -> default`
|
|
138
|
+
for every call that still needs an answer;
|
|
139
|
+
- session only in memory (no storage, no request) → steps through the real login form, saved as a macro.
|
|
140
|
+
|
|
141
|
+
4. **Check that it worked:** print `location.href` (you stayed on the route) and the request list (every request
|
|
142
|
+
the screen needs is `-> mock …`, none fails), then snap.
|
|
143
|
+
|
|
144
|
+
Put the result in the project's `webview-preview.config.mjs` (or `preview/mocks.json`) so the next run — yours or a
|
|
145
|
+
person's — gets there in one command. Mention in your report which requests you mocked and with what data; mocked
|
|
146
|
+
data can hide a real bug in how the app handles the real server.
|
|
147
|
+
|
|
148
|
+
## Keyboard, mouse and focus
|
|
149
|
+
|
|
150
|
+
Use the real input steps, not `js:` clicks, whenever the question is about what a person experiences:
|
|
151
|
+
|
|
152
|
+
| Question | Steps |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Does Tab go in the right order, does a focus trap hold? | `click:` the first field, then `key:Tab` / `key:Shift+Tab`, `print:document.activeElement…` after each |
|
|
155
|
+
| Is the focus ring visible and not clipped? | `key:Tab` until the element is focused, then `snap:ring@<selector>` (8 px margin included) |
|
|
156
|
+
| Does the hover state look right? | `hover:<selector>`, `wait:300` for the transition, `snap:x@<selector>` |
|
|
157
|
+
| Does typing work (validation, masks, Cyrillic…)? | `click:<input>`, `type:<text>`, `print:` the value / error text |
|
|
158
|
+
| Does Escape / Enter / arrows do the right thing? | `key:Escape`, `key:Enter`, `key:ArrowDown` |
|
|
159
|
+
|
|
160
|
+
- Input steps go through the engine's real input path: focus moves, `:hover` / `:focus-visible` apply and paint,
|
|
161
|
+
default actions happen (Enter submits, Space toggles a checkbox). Both engines produce the same DOM events.
|
|
162
|
+
- `js:element.click()` and `dispatchEvent(new KeyboardEvent(…))` only run handlers — no focus move, no hover, no
|
|
163
|
+
default action. Use them to reach a state quickly, not to test input.
|
|
164
|
+
- Input steps do not wait afterwards. Add `wait:<ms>` before a snapshot when something animates.
|
|
165
|
+
- A missing or hidden element in `click:` / `hover:` / `snap@` makes the run fail (exit code 1) — check the selector.
|
|
166
|
+
|
|
167
|
+
## Comparing before and after
|
|
168
|
+
|
|
169
|
+
To prove a change touched only what it should, take a baseline before editing and compare after:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
npx webview-preview /screen <steps> snap:screen "snap:panel@[data-test=panel]" --update-baseline # before the change
|
|
173
|
+
# … edit …
|
|
174
|
+
npx webview-preview /screen <steps> snap:screen "snap:panel@[data-test=panel]" --compare # after
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`same` everywhere you did not mean to change anything is strong evidence; a red area in `*.diff.png` outside your
|
|
178
|
+
change is a regression to explain. Use a separate `--baseline` folder for such throw-away checks so you do not
|
|
179
|
+
overwrite the project's committed baselines. Run `webview-preview clean` when you are done with the snapshots.
|
|
180
|
+
|
|
181
|
+
## Still out of reach — and a few traps
|
|
182
|
+
|
|
183
|
+
**Fractional scroll.** `scrollTop = 55.5` is rounded; real wheel and touchpad scrolling can stop between pixels. A
|
|
184
|
+
1 px glitch the user sees after scrolling may not reproduce here — reason about it and say it was not reproduced.
|
|
185
|
+
|
|
186
|
+
**State from earlier runs is gone.** Every run starts with empty storage (`--keep-storage` turns that off in
|
|
187
|
+
WebKitGTK). The app opens in its default state — the default tab, no saved session — so reach the state you need with
|
|
188
|
+
steps or `init`; do not rely on what an earlier run clicked.
|
|
189
|
+
|
|
190
|
+
**Windows' WebView2 itself.** `msedge` is the same engine, not the same embedding; window chrome, scrollbars of the
|
|
191
|
+
host and DPI handling of a native window can differ.
|
|
192
|
+
|
|
193
|
+
## Reading snapshots
|
|
194
|
+
|
|
195
|
+
- WebKitGTK renders off-screen at 1×: PNG pixels = CSS pixels. With `--headed` a HiDPI screen gives 2× (a 1200×650
|
|
196
|
+
window → 2400×1300 PNG). Playwright engines follow `--scale`.
|
|
197
|
+
- Prefer `snap:name@selector` to cropping by hand; it includes an 8 px margin for rings and shadows.
|
|
198
|
+
- Enlarge with nearest-neighbour scaling before judging 1–2 px details.
|
|
199
|
+
- To compare two moments, use `--compare`, or paste crops side by side into one image.
|
|
200
|
+
|
|
201
|
+
## Checklist before saying "done"
|
|
202
|
+
|
|
203
|
+
- The numbers you defined in step 1 match, in the engine the user runs (`webkitgtk` for Tauri on Linux, `msedge` for
|
|
204
|
+
Windows).
|
|
205
|
+
- No `js failed` / `print failed` / selector errors you did not expect; exit code 0.
|
|
206
|
+
- Keyboard and hover behaviour checked with `key:` / `hover:` where the change touches them.
|
|
207
|
+
- You looked at one snapshot of the final state.
|
|
208
|
+
- You listed what could not be checked: fractional scroll, the other platform's engine, real backend data.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 n0t4u404
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|