inspectra-engine
v1.2.1
Published
Playwright UI testing and accessibility toolkit for Node.js. Compare URL, Figma and PNG baselines using Shivax, with DOM snapshots, layout measurements and JSON reports.
Maintainers
Keywords
Readme
Inspectra: Playwright UI Testing and Accessibility for Node.js
Inspectra (inspectra-engine) compares a website against a baseline and reports measured UI differences. The baseline can be another URL, a Figma frame or a PNG screenshot. Inspectra opens pages with Playwright, extracts layout, typography and structure, and compares the results with ShivAX. It runs axe-core accessibility checks too, and writes a JSON report with screenshots.
Requires Node.js 22.15+. ESM only. TypeScript declarations are included.
npm package · Source code · Report an issue · Changelog
What's new in 1.2.1 (unreleased)
- Restore SHIVAX 0.2 comparison compatibility while keeping governed feedback on 0.3.x.
- Make standalone installs and explicit candidate testing reproducible, with validated package staging.
- Improve OCR availability reporting, compatibility checks and artifact hygiene.
Added in 1.2.0
- Learning feedback proposals.
inspectra-engine/feedbackturns reviewer decisions about findings into ShivAX 0.3.0 governed-feedback proposals. See Learning feedback proposals. - ShivAX 0.3.0 support. The
shivaxrange is now^0.2.0 || ^0.3.0. Comparison works with both, and nothing breaks. - Dependencies. Playwright
^1.65.0and CSpell^10.4.1.
1.1.0 DRISHTI added the browser network policy, the runner scan policy and inspectra-engine/shivax, and required shivax@^0.2.0. See the changelog for every release.
Features
- Capture a baseline and a target with the same viewport and device pixel ratio.
- Measure text, typography, spacing, size, hierarchy and layout.
- Compare structured measurements, with screenshots as evidence. The main flow does not diff pixels.
- Use a URL, Figma or PNG baseline. Values inferred from images carry provenance and confidence.
- Run axe-core accessibility checks, plus optional styling, layout and spelling checks.
- Get JSON reports, scan events, cancellation and a local HTTP runner.
- Keep scan data apart with explicit company or personal scopes.
- Limit network access per capture.
Installation
npm install inspectra-engine
npx playwright install chromium- Figma baselines need a Figma token in the Node.js process.
- PNG baselines need local OCR: macOS Vision, or Tesseract with English data. You can supply your own OCR function instead.
For source checkouts, use the standalone development and candidate-testing guide. See local OCR setup and availability checks for PNG tests.
Quick start
import { compare } from 'inspectra-engine';
const result = await compare({
baseline: 'https://example.com',
target: 'https://staging.example.com',
viewport: { width: 1440, height: 900 },
scope: { kind: 'company', id: 'my-company' },
output: `./reports/scan-${Date.now()}`,
});
console.log(result.issues);
console.log(result.artifacts.report);The output folder must not exist yet. A finished comparison writes report.json, baseline.png and target.png to it.
Common options:
| Option | Default | Purpose |
| --- | --- | --- |
| viewport | 1440x900 | CSS viewport for both pages |
| accessibility | true | Run axe-core WCAG A/AA checks |
| uiChecks | false | Add styling, layout and completeness checks (URL baselines) |
| spelling | false | Add spelling checks (needs uiChecks) |
| network | none | Limit what pages can load. See below |
| signal | none | AbortSignal to cancel the scan |
| onEvent | none | Receive progress, issue and requestblocked events |
To compare against Figma, pass baseline: { type: 'figma', url, nodeId }. For a PNG, pass baseline: { type: 'png', path, viewport: { width, height, devicePixelRatio } }. Record the viewport and DPR when you take the screenshot. Treat PNG results as estimates that need review.
Main functions
Import these from inspectra-engine or inspectra-engine/node.
| Function | Purpose |
| --- | --- |
| compare(options) | Compare a URL, Figma or PNG baseline with a target page and save artifacts. |
| captureUISnapshot(page, options) | Turn an open Playwright page into a ShivAX UI snapshot. |
| captureUIPage(page, options) | Capture a snapshot, extra metadata and an optional screenshot. |
| captureUISnapshotPair(options) | Capture two URLs with the same browser settings. |
| scanAccessibility(page, options) | Run axe-core checks on an open page. |
| captureFigmaBaseline(source, options) | Fetch a Figma frame as a snapshot and preview. |
| capturePNGBaseline(source, options) | Turn a PNG into a snapshot with estimated values. |
| startLocalRunner(options) | Start the loopback HTTP runner. |
| createNetworkPolicy(options) | Build a policy that limits what a capture can load. |
inspectra-engine/client is safe to import in a browser. It exports report types and COMPARISON_VERSION, and it does not run scans. inspectra-engine/shivax holds the optional ShivAX integration, and inspectra-engine/feedback the optional learning feedback adapter.
Scopes label data. They do not replace authentication or storage access control in your app.
CLI and local runner
# Compare two pages
npx inspectra compare --baseline https://example.com --target http://localhost:3000 \
--output ./report --scope-kind personal --scope-id me
# Start the local runner for a frontend app
npx inspectra dev --origin http://localhost:5173 --scan-origin http://localhost:3000compareexits with 0 on success (findings included), 1 on error and 130 when cancelled.devreads.envfrom the current folder. Shell variables override.env, and flags override both. Copy.env.exampleto start.- The runner binds to
127.0.0.1. It prints a startup token that lasts 15 minutes and does not renew. Each scan session gets its own token. --originlists the frontend origins allowed to call the runner.--scan-originlists the origins scans can load.
See examples/README.md for a full runner walkthrough with curl.
Browser network policy
By default, a capture can load anything the page asks for, as in 1.0.x. To limit it, pass network:
const result = await compare({
baseline: 'http://localhost:3000',
target: 'http://localhost:3001',
scope: { kind: 'personal', id: 'local-user' },
output: `./reports/scan-${Date.now()}`,
network: {
allowedOrigins: ['http://localhost:3000', 'http://localhost:3001', 'https://fonts.gstatic.com'],
// Optional. Throw to block a request.
check: (url) => { if (url.includes('/admin')) throw new Error('denied'); },
},
});How the policy works:
allowedOriginstakes exact HTTP(S) origins. Paths, wildcards and credentials are not allowed. Awss:orws:request counts as itshttps:orhttp:origin.- If you set both
allowedOriginsandcheck, a request must pass both.checkmust be synchronous. If it returns a promise, Inspectra blocks the request. - Inspectra checks every request: page loads, each redirect hop, subresources, frames, workers, beacons, WebSocket and HTTPS tunnels, and popups. It closes popups.
data:,blob:andabout:URLs make no network request, so the policy allows them. It blocksfile:and all other schemes.- Inspectra checks the start URLs before it launches a browser or creates an output folder.
- If a blocked font or image stops a capture, the error lists the blocked origins.
- The first blocked request to each origin emits a
{ type: 'page', name: 'requestblocked', side, origin }event, for up to 64 origins per page. Events and errors show origins, not full URLs. - Guarded contexts turn off service workers, downloads and WebRTC.
Why a proxy: Playwright's route.continue() follows redirects without passing them back to the route handler, so a redirect could reach a blocked origin. Each guarded browser context sends its traffic through its own loopback proxy, which has a one-time password. The proxy checks every connection. For plain HTTP and first-hop requests, check gets the full URL. For HTTPS and WebSocket tunnels, it gets scheme://host:port/.
Limits:
- The policy matches hostnames, not IP addresses, so it cannot stop DNS rebinding. For strict isolation, list IP origins, or run the scanner behind your own DNS and egress controls.
- Figma REST calls and PNG decoding do not use the browser.
networkdoes not cover them.
For the local runner, set INSPECTRA_SCAN_ALLOWED_ORIGINS or pass --scan-origin once per origin. This sets one policy for every scan, and scan requests cannot change it. The runner rejects a disallowed baseline or target with HTTP 400. An empty list means no limit.
ShivAX integration (optional)
ShivAX 0.2.0 and 0.3.0 ship InspectraInspectionProvider in shivax/adapters/inspectra. That provider captures pages with Inspectra's captureUIPage. It expects a trusted host to open each page and enforce network rules. inspectra-engine/shivax is that host. It does not replace or copy ShivAX's provider, bots, firewall or comparison code.
import { createShivaxInspectionProvider } from 'inspectra-engine/shivax';
import { registerInspectionService } from 'shivax/integrations';
const { provider, host } = await createShivaxInspectionProvider({
allowedOrigins: ['https://app.example.com'],
});
registerInspectionService(tools, provider, { authorizeURL: (url) => url.startsWith('https://app.example.com/') });
// On shutdown:
await host.close();Each capture gets a new, isolated browser context with its own proxy. A request must pass two checks: the host's allowedOrigins, and ShivAX's checkDestination for that capture, which runs the firewall's network policy and lease checks. Both checks apply to every page load, redirect hop, subresource and tunnel. The host checks the final page URL too.
- Deny by default. The host refuses a capture that has no ShivAX security contract. You get this contract from a firewall with
protectExecution. The host refuses a contract that belongs to a different principal, session or scope. For local development, setallowUnprotected: true. TheallowedOriginslist still applies. - Cancellation and lockdown close the page and its proxy.
close()resolves after cleanup finishes. - Screenshots.
screenshots: { directory }saves one PNG per capture, with a unique name. ShivAX's firewall allows file writes only to exact paths you list in advance, and an operation that writes files must bestate-changing, which needs approval. Under a firewall, usescreenshots: { allocate(request, identity) { return { path, reference }; } }to return a path your policy lists. ShivAX sees thereference, not the path. Callhost.screenshotPath(reference)to find the file. - Limits.
maxLeasescaps open pages per host (default 8, range 1 to 64). The host rejectslaunchOptions.argsthat change proxy, DNS, certificate, remote debugging or web security settings. - If you build
InspectraInspectionProvideryourself, callcreateShivaxPageHost(options). It returns{ openPage, screenshotPath, activeLeases, close }.
Use ShivAX 0.3.1 for a normal co-install with current Inspectra. ShivAX 0.2.0 and 0.3.0 pin their optional peer inspectra-engine to 1.0.4, so their historical compatibility tests need --legacy-peer-deps. Version 0.3.1 fixes that peer range; comparison remains compatible with 0.2.x.
Importing inspectra-engine or inspectra-engine/node does not load this module or any ShivAX AI entry point. Comparison stays deterministic. It needs no model provider and no network access beyond the pages you scan.
Learning feedback proposals (optional, needs ShivAX 0.3.0 SARASWATI)
inspectra-engine/feedback turns a reviewer's decisions about findings in a comparison report into ShivAX governed-feedback proposals. A proposal carries no approval. Only your trusted host can make it eligible for training, by attesting it with ShivAX's createReviewAttestor (a Jay-verified reviewer session) before curateDataset.
import { createFeedbackProposals, selectMeasuredEvidence } from 'inspectra-engine/feedback';
const { proposals, pending, rejected } = await createFeedbackProposals(report, [
{ issueId: report.issues[0].id, decision: 'accept' },
{ issueId: report.issues[1].id, decision: 'correct', correction: { severity: 'low' } },
{ issueId: report.issues[2].id, decision: 'needs-review' },
], {
owner: { scopeKind: 'company', scopeId: 'acme' }, // must equal report.scope
references: { baseline: 'page:home.v1', target: 'page:home.build-42', report: 'scan-0001' }, // opaque, not URLs
consent: { ref: 'consent-acme-qa', purposes: ['training', 'evaluation'] },
createdAt: Date.now(),
retentionMs: 30 * 86_400_000,
});
// Trusted host only: attest with a Jay reviewer session, then curate.
// curateDataset(records, attestations, policy, { confirmReview, consentActive, select: selectMeasuredEvidence }, signal)- Decisions are
accept,reject,correct(with acorrection) andneeds-review.needs-reviewgives no feedback. - The report's own
reviewStatusandreviewfields never count as decisions. A review object with any other field, such asverifiedorapproved, is rejected. - Every measurement keeps its provenance:
computed(browser CSS and geometry),design(Figma) orinferred(PNG, OCR, vision or heuristic estimates). A PNG value that claims to be computed is rejected.selectMeasuredEvidencekeeps inferred findings out of a training set. - Proposals never contain URLs, screenshot paths, image data, page text, Figma tokens or file keys. The adapter makes no network calls and launches no browser.
- With ShivAX 0.2.x the adapter throws
ShivaxFeedbackUnavailableError. Comparison itself does not need it.
See docs/integrations/shivax-learning-feedback.md for the schema and limits.
FAQ
Does Inspectra compare screenshots pixel by pixel?
No. It compares measured values: geometry, computed styles and structure. Screenshots are evidence for review. Values inferred from a PNG keep their confidence and provenance, and Inspectra does not present them as real CSS values.
Can Inspectra run inside a React app in the browser?
No. Scanning needs Node.js and Playwright. A browser app can import types from inspectra-engine/client and call the local runner or your own worker.
Do I need a hosted service?
No. Inspectra runs wherever your Node.js code runs, such as a local process or a worker your app owns.
Changelog
See CHANGELOG.md.
License
Apache-2.0. Copyright 2026 Urvesh Gohil. Third-party dependencies keep their own licenses.
