webview-preview 0.0.0-stage → 0.0.1

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 ADDED
@@ -0,0 +1,207 @@
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 preview page exposes helpers (`window.preview.signIn()`, `window.preview.type(name, value)`), use
64
+ them — they are faster and already correct.
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`, helpers. 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 `invoke`) → a stub of `window.__TAURI_INTERNALS__` in `init`;
137
+ - session only in memory (no storage, no request) → steps through the real login form, or a helper the project
138
+ already exposes (`window.preview.signIn()`).
139
+
140
+ 4. **Check that it worked:** print `location.href` (you stayed on the route) and the request list (every request
141
+ the screen needs is `-> mock …`, none fails), then snap.
142
+
143
+ Put the result in the project's `webview-preview.config.mjs` (or `preview/mocks.json`) so the next run — yours or a
144
+ person's — gets there in one command. Mention in your report which requests you mocked and with what data; mocked
145
+ data can hide a real bug in how the app handles the real server.
146
+
147
+ ## Keyboard, mouse and focus
148
+
149
+ Use the real input steps, not `js:` clicks, whenever the question is about what a person experiences:
150
+
151
+ | Question | Steps |
152
+ |---|---|
153
+ | 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 |
154
+ | Is the focus ring visible and not clipped? | `key:Tab` until the element is focused, then `snap:ring@<selector>` (8 px margin included) |
155
+ | Does the hover state look right? | `hover:<selector>`, `wait:300` for the transition, `snap:x@<selector>` |
156
+ | Does typing work (validation, masks, Cyrillic…)? | `click:<input>`, `type:<text>`, `print:` the value / error text |
157
+ | Does Escape / Enter / arrows do the right thing? | `key:Escape`, `key:Enter`, `key:ArrowDown` |
158
+
159
+ - Input steps go through the engine's real input path: focus moves, `:hover` / `:focus-visible` apply and paint,
160
+ default actions happen (Enter submits, Space toggles a checkbox). Both engines produce the same DOM events.
161
+ - `js:element.click()` and `dispatchEvent(new KeyboardEvent(…))` only run handlers — no focus move, no hover, no
162
+ default action. Use them to reach a state quickly, not to test input.
163
+ - Input steps do not wait afterwards. Add `wait:<ms>` before a snapshot when something animates.
164
+ - A missing or hidden element in `click:` / `hover:` / `snap@` makes the run fail (exit code 1) — check the selector.
165
+
166
+ ## Comparing before and after
167
+
168
+ To prove a change touched only what it should, take a baseline before editing and compare after:
169
+
170
+ ```
171
+ npx webview-preview /screen <steps> snap:screen "snap:panel@[data-test=panel]" --update-baseline # before the change
172
+ # … edit …
173
+ npx webview-preview /screen <steps> snap:screen "snap:panel@[data-test=panel]" --compare # after
174
+ ```
175
+
176
+ `same` everywhere you did not mean to change anything is strong evidence; a red area in `*.diff.png` outside your
177
+ change is a regression to explain. Use a separate `--baseline` folder for such throw-away checks so you do not
178
+ overwrite the project's committed baselines. Run `webview-preview clean` when you are done with the snapshots.
179
+
180
+ ## Still out of reach — and a few traps
181
+
182
+ **Fractional scroll.** `scrollTop = 55.5` is rounded; real wheel and touchpad scrolling can stop between pixels. A
183
+ 1 px glitch the user sees after scrolling may not reproduce here — reason about it and say it was not reproduced.
184
+
185
+ **State from earlier runs is gone.** Every run starts with empty storage (`--keep-storage` turns that off in
186
+ WebKitGTK). The app opens in its default state — the default tab, no saved session — so reach the state you need with
187
+ steps or `init`; do not rely on what an earlier run clicked.
188
+
189
+ **Windows' WebView2 itself.** `msedge` is the same engine, not the same embedding; window chrome, scrollbars of the
190
+ host and DPI handling of a native window can differ.
191
+
192
+ ## Reading snapshots
193
+
194
+ - WebKitGTK renders off-screen at 1×: PNG pixels = CSS pixels. With `--headed` a HiDPI screen gives 2× (a 1200×650
195
+ window → 2400×1300 PNG). Playwright engines follow `--scale`.
196
+ - Prefer `snap:name@selector` to cropping by hand; it includes an 8 px margin for rings and shadows.
197
+ - Enlarge with nearest-neighbour scaling before judging 1–2 px details.
198
+ - To compare two moments, use `--compare`, or paste crops side by side into one image.
199
+
200
+ ## Checklist before saying "done"
201
+
202
+ - The numbers you defined in step 1 match, in the engine the user runs (`webkitgtk` for Tauri on Linux, `msedge` for
203
+ Windows).
204
+ - No `js failed` / `print failed` / selector errors you did not expect; exit code 0.
205
+ - Keyboard and hover behaviour checked with `key:` / `hover:` where the change touches them.
206
+ - You looked at one snapshot of the final state.
207
+ - 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.