Browser testing harness

End-to-end test harness backed by headless Chrome via chromedp. It runs the project's own codegen, boots a real Piko server as a subprocess, and gives Go tests a fluent page driver.

Overview

The browser harness is a test helper, not a runtime service. There is no Piko bootstrap option for it. It lives entirely inside _test.go files. Construct one in TestMain, call Setup(), then write each test against an isolated *Page from browser.New(t).

Setup() runs the project's piko codegen, allocates a free port, and starts the server as a subprocess. It then brings up a chromedp-controlled Chrome session pointed at that server. Each test that calls browser.New(t) gets its own *Page backed by an incognito browser context, so tests run in parallel without cross-contamination. You only write browser.New(t), the harness threads itself through the global it set up in TestMain.

Because the harness boots a real server the same way production does, it gives high-fidelity end-to-end coverage instead of an in-process stub. It runs the project's own generator and server entrypoints under PIKO_E2E_MODE on an auto-allocated port, and waits for the server to report ready before tests run.

Golden assertions and screenshots

Golden assertions diff normalised HTML, not images. Page.MatchGolden(selector, name) and Assertion.MatchesGolden(name) capture the element's DOM, normalise it, and compare it against testdata/golden/<name>.html relative to the test working directory. To create or refresh a golden file, set PIKO_UPDATE_GOLDEN=1 (or pass the -update-goldens flag, or browser.WithUpdateGoldens(true) for spec tests).

Screenshots and PDFs are separate artefacts, not golden inputs. Page.Screenshot, Page.SaveScreenshot, Page.PrintToPDF, and Page.SavePDF write image and PDF output to the output directory. The standalone Capturer drives its own browser to capture a full-page screenshot plus the page HTML for a given URL.

Piko-aware page helpers

The page driver exposes operations that a generic chromedp setup does not. These are the reason to use the harness for Piko apps. PikoPartialReload and PikoWaitForPartialReload drive partial reloads. PikoBusEmit and PikoBusWaitForEvent emit and await event bus events. PikoDispatchFragmentMorph applies a fragment morph to a selector. The PikoDebug* methods (for example PikoDebugIsConnected, PikoDebugGetRegisteredCallbacks, PikoDebugGetAllConnectedPartials) inspect partial connection and lifecycle state.

Interactive debugging

Step-through debugging runs the browser headed. WithInteractive(true) enables the bubbletea TUI and forces headless off. WithSimpleInteractive() uses a basic ANSI mode for terminals without TUI support. Both pause between actions so you can inspect the page.

Requirements

  • A Chrome or Chromium binary discoverable via chromedp, typically on PATH or in a standard install location.
  • A piko-shaped project at the directory passed via WithProjectDir (defaults to the test working directory). Setup() runs go run ./cmd/generator/main.go all to regenerate code, then starts the server from a prebuilt ./server binary if present, otherwise go run ./cmd/server. An arbitrary Go project without these entrypoints fails at setup.

Configuration

The canonical wiring carries the e2e build tag, so these tests stay out of a plain go test run and need -tags e2e. Create one harness in TestMain, call Cleanup(), and propagate the test exit code.

//go:build e2e

package app_test

import (
    "fmt"
    "os"
    "testing"

    "piko.sh/piko/wdk/browser"
)

var harness *browser.Harness

func TestMain(m *testing.M) {
    harness = browser.NewHarness(
        browser.WithProjectDir("."),                  // optional; defaults to "."
        browser.WithHeadless(true),                   // optional; default true
        browser.WithEnv("PIKO_ENV", "test"),          // optional; passed to the server
    )

    if err := harness.Setup(); err != nil {
        fmt.Fprintf(os.Stderr, "E2E setup failed: %v\n", err)
        os.Exit(1)
    }

    code := m.Run()
    harness.Cleanup()
    os.Exit(code)
}

Cleanup() stops the server, closes the browser, releases the sandboxes, and removes the temp build directory. Without it the server subprocess, the browser, and the temp directory leak, and the global harness is never cleared.

A test then drives a page through the fluent API. Every action method returns *Page for chaining.

//go:build e2e

func TestCalculator(t *testing.T) {
    p := browser.New(t)
    defer p.Close()

    p.Navigate("/calc").
        Fill("#num1", "5").
        Fill("#num2", "3").
        Click("#add")

    p.Assert("#result").HasText("8")
    p.MatchGolden("#result", "calc-result")
}

NewHarness takes functional options. There is no Config struct. There is no BaseURL field, the harness picks a port and exposes it via harness.ServerURL(). There is no construction-time Timeout field, per-action timeouts flow through WaitOption values on the *Page API.

Environment and sandboxing

The harness injects PIKO_E2E_MODE=true into both the codegen and server processes, and PIKO_PORT (the chosen port) into the server. This is how it gets a free port and a deterministic URL. The harness adds extra variables from WithEnv on top. Pass a WithSandboxFactory to sandbox the source directory read-only and the output directory read-write through safedisk. Without a factory the harness falls back to a no-op sandbox.

Bootstrap

Browser tests do not bootstrap Piko itself. They invoke the project's generator and server as subprocesses. There is no piko.With* option to register. Test code uses the harness directly.

See also

Framework docs:

External:

  • chromedp, the underlying Chrome DevTools Protocol driver.