webview-preview 0.0.1 → 1.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/AGENTS.md +47 -7
- package/README.md +203 -69
- package/bin/webview-preview.mjs +76 -4
- package/package.json +4 -1
- package/src/core/config.mjs +5 -4
- package/src/core/steps.mjs +89 -3
- package/src/core/watch.mjs +31 -0
- package/src/engines/playwright.mjs +30 -3
- package/src/engines/webkitgtk.mjs +15 -5
- package/src/engines/webkitgtk.py +89 -11
- package/src/index.d.ts +38 -2
- package/src/index.mjs +174 -29
- package/src/page/console.mjs +84 -0
- package/src/page/globals.mjs +37 -0
- package/src/page/init.mjs +2 -2
- package/src/page/mock.mjs +3 -2
- package/src/page/network.mjs +79 -0
- package/src/page/source.mjs +55 -0
- package/src/page/tauri.mjs +95 -0
- package/src/snapshots/compare.mjs +10 -2
- package/src/snapshots/film.mjs +51 -0
- package/src/snapshots/report.mjs +71 -0
package/AGENTS.md
CHANGED
|
@@ -16,6 +16,9 @@ Read the [README](./README.md) for install and options. Everything here assumes
|
|
|
16
16
|
4. Open the PNG only after the numbers make sense. Crop it to the area you care about before looking.
|
|
17
17
|
5. Change, run again, compare the same numbers.
|
|
18
18
|
|
|
19
|
+
Do not use `--watch`: it never exits. It is for a person keeping a snapshot open while editing; you run the command
|
|
20
|
+
again after each change.
|
|
21
|
+
|
|
19
22
|
When you report back, say which engine you checked and what you could **not** check (see the limits below).
|
|
20
23
|
|
|
21
24
|
## Writing steps
|
|
@@ -60,8 +63,15 @@ element.getClientRects().length > 0
|
|
|
60
63
|
input.dispatchEvent(new Event('input', { bubbles: true })) }
|
|
61
64
|
```
|
|
62
65
|
|
|
63
|
-
If the project's
|
|
64
|
-
|
|
66
|
+
If the project's config has macros (`macros` in `webview-preview.config.mjs` — signing in, opening a dialog), use them
|
|
67
|
+
(`macro:signIn`): they are already correct. A flow you repeat more than twice belongs there too.
|
|
68
|
+
|
|
69
|
+
## Errors in the page
|
|
70
|
+
|
|
71
|
+
After the steps the tool prints the page's `console.error` calls, uncaught exceptions and unhandled rejections
|
|
72
|
+
(`console.error: …`, `uncaught: …`, `unhandled rejection: …`). Read them before you look at a snapshot: a blank or
|
|
73
|
+
half-rendered picture almost always has its cause there. Mention any you did not cause — they may be bugs the user
|
|
74
|
+
should know about. Add `--fail-on-error` when the run must fail on them.
|
|
65
75
|
|
|
66
76
|
## Measuring
|
|
67
77
|
|
|
@@ -83,7 +93,9 @@ screenshot scale.
|
|
|
83
93
|
"print:document.querySelector('form').getBoundingClientRect().width"
|
|
84
94
|
```
|
|
85
95
|
|
|
86
|
-
**Frame by frame.** To see
|
|
96
|
+
**Frame by frame.** To *see* an animation, use `film:` right after the step that starts it —
|
|
97
|
+
`"click:text=New project" "film:open:500@[role=dialog]"` gives one PNG with 10 frames over 500 ms, cropped to the
|
|
98
|
+
dialog. To *measure* whether something animates smoothly, jumps, or flickers, record every animation frame into
|
|
87
99
|
`window` and print the log afterwards. Keep only the frames where the value changed:
|
|
88
100
|
|
|
89
101
|
```
|
|
@@ -112,7 +124,7 @@ for y in range(320 * scale, 340 * scale):
|
|
|
112
124
|
|
|
113
125
|
The screen you need is often behind a login, or empty because the backend is not running. **Never change the app's
|
|
114
126
|
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`,
|
|
127
|
+
it goes into the preview config: `init`, `mock`, `tauri`, `globals`, `macros`. The app's code stays as it is.
|
|
116
128
|
|
|
117
129
|
1. **Find out what stands in the way.** Run with `--log-requests` and print the list:
|
|
118
130
|
|
|
@@ -133,9 +145,10 @@ it goes into the preview config: `init`, `mock`, helpers. The app's code stays a
|
|
|
133
145
|
- session in storage → `init`: `localStorage.setItem('<key>', '<value in the exact format the code parses>')`;
|
|
134
146
|
- session from a request → `mock` that request (`"GET /api/me": { json: { … } }`) and anything else the screen
|
|
135
147
|
loads; take field names from the types / interfaces in the code;
|
|
136
|
-
- desktop bridge
|
|
137
|
-
|
|
138
|
-
|
|
148
|
+
- desktop bridge → `tauri: { commands: { … } }` for Tauri, `globals` with `anyApi({ … })` for Electron's
|
|
149
|
+
preload API or Wails bindings; the request list shows `invoke <command> -> null` / `call <path> -> default`
|
|
150
|
+
for every call that still needs an answer;
|
|
151
|
+
- session only in memory (no storage, no request) → steps through the real login form, saved as a macro.
|
|
139
152
|
|
|
140
153
|
4. **Check that it worked:** print `location.href` (you stayed on the route) and the request list (every request
|
|
141
154
|
the screen needs is `-> mock …`, none fails), then snap.
|
|
@@ -144,6 +157,10 @@ Put the result in the project's `webview-preview.config.mjs` (or `preview/mocks.
|
|
|
144
157
|
person's — gets there in one command. Mention in your report which requests you mocked and with what data; mocked
|
|
145
158
|
data can hide a real bug in how the app handles the real server.
|
|
146
159
|
|
|
160
|
+
Loading and offline states are reachable too: `--latency 2000` (or the step `latency:2000`) keeps every request
|
|
161
|
+
waiting, so you can snap the spinner or skeleton; `offline` / `online` steps drop the connection for `fetch` / XHR,
|
|
162
|
+
switch `navigator.onLine` and fire the events, so an app's offline banner or retry logic shows up for real.
|
|
163
|
+
|
|
147
164
|
## Keyboard, mouse and focus
|
|
148
165
|
|
|
149
166
|
Use the real input steps, not `js:` clicks, whenever the question is about what a person experiences:
|
|
@@ -151,6 +168,7 @@ Use the real input steps, not `js:` clicks, whenever the question is about what
|
|
|
151
168
|
| Question | Steps |
|
|
152
169
|
|---|---|
|
|
153
170
|
| 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 |
|
|
171
|
+
| Does Tab go through the right elements, in order, and stay inside a dialog? | `focusorder` (after opening the dialog); every stop should say `in dialog`, and it should loop back |
|
|
154
172
|
| Is the focus ring visible and not clipped? | `key:Tab` until the element is focused, then `snap:ring@<selector>` (8 px margin included) |
|
|
155
173
|
| Does the hover state look right? | `hover:<selector>`, `wait:300` for the transition, `snap:x@<selector>` |
|
|
156
174
|
| Does typing work (validation, masks, Cyrillic…)? | `click:<input>`, `type:<text>`, `print:` the value / error text |
|
|
@@ -177,6 +195,22 @@ npx webview-preview /screen <steps> snap:screen "snap:panel@[data-test=panel]" -
|
|
|
177
195
|
change is a regression to explain. Use a separate `--baseline` folder for such throw-away checks so you do not
|
|
178
196
|
overwrite the project's committed baselines. Run `webview-preview clean` when you are done with the snapshots.
|
|
179
197
|
|
|
198
|
+
When the user should see the result too, add `--report` and give them the path to `report.html`: baseline, this run
|
|
199
|
+
and the difference side by side, with the run's output on top.
|
|
200
|
+
|
|
201
|
+
## Sizes, theme and motion
|
|
202
|
+
|
|
203
|
+
A layout change can break only the narrow window or only the dark theme. Check them in the same run instead of
|
|
204
|
+
asking the user:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
npx webview-preview /screen <steps> snap:screen --size 1200x650,800x600
|
|
208
|
+
npx webview-preview /screen <steps> snap:screen --color-scheme dark
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Each size runs the steps from the start and names the snapshots `<prefix><WxH>-<name>.png`. `--reduced-motion`
|
|
212
|
+
makes a page that respects it skip its transitions, so a snapshot right after a click shows the final state.
|
|
213
|
+
|
|
180
214
|
## Still out of reach — and a few traps
|
|
181
215
|
|
|
182
216
|
**Fractional scroll.** `scrollTop = 55.5` is rounded; real wheel and touchpad scrolling can stop between pixels. A
|
|
@@ -186,6 +220,9 @@ overwrite the project's committed baselines. Run `webview-preview clean` when yo
|
|
|
186
220
|
WebKitGTK). The app opens in its default state — the default tab, no saved session — so reach the state you need with
|
|
187
221
|
steps or `init`; do not rely on what an earlier run clicked.
|
|
188
222
|
|
|
223
|
+
**Tab on macOS.** WebKit on a Mac tabs to text fields only unless Full Keyboard Access is on; buttons and links are
|
|
224
|
+
skipped. That is what users of the app on a Mac get too — use `key:Alt+Tab` there to walk every element.
|
|
225
|
+
|
|
189
226
|
**Windows' WebView2 itself.** `msedge` is the same engine, not the same embedding; window chrome, scrollbars of the
|
|
190
227
|
host and DPI handling of a native window can differ.
|
|
191
228
|
|
|
@@ -202,6 +239,9 @@ host and DPI handling of a native window can differ.
|
|
|
202
239
|
- The numbers you defined in step 1 match, in the engine the user runs (`webkitgtk` for Tauri on Linux, `msedge` for
|
|
203
240
|
Windows).
|
|
204
241
|
- No `js failed` / `print failed` / selector errors you did not expect; exit code 0.
|
|
242
|
+
- What you measured by eye is pinned with `assert:` — `assert:document.activeElement.matches('[role=dialog] button')`,
|
|
243
|
+
`assert:getComputedStyle(document.querySelector('.card')).borderRadius === '12px'` — so a later run proves it in one
|
|
244
|
+
line. `assert:` shows only the final value; for `a === b` print `a` first if it fails.
|
|
205
245
|
- Keyboard and hover behaviour checked with `key:` / `hover:` where the change touches them.
|
|
206
246
|
- You looked at one snapshot of the final state.
|
|
207
247
|
- You listed what could not be checked: fractional scroll, the other platform's engine, real backend data.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# webview-preview
|
|
2
2
|
|
|
3
|
+
[](https://github.com/n0t4u404/webview-preview/actions/workflows/test.yml)
|
|
4
|
+
|
|
3
5
|
See your UI the way your desktop app really renders it. `webview-preview` opens a page in the **same engine your app
|
|
4
6
|
runs on** — WebKitGTK on Linux, Edge (the WebView2 engine) on Windows, WebKit on macOS — runs a list of steps
|
|
5
7
|
(click, type, wait, measure) and saves PNG snapshots.
|
|
@@ -15,15 +17,23 @@ npx webview-preview https://example.com snap:home
|
|
|
15
17
|
# → .webview-preview/webkitgtk-home.png (on Linux)
|
|
16
18
|
```
|
|
17
19
|
|
|
20
|
+
**What it is — and what it is not.** A tool to *look at* a screen in the real engine: open it in the state you need,
|
|
21
|
+
press keys, measure, snap, compare with yesterday. Steps stay one-liners you can type in a terminal; mocks and
|
|
22
|
+
desktop stubs only say *what to answer* so the screen renders. It is not a test runner: no test suites, no stateful
|
|
23
|
+
mocks, no `expect` library, no retries. For end-to-end tests use Playwright Test or WebdriverIO — the stubs here are
|
|
24
|
+
plain init scripts and fit there too.
|
|
25
|
+
|
|
18
26
|
- [Which engine matches my app](#which-engine-matches-my-app)
|
|
19
27
|
- [Install](#install)
|
|
20
28
|
- [Quick start](#quick-start)
|
|
21
29
|
- [Config file](#config-file)
|
|
22
30
|
- [Steps](#steps)
|
|
31
|
+
- [Macros](#macros)
|
|
32
|
+
- [Errors in the page](#errors-in-the-page)
|
|
23
33
|
- [Command line](#command-line)
|
|
24
34
|
- [JavaScript API](#javascript-api)
|
|
25
35
|
- [Login, backend and other things outside the page](#login-backend-and-other-things-outside-the-page)
|
|
26
|
-
- [
|
|
36
|
+
- [Desktop bridges: Tauri, Electron, Wails](#desktop-bridges-tauri-electron-wails)
|
|
27
37
|
- [Comparing with a baseline](#comparing-with-a-baseline)
|
|
28
38
|
- [Cleaning up snapshots](#cleaning-up-snapshots)
|
|
29
39
|
- [Recipes](#recipes)
|
|
@@ -148,7 +158,12 @@ npx webview-preview /settings snap:settings
|
|
|
148
158
|
| `server.cwd` | Folder for `server.command`. | the config's folder |
|
|
149
159
|
| `server.readyTimeoutMs` | How long to wait for the port. | `30000` |
|
|
150
160
|
| `engine` | See [engines](#which-engine-matches-my-app). | `auto` |
|
|
151
|
-
| `size` | Window size in CSS pixels, `WxH`. | `1200x650` |
|
|
161
|
+
| `size` | Window size in CSS pixels, `WxH`. Several sizes — `"1200x650,800x600"` or a list — run the steps once per size; snapshots get the size in the name: `webkitgtk-800x600-page.png`. | `1200x650` |
|
|
162
|
+
| `colorScheme` | `prefers-color-scheme` for the page: `light` or `dark`. | system |
|
|
163
|
+
| `reducedMotion` | `prefers-reduced-motion: reduce` for the page. | `false` |
|
|
164
|
+
| `report` | Write `<out>/report.html` after every run. See [Comparing with a baseline](#comparing-with-a-baseline). | `false` |
|
|
165
|
+
| `watch` | What `--watch` follows: a folder, a file or a list, relative to the config's folder. The config file itself is always followed. | `src` if it exists, else the config's folder |
|
|
166
|
+
| `offline` / `latency` | Start offline / delay every `fetch` / XHR request by ms. See [Slow network and offline](#4-slow-network-and-offline). | `false` / `0` |
|
|
152
167
|
| `out` | Snapshot folder, relative to the config's folder. | `.webview-preview` |
|
|
153
168
|
| `prefix` | File name prefix. | `<engine>-` |
|
|
154
169
|
| `scale` | Device pixel ratio for Playwright engines (`2` = HiDPI). WebKitGTK: off-screen snapshots are 1×, `--headed` ones follow your screen. | `1` |
|
|
@@ -160,6 +175,11 @@ npx webview-preview /settings snap:settings
|
|
|
160
175
|
| `init` | Code that runs **before the page's own scripts**, on every load: a string, `{ file: "path" }`, or a list of them. See [below](#1-init--set-things-up-before-the-app-starts). | — |
|
|
161
176
|
| `mock` | Answers for `fetch` / `XMLHttpRequest` requests: `{ "GET /api/me": { json: { … } } }`. See [below](#2-mock--answer-backend-requests). | — |
|
|
162
177
|
| `logRequests` | List every `fetch` / `XMLHttpRequest` request in `window.__webviewPreviewRequests` (also on whenever `mock` is set). | `false` |
|
|
178
|
+
| `tauri` | Stub the Tauri bridge: `true`, or `{ commands: { … }, label: "main" }`. See [Desktop bridges](#desktop-bridges-tauri-electron-wails). | — |
|
|
179
|
+
| `globals` | Objects to put on `window` before the page starts, by path: `{ "window.api": anyApi({ … }) }`. See [Desktop bridges](#desktop-bridges-tauri-electron-wails). | — |
|
|
180
|
+
| `locale` | `navigator.language` for the page, `en-US`, `ru-RU` … Playwright engines also switch `Intl` and `Accept-Language`; WebKitGTK only `navigator.language`. | system |
|
|
181
|
+
| `macros` | Named lists of steps, run with `macro:<name>`. See [Macros](#macros). | — |
|
|
182
|
+
| `failOnError` | Exit with 1 when the page logged `console.error` or threw. See [Errors in the page](#errors-in-the-page). | `false` |
|
|
163
183
|
|
|
164
184
|
Command-line options win over the config file.
|
|
165
185
|
|
|
@@ -211,7 +231,13 @@ Steps run one after another, after the page has loaded and settled for 1.5 s.
|
|
|
211
231
|
| `click:<selector>` | Clicks the middle of the element with the real mouse — focus moves, `:active` and `:focus` apply like for a person. |
|
|
212
232
|
| `print:<expression>` | Prints the value: strings as they are, DOM elements as their HTML (first 200 characters), everything else as JSON. Promises are not awaited. |
|
|
213
233
|
| `measure:<selector>` | Prints one line about the element: how many match, position and size, margin / padding / border, visible or hidden, display, opacity, font, colours and text. |
|
|
234
|
+
| `offline` / `online` | Drops the connection for `fetch` / XHR and brings it back, with `navigator.onLine` and the events, then waits 700 ms like `js:`. See [Slow network and offline](#4-slow-network-and-offline). |
|
|
235
|
+
| `latency:<ms>` | Delays every `fetch` / XHR request from now on; `latency:0` turns it off. |
|
|
236
|
+
| `focusorder` / `focusorder:<n>` | Presses `Tab` with the real keyboard n times (20 by default) and prints where focus went, one line per stop — tag, role, name, label, "in dialog" — and `→ back to 1 after N stops` when it loops. Shows a focus trap, a skipped field or focus escaping a dialog at a glance. Starts from the current focus. |
|
|
237
|
+
| `assert:<expression>` | Checks that the expression is truthy: prints `assert ok: …`, or `assert failed: … — got <value>` / `— threw <message>` and makes the run exit with 1. The other steps still run. |
|
|
238
|
+
| `film:<name>:<ms>[/<n>][@<selector>]` | Takes n frames (10 by default, up to 60) evenly over ms, starting now, and puts them into one `<prefix><name>.png`, 5 per row, left to right. With a selector every frame is cropped to the area the element covered during the whole film (plus `--snap-margin`). Prints the real time of every frame: `film open: 10 frames at 0 55 111 … 500 ms`. Put it right after the step that starts the animation. |
|
|
214
239
|
| `resize:<W>x<H>` | Resizes the window. |
|
|
240
|
+
| `macro:<name>` | Runs the steps of a macro from the config. See [Macros](#macros). |
|
|
215
241
|
|
|
216
242
|
### Selectors
|
|
217
243
|
|
|
@@ -247,6 +273,50 @@ npx webview-preview / \
|
|
|
247
273
|
|
|
248
274
|
On Windows PowerShell use the same double quotes; inside them, keep JavaScript strings in single quotes.
|
|
249
275
|
|
|
276
|
+
## Macros
|
|
277
|
+
|
|
278
|
+
Steps you repeat — signing in, opening a dialog — go into the config under a name:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
export default {
|
|
282
|
+
macros: {
|
|
283
|
+
signIn: [
|
|
284
|
+
"js:location.hash = '#/sign-in'",
|
|
285
|
+
"waitfor:input[name=email]",
|
|
286
|
+
"click:input[name=email]",
|
|
287
|
+
"type:demo@example.com",
|
|
288
|
+
"click:input[name=password]",
|
|
289
|
+
"type:secret",
|
|
290
|
+
"click:button[type=submit]",
|
|
291
|
+
"waitfor:[role=tab]"
|
|
292
|
+
],
|
|
293
|
+
openSettings: ["macro:signIn", "click:text=Settings", "waitfor:[role=dialog]"]
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
npx webview-preview / macro:openSettings snap:settings
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
A macro may run other macros; one that runs itself stops with an error. Prefer real clicks and typing over setting
|
|
303
|
+
values from code: the run then goes through the same handlers, focus and validation as for a person.
|
|
304
|
+
|
|
305
|
+
## Errors in the page
|
|
306
|
+
|
|
307
|
+
Every run collects what went wrong inside the page — `console.error` calls, uncaught exceptions and unhandled promise
|
|
308
|
+
rejections — and prints them after the steps, the same message once with a count:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
console.error: Failed to load {"code":500} (×2)
|
|
312
|
+
unhandled rejection: Error: no session — global code@http://localhost:5173/src/app.tsx:4:25
|
|
313
|
+
uncaught: TypeError: null is not an object (evaluating 'user.name') — onclick@http://localhost:5173/src/app.tsx:12:5
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
A blank snapshot usually has its reason here. With `--fail-on-error` (`failOnError: true`) the run exits with 1 when
|
|
317
|
+
there is anything to print — useful in CI. Pages opened from `file://` report uncaught errors only as
|
|
318
|
+
"Script error." in WebKit; serve the page over `http://` to see them.
|
|
319
|
+
|
|
250
320
|
## Command line
|
|
251
321
|
|
|
252
322
|
```
|
|
@@ -256,7 +326,11 @@ webview-preview [route-or-url] [steps...] [options]
|
|
|
256
326
|
| Option | |
|
|
257
327
|
|---|---|
|
|
258
328
|
| `--engine <name>` | `auto`, `webkitgtk`, `msedge`, `chrome`, `chromium`, `webkit` |
|
|
259
|
-
| `--size <WxH>` | Window size |
|
|
329
|
+
| `--size <WxH>` | Window size; several, comma-separated: `1200x650,800x600` |
|
|
330
|
+
| `--color-scheme <light\|dark>` | `prefers-color-scheme` for the page |
|
|
331
|
+
| `--reduced-motion` | `prefers-reduced-motion: reduce` for the page |
|
|
332
|
+
| `--watch` | Run again whenever a file changes; the server stays up between runs. Ctrl+C to stop |
|
|
333
|
+
| `--offline` / `--latency <ms>` | Start offline / delay every `fetch` / XHR request |
|
|
260
334
|
| `--out <dir>` | Snapshot folder |
|
|
261
335
|
| `--prefix <text>` | File name prefix |
|
|
262
336
|
| `--scale <n>` | Device pixel ratio (Playwright engines) |
|
|
@@ -264,6 +338,7 @@ webview-preview [route-or-url] [steps...] [options]
|
|
|
264
338
|
| `--snap-margin <px>` | Room around `snap:name@selector` |
|
|
265
339
|
| `--wait-timeout <ms>` | How long `waitfor:` waits |
|
|
266
340
|
| `--keep-storage` | WebKitGTK: keep storage between runs |
|
|
341
|
+
| `--report` | Write `<out>/report.html` with the output and all snapshots (baseline and difference after `--compare`) |
|
|
267
342
|
| `--compare` / `--update-baseline` | Compare snapshots with baselines / replace the baselines |
|
|
268
343
|
| `--baseline <dir>` / `--max-diff-pixels <n>` / `--compare-tolerance <n>` | Baseline folder, allowed difference |
|
|
269
344
|
| `--base-url <url>` | Base for routes |
|
|
@@ -271,6 +346,9 @@ webview-preview [route-or-url] [steps...] [options]
|
|
|
271
346
|
| `--init-file <file>` | Same, from a file (repeatable) |
|
|
272
347
|
| `--mock-file <file>` | Mocks from a `.json` or `.mjs` file (repeatable, added to the config's `mock`) |
|
|
273
348
|
| `--log-requests` | Only list requests, answer nothing |
|
|
349
|
+
| `--tauri` | Stub the Tauri bridge (answers from `tauri.commands` in the config) |
|
|
350
|
+
| `--locale <tag>` | `navigator.language` for the page |
|
|
351
|
+
| `--fail-on-error` | Exit with 1 when the page logged `console.error` or threw |
|
|
274
352
|
| `--config <file>` | Use this config file |
|
|
275
353
|
| `--no-config` | Ignore config files |
|
|
276
354
|
| `-h`, `--help` / `-v`, `--version` | |
|
|
@@ -309,6 +387,9 @@ result.failed; // false
|
|
|
309
387
|
`preview()` reads the config file the same way the CLI does; pass `config: false` to skip it or `config: "path"` to
|
|
310
388
|
choose one. Types are included.
|
|
311
389
|
|
|
390
|
+
`watchPreview(options, onRun)` is `--watch`: it runs once, then again on every change, calls `onRun(result)` after
|
|
391
|
+
each run and resolves to a function that stops watching and the server.
|
|
392
|
+
|
|
312
393
|
## Login, backend and other things outside the page
|
|
313
394
|
|
|
314
395
|
A real app rarely shows the screen you want straight away: it checks a session and sends you to a login page, it
|
|
@@ -321,8 +402,8 @@ Change what the page *receives* instead, only for the preview run:
|
|
|
321
402
|
| A session or token kept in `localStorage`, `sessionStorage`, cookies | [`init`](#1-init--set-things-up-before-the-app-starts) |
|
|
322
403
|
| Requests to your backend (`fetch`, `axios`, `XMLHttpRequest`) | [`mock`](#2-mock--answer-backend-requests) |
|
|
323
404
|
| You don't know yet what the app requests | [`--log-requests`](#3-find-out-what-the-app-requests) |
|
|
324
|
-
| Desktop APIs (Tauri `invoke`,
|
|
325
|
-
| Login that lives only in memory, or a flow you want to click through | steps
|
|
405
|
+
| Desktop APIs (Tauri `invoke`, Electron preload, Wails bindings) | [`tauri`, `globals`](#desktop-bridges-tauri-electron-wails) |
|
|
406
|
+
| Login that lives only in memory, or a flow you want to click through | steps, kept as a [macro](#macros): `macro:signIn` |
|
|
326
407
|
|
|
327
408
|
### 1. `init` — set things up before the app starts
|
|
328
409
|
|
|
@@ -419,92 +500,99 @@ With `mock` set, the same list shows which requests were answered: `GET /api/me
|
|
|
419
500
|
desktop side does by itself (a Tauri command that calls a server from Rust). For desktop calls, stub the bridge in
|
|
420
501
|
`init`; for the rest, run the real service locally.
|
|
421
502
|
|
|
422
|
-
|
|
503
|
+
### 4. Slow network and offline
|
|
423
504
|
|
|
424
|
-
|
|
425
|
-
screen you want to see may never render.
|
|
505
|
+
How does the screen look while data is on its way, and what happens when the connection drops?
|
|
426
506
|
|
|
427
|
-
|
|
428
|
-
|
|
507
|
+
```sh
|
|
508
|
+
npx webview-preview /dashboard --latency 2000 wait:300 snap:loading wait:2500 snap:loaded
|
|
509
|
+
npx webview-preview /dashboard waitfor:.instance-card offline snap:offline online wait:1000 snap:back-online
|
|
510
|
+
```
|
|
429
511
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
512
|
+
| | |
|
|
513
|
+
|---|---|
|
|
514
|
+
| `latency:<ms>` step / `--latency <ms>` / `latency` in the config | Every `fetch` / XHR request waits that long first, answers from `mock` too. `latency:0` turns it off. |
|
|
515
|
+
| `offline` / `online` steps, `--offline` / `offline: true` | `navigator.onLine` turns `false`, the page gets the `offline` event, `fetch` rejects with `TypeError: Failed to fetch`, XHR fires `error`. `online` brings it all back and fires `online`. `--offline` starts that way; the page itself still loads. |
|
|
433
516
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
517
|
+
While offline, the request list shows `GET /api/me -> offline`. Like `mock`, this covers `fetch` and XHR only:
|
|
518
|
+
pictures, WebSocket and the desktop side keep their connection.
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
Your frontend calls the desktop side: Tauri's `invoke`, an Electron preload API, Wails bindings. Outside the app
|
|
522
|
+
there is no desktop side, so those calls fail — and the screen you want may never render. `webview-preview` puts a
|
|
523
|
+
stand-in in place before the page starts, so you open **your real entry page**, not a copy made for previews.
|
|
524
|
+
|
|
525
|
+
Every call to a stand-in is listed in `window.__webviewPreviewRequests`, next to `fetch` / XHR requests — print it
|
|
526
|
+
to see what the page asks for and what is still unanswered:
|
|
527
|
+
|
|
528
|
+
```sh
|
|
529
|
+
npx webview-preview / wait:2000 "print:__webviewPreviewRequests.join('\n')"
|
|
530
|
+
# invoke plugin:app|version -> stub
|
|
531
|
+
# invoke get_settings -> null
|
|
532
|
+
# call window.api.onUpdate -> default
|
|
439
533
|
```
|
|
440
534
|
|
|
535
|
+
### Tauri
|
|
536
|
+
|
|
441
537
|
```js
|
|
442
|
-
// webview-preview.config.mjs
|
|
443
538
|
export default {
|
|
444
539
|
baseUrl: "http://localhost:1420",
|
|
445
540
|
server: { command: "npx vite --port 1420 --strictPort", port: 1420 },
|
|
446
|
-
|
|
541
|
+
tauri: {
|
|
542
|
+
commands: {
|
|
543
|
+
"plugin:app|version": "1.4.0",
|
|
544
|
+
get_settings: { theme: "dark" },
|
|
545
|
+
greet: ({ name }) => `Hello, ${name}`
|
|
546
|
+
}
|
|
547
|
+
}
|
|
447
548
|
}
|
|
448
549
|
```
|
|
449
550
|
|
|
450
|
-
|
|
451
|
-
stubs, give the preview its own entry:
|
|
452
|
-
|
|
453
|
-
1. **A stub** — set before anything imports `@tauri-apps/api`:
|
|
454
|
-
|
|
455
|
-
```ts
|
|
456
|
-
// src/preview/tauri-stub.ts
|
|
457
|
-
const answers: Record<string, unknown> = { app_version: "0.0.1" }
|
|
551
|
+
Or just `--tauri` on the command line. What you get (Tauri 2, `@tauri-apps/api`):
|
|
458
552
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
553
|
+
- `invoke` answers from `commands` — a value, or a function of the arguments (it may be `async`). A command not in
|
|
554
|
+
the list gets `null`. Plugin commands are named as `@tauri-apps/api` sends them: `plugin:<plugin>|<command>`.
|
|
555
|
+
- Built-in answers you can override: `plugin:app|version` / `name` / `tauri_version`, window state
|
|
556
|
+
(`plugin:window|is_maximized` → `false`, `is_focused` → `true`, `scale_factor`, `inner_size` …).
|
|
557
|
+
- Events work inside the page: `listen`, `once`, `emit`, `unlisten`. To act as the Rust side, emit from a step:
|
|
558
|
+
`js:__webviewPreview.emit("download-progress", { percent: 40 })`.
|
|
559
|
+
- `getCurrentWindow()` / `getCurrentWebview()` get the label `main` (`label` changes it), `isTauri()` is `true`,
|
|
560
|
+
`convertFileSrc` and `path.sep` work.
|
|
467
561
|
|
|
468
|
-
|
|
562
|
+
Functions in `commands` are copied into the page as source code, so they cannot use variables from the config
|
|
563
|
+
file — only their arguments and what is in the page.
|
|
469
564
|
|
|
470
|
-
|
|
471
|
-
// src/preview/index.tsx
|
|
472
|
-
import "./tauri-stub";
|
|
473
|
-
import { createRoot } from "react-dom/client";
|
|
474
|
-
import { App } from "../app";
|
|
565
|
+
### Electron, Wails and other bridges — `globals`
|
|
475
566
|
|
|
476
|
-
|
|
477
|
-
|
|
567
|
+
`globals` puts objects on `window` by path before the page starts. `anyApi()` is a stand-in that answers **any**
|
|
568
|
+
call: awaiting it gives `null`, calling the result works too (handy for "subscribe" functions that return an
|
|
569
|
+
"unsubscribe"). Pass the members you care about:
|
|
478
570
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
3. **Helpers** for states that are tedious to reach with clicks — signing in, switching language, going offline:
|
|
483
|
-
|
|
484
|
-
```ts
|
|
485
|
-
Object.assign(window, {
|
|
486
|
-
preview: {
|
|
487
|
-
signIn: () => { /* fill the form, submit */ },
|
|
488
|
-
language: (code: string) => i18n.changeLanguage(code)
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
```
|
|
571
|
+
```js
|
|
572
|
+
import { anyApi, defineConfig } from "webview-preview";
|
|
492
573
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
574
|
+
export default defineConfig({
|
|
575
|
+
globals: {
|
|
576
|
+
// Electron: what your preload exposes with contextBridge.exposeInMainWorld("api", …)
|
|
577
|
+
"window.api": anyApi({
|
|
578
|
+
getVersion: async () => "1.4.0",
|
|
579
|
+
platform: "linux"
|
|
580
|
+
}),
|
|
581
|
+
|
|
582
|
+
// Wails: generated bindings and the runtime
|
|
583
|
+
"window.go": anyApi(),
|
|
584
|
+
"window.runtime": anyApi()
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
```
|
|
496
588
|
|
|
497
|
-
|
|
589
|
+
Plain values work as well: `"window.__APP_CONFIG__": { apiUrl: "http://localhost:3000" }`.
|
|
498
590
|
|
|
499
|
-
|
|
500
|
-
export default {
|
|
501
|
-
baseUrl: "http://localhost:1420/src/preview/index.html#",
|
|
502
|
-
server: { command: "npx vite --port 1420 --strictPort", port: 1420 }
|
|
503
|
-
}
|
|
504
|
-
```
|
|
591
|
+
### What stays different
|
|
505
592
|
|
|
506
|
-
Real Rust is not involved
|
|
507
|
-
checking a screen in seconds
|
|
593
|
+
Real Rust, Node or Go is not involved: anything that depends on it shows what the stand-in answers. Window chrome,
|
|
594
|
+
native menus and dialogs are not there. That is the trade-off for checking a screen in seconds without building
|
|
595
|
+
the app.
|
|
508
596
|
|
|
509
597
|
## Comparing with a baseline
|
|
510
598
|
|
|
@@ -526,6 +614,9 @@ npx webview-preview /settings snap:settings "snap:menu@[role=menu]" --compare
|
|
|
526
614
|
The `.diff.png` shows the snapshot in grey with the changed pixels in red. More than `maxDiffPixels` changed
|
|
527
615
|
(default `0`) → exit code `1`.
|
|
528
616
|
- **The change is intended**: `--update-baseline` replaces the baselines with this run's snapshots.
|
|
617
|
+
- **See it all on one page**: add `--report`. `<out>/report.html` shows the run's output and every snapshot —
|
|
618
|
+
after `--compare` with the baseline, this run and the difference side by side, changed ones marked red. Open it in
|
|
619
|
+
any browser; it works without a server and follows the system's light or dark theme.
|
|
529
620
|
|
|
530
621
|
Snapshots differ between engines and screen scales, so the engine is part of the file name (`webkitgtk-…`,
|
|
531
622
|
`msedge-…`) and each engine has its own baselines. `compareTolerance` (default `2` of 255 per channel) ignores
|
|
@@ -557,8 +648,38 @@ for engine in webkitgtk chrome; do npx webview-preview /settings --engine $engin
|
|
|
557
648
|
|
|
558
649
|
```sh
|
|
559
650
|
npx webview-preview / snap:wide resize:900x500 wait:300 snap:narrow
|
|
651
|
+
npx webview-preview / snap:home --size 1200x650,800x600,480x800
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**Keep a snapshot fresh while you edit** — leave it running next to the editor and keep the PNG open:
|
|
655
|
+
|
|
656
|
+
```sh
|
|
657
|
+
npx webview-preview /settings waitfor:.settings snap:settings "measure:.settings" --watch
|
|
560
658
|
```
|
|
561
659
|
|
|
660
|
+
Every save runs the steps again from a clean page (the dev server stays up), prints the numbers and rewrites the
|
|
661
|
+
snapshot. A change of the config file is picked up too.
|
|
662
|
+
|
|
663
|
+
**See an animation, not only where it ends** — a dialog that opens:
|
|
664
|
+
|
|
665
|
+
```sh
|
|
666
|
+
npx webview-preview / "click:text=New project" "film:open:500@[role=dialog]"
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
One picture with 10 frames: you see whether it fades and grows smoothly, starts from the wrong place, jumps at the end
|
|
670
|
+
or plays twice. Frames are real screenshots taken as fast as the engine allows, so the printed times are what to
|
|
671
|
+
trust, not the planned spacing.
|
|
672
|
+
|
|
673
|
+
**Light and dark, with and without motion:**
|
|
674
|
+
|
|
675
|
+
```sh
|
|
676
|
+
for scheme in light dark; do npx webview-preview / snap:home-$scheme --color-scheme $scheme; done
|
|
677
|
+
npx webview-preview / click:.menu-button snap:menu-open --reduced-motion
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
With `--reduced-motion` a page that respects it skips its transitions, so a snapshot right after a click shows the
|
|
681
|
+
final state instead of a frame in the middle of the animation — and you see whether the page respects it at all.
|
|
682
|
+
|
|
562
683
|
**Measure instead of looking** — sizes and positions are more reliable than eyes:
|
|
563
684
|
|
|
564
685
|
```sh
|
|
@@ -599,6 +720,8 @@ More of these — animations frame by frame, measuring, getting past login — i
|
|
|
599
720
|
| `WebKitGTK engine needs Linux with python3-gi and WebKit2 4.1` | Install the [Linux packages](#linux--webkitgtk). |
|
|
600
721
|
| `This engine needs playwright-core` | `npm i -D playwright-core`. |
|
|
601
722
|
| `Executable doesn't exist` (Playwright) | `npx playwright install webkit` (or `chromium`); for `msedge` / `chrome` install the browser itself. |
|
|
723
|
+
| macOS, `webkit`: `Tab` skips buttons and links | That is macOS: with Full Keyboard Access off (the default) WebKit tabs to text fields only — a Tauri app on a Mac behaves the same. Use `key:Alt+Tab` to reach every element, as Option+Tab does in Safari. |
|
|
724
|
+
| WebKitGTK in CI | Run under `xvfb-run -a` (it needs an X display even off-screen); `sudo apt-get install -y python3-gi gir1.2-webkit2-4.1 xvfb`. |
|
|
602
725
|
| `Server did not open port …` | The `server.command` failed or uses another port. Run it by hand to see why; raise `readyTimeoutMs` for slow starts. |
|
|
603
726
|
| `"/route" is not a full URL` | Set `baseUrl` in the config or pass `--base-url`. |
|
|
604
727
|
| Blank snapshot | The page needs more time or threw on start: add `wait:2000`, and `print:document.body.innerHTML.length`. |
|
|
@@ -619,6 +742,17 @@ agent to it (for example from `CLAUDE.md` or `AGENTS.md` in your project):
|
|
|
619
742
|
UI changes: check them in the real engine with `npx webview-preview` — see node_modules/webview-preview/AGENTS.md.
|
|
620
743
|
```
|
|
621
744
|
|
|
745
|
+
## Development
|
|
746
|
+
|
|
747
|
+
```sh
|
|
748
|
+
npm test # the default engine for this system
|
|
749
|
+
WEBVIEW_PREVIEW_ENGINE=chrome npm test # one engine
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
The tests open `test/fixture/index.html` and check snapshots, input, focus order, errors, mock, offline, sizes,
|
|
753
|
+
`film:`, compare and the report. GitHub Actions runs them on every push: WebKitGTK and Chromium on Linux, Edge and
|
|
754
|
+
Chrome on Windows, WebKit on macOS; snapshots and reports of every run are kept as artifacts.
|
|
755
|
+
|
|
622
756
|
## License
|
|
623
757
|
|
|
624
758
|
MIT © n0t4u404
|