Install
$ agentstack add skill-uiverify-uiverify-playwright-visual-testing ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Deterministic Playwright captures (archive-replay)
Mental model
@uiverify/playwright captures each test's final DOM + every resource the page loaded into an archive; UI Verify re-renders and pixel-diffs that archive server-side. So:
> Whatever the page was at the instant of capture is baked into the archive forever. Your job is to > drive the page to one canonical state before the snapshot. UI Verify can't un-see a random A/B variant, > a live feed, or a piece of lazy content that never loaded.
The snapshot fires automatically when the test passes, so your beforeEach + navigation is the determinism surface. Integration is one import:
- import { test, expect } from '@playwright/test';
+ import { test, expect } from '@uiverify/playwright';
What UI Verify handles server-side (don't hand-fix these): it re-renders the archived DOM with animations disabled, prefers-reduced-motion emulated, and fonts and images settled. So pixel-level settling — waiting on document.fonts.ready, sticky-header animation, image decode — is not your job. Your job is narrower: make sure the content and state the archive records is the same every run.
The core skill: recognize the class, then pull the lever at record time
Everything below is the same move — something rendered differently run-to-run and it wasn't a real change; find what injected the variation and pin it before the snapshot.
| Symptom in the diff | Cause class | Lever | |---|---|---| | Whole page reflows; height differs between runs | Feature flag / A-B experiment rendering a different variant per load (Optimizely, LaunchDarkly, VWO, your own) | Stub the flag calls so one fixed variant renders → recipe 1 | | A list / feed / counts differ | Live data from an API | Mock the endpoint with a fixed response → recipe 2 | | Timestamps, "today", a year, a chart's day axis drift | The clock (Date.now) at render time | Freeze Date before navigation → recipe 3 | | A banner / chat / consent overlay appears or shifts | Third-party widget injecting DOM | Stub or dismiss it → recipe 1 + 4 | | Blank/placeholder where an image should be | Lazy content that never loaded, so its bytes aren't in the archive | Trigger it at record time → recipe 5 | | Same content, packed or ordered differently each run | Dynamic layout that reflows or reorders on its own | Mask it (or pin a fixed order/size) → recipe 6 |
Prefer removing a whole class at the source: test a build/staging with flags and third-party scripts off and rows 1 and 4 disappear before you write any code.
Recipes
1. Stub feature-flag / third-party scripts (register in beforeEach, before goto):
await page.route(/optimizely|optly|launchdarkly|segment|intercom/i, (route) =>
route.fulfill({ status: 204, body: '' }).catch(() => undefined),
);
Use fulfill({ status: 204 }), not route.abort() — an aborted request reads as pending network activity and can hang the page's readiness wait. A 204 completes it empty: the script "loads," resolves no variant, and the page renders its control DOM. Identify the culprit by loading the page a few times and watching document.body.scrollHeight flip between values.
2. Mock live data: intercept the API route and fulfill a fixed JSON fixture — same shape every run.
await page.route('**/api/items', (route) =>
route.fulfill({ json: fixtures.items }),
);
3. Freeze the clock (before navigation — it changes what the app renders into the DOM). Use Playwright's built-in clock so Date, Date.now(), and Date()-without-new all stay correct:
await page.clock.setFixedTime(new Date('2020-01-01T00:00:00Z'));
4. Dismiss overlays (best-effort; absent = fine):
await page.getByRole('button', { name: 'Accept All' }).click({ timeout: 4000 }).catch(() => {});
5. Trigger lazy content so its bytes land in the archive. This is the one "settle" that is yours — not for pixel-stability (UI Verify handles that) but because content that never loaded was never recorded. Scroll it into view before the snapshot:
await page.evaluate(async () => {
for (let y = 0; y setTimeout(r, 60));
}
scrollTo(0, 0);
});
6. Neutralize what can't be pinned — there's no separate mask step; UI Verify diffs the archived DOM, so hide the unstable element in the DOM before the snapshot and its varying content never lands in the archive:
// target the container that reflows or reorders between runs
await page
.locator('[data-testid="dynamic-region"]')
.evaluateAll((els) => els.forEach((el) => (el.style.visibility = "hidden")));
Diagnosing a flaky diff
- Localized band vs whole-page shift? A whole-page shift = something changed the page height (a
variant, missing lazy content, or JS layout), reflowing everything — not a style change.
- Whole-page → compare
document.body.scrollHeightacross runs. Two discrete values = a flag / third
party (recipe 1). Continuous variation = JS layout (recipe 6, neutralize it).
- Content differs = live data (recipe 2) or the clock (recipe 3).
Then upload the archive:
npx playwright test # archives land in uiverify-archive/
uiverify upload --static-dir uiverify-archive
Anti-patterns
route.abort()to block a script → can hang page readiness; usefulfill(204).- Hand-settling fonts / sticky animation / image decode → UI Verify re-renders and settles those; only
content loading (recipe 5) is your concern.
- Testing live prod with flags on → you're archiving a random variant.
- Unseeded
crypto/uuid/faker or live data → the archive is only as stable as what rendered into it.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: uiverify
- Source: uiverify/uiverify
- License: MIT
- Homepage: https://uiverify.ai/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.