# Visual stand (Level-2 pixels)
Reproducible, case-driven screenshots. **No crop rectangles. No “sleep then shoot”.**
Hosts are pinned; readiness is polled (`awaitStable`). Goldens update **one case at a time**.
## Rules
1. Wait on a real condition (two identical frame hashes + non-uniform bitmap), with a deadline.
2. Capture the **element** (`AppWidgetHostView`, SwiftUI view bounds, `#root`) — never a full-screen crop.
3. Environment pinned: locale `en_US`, UTC, font scale 1.0, animations off, theme from the case.
4. Time frozen (`2026-08-01T12:00:00Z` on desktop; emulator/sim date in up-scripts). Fixtures use absolute ISO.
5. Store isolated per case (Android: reset prefs + delete appWidgetId).
6. Record only with an explicit flag **and** `CASE=<name>`.
## Layout
```
tests/cases/<name>.json # { fixture, size, theme, locale, macos? }
tests/fixtures/... # IR
tests/golden/{android,ios,desktop,macos,windows,linux}/<name>.png
out/{platform}/ # actual + diff (gitignored)
```
Add a check by dropping a JSON under `tests/cases/` — runners pick it up automatically.
Example case:
```json
{
"fixture": "presets/weather",
"size": "small",
"theme": "dark",
"locale": "en_US",
"macos": { "width": 158, "height": 158 }
}
```
`macos` is the **native WidgetKit canvas on Mac** (not iPhone points, not the desktop WKWebView window). Defaults: small 158², medium 338×158, large 338×354.
## Android (AppWidgetHost)
Real Glance → RemoteViews → `AppWidgetHostView`. No launcher, no manual placement.
```bash
just android-up
# grantbind is applied for git.s00d.widgets and git.s00d.widgets.test
# Goldens are written under the instrumentation (.test) package path.
just test-android-visual
just record-android weather.small
adb pull \
/storage/emulated/0/Android/data/git.s00d.widgets.test/files/widgets-golden/ \
tests/golden/android/
```
Filter / record via Gradle:
```bash
cd android && ./gradlew :connectedDebugAndroidTest \
-Pgolden.record=true \
-Pcase=weather.small \
-Pandroid.testInstrumentationRunnerArguments.class=git.s00d.widgets.WidgetRenderTest
```
Robolectric (`pnpm test:android`) remains **Level-1 geometry only** — it does not own pixel goldens.
**Note:** `tests/golden/android/*.png` seeded from the old Robolectric path will not match AppWidgetHost until you re-record on a pinned emulator (`just record-android <case>` + `adb pull`).
## iOS (SwiftUI + WidgetKit chrome, not SpringBoard)
Renders `DynamicElementView` with shared `WidgetChrome` (container background + continuous corner radius). Not SpringBoard chrome.
```bash
just ios-up # optional pinned simulator for xcodebuild destinations
just test-ios-visual
CASE=weather.small GOLDEN_RECORD=1 just test-ios-visual
# or:
just record-ios weather.small
```
`swift test` runs ImageRenderer on the host (macOS). Use `ios-up` when you switch to `xcodebuild test` on a simulator destination.
Bootstrap after chrome changes (migration only — not day-to-day):
```bash
GOLDEN_RECORD=1 GOLDEN_RECORD_ALL=1 swift test --filter RenderTests/testCases
```
Day-to-day always use `CASE=<name>` + `GOLDEN_RECORD=1` for a single case.
## macOS Level A — AppKit `DynamicElementView` (no sim)
On a Mac there are **three** different surfaces — do not mix them:
| Desktop probe window | `widget.html` in WKWebView | Playwright (`tests/golden/desktop`) |
| Native WidgetKit | Notification Center / desktop widget | same `DynamicElementView` as iOS + **AppKit** branches |
| Transport / packaging | App Group / sandbox / UserDefaults, ad-hoc sign, `build-widget.sh` | **Level C** (below) |
## Cross-platform render receipts
Config writes are fire-and-forget on every platform (`updateAll`, `reloadAllTimelines`, `emit`). Renderers write a **receipt** after paint into a sibling store (`widget_receipts.json` / `__tauri_widget_receipts__`) — never the config map (would bump nonce).
```ts
const live = await getWidgetDiagnostics("group.com.example.app");
// [{ widgetId, instance, size, nonce, source, rendered, skipped, ts }]
```
- **macOS:** host uses one config-chosen transport; receipts are diagnostics only (`getWidgetDiagnostics`).
- **iOS:** `transport=appGroup` only — other values fail at plugin init.
- **Android:** receipts list live `appWidgetId`s so `syncConfigToGlanceState` targets confirmed instances.
- **Desktop:** register listeners **before** `loadConfig()`; `widget.html` reports `source: push|pull` after render.
## Level C — macOS transports + packaging (no pixels)
Real host bugs live here: single-driver write/read, action queue, App Group entitlements, `.appex` embed.
**C1 — transports** (seconds; override root, never mutate process `HOME`):
```bash
just test-macos-transports
# Rust unit (config driver / fakes): cargo test -p tauri-plugin-widgets transport::
# Rust file IO: cargo test --test macos_transports
# Swift: cd swift && swift test --filter TransportTests
```
Env knobs: `WIDGET_TRANSPORT`, `WIDGET_CONTAINER_ROOT`, `WIDGET_EXTENSION_BUNDLE`, `WIDGET_APP_GROUP_DATA_FILE` (and Swift `WIDGET_SANDBOX_DATA_FILE`).
**C2 — build pipeline** (slow; needs `xcodegen` + Xcode):
```bash
just test-macos-pipeline
# → tests/macos/pipeline.sh
```
Asserts: `init-macos` → `beforeBundleCommand` + `macOS.files` in conf → signed `.appex` → App Group in entitlements → `widgetkit-extension` → stub bundle via `macOS.files` path under `Contents/PlugIns/`.
Level A runs `swift test` on the host in seconds and hits `#elseif canImport(AppKit)` (`Color.adaptive` via `NSAppearance`, semantic `NSColor`, `NSImage` decode) — which an iOS-simulator run never executes.
```bash
just test-macos-visual
just record-macos weather.small
# bootstrap:
cd swift && GOLDEN_RECORD=1 GOLDEN_RECORD_ALL=1 swift test --filter MacRenderTests
```
Renders via `NSHostingView` at **`case.macos` points** into `tests/golden/macos/`. Not Notification Center chrome; not `widget.html`.
## Desktop (Playwright)
```bash
pnpm test:visual:desktop
just record-desktop weather.small
```
`#root` screenshot after `awaitStable`. Platform folder: `desktop` (darwin), `linux`, `windows` (Playwright desktop webview — not Widgets Board).
## Windows Widgets Board (Adaptive Cards PreviewHost)
Native AC pixels (not `widget.html`):
```bash
# Adaptive Card JSON snapshots (transpile)
pnpm -C scripts cli gen-adaptive-snapshots
# UTM: capture PNG via PreviewHost (AdaptiveCards.Rendering.Wpf, Smoke=false)
just record-windows weather.small
just test-windows-visual
node tests/windows/compare.mjs
```
PNG goldens live in `tests/golden/windows/` (record from VM). See [windows-surfaces.md](windows-surfaces.md).
## Linux desktop (Docker)
Live Tauri webview inside Ubuntu 24.04 (Xvfb + openbox). **CI gate is `xprop`**, not pixels:
- `_NET_WM_WINDOW_TYPE_DESKTOP`
- `_NET_WM_STATE_SKIP_TASKBAR`
Wayland/layer-shell checked separately under sway headless. See [linux-harness.md](linux-harness.md).
```bash
just linux-up
just test-linux-x11
just shot-linux weather small
just record-linux weather.small # copies triage PNG → tests/golden/linux/
```
Geometry Level-1 still lives in `tests/expected/geometry` and updates with `UPDATE_SNAPSHOTS=1` (bulk OK for trees). Pixel goldens never bulk-overwrite.
## Audit (batch contact sheets)
```bash
pnpm audit:sheets
# → out/audit/<case>.png + out/audit/index.html
# panels: Desktop | iOS | macOS | Android | Windows | Linux
open out/audit/index.html
```
Checklist when reviewing: padding/spacing, background fill, color/contrast, overflow/clip, empty/uniform, chrome mismatch.
Findings from the last full pass live in `out/audit/CATALOG.md` (gitignored under `out/`).
## Commands cheat sheet
| Desktop visual | `pnpm test:visual:desktop` |
| Record desktop case | `CASE=x.y GOLDEN_RECORD=1 pnpm test:visual:desktop` |
| Windows AC JSON snapshots | `pnpm -C scripts cli gen-adaptive-snapshots` |
| Windows PNG goldens (UTM) | `just test-windows-visual` / `just record-windows weather.small` |
| Windows xwin check | `just check-windows-xwin` |
| Windows example xwin | `just build-windows-example-xwin` |
| Android visual | `just test-android-visual` |
| Record android case | `just record-android x.y` then `adb pull …` |
| iOS visual | `just test-ios-visual` |
| Record ios case | `just record-ios x.y` |
| macOS AppKit visual | `just test-macos-visual` |
| Record macOS case | `just record-macos x.y` |
| macOS transports (C1) | `just test-macos-transports` |
| macOS pipeline (C2) | `just test-macos-pipeline` |
| Linux up / x11 gate | `just linux-up` / `just test-linux-x11` |
| Linux shot / record | `just shot-linux weather small` / `just record-linux weather.small` |
| Linux Wayland / fallback | `just test-linux-wayland` / `just test-linux-fallback` |
| Hosts status | `just hosts` |
See also [render-testing.md](./render-testing.md) for Level-1 geometry.