PageSourceSearch

https://testiv.ai/docs/assets/js/0e384e19.10a199fc.js

js testiv.ai collected 2026-10-05 15:02:53 UTC 21,036 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunktestivai_docs=self.webpackChunktestivai_docs||[]).push([[976],{7832(e,s,r){r.r(s),r.d(s,{assets:()=>a,contentTitle:()=>l,default:()=>h,frontMatter:()=>d,metadata:()=>n,toc:()=>c});const n=JSON.parse('{"id":"intro","title":"Getting Started","description":"Add visual regression testing to your test suite in under 5 minutes \u2014 fully local, no account needed.","source":"@site/docs/intro.md","sourceDirName":".","slug":"/intro","permalink":"/docs/intro","draft":false,"unlisted":false,"editUrl":"https://github.com/mcbuddy/testivai-oss/tree/main/docs/intro.md","tags":[],"version":"current","sidebarPosition":1,"frontMatter":{"sidebar_position":1,"title":"Getting Started","slug":"/intro"},"sidebar":"docs","next":{"title":"How It Works","permalink":"/docs/how-it-works"}}');var i=r(6106),t=r(6005);const d={sidebar_position:1,title:"Getting Started",slug:"/intro"},l="Getting Started",a={},c=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Path A \u2014 Dedicated adapters (recommended)",id:"path-a--dedicated-adapters-recommended",level:2},{value:"Playwright",id:"playwright",level:3},{value:"1. Install",id:"1-install",level:3},{value:"2. (Optional) Customize settings",id:"2-optional-customize-settings",level:3},{value:"3. Add the reporter",id:"3-add-the-reporter",level:3},{value:"4. Add a capture call",id:"4-add-a-capture-call",level:3},{value:"5. Run",id:"5-run",level:3},{value:"WebdriverIO",id:"webdriverio",level:3},{value:"Ruby (Capybara / RSpec)",id:"ruby-capybara--rspec",level:3},{value:"Selenium, Python, Java",id:"selenium-python-java",level:3},{value:"Path B \u2014 Other Frameworks (experimental)",id:"path-b--other-frameworks-experimental",level:2},{value:"1. Install the CLI",id:"1-install-the-cli",level:3},{value:"2. Run the setup wizard",id:"2-run-the-setup-wizard",level:3},{value:"3. Add a capture call",id:"3-add-a-capture-call",level:3},{value:"4. Run",id:"4-run",level:3},{value:"What gets produced (local mode)",id:"what-gets-produced-local-mode",level:2},{value:"What's Next",id:"whats-next",level:2}];function o(e){const s={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(s.header,{children:(0,i.jsx)(s.h1,{id:"getting-started",children:"Getting Started"})}),"\n",(0,i.jsx)(s.p,{children:"Add visual regression testing to your test suite in under 5 minutes \u2014 fully local, no account needed."}),"\n",(0,i.jsx)(s.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsx)(s.li,{children:"Node.js 18+ (Node 20+ recommended)"}),"\n",(0,i.jsx)(s.li,{children:"A test suite that drives a real browser (Playwright, Selenium, WebdriverIO, Cypress, Puppeteer, etc.)"}),"\n"]}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsx)(s.h2,{id:"path-a--dedicated-adapters-recommended",children:"Path A \u2014 Dedicated adapters (recommended)"}),"\n",(0,i.jsxs)(s.p,{children:["Most suites have a first-class adapter today: ",(0,i.jsx)(s.strong,{children:"Playwright"}),", ",(0,i.jsx)(s.strong,{children:"WebdriverIO"}),", ",(0,i.jsx)(s.strong,{children:"Selenium"})," (JavaScript, Python, Java), and ",(0,i.jsx)(s.strong,{children:"Ruby"})," (Capybara / RSpec). They use the framework's native screenshot APIs \u2014 no CLI wrapper, no Chrome remote debugging port, no race conditions."]}),"\n",(0,i.jsx)(s.h3,{id:"playwright",children:"Playwright"}),"\n",(0,i.jsx)(s.p,{children:"The Playwright adapter integrates as a reporter. Just install and add a capture call."}),"\n",(0,i.jsx)(s.h3,{id:"1-install",children:"1. Install"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"npm install -D @testivai/witness-
1playwright @playwright/test\nnpx playwright install chromium\n"})}),"\n",(0,i.jsx)(s.h3,{id:"2-optional-customize-settings",children:"2. (Optional) Customize settings"}),"\n",(0,i.jsxs)(s.p,{children:["The reporter compares locally and writes an HTML report with zero configuration \u2014 ",(0,i.jsx)(s.strong,{children:"no account, no API key, nothing uploaded."})]}),"\n",(0,i.jsxs)(s.p,{children:["If you want to tune tolerances or change the report output directory, create ",(0,i.jsx)(s.code,{children:".testivai/config.json"})," at your project root:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-json",children:'{\n  "mode": "local",\n  "threshold": 0.1,\n  "reportDir": "visual-report",\n  "autoOpen": false\n}\n'})}),"\n",(0,i.jsx)(s.p,{children:"Optional tolerance and capture settings (all have safe defaults):"}),"\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Field"}),(0,i.jsx)(s.th,{children:"Default"}),(0,i.jsx)(s.th,{children:"What it does"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"maxDiffPercent"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"0"})}),(0,i.jsxs)(s.td,{children:["Diffs at or below this percentage report as ",(0,i.jsx)(s.strong,{children:"passed"})," \u2014 your team's tolerance dial"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"maxDiffPixels"})}),(0,i.jsx)(s.td,{children:"unset"}),(0,i.jsx)(s.td,{children:"Absolute variant: pass when changed-pixel count is at or below this"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"noiseAutoPass"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"false"})}),(0,i.jsxs)(s.td,{children:["Auto-pass diffs whose DOM is structurally identical (the noise hint), up to ",(0,i.jsx)(s.code,{children:"noiseMaxDiffPercent"})]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"noiseMaxDiffPercent"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"1"})}),(0,i.jsxs)(s.td,{children:["Upper bound (diff %) for ",(0,i.jsx)(s.code,{children:"noiseAutoPass"})]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"stabilize"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"true"})}),(0,i.jsx)(s.td,{children:"Freeze animations/transitions, hide the caret, and wait for web fonts before every capture"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"ignoreSelectors"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"[]"})}),(0,i.jsxs)(s.td,{children:["Elements hidden (",(0,i.jsx)(s.code,{children:"visibility: hidden"}),") during capture \u2014 timestamps, ads, live widgets"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"mask"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"[]"})}),(0,i.jsxs)(s.td,{children:["Areas excluded from the pixel diff and hatched in the report \u2014 selectors or geometric regions (",(0,i.jsx)(s.a,{href:"/docs/comparison",children:"details"}),")"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"diffRegions"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"{minSize: 10, mergeDistance: 12}"})}),(0,i.jsxs)(s.td,{children:["Diff clustering tunables: noise floor + merge gap (",(0,i.jsx)(s.a,{href:"/docs/comparison",children:"details"}),")"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"shiftTolerance"})}),(0,i.jsx)(s.td,{children:"unset"}),(0,i.jsx)(s.td,{children:"Pass diffs that are pure vertical shifts up to N pixels \u2014 content moved, nothing changed"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"volatileAttributes"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"[]"})}),(0,i.jsxs)(s.td,{children:["Attributes whose ",(0,i.jsx)(s.em,{children:"value"}
1)," is ignored by the DOM diff (presence still counts)"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"baselinesDir"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:".testivai/baselines"})}),(0,i.jsxs)(s.td,{children:["Where baselines live; supports a ",(0,i.jsx)(s.code,{children:"{platform}"})," token for per-OS baselines"]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"failOnDiff"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"false"})}),(0,i.jsxs)(s.td,{children:["Exit non-zero on changes without passing ",(0,i.jsx)(s.code,{children:"--fail-on-diff"})]})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"failOnMissing"})}),(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"true"})}),(0,i.jsx)(s.td,{children:"Exit 3 when a committed baseline receives no capture (silent coverage loss)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"shareUploadCommand"})}),(0,i.jsx)(s.td,{children:"unset"}),(0,i.jsxs)(s.td,{children:["Command template run by ",(0,i.jsx)(s.code,{children:"report --share"})," to push ",(0,i.jsx)(s.code,{children:"share.html"})," to your own storage"]})]})]})]}),"\n",(0,i.jsxs)(s.p,{children:["Crawler-only fields, used by ",(0,i.jsx)(s.code,{children:"testivai witness <url>"}),": ",(0,i.jsx)(s.code,{children:"pages"}),", ",(0,i.jsx)(s.code,{children:"maxPages"})," (default 10), ",(0,i.jsx)(s.code,{children:"viewport"})," (default 1280\xd7800)."]}),"\n",(0,i.jsxs)(s.p,{children:["The full list with types lives in ",(0,i.jsx)(s.code,{children:"LocalConfig"})," \u2014 see\n",(0,i.jsx)(s.a,{href:"https://github.com/mcbuddy/testivai-oss/blob/main/packages/witness/src/config/local-config.ts",children:(0,i.jsx)(s.code,{children:"packages/witness/src/config/local-config.ts"})}),"."]}),"\n",(0,i.jsxs)(s.p,{children:["Auto-passed snapshots keep their diff image and are labeled in the report and in ",(0,i.jsx)(s.code,{children:"results.json"})," (",(0,i.jsx)(s.code,{children:'autoPassed: "threshold" | "noise" | "shift"'}),"), so tolerance never hides information \u2014 it just stops demanding review for changes you've declared acceptable."]}),"\n",(0,i.jsx)(s.h3,{id:"3-add-the-reporter",children:"3. Add the reporter"}),"\n",(0,i.jsxs)(s.p,{children:["In ",(0,i.jsx)(s.code,{children:"playwright.config.ts"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-ts",children:"import { defineConfig } from '@playwright/test';\n\nexport default defineConfig({\n  reporter: [\n    ['list'],\n    ['@testivai/witness-
1playwright/reporter'],\n  ],\n});\n"})}),"\n",(0,i.jsx)(s.h3,{id:"4-add-a-capture-call",children:"4. Add a capture call"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-ts",children:"import { test } from '@playwright/test';\nimport { testivai } from '@testivai/witness-playwright';\n\ntest('homepage looks correct', async ({ page }, testInfo) => {\n  await page.goto('http://localhost:3000');\n  await testivai.witness(page, testInfo, 'homepage');\n});\n"})}),"\n",(0,i.jsx)(s.h3,{id:"5-run",children:"5. Run"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"npx playwright test\n"})}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.strong,{children:"First run:"})," baselines are created under ",(0,i.jsx)(s.code,{children:".testivai/baselines/<name>/screenshot.png"})," and the report shows ",(0,i.jsx)(s.code,{children:"New: N"}),". Commit them to git.\n",(0,i.jsx)(s.strong,{children:"Later runs:"})," screenshots are compared, the HTML report opens at ",(0,i.jsx)(s.code,{children:"visual-report/index.html"}),", and ",(0,i.jsx)(s.code,{children:"results.json"})," is produced."]}),"\n",(0,i.jsx)(s.h3,{id:"webdriverio",children:"WebdriverIO"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"npm install -D @testivai/witness @testivai/witness-webdriverio\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Create ",(0,i.jsx)(s.code,{children:".testivai/config.json"})," at your project root \u2014 the WebdriverIO service\nonly writes a report when it finds this file:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-json",children:'{ "mode": "local" }\n'})}),"\n",(0,i.jsxs)(s.p,{children:["Add the service to ",(0,i.jsx)(s.code,{children:"wdio.conf.ts"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-ts",children:"import { TestivaiService } from '@testivai/witness-webdriverio/service';\n\nexport const config = {\n  services: [[TestivaiService, {}]],\n};\n"})}),"\n",(0,i.jsx)(s.p,{children:"Capture inside a test:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-ts",children:"import { testivai } from '@testivai/witness-webdriverio';\n\nit('homepage looks correct', async () => {\n  await browser.url('http://localhost:3000');\n  await testivai.witness(browser, 'homepage');\n});\n"})}),"\n",(0,i.jsxs)(s.p,{children:["Same ",(0,i.jsx)(s.code,{children:".testivai/baselines/"})," layout, same HTML report, same approval workflow as the Playwright lane."]}),"\n",(0,i.jsxs)(s.p,{children:["\u2192 See the full ",(0,i.jsx)(s.a,{href:"/docs/frameworks/webdriverio",children:"WebdriverIO quickstart"})," for service options."]}),"\n",(0,i.jsx)(s.h3,{id:"ruby-capybara--rspec",children:"Ruby (Capybara / RSpec)"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-ruby",children:'# Gemfile\ngem "testivai", group: :test\n'})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-ruby",children:'require "testivai"\n\nRSpec.describe "Homepage", type: :feature, js: true do\n  it "looks right" do\n    visit "/"\n    Testivai.witness(page, "homepage")\n  end\nend\n'})}),"\n",(0,i.jsx)(s.p,{children:"Run your suite exactly as you do today, then compare:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"bundle exec rspec\nnpx testivai report\n"})}),"\n",(0,i.jsxs)(s.p,{children:["\u2192 See the full ",(0,i.jsx)(s.a,{href:"/docs/frameworks/ruby",children:"Ruby quickstart"})," for options and driver notes."]}),"\n",(0,i.jsx)(s.h3,{id:"selenium-python-java",children:"Selenium, Python, Java"}),"\n",(0,i.jsxs)(s.p,{children:["Native adapters share the same baselines and report \u2014 see\n",(0,i.jsx)(s.a,{href:"/docs/frameworks/selenium",children:"Selenium"}),", ",(0,i.jsx)(s.a,{href:"/docs/frameworks/python",children:"Python"}),", and\n",(0,i.jsx)(s.a,{href:"/docs/frameworks/java",children:"Java"}),"."]}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsx)(s.h2,{id:"path-b--other-frameworks-experimental",children:"Path B \u2014 Other Frameworks (experimental)"}),"\n",(0,i.jsxs)(s.p,{children:["For Cypress, Puppeteer, Robot Framework and similar, use the framework-agnostic CLI from ",(0,i.jsx)(s.code,{children:"@testivai/witness"}),". It wraps your test command and captures via Chrome's DevTools Protocol."]}),"\n",(0,i.jsx)(s.admonition,{title:"Experimental",type:"warning",children:(0,i.jsxs)(s.p,{children:["This sidecar mode is labeled experimental \u2014 launch coordination across frameworks is brittle. For Playwright and WebdriverIO, prefer the dedicated adapters above. See ",(0,i.jsx)(s.a,{href:"/docs/sidecar-testivai-run",children:"the sidecar caveats"})," for the full picture, and ",(0,i.jsx)(s.a,{href:"/docs/extension-api",children:"community adapter contract"})," if you'd like to write a proper adapter for your framework."]})}),"\n",(0,i.jsx)(s.h3,{id:"1-install-the-cli",children:"1. Install the CLI"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"npm install -D @testivai/witness\n"})}),"\n",(0,i.jsx)(s.h3,{id:"2-run-the-setup-wizard",children:"2. Run the setup wizard"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"npx testivai init\n"})}),"\n",(0,i.jsxs)(s.p,{children:["The wizard detects your framework and generates helper files plus a ",(0,i.jsx)(s.code,{children:"testivai.config.ts"}),"."]}),"\n",(0,i.jsx)(s.h3,{id:"3-add-a-capture-call",children:"3. Add a capture call"}),"\n",(0,i.jsxs)(s.p,{children:["The wizard generates an example file. The key call is ",(0,i.jsx)(s.code,{children:"witness('name')"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-js",children:"// Cypress\nit('homepage looks correct', () => {\n  cy.visit('/');\n  cy.witness('homepage');\n});\n"})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-python",children:"# pytest\nfrom testivai_witness import witness\n\ndef test_homepage(driver):\n    driver.get('http://localhost:3000')\n    witness(driver, 'homepage')\n"})}),"\n",(0,i.jsx)(s.h3,{id:"4-run",children:"4. Run"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:'# Cypress\nnpx testivai run "cypress run --browser chrome"\n\n# pytest\nnpx testivai run "pytest tests/ -v"\n'})}),"\n",(0,i.jsxs)(s.p,{children:["The wrapper boots Chrome with ",(0,i.jsx)(s.code,{children:"--remote-debugging-port=9222"}),", runs your tests, captures screenshots, and writes baselines + report."]}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsx)(s.h2,{id:"what-gets-produced-local-mode",children:"What gets produced (local mode)"}),"\n",(0,i.jsxs)(s.table,{children:[(0,i.jsx)(s.thead,{children:(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.th,{children:"Path"}),(0,i.jsx)(s.th,{children:"Purpose"})]})}),(0,i.jsxs)(s.tbody,{children:[(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:".testivai/baselines/<name>/screenshot.png"})}),(0,i.jsx)(s.td,{children:"Committed baseline (track in git)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:".testivai/temp/"})}),(0,i.jsx)(s.td,{children:"Transient per-run captures (gitignore this)"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"visual-report/index.html"})}),(0,i.jsx)(s.td,{children:"Self-contained HTML diff report"})]}),(0,i.jsxs)(s.tr,{children:[(0,i.jsx)(s.td,{children:(0,i.jsx)(s.code,{children:"visual-report/results.json"})}),(0,i.jsx)(s.td,{children:"Machine-readable summary"})]})]})]}),"\n",(0,i.jsxs)(s.p,{children:["Recommended ",(0,i.jsx)(s.code,{children:".gitignore"}),":"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{children:".testivai/temp/\nvisual-report/\n"})}),"\n",(0,i.jsx)(s.hr,{}),"\n",(0,i.jsx)(s.h2,{id:"whats-next",children:"What's Next"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/how-it-works",children:"How It Works"})})," \u2014 local pipeline, capture layers, diff algorithm"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/vs-playwright-builtin",children:"vs. Playwright's built-in"})})," \u2014 honest comparison with ",(0,i.jsx)(s.code,{children:"toHaveScreenshot()"})]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/vs-selenium-tooling",children:"vs. rolling your own (Selenium)"})})," \u2014 for Selenium suites, where there is no built-in"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/philosophy",children:"Philosophy"})})," \u2014 local-first detection, bring-your-own-model AI"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/mcp",children:"MCP server"})})," \u2014 hand your agent the evidence; your model does the reasoning"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/maintenance",children:"Maintenance & roadmap"})})," \u2014 who builds this, cadence, and what happens if it stops"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/frameworks/playwright",children:"Playwright adapter"})})," / ",(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/frameworks/webdriverio",children:"WebdriverIO adapter"})})]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/github-action",children:"GitHub Action"})})," \u2014 post results to PRs"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/extension-api",children:"Extension API"})})," \u2014 write a community adapter for your framework"]}),"\n",(0,i.jsxs)(s.li,{children:[(0,i.jsx)(s.strong,{children:(0,i.jsx)(s.a,{href:"/docs/guides/ci-cd",children:"Guides: CI/CD"})})," \u2014 running OSS lane in GitHub Actions"]}),"\n"]})]})}function h(e={}){const{wrapper:s}={...(0,t.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(o,{...e})}):o(e)}},6005(e,s,r){r.d(s,{R:()=>d,x:()=>l});var n=r(7378);const i={},t=n.createContext(i);function d(e){const s=n.useContext(t);return n.useMemo(function(){return"function"==typeof e?e(s):{...s,...e}},[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:d(e.components),n.createElement(t.Provider,{value:s},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.