A small Chrome DevTools Protocol client for .NET, and the synthetic journeys built on it: plain text browser checks that replay in agent-browser, dotnet test and a CLI.
A small Chrome DevTools Protocol client for .NET: find and launch Chrome, drive pages with agent-browser style locators, and read the accessibility tree. Native AOT and trim clean, with no dependencies beyond the BCL.
It is the browser underneath synthetic journeys: plain text files written in agent-browser's command language that
replay in agent-browser, in dotnet test (with Nullean.Snapshots) and from
a command line tool. The npm package @nullean/journeys runs the same
journeys on Playwright Test and Elastic Synthetics (see js/journeys).
# Get started from the landing page
open /
find role link click --name "Get started"
wait main
snapshot -c -s main
Status: early, 0.x. See ROADMAP.md.
dotnet add package Nullean.Cdp # the CDP client
dotnet add package Nullean.Cdp.Journeys # parse and run journeys from .NET
dotnet tool install -g Nullean.Cdp.Cli # the cdp commandusing Nullean.Cdp;
using Nullean.Cdp.Accessibility;
await using var browser = await Browser.LaunchAsync(); // headless, temporary profile
await using var context = await browser.NewContextAsync(); // own cookies and storage
var page = await context.NewPageAsync();
await page.GotoAsync("http://localhost:5000/");
Console.Write(await page.SnapshotAsync(new AxSnapshotOptions { Compact = true, Selector = "main" }));
// - main
// - heading "Fixture shop" [level=1]
// - textbox "Search"
// - checkbox "Only in stock" [checked=true]Locators wait for their element and act like a user:
await page.Label("Search").FillAsync("lamp");
await page.Role("checkbox", "Only in stock").CheckAsync();
await page.Role("button", "Go").ClickAsync(); // waits for any navigation it starts
await page.Text("3 results").WaitAsync();page.SendAsync(method, params) and browser.Connection reach any CDP command the library does not wrap.
Nullean.Cdp.Journeys runs synthetic journeys: text files with one agent-browser command per line.
# checkout.journey
open /shop
find label Search fill lamp
press Enter
wait --text "results"
find role button click --name "Add to basket"
snapshot -c -s main # a checkpoint: its output is recorded
get text .basket-count # another
using Nullean.Cdp.Journeys;
var journey = Journey.Load("journeys/checkout.journey");
var result = await JourneyRunner.RunAsync(journey, browser, new JourneyOptions { BaseUrl = new("http://localhost:5000/") });
foreach (var checkpoint in result.Checkpoints)
Console.WriteLine($"{checkpoint.Command}\n{checkpoint.Output}");
if (result.Failure is { } failure)
Console.WriteLine(failure.Describe(result.Source)); // checkout.journey:5: find role button ... and why- The commands are agent-browser's, written the same way: navigation, every element action (
click,dblclick,hover,focus,fill,type,check,select,upload,drag,scroll, ...),pressandkeyboard,find(role, text, label, ..., first, last, nth),wait(element, time, text, URL, load state),set(viewport, media, device, geo, offline, headers, credentials), and the checkpointssnapshot,get,is,console,errors,network requests(--failedfor 4xx, 5xx and requests that got no answer),links(checks the page's links: same-site by default,--externalfor other sites in the allow list),a11y(our own accessibility rules), andtimings,vitalsandmetrics(raw, or--budget lcp=2500 cls=0.1for stable transcripts).--strictonnetwork requests,linksanda11yfails the step instead of listing.screenshot [selector] <path.png> [--full]andnetwork har start/network har stop <path>write into the artifacts folder. Left out on purpose: refs,eval, cookies and session state, tabs and downloads. Anything unsupported is an error that names the line. - No refs.
@e3changes from run to run, so journeys usefind role … --name,find text,find labelor CSS. - Quoting is a shell's: double or single quotes, backslash escapes.
#starts a comment, but#idis a selector. - URLs:
open /shopresolves against the base URL like a link in a page would, so a base URL for a folder needs its trailing slash. - Safe to share: a run reaches only the base URL's host and the hosts in the journey's
allowlines, on every request including clicks, redirects and popups;file://stays in the base folder; downloads are denied. Passwords are$env:NAMEreferences, typed when the step runs and redacted from everything the run reports. - Replay in agent-browser:
journey.ToAgentBrowserBatch(baseUrl)gives the batch JSON, foragent-browser batch --bail. A journey using every supported command runs there unchanged.
cdp check journeys/*.journey # parse only, every error with its line
cdp run journeys/checkout.journey --base-url http://localhost:5000/
cdp run site/journeys/*.journey --base-url ./site --json
cdp snapshot http://localhost:5000/ --compact # one page's accessibility tree
cdp batch journeys/checkout.journey --base-url http://localhost:5000/ | agent-browser batch --bail
cdp chrome # which Chrome, or where it lookedEverything the page produced is printed between --- PAGE_CONTENT nonce=… --- markers with a nonce per run, so an
agent reading the output can tell the page's words from the tool's. Exit codes: 0 ok, 1 failed, 2 usage, 3 no browser.
The command name cdp is a working name.
CDP_CHROME, then CHROME_PATH, then the standard install locations for Chrome, Chrome for Testing, Chromium and
Edge on each OS, then google-chrome, chromium and chrome on the PATH. When nothing is found, the error lists
every place it looked.
| Package | What it is | |
|---|---|---|
Nullean.Cdp |
the CDP client: find and launch Chrome, locators, the accessibility tree. No dependencies beyond the BCL | |
Nullean.Cdp.Journeys |
synthetic journeys: parse, run with Nullean.Cdp, export to agent-browser batch JSON | |
Nullean.Cdp.Cli |
the cdp command: check, run, snapshot, screenshot, batch, parse, chrome |
|
@nullean/journeys (npm) |
the same journeys on Playwright Test and Elastic Synthetics |
- Documentation: https://cdp.nullean.net
- ROADMAP.md and DECISIONS.md
- Releases
- Contributing
MIT licensed, see LICENSE.txt. Copyright Nullean and contributors.
