Testivai oss agent
Local-first visual regression testing for Playwright, Selenium, WebdriverIO, Python, Java and Ruby.
Inside this agent
README.md · the author's own words# TestivAI Open Source [](https://www.npmjs.com/package/@testi… [](htt… [](… [](./LICENSE) **Local-first visual regression testing SDKs for modern web applications.** This is the home of TestivAI. It contains everything you need to capture, diff, and report visual regressions **fully locally** — MIT-licen… > **[See a live report →](https://testivai.github.io/testivai-example/)** — a real TestivAI OSS report rendered in your browser, straight f… [](https://test… ## Why TestivAI?
Word-for-word excerpt, not a result. Read the full README.md ↓
Files of Testivai oss
testivai/
Show the full text368 lines
TestivAI Open Source
Local-first visual regression testing SDKs for modern web applications.
This is the home of TestivAI. It contains everything you need to capture, diff, and report visual regressions fully locally — MIT-licensed, no account, no server.
See a live report → — a real TestivAI OSS report rendered in your browser, straight from CI. No install, no signup.
Why TestivAI?
Pixel-only visual testing drowns you in false positives — a font re-hint or an anti-aliasing shift across machines lights up as a "change," and you spend your time re-approving noise.
TestivAI pairs the screenshot with a snapshot of the page DOM (every adapter and the crawler capture one by default; the experimental testivai run sidecar does not) and, from most test-framework adapters, a digest of every element's computed styles. When pixels differ but the DOM and the computed styles are identical and the diff is small and scattered, the report labels the diff likely render noise instead of crying wolf. When the DOM and styles match but the diff sits in one area, it says that too: the style check covers a fixed list of properties, not size, overflow, transform or images, so that is a bad capture or a real change, not noise. It still shows the diff; it tells you what it most likely is. When only the styles changed, it says so: a style-only change is real. When the styles could not be compared, it says styles not compared and does not call the diff noise, because a stylesheet-only change is not ruled out. When the DOM actually changed, you see exactly what (2 added, 1 removed). That layered signal is the difference between a flaky test wall and a report you trust.
- Fully local, no account — captures, diffs, and a self-contained HTML report all stay on your machine.
- DOM + style-aware noise hint — tells likely render jitter from real changes, and catches the stylesheet-only case. When both captures carry an element map (the Playwright JS/TS, WebdriverIO, Selenium and Ruby adapters record one), an identical DOM with changed computed styles reads as "Styles changed on
button.cta", never as noise. Without an element map on both sides (thetestivai witness <url>crawler, playwright-python, playwright-java), computed styles are not compared, and an identical-DOM diff reads "styles not compared", not "likely render noise". - Auditable masks & region-level diffs — exclude dynamic areas (selectors or coordinates) with the masked region hatched in the diff, and get "3 changed regions" with bounding boxes instead of a raw pixel percentage.
- Element attribution & exact shift detection — the report names which element changed ("
div.card:nth-of-type(2)shifted +8px vertically — content unchanged") and spots the injected-banner case ("everything below y=80 moved +24px"), derived from layout, not pixel guesswork. No local-first tool does this. - First-class adapters — Playwright (TS/JS and Python, Java experimental) and WebdriverIO, using each framework's native APIs; every language shares one set of baselines and one report.
- Mobile web, no extra package — Playwright device emulation works through the same
witness()call: one project per device profile, baselines keyed per device automatically, and the DOM and computed-style attribution survives emulation, so a change behind a@mediaquery is caught on mobile and correctly passes on desktop. See the mobile web guide. - PR-native workflow — a GitHub Action posts the diff and approves baselines from a
/testivai approvecomment. - No lock-in — MIT license, baselines live in your git, and
results.jsonis a semver-governed public contract.
Evaluating this for a team? Maintenance & roadmap covers who builds it, the release cadence, and what happens to your setup if maintenance ever stops.
Already using Playwright's
toHaveScreenshot()? It's good, and if a pixel diff answers your question you should keep using it. Here's an honest look at what TestivAI adds and when it isn't worth the dependency.
Eyes for your coding agent
If an AI agent (Claude Code, Cursor, Copilot, …) writes your UI code, someone still has to check what the UI looks like — and it shouldn't be you, one screenshot at a time. TestivAI is built to be that check:
- No account, no API key, no network — an agent can run it inside any sandbox without you provisioning secrets.
- Machine-readable output — every run writes
visual-report/results.json(a semver-governed schema) with per-snapshot diff percentages and DOM change summaries, so an agent can read the result and self-correct. - Noise-aware verdicts — the DOM and computed-style checks tell the agent whether a pixel diff is likely render noise (identical DOM, styles compared and identical, a small and scattered diff), a concentrated or large diff (identical DOM and styles, but the diff sits in one area or covers over 1% of pixels: a bad capture or a change the style check does not cover), a style-only change (identical DOM, restyled elements), a real structural change (
2 added, 1 removed), or styles not compared (identical DOM, but no style data to compare, so a stylesheet-only change is not ruled out). It doesn't chase anti-aliasing ghosts, and it doesn't label a diff it couldn't check as noise. - Explanations, bring-your-own-model — the MCP
explain_snapshottool hands your agent layered evidence (which selectors shifted vs changed, whole-page shift detection, style-only changes) and your model writes the narrative: "card #2 shifted +24px — likely the banner injected above it." No hosted AI service in the loop. - Human approval stays in the PR — the agent iterates locally; you approve baselines with one
/testivai approvecomment.
Paste this into your project's AGENTS.md / CLAUDE.md to wire it up (full guide with MCP setup, a real agent transcript, and the approval rule: docs/guides/ai-agents.md):
## Visual verification
After changing any UI code, run `npx playwright test` (TestivAI captures
screenshots automatically), then read `visual-report/results.json`.
- `status: "changed"` with `dom.changed: true` → describe the DOM summary and
ask whether the change is intended before approving.
- `status: "changed"` with `dom.changed: false` and `dom.styleCheck: "mismatch"`
→ style-only change; real, not noise. Compare `dom.styleChanges` with what
you intended.
- `status: "changed"` with `dom.noiseHint: true`, `dom.styleCheck: "match"`,
`diffPercent` at most 1, and no `regions` or regions spread out (their
combined box covers more than a third of the page) → likely render noise;
mention it but don't block.
- `status: "changed"` with `dom.noiseHint: true` and `dom.styleCheck: "match"`
but every region within a third of the page, or `diffPercent` over 1 → not
noise: a bad capture or a change the style check does not cover (size,
overflow, transform, images). Look at the diff image before approving.
- `status: "changed"` with `dom.noiseHint: true` and `dom.styleCheck`
`"unavailable"` or missing → styles not compared: could be render noise or a
stylesheet-only change. Don't treat it as noise: look at the diff image before approving.
- Never run `testivai approve` yourself — baseline approval is a human decision.
Packages
Live versions are shown by the badges at the top of this README.
| Package | Description |
|---|---|
@testivai/witness |
Core SDK: CLI, local diffing, baselines, HTML report generator |
@testivai/witness-playwright |
Playwright reporter/adapter built on top of @testivai/witness |
@testivai/witness-webdriverio |
WebdriverIO service + capture function (local mode) |
@testivai/witness-selenium |
Selenium WebDriver capture adapter (Python/Java Selenium live in python/ and java/) |
@testivai/mcp |
MCP server — visual results + diff images for AI coding agents |
testivai (PyPI) |
Python adapter for playwright-python + pytest plugin — same baselines & report |
ai.testiv:testivai |
Java adapter for playwright-java + JUnit 5 extension (experimental) |
testivai (RubyGems) |
Ruby adapter for Capybara / RSpec / Cucumber — same baselines & report |
Plus:
action/— GitHub Action for PR-based visual approvalsexamples/— minimal real-world example projectsexamples/site-check/— an hourly GitHub Actions check that fails when a page is up but broken (stylesheet, script or font missing, JavaScript error, blank page), with one issue per outage. testiv.ai runs it on itself.
docs/— public documentatione2e/— OSS smoke E2E test suite
No test suite? One command.
AI-built and vibe-coded apps (Lovable, Bolt, v0, ...) usually ship with zero tests. You still get the full safety net:
npx testivai witness http://localhost:3000
TestivAI launches a headless Chrome, discovers your pages (or takes
--pages "/,/pricing"), and captures a screenshot and the DOM of each one.
Baselines, pixel and DOM diffs, the HTML report and PR approvals work as
below, no test framework required. The crawler records no element map, so
computed styles are not compared: a pixel diff with an identical DOM reads
styles not compared (could be render noise or a stylesheet-only change),
never likely noise, and there is no selector attribution or page-shift
detection. See the vibe-coded apps guide.
Quick Start (Playwright, Local Mode)
# 1. Install
npm install -D @testivai/witness-playwright @playwright/test
npx playwright install chromium
// 2. (OPTIONAL) Customize tolerances and report settings.
// Everything runs locally — no config needed.
// Only create this file if you want to tune threshold, reportDir, etc.
// File: .testivai/config.json
{
"threshold": 0.1, // per-pixel color sensitivity (0-1)
"maxDiffPercent": 0, // pass diffs at or below this % (your tolerance dial)
"noiseAutoPass": false, // true: diffs with identical DOM AND matching computed styles, not concentrated in one area, within noiseMaxDiffPercent, pass
"stabilize": true, // freeze animations, hide caret, wait for fonts
"ignoreSelectors": [], // e.g. [".live-chat", "[data-testid=clock]"]
"reportDir": "visual-report",
"autoOpen": false
}
// 3. Wire the reporter — playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['list'],
['@testivai/witness-playwright/reporter'],
],
});
// 4. Add a capture call — tests/example.spec.ts
import { test } from '@playwright/test';
import { witness } from '@testivai/witness-playwright';
test('homepage looks correct', async ({ page }, testInfo) => {
await page.goto('http://localhost:3000');
await witness(page, testInfo, 'homepage');
});
# 5. Run
npx playwright test
First run: baselines are written to .testivai/baselines/.
Later runs: screenshots are diffed and a self-contained HTML report is written to ./visual-report/.
What you get out of the box (free, no account)
- Full-page screenshot capture via Playwright
- Stabilized captures by default — animations/transitions frozen, caret hidden, web fonts awaited (the top causes of flaky visual tests, neutralized before every screenshot)
- Local pixel diff with configurable threshold
- Tunable pass criteria, all opt-in —
maxDiffPercent/maxDiffPixelstolerances,shiftTolerancefor pure element shifts, andnoiseAutoPass, which passes a diff only when the DOM is identical, the computed styles were compared and match, and the diff is not concentrated in one area (never one whose styles were not compared). Nothing auto-passes until you configure it, and auto-passed snapshots stay labeled in the report ignoreSelectorsandmaskfor dynamic content such as timestamps (global or per-snapshot): stabilization does not hide changing content, so you list it- Self-contained HTML report (
visual-report/index.html) - Machine-readable results (
visual-report/results.json) - Committed baselines under
.testivai/baselines/(justgit addthem)
CI Integration (GitHub Actions)
Copy this single workflow file into your repository. It handles both running the visual regression tests and processing /testivai approve commands from PR comments — no extra secrets, no external services required.
# .github/workflows/testivai-oss.yml
name: TestivAI OSS
on:
pull_request:
branches: [main]
issue_comment:
types: [created] # listens for /testivai approve commands
permissions:
contents: write # approve action commits updated baselines to the branch
pull-requests: write # post PR diff comment
statuses: write # set pass/fail indicator on the PR
actions: read # approve action finds the CI run and downloads its report artifact
jobs:
# Runs on every PR — captures screenshots, diffs against baselines, posts report
visual-regression:
name: Visual Regression (OSS)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install chromium --with-deps
- run: npm run build
- run: npm run test:oss # runs playwright.oss.config.ts
- name: Post results + upload report
uses: testivai/testivai-oss@v1
if: always()
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
report-dir: visual-report # where @testivai/witness writes results.json
# Runs when a collaborator comments /testivai approve on the PR
approve-baselines:
name: Approve Baselines
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request != null &&
startsWith(github.event.comment.body, '/testivai')
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: testivai/testivai-oss/approve@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
workflow: testivai-oss.yml # this file's name — used to find the report artifact
# Optional: Claude triages every diff as glitch / intended change / bug /
# possible bug. Skips (no tokens) without the secret, or with enabled: false.
ai-triage:
name: AI triage
needs: visual-regression
if: ${{ !cancelled() && github.event_name == 'pull_request' }}
concurrency: { group: 'ai-triage-${{ github.event.pull_request.number }}', cancel-in-progress: true }
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
continue-on-error: true # no report when the visual job failed early: the triage skips
with: { name: testivai-visual-report, path: visual-report }
- uses: testivai/testivai-oss/review@v1
with:
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
# claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # instead, for a Claude subscription
# enabled: false # turn it off
The ai-triage job runs on every pull request with a changed or new
snapshot or a missing baseline (unless .testivai/config.json sets
failOnMissing: false) once an ANTHROPIC_API_KEY secret is set (or a
CLAUDE_CODE_OAUTH_TOKEN secret, passed as claude-code-oauth-token), and
posts one advisory comment: which diffs look like glitches, which
match the PR, and which look like bugs. It never changes the check or a
baseline. See AI triage of every diff.
Approve changed baselines
After CI posts the diff report on your PR, review the testivai-visual-report artifact, then comment:
| Comment | Effect |
|---|---|
/testivai approve homepage |
Approves one named snapshot |
/testivai approve --all |
Approves every changed snapshot at once |
What happens:
- Action verifies you have write access to the repository (others get a polite rejection)
- Downloads the
testivai-visual-reportartifact from the latest CI run on your branch - Copies approved screenshots into
.testivai/baselines/and commits them to your PR branch - Posts a confirmation comment listing what was approved
- CI runs again on the new commit — approved snapshots now pass
Step 5 needs a token other than GITHUB_TOKEN: GitHub starts no workflow run for a push made with GITHUB_TOKEN. Give the approve step a fine-grained personal access token (github-token: ${{ secrets.TESTIVAI_APPROVE_TOKEN || secrets.GITHUB_TOKEN }}), or push another commit after approving. See Approve with a token that re-runs CI.
What the PR comment looks like
TestivAI Visual Report
4 passed | 4 changed | 1 new — 9 total
Changed Snapshots
▼ homepage — 0.31% different
DOM unchanged — pixel diff is likely render noise (anti-aliasing, font hinting). Styles verified unchanged; the diff is small and scattered.
▼ products — 0.03% different
DOM and styles unchanged, but the diff is concentrated in one area — 3 regions within 155×52 px, 0.8% of the page: a bad capture, or a change the style check does not cover (size, overflow, position, transform, text decoration, filters, pseudo-elements, or images and other assets). Compare the images before approving.
▼ pricing — 0.42% different
DOM unchanged, styles not compared — could be render noise or a stylesheet-only change (no comparable style digests on both sides). Compare the images before approving.
▼ dashboard — 8.91% different
DOM changed — 2 added, 1 removed.
Real-World Example
A complete, minimal consumer project lives at testivai-example: a static page, three witness() calls, the PR /testivai approve flow, and a live report on Pages — all against the published packages.
Repository Layout
packages/
witness/ @testivai/witness — CLI, diff engine, baselines, report
playwright/ @testivai/witness-playwright — Playwright reporter + capture
webdriverio/ @testivai/witness-webdriverio — WebdriverIO service + capture
selenium/ @testivai/witness-selenium — Selenium adapter
mcp/ @testivai/mcp — MCP server for AI agents
action/ GitHub Action for PR comments
approve/ GitHub Action for /testivai approve
examples/ framework-specific minimal examples
docs/ public documentation (Markdown)
e2e/ OSS smoke E2E
Development
# Prereqs: Node 20+, pnpm 10+
pnpm install
pnpm build # tsc all packages
pnpm test # unit tests across all packages
pnpm e2e # smoke E2E
pnpm pack:dry # validate publish artifacts
Contributing
Bug reports, feature requests, and PRs welcome. Please see:
Releases
Releases are published to npm under the latest dist-tag, with provenance
attestations. The flow is Changesets-driven: a PR that changes a published
package adds a changeset, merging it opens a "version packages" PR collecting
the pending bumps, and merging that publishes. See
.changeset/README.md for the contributor side and
.github/workflows/release.yml for the
workflow itself.
Attribution
This repository was extracted from the private TestivAI monorepo with a clean initial git history. Original development history is preserved internally; this public repository is the new source of truth for the SDKs going forward.
License
MIT — see LICENSE.
| 1 | # TestivAI Open Source |
| 2 | |
| 3 | [![@testivai/witness]](https://www.npmjs.com/package/@testivai/witness) |
| 4 | [![@testivai/witness-playwright]](https://www.npmjs.com/package/@testivai/witness-playwright) |
| 5 | [![@testivai/witness-webdriverio]](https://www.npmjs.com/package/@testivai/witness-webdriverio) |
| 6 | [![License: MIT]](./LICENSE) |
| 7 | |
| 8 | **Local-first visual regression testing SDKs for modern web applications.** |
| 9 | |
| 10 | This is the home of TestivAI. It contains everything you need to capture, diff, and report visual regressions **fully locally** — MIT-licensed, no account, no server. |
| 11 | |
| 12 | > **[See a live report →]** — a real TestivAI OSS report rendered in your browser, straight from CI. No install, no signup. |
| 13 | |
| 14 | [![Real TestivAI report — style-only-change verdict, selector-attributed regions, heatmap diff]](https://testivai.github.io/testivai-example/) |
| 15 | |
| 16 | ## Why TestivAI? |
| 17 | |
| 18 | Pixel-only visual testing drowns you in false positives — a font re-hint or an anti-aliasing shift across machines lights up as a "change," and you spend your time re-approving noise. |
| 19 | |
| 20 | **TestivAI pairs the screenshot with a snapshot of the page DOM** (every adapter and the crawler capture one by default; the experimental `testivai run` sidecar does not) and, from most test-framework adapters, a digest of every element's computed styles. When pixels differ but the DOM and the computed styles are identical and the diff is small and scattered, the report labels the diff **likely render noise** instead of crying wolf. When the DOM and styles match but the diff sits in one area, it says that too: the style check covers a fixed list of properties, not size, overflow, transform or images, so that is a bad capture or a real change, not noise. It still shows the diff; it tells you what it most likely is. When only the styles changed, it says so: a style-only change is real. When the styles could not be compared, it says *styles not compared* and does not call the diff noise, because a stylesheet-only change is not ruled out. When the DOM actually changed, you see exactly what (`2 added, 1 removed`). That layered signal is the difference between a flaky test wall and a report you trust. |
| 21 | |
| 22 | **Fully local, no account** — captures, diffs, and a self-contained HTML report all stay on your machine. |
| 23 | **DOM + style-aware noise hint** — tells likely render jitter from real changes, and catches the stylesheet-only case. When both captures carry an element map (the Playwright JS/TS, WebdriverIO, Selenium and Ruby adapters record one), an identical DOM with changed computed styles reads as "Styles changed on `button.cta`", never as noise. Without an element map on both sides (the `testivai witness <url>` crawler, playwright-python, playwright-java), computed styles are not compared, and an identical-DOM diff reads "styles not compared", not "likely render noise". |
| 24 | **Auditable masks & region-level diffs** — exclude dynamic areas (selectors or coordinates) with the masked region hatched in the diff, and get "3 changed regions" with bounding boxes instead of a raw pixel percentage. |
| 25 | **Element attribution & exact shift detection** — the report names *which element* changed ("`div.card:nth-of-type(2)` shifted +8px vertically — content unchanged") and spots the injected-banner case ("everything below y=80 moved +24px"), derived from layout, not pixel guesswork. No local-first tool does this. |
| 26 | **First-class adapters** — Playwright (TS/JS **and Python**, Java experimental) and WebdriverIO, using each framework's native APIs; every language shares one set of baselines and one report. |
| 27 | **Mobile web, no extra package** — Playwright device emulation works through the same `witness()` call: one project per device profile, baselines keyed per device automatically, and the DOM and computed-style attribution survives emulation, so a change behind a `@media` query is caught on mobile and correctly passes on desktop. See the [mobile web guide]. |
| 28 | **PR-native workflow** — a GitHub Action posts the diff and approves baselines from a `/testivai approve` comment. |
| 29 | **No lock-in** — MIT license, baselines live in your git, and `results.json` is a semver-governed public contract. |
| 30 | |
| 31 | > **Evaluating this for a team?** [Maintenance & roadmap] |
| 32 | > covers who builds it, the release cadence, and what happens to your setup if |
| 33 | > maintenance ever stops. |
| 34 | |
| 35 | > **Already using Playwright's `toHaveScreenshot()`?** It's good, and if a pixel |
| 36 | > diff answers your question you should keep using it. Here's an honest look at |
| 37 | > [what TestivAI adds and when it isn't worth the dependency]. |
| 38 | |
| 39 | ## Eyes for your coding agent |
| 40 | |
| 41 | If an AI agent (Claude Code, Cursor, Copilot, …) writes your UI code, someone still has to check what the UI *looks like* — and it shouldn't be you, one screenshot at a time. TestivAI is built to be that check: |
| 42 | |
| 43 | **No account, no API key, no network** — an agent can run it inside any sandbox without you provisioning secrets. |
| 44 | **Machine-readable output** — every run writes `visual-report/results.json` (a [semver-governed schema]) with per-snapshot diff percentages and DOM change summaries, so an agent can read the result and self-correct. |
| 45 | **Noise-aware verdicts** — the DOM and computed-style checks tell the agent whether a pixel diff is *likely render noise* (identical DOM, styles compared and identical, a small and scattered diff), a *concentrated or large diff* (identical DOM and styles, but the diff sits in one area or covers over 1% of pixels: a bad capture or a change the style check does not cover), a *style-only change* (identical DOM, restyled elements), a *real structural change* (`2 added, 1 removed`), or *styles not compared* (identical DOM, but no style data to compare, so a stylesheet-only change is not ruled out). It doesn't chase anti-aliasing ghosts, and it doesn't label a diff it couldn't check as noise. |
| 46 | **Explanations, bring-your-own-model** — the MCP `explain_snapshot` tool hands your agent layered evidence (which selectors shifted vs changed, whole-page shift detection, style-only changes) and your model writes the narrative: *"card #2 shifted +24px — likely the banner injected above it."* No hosted AI service in the loop. |
| 47 | **Human approval stays in the PR** — the agent iterates locally; you approve baselines with one `/testivai approve` comment. |
| 48 | |
| 49 | Paste this into your project's `AGENTS.md` / `CLAUDE.md` to wire it up (full guide with MCP setup, a real agent transcript, and the approval rule: [docs/guides/ai-agents.md]): |
| 50 | |
| 51 | |
| 52 | ## Visual verification |
| 53 | After changing any UI code, run `npx playwright test` (TestivAI captures |
| 54 | screenshots automatically), then read `visual-report/results.json`. |
| 55 | - `status: "changed"` with `dom.changed: true` → describe the DOM summary and |
| 56 | ask whether the change is intended before approving. |
| 57 | - `status: "changed"` with `dom.changed: false` and `dom.styleCheck: "mismatch"` |
| 58 | → style-only change; real, not noise. Compare `dom.styleChanges` with what |
| 59 | you intended. |
| 60 | - `status: "changed"` with `dom.noiseHint: true`, `dom.styleCheck: "match"`, |
| 61 | `diffPercent` at most 1, and no `regions` or regions spread out (their |
| 62 | combined box covers more than a third of the page) → likely render noise; |
| 63 | mention it but don't block. |
| 64 | - `status: "changed"` with `dom.noiseHint: true` and `dom.styleCheck: "match"` |
| 65 | but every region within a third of the page, or `diffPercent` over 1 → not |
| 66 | noise: a bad capture or a change the style check does not cover (size, |
| 67 | overflow, transform, images). Look at the diff image before approving. |
| 68 | - `status: "changed"` with `dom.noiseHint: true` and `dom.styleCheck` |
| 69 | `"unavailable"` or missing → styles not compared: could be render noise or a |
| 70 | stylesheet-only change. Don't treat it as noise: look at the diff image before approving. |
| 71 | - Never run `testivai approve` yourself — baseline approval is a human decision. |
| 72 | |
| 73 | |
| 74 | ## Packages |
| 75 | |
| 76 | Live versions are shown by the badges at the top of this README. |
| 77 | |
| 78 | | Package | Description | |
| 79 | |---|---| |
| 80 | | [`@testivai/witness`] | Core SDK: CLI, local diffing, baselines, HTML report generator | |
| 81 | | [`@testivai/witness-playwright`] | Playwright reporter/adapter built on top of `@testivai/witness` | |
| 82 | | [`@testivai/witness-webdriverio`] | WebdriverIO service + capture function (local mode) | |
| 83 | | [`@testivai/witness-selenium`] | Selenium WebDriver capture adapter (Python/Java Selenium live in `python/` and `java/`) | |
| 84 | | [`@testivai/mcp`] | MCP server — visual results + diff images for AI coding agents | |
| 85 | | [`testivai` (PyPI)] | Python adapter for playwright-python + pytest plugin — same baselines & report | |
| 86 | | [`ai.testiv:testivai`] | Java adapter for playwright-java + JUnit 5 extension (experimental) | |
| 87 | | [`testivai` (RubyGems)] | Ruby adapter for Capybara / RSpec / Cucumber — same baselines & report | |
| 88 | |
| 89 | Plus: |
| 90 | [`action/`] — GitHub Action for PR-based visual approvals |
| 91 | [`examples/`] — minimal real-world example projects |
| 92 | [`examples/site-check/`] — an hourly GitHub Actions check that fails when a page is up but broken (stylesheet, script or font missing, JavaScript error, blank page), with one issue per outage. testiv.ai runs it on itself. |
| 93 | [`docs/`] — public documentation |
| 94 | [`e2e/`] — OSS smoke E2E test suite |
| 95 | |
| 96 | ## No test suite? One command. |
| 97 | |
| 98 | AI-built and vibe-coded apps (Lovable, Bolt, v0, ...) usually ship with zero |
| 99 | tests. You still get the full safety net: |
| 100 | |
| 101 | |
| 102 | npx testivai witness http://localhost:3000 |
| 103 | |
| 104 | |
| 105 | TestivAI launches a headless Chrome, discovers your pages (or takes |
| 106 | `--pages "/,/pricing"`), and captures a screenshot and the DOM of each one. |
| 107 | Baselines, pixel and DOM diffs, the HTML report and PR approvals work as |
| 108 | below, no test framework required. The crawler records no element map, so |
| 109 | computed styles are not compared: a pixel diff with an identical DOM reads |
| 110 | *styles not compared* (could be render noise or a stylesheet-only change), |
| 111 | never likely noise, and there is no selector attribution or page-shift |
| 112 | detection. See the [vibe-coded apps guide]. |
| 113 | |
| 114 | ## Quick Start (Playwright, Local Mode) |
| 115 | |
| 116 | |
| 117 | # 1. Install |
| 118 | npm install -D @testivai/witness-playwright @playwright/test |
| 119 | npx playwright install chromium |
| 120 | |
| 121 | |
| 122 | |
| 123 | // 2. (OPTIONAL) Customize tolerances and report settings. |
| 124 | // Everything runs locally — no config needed. |
| 125 | // Only create this file if you want to tune threshold, reportDir, etc. |
| 126 | // File: .testivai/config.json |
| 127 | { |
| 128 | "threshold": 0.1, // per-pixel color sensitivity (0-1) |
| 129 | "maxDiffPercent": 0, // pass diffs at or below this % (your tolerance dial) |
| 130 | "noiseAutoPass": false, // true: diffs with identical DOM AND matching computed styles, not concentrated in one area, within noiseMaxDiffPercent, pass |
| 131 | "stabilize": true, // freeze animations, hide caret, wait for fonts |
| 132 | "ignoreSelectors": [], // e.g. [".live-chat", "[data-testid=clock]"] |
| 133 | "reportDir": "visual-report", |
| 134 | "autoOpen": false |
| 135 | } |
| 136 | |
| 137 | |
| 138 | |
| 139 | // 3. Wire the reporter — playwright.config.ts |
| 140 | import { defineConfig } from '@playwright/test'; |
| 141 | |
| 142 | export default defineConfig({ |
| 143 | reporter: [ |
| 144 | ['list'], |
| 145 | ['@testivai/witness-playwright/reporter'], |
| 146 | ], |
| 147 | }); |
| 148 | |
| 149 | |
| 150 | |
| 151 | // 4. Add a capture call — tests/example.spec.ts |
| 152 | import { test } from '@playwright/test'; |
| 153 | import { witness } from '@testivai/witness-playwright'; |
| 154 | |
| 155 | test('homepage looks correct', async ({ page }, testInfo) => { |
| 156 | await page.goto('http://localhost:3000'); |
| 157 | await witness(page, testInfo, 'homepage'); |
| 158 | }); |
| 159 | |
| 160 | |
| 161 | |
| 162 | # 5. Run |
| 163 | npx playwright test |
| 164 | |
| 165 | |
| 166 | **First run:** baselines are written to `.testivai/baselines/`. |
| 167 | **Later runs:** screenshots are diffed and a self-contained HTML report is written to `./visual-report/`. |
| 168 | |
| 169 | ## What you get out of the box (free, no account) |
| 170 | |
| 171 | Full-page screenshot capture via Playwright |
| 172 | **Stabilized captures by default** — animations/transitions frozen, caret hidden, web fonts awaited (the top causes of flaky visual tests, neutralized before every screenshot) |
| 173 | Local pixel diff with configurable threshold |
| 174 | **Tunable pass criteria, all opt-in** — `maxDiffPercent` / `maxDiffPixels` tolerances, `shiftTolerance` for pure element shifts, and `noiseAutoPass`, which passes a diff only when the DOM is identical, the computed styles were compared and match, and the diff is not concentrated in one area (never one whose styles were not compared). Nothing auto-passes until you configure it, and auto-passed snapshots stay labeled in the report |
| 175 | `ignoreSelectors` and `mask` for dynamic content such as timestamps (global or per-snapshot): stabilization does not hide changing content, so you list it |
| 176 | Self-contained HTML report (`visual-report/index.html`) |
| 177 | Machine-readable results (`visual-report/results.json`) |
| 178 | Committed baselines under `.testivai/baselines/` (just `git add` them) |
| 179 | |
| 180 | ## CI Integration (GitHub Actions) |
| 181 | |
| 182 | Copy this single workflow file into your repository. It handles both running the visual regression tests **and** processing `/testivai approve` commands from PR comments — no extra secrets, no external services required. |
| 183 | |
| 184 | |
| 185 | # .github/workflows/testivai-oss.yml |
| 186 | name: TestivAI OSS |
| 187 | |
| 188 | on: |
| 189 | pull_request: |
| 190 | branches: [main] |
| 191 | issue_comment: |
| 192 | types: [created] # listens for /testivai approve commands |
| 193 | |
| 194 | permissions: |
| 195 | contents: write # approve action commits updated baselines to the branch |
| 196 | pull-requests: write # post PR diff comment |
| 197 | statuses: write # set pass/fail indicator on the PR |
| 198 | actions: read # approve action finds the CI run and downloads its report artifact |
| 199 | |
| 200 | jobs: |
| 201 | |
| 202 | # Runs on every PR — captures screenshots, diffs against baselines, posts report |
| 203 | visual-regression: |
| 204 | name: Visual Regression (OSS) |
| 205 | if: github.event_name == 'pull_request' |
| 206 | runs-on: ubuntu-latest |
| 207 | timeout-minutes: 15 |
| 208 | steps: |
| 209 | - uses: actions/checkout@v4 |
| 210 | - uses: actions/setup-node@v4 |
| 211 | with: { node-version: '20', cache: 'npm' } |
| 212 | - run: npm ci |
| 213 | - run: npx playwright install chromium --with-deps |
| 214 | - run: npm run build |
| 215 | - run: npm run test:oss # runs playwright.oss.config.ts |
| 216 | |
| 217 | - name: Post results + upload report |
| 218 | uses: testivai/testivai-oss@v1 |
| 219 | if: always() |
| 220 | with: |
| 221 | github-token: ${{ secrets.GITHUB_TOKEN }} |
| 222 | report-dir: visual-report # where @testivai/witness writes results.json |
| 223 | |
| 224 | # Runs when a collaborator comments /testivai approve on the PR |
| 225 | approve-baselines: |
| 226 | name: Approve Baselines |
| 227 | if: | |
| 228 | github.event_name == 'issue_comment' && |
| 229 | github.event.issue.pull_request != null && |
| 230 | startsWith(github.event.comment.body, '/testivai') |
| 231 | runs-on: ubuntu-latest |
| 232 | timeout-minutes: 10 |
| 233 | steps: |
| 234 | - uses: testivai/testivai-oss/approve@v1 |
| 235 | with: |
| 236 | github-token: ${{ secrets.GITHUB_TOKEN }} |
| 237 | workflow: testivai-oss.yml # this file's name — used to find the report artifact |
| 238 | |
| 239 | # Optional: Claude triages every diff as glitch / intended change / bug / |
| 240 | # possible bug. Skips (no tokens) without the secret, or with enabled: false. |
| 241 | ai-triage: |
| 242 | name: AI triage |
| 243 | needs: visual-regression |
| 244 | if: ${{ !cancelled() && github.event_name == 'pull_request' }} |
| 245 | concurrency: { group: 'ai-triage-${{ github.event.pull_request.number }}', cancel-in-progress: true } |
| 246 | runs-on: ubuntu-latest |
| 247 | timeout-minutes: 15 |
| 248 | permissions: |
| 249 | contents: read |
| 250 | pull-requests: write |
| 251 | steps: |
| 252 | - uses: actions/checkout@v4 |
| 253 | - uses: actions/download-artifact@v4 |
| 254 | continue-on-error: true # no report when the visual job failed early: the triage skips |
| 255 | with: { name: testivai-visual-report, path: visual-report } |
| 256 | - uses: testivai/testivai-oss/review@v1 |
| 257 | with: |
| 258 | anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} |
| 259 | # claude-code-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # instead, for a Claude subscription |
| 260 | # enabled: false # turn it off |
| 261 | |
| 262 | |
| 263 | The `ai-triage` job runs on every pull request with a changed or new |
| 264 | snapshot or a missing baseline (unless .testivai/config.json sets |
| 265 | failOnMissing: false) once an `ANTHROPIC_API_KEY` secret is set (or a |
| 266 | `CLAUDE_CODE_OAUTH_TOKEN` secret, passed as `claude-code-oauth-token`), and |
| 267 | posts one advisory comment: which diffs look like glitches, which |
| 268 | match the PR, and which look like bugs. It never changes the check or a |
| 269 | baseline. See [AI triage of every diff]. |
| 270 | |
| 271 | ### Approve changed baselines |
| 272 | |
| 273 | After CI posts the diff report on your PR, review the `testivai-visual-report` artifact, then comment: |
| 274 | |
| 275 | | Comment | Effect | |
| 276 | |---|---| |
| 277 | | `/testivai approve homepage` | Approves one named snapshot | |
| 278 | | `/testivai approve --all` | Approves every changed snapshot at once | |
| 279 | |
| 280 | **What happens:** |
| 281 | Action verifies you have **write** access to the repository (others get a polite rejection) |
| 282 | Downloads the `testivai-visual-report` artifact from the latest CI run on your branch |
| 283 | Copies approved screenshots into `.testivai/baselines/` and commits them to your PR branch |
| 284 | Posts a confirmation comment listing what was approved |
| 285 | CI runs again on the new commit — approved snapshots now pass |
| 286 | |
| 287 | Step 5 needs a token other than `GITHUB_TOKEN`: GitHub starts no workflow run for a push made with `GITHUB_TOKEN`. Give the approve step a fine-grained personal access token (`github-token: ${{ secrets.TESTIVAI_APPROVE_TOKEN || secrets.GITHUB_TOKEN }}`), or push another commit after approving. See [Approve with a token that re-runs CI]. |
| 288 | |
| 289 | ### What the PR comment looks like |
| 290 | |
| 291 | |
| 292 | TestivAI Visual Report |
| 293 | |
| 294 | 4 passed | 4 changed | 1 new — 9 total |
| 295 | |
| 296 | Changed Snapshots |
| 297 | |
| 298 | ▼ homepage — 0.31% different |
| 299 | DOM unchanged — pixel diff is likely render noise (anti-aliasing, font hinting). Styles verified unchanged; the diff is small and scattered. |
| 300 | |
| 301 | ▼ products — 0.03% different |
| 302 | DOM and styles unchanged, but the diff is concentrated in one area — 3 regions within 155×52 px, 0.8% of the page: a bad capture, or a change the style check does not cover (size, overflow, position, transform, text decoration, filters, pseudo-elements, or images and other assets). Compare the images before approving. |
| 303 | |
| 304 | ▼ pricing — 0.42% different |
| 305 | DOM unchanged, styles not compared — could be render noise or a stylesheet-only change (no comparable style digests on both sides). Compare the images before approving. |
| 306 | |
| 307 | ▼ dashboard — 8.91% different |
| 308 | DOM changed — 2 added, 1 removed. |
| 309 | |
| 310 | |
| 311 | |
| 312 | |
| 313 | ## Real-World Example |
| 314 | |
| 315 | A complete, minimal consumer project lives at [`testivai-example`]: a static page, three `witness()` calls, the PR `/testivai approve` flow, and a [live report on Pages] — all against the published packages. |
| 316 | |
| 317 | ## Repository Layout |
| 318 | |
| 319 | |
| 320 | packages/ |
| 321 | witness/ @testivai/witness — CLI, diff engine, baselines, report |
| 322 | playwright/ @testivai/witness-playwright — Playwright reporter + capture |
| 323 | webdriverio/ @testivai/witness-webdriverio — WebdriverIO service + capture |
| 324 | selenium/ @testivai/witness-selenium — Selenium adapter |
| 325 | mcp/ @testivai/mcp — MCP server for AI agents |
| 326 | action/ GitHub Action for PR comments |
| 327 | approve/ GitHub Action for /testivai approve |
| 328 | examples/ framework-specific minimal examples |
| 329 | docs/ public documentation (Markdown) |
| 330 | e2e/ OSS smoke E2E |
| 331 | |
| 332 | |
| 333 | ## Development |
| 334 | |
| 335 | |
| 336 | # Prereqs: Node 20+, pnpm 10+ |
| 337 | pnpm install |
| 338 | pnpm build # tsc all packages |
| 339 | pnpm test # unit tests across all packages |
| 340 | pnpm e2e # smoke E2E |
| 341 | pnpm pack:dry # validate publish artifacts |
| 342 | |
| 343 | |
| 344 | ## Contributing |
| 345 | |
| 346 | Bug reports, feature requests, and PRs welcome. Please see: |
| 347 | [`CONTRIBUTING.md`] |
| 348 | [`CODE_OF_CONDUCT.md`] |
| 349 | [`.github/ISSUE_TEMPLATE/`] |
| 350 | |
| 351 | ## Releases |
| 352 | |
| 353 | Releases are published to npm under the `latest` dist-tag, with provenance |
| 354 | attestations. The flow is Changesets-driven: a PR that changes a published |
| 355 | package adds a changeset, merging it opens a "version packages" PR collecting |
| 356 | the pending bumps, and merging *that* publishes. See |
| 357 | [`.changeset/README.md`] for the contributor side and |
| 358 | [`.github/workflows/release.yml`] for the |
| 359 | workflow itself. |
| 360 | |
| 361 | ## Attribution |
| 362 | |
| 363 | This repository was extracted from the private TestivAI monorepo with a clean initial git history. Original development history is preserved internally; this public repository is the new source of truth for the SDKs going forward. |
| 364 | |
| 365 | ## License |
| 366 | |
| 367 | MIT — see [LICENSE]. |
| 368 |
Discussion
Alternatives
Browse more free AI agents or everything in Development.
