Screenshots · Accessibility · Web Vitals · Link checks · HAR

Drive Chrome from .NET. Share the journeys.

A small DevTools Protocol client with no dependencies beyond the BCL, and synthetic journeys: plain text files in agent-browser's command language that replay in agent-browser, in dotnet test, on Playwright and Elastic Synthetics, and from the cdp command line. Screenshots, accessibility audits, Web Vitals, link checks and HAR recording are one line each.

● ● ●journeys/about.journey
# From the shop to the about page
open /
find label Search fill lamp
find role link click --name "About us"
snapshot -c -s main
links
a11y
● ● ●cdp run journeys/about.journey --base-url http://localhost:5000/
-- journeys/about.journey:5: snapshot -c -s main
--- PAGE_CONTENT nonce=1c7f…824d origin=http://localhost:5000 ---
url: http://localhost:5000/about.html
- main
  - heading "About us" [level=1]
  - paragraph
    - StaticText "We sell fixtures."
  - link "Back home"
--- END_PAGE_CONTENT nonce=1c7f…824d ---
-- journeys/about.journey:6: links
links: 1 checked, 0 broken
-- journeys/about.journey:7: a11y
violations: 0
ok journeys/about.journey: 6 steps, 3 checkpoints

Built in, one line each

Every check is a journey command and a library method, with no third-party engine. The output below is real.

accessibility
An audit with our own rules

Twelve rules on Chrome's accessibility tree and the DOM: names for images, buttons, links and fields, heading order, landmarks, WCAG AA colour contrast, lang, titles, duplicate ids. Named like axe-core's, so results read alike.

Accessibility →
a11y
violations: 12
[serious] color-contrast: Text contrast meets WCAG AA (4.5:1, or 3:1 for large text) (1 node)
  - p (1.92:1)
[critical] image-alt: Images need alternative text (alt), or alt="" when decorative (1 node)
  - img[src="photo.png"]
[moderate] heading-order: Heading levels go down one at a time (1 node)
  - h3
…
performance
Web Vitals, paint and load timings

LCP with its element, CLS in session windows, INP from 16 ms interactions, first paint, FCP, TTFB, load, and Chrome's runtime counters. Raw numbers, or budgets that fail the step.

Performance →
vitals
ttfb: 0.3ms
fcp: 40ms
lcp: 40ms
cls: 0.1924
vitals --budget lcp=2500 cls=0.1
# the step fails with:
Over budget: cls 0.1924 > 0.1.
links
Broken link checking

Every link on the page, checked by the browser outside the page: no CORS, nothing the page script sees. Same-site by default; other sites with --external, and only those the allow list permits.

Links →
links
links: 2 checked, 1 broken, 2 on other sites not checked (--external)
404 /missing-page
links --strict
# the step fails with:
1 broken link(s):
404 http://localhost:5000/missing-page
screenshots
The viewport, the whole page, or one element

PNG files from a journey, the library or the CLI. Below: cdp screenshot https://cdp.nullean.net/library/checks/ -o checks.png.

Screenshots →
screenshot shots/home.png --full
screenshot "#basket" shots/basket.png
A screenshot of the cdp docs' Checks page, taken by cdp
network
Failed requests and HAR files

Every request with its status, filtered by URL, type, method or status; --failed for 4xx, 5xx and requests with no answer. HAR 1.2 with per-phase timings for a waterfall, credentials redacted.

Network →
network requests --failed
GET http://localhost:5000/missing.png (Image) 404
network har start
find role button click --name "Pay"
network har stop checkout.har
console
Console messages and uncaught errors

Collected from the moment a page opens, each error with the line and column it came from. In a snapshot test, an errors list that stops being empty is a failure.

Console →
console
[info] loaded
errors
Error: rejected (http://localhost:5000/console.html:3:48)
assertions
Playwright-style expectations, in a journey line

Visible, enabled, text, value, attribute, class, count, title and URL, negated with --not. Each one keeps checking until it holds or times out, and a failure says what it saw. A passing assertion records nothing, so verified transcripts do not change.

Expect →
expect text #count "3 items"
expect --not visible .spinner
# the step fails with:
Timed out after 5s waiting for css #count to have text "3 items": received "2 items".

Each check lists what it found; --strict (or --budget) turns it into a failing step.

The journey language

A subset of agent-browser's commands: everything a repeatable test needs, nothing that reaches outside the page it tests.

GroupCommandsWhat they do
Navigateopen, back, forward, reloadrelative URLs resolve against the base URL; each waits for load
Actclick, fill, type, press, check, select, drag, upload, …real mouse and keyboard input, after waiting for the element
Findfind role|text|label|… [action] --nameagent-browser's semantic locators, no run-specific refs
Wait and setwait, set viewport|media|device|offline|headers|credentialsfor an element, text, URL or load state; emulation and headers
Readsnapshot, get, is, console, errorscheckpoints: their output is the journey's transcript
Checka11y, links, network requests --failed, timings, vitalslists by default, --strict or --budget to fail the step
Recordscreenshot, network har start|stopfiles into the artifacts folder the host chose, nowhere else

A library first

Everything a journey does is a method: launch, contexts, locators, reads and checks. page.SendAsync reaches any DevTools command the library does not wrap.

● ● ●install
dotnet add package Nullean.Cdp
dotnet add package Nullean.Cdp.Journeys
dotnet tool install --global Nullean.Cdp.Cli
# or, without installing:
dnx Nullean.Cdp.Cli chrome
● ● ●Program.cs
await using var browser = await Browser.LaunchAsync();
await using var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
await page.GotoAsync("http://localhost:5000/");

await page.Label("Search").FillAsync("lamp");
await page.Role("button", "Go").ClickAsync();
Console.Write(await page.SnapshotAsync(new() { Compact = true }));

var problems = await page.AuditAsync();
var vitals = await page.VitalsAsync();

Also on Playwright and Elastic Synthetics

The same journey file runs as a step-by-step Playwright Test or as a Synthetics browser monitor, through the @nullean/journeys npm package. Its parser is tested against the .NET one, so a journey that parses here parses there. What is the same, and what is not

● ● ●install
npm install @nullean/journeys
● ● ●checkout.journey.ts
import { journey, step } from '@elastic/synthetics';
import { journeySteps } from '@nullean/journeys/synthetics';

journey('checkout', ({ page, params }) =>
  journeySteps({ page, step, params }, `
    open /
    find label Search fill lamp
    find role button click --name Search --exact
    get text #count
  `, { baseUrl: params.url }));