Tutorial: your first script

Build a browser automation script from scratch, run it, make it fail on purpose, and turn it into a Go test.

By the end of this you will have written a script that drives a browser, asserted something about the page, watched the assertion fail and fixed it, and run the same script as a Go test. It takes about ten minutes.

You need cdpscript installed and a Chromium-based browser. See the Quickstart if you have not done that yet.

1. Write the script

A script is a txtar archive: a plain text file holding one or more named files. The one named main.cdp is the script; anything else is a fixture written to disk before the run.

Create greet.txtar:

-- main.cdp --
goto data:text/html,%3Ctitle%3EGreeting%3C/title%3E%3Ch1%3EHello,%20cdp%3C/h1%3E
wait h1
assert text h1 'Hello, cdp'
title
screenshot greet.png

Five commands: load a page, wait for an element, assert its text, print the page title, save a screenshot.

The URL is percent-encoded for a reason. Script arguments split on whitespace, so a URL containing a literal space is truncated at the space and you silently load a different page than you meant to. %20 avoids that.

2. Run it

cdpscript -headless -o . greet.txtar
Greeting
Saved screenshot to greet.png (4699 bytes)

-o . puts artifacts in the current directory; -headless keeps the window hidden. Drop -headless to watch it happen — useful when a script does something you did not expect.

The byte count depends on your fonts and screen, so expect a different number.

3. Make it fail

Assertions are the point of a script; a script that cannot fail is not testing anything. Change the expected text to something wrong:

assert text h1 'Goodbye, cdp'
cdpscript: greet.txtar:3: assert text h1 'Goodbye, cdp': assertion failed:
text "Hello, cdp" does not contain "Goodbye, cdp"
echo $?
3

The failure names the file, the line, the command, and both sides of the comparison. Exit status 3 means specifically “an assertion failed”, as distinct from 1 for a broken script and 2 for a usage error — so a shell caller can tell “the page was wrong” from “the script was wrong”.

Put the correct text back before continuing.

4. Carry a fixture in the archive

The other files in the archive are written to a temporary directory that becomes the script’s working directory, so commands that take a filename can use a plain relative path. Create a second archive, helper-demo.txtar, leaving greet.txtar as it is:

-- main.cdp --
goto data:text/html,%3Ch1%3EX%3C/h1%3E
jsfile helper.js
assert text h1 'patched'

-- helper.js --
document.querySelector('h1').textContent = 'patched';
cdpscript -headless helper-demo.txtar
patched

This is how jsfile, upload, and the other file-taking commands reach fixtures without depending on anything outside the archive. Note that it works for filenames, not URLs — there is no variable holding the temporary directory, so you cannot goto an embedded HTML file. To test against real pages, serve them, which is what the test harness in the next step does for you.

5. Run it as a Go test

The same archive runs under go test through the cdpscripttest package, so a script that reproduces a bug becomes the regression test for it without being rewritten.

You have been working in a bare directory, so make it a module first. Fetch cdpscripttest by its own path — fetching github.com/tmc/cdp alone leaves the test without the dependencies that package needs:

go mod init tutdemo
go get github.com/tmc/cdp/cdpscripttest@latest

Then save this beside greet.txtar, as greet_test.go:

//go:build cdp

package tutdemo_test

import (
	"testing"

	"github.com/tmc/cdp/cdpscripttest"
)

func TestGreet(t *testing.T) {
	err := cdpscripttest.RunCDPScript(t.Context(), "greet.txtar",
		cdpscripttest.CDPScriptRunOptions{})
	if err != nil {
		t.Fatal(err)
	}
}
go test -tags cdp -p 1 -parallel 1 -run TestGreet ./...
ok  	tutdemo	1.842s

The tutdemo in that line is the module name you passed to go mod init.

The cdp build tag keeps browser-dependent tests out of an ordinary go test ./.... -p 1 stops several packages from launching browsers at once; -parallel 1 also serializes fixtures inside one package. See Testing for why that matters.

For a suite of scripts rather than one, loop over a glob and call RunCDPScript for each. If your fixtures need a server, start one and pass its address in Env as FIXTURE_BASE_URL — nothing sets that variable for you. See cdpscript, which also explains why the cdpscripttest command must not be used on this kind of archive.

Next steps

Last updated: 2026-09-23