e2e-web (Playwright) #
Headless-web e2e flows for the Streamplace app — the web counterpart to the
mobile .maestro/ suite. Shared harness (streamplace e2e) and testIDs;
Playwright headless Chromium drives the web-specific surface because Maestro
is mobile-only.
How it fits together #
- Harness:
streamplace e2e(seepkg/cmd/e2e.go) boots a node + local PDS/PLC + a looping WHIP test stream and printsSERVER_URL/ACCOUNT_HANDLE/ACCOUNT_DID(andE2E_FIXTURE_MP4, the local file it streams). The node also serves the web app (embedded via//go:embed all:dist/**), so Playwright just points a browser atSERVER_URL. - Shared testIDs: react-native-web maps
testID->data-testid, so the very same IDs the Maestro flows use (home-stream-card,settings-use-custom-node,settings-custom-node-url,settings-save-node) are Playwright selectors here — no separate web instrumentation. - Server setup: the app ships pointed at production, so
global-setup.tsopens/settings/advanced, enters the test node's URL, and saves the resulting browser state (storageState). Every flow reuses it and starts already pointed at the harness — the web analogue of.maestro/setup/server-setup.yaml.
Adding scenarios #
Name specs flows/<feature-or-behavior-slug>.spec.ts, without sequence numbers.
Playwright automatically discovers specs under flows/; adding a scenario
does not require editing the config, a manifest, or this README.
Each test must establish its own prerequisites: navigate to the page it needs,
log in through loginThroughPds when authentication is required, and wait for
the relevant stream or application state. Tests get isolated browser contexts
seeded with the harness node configuration, not authentication from another
test. Never rely on prior test state or filename execution order. The suite runs
serially to limit contention on its shared harness account and looping stream,
not to establish dependencies between scenarios.
Mobile coverage convention #
For shared behavior, use the corresponding semantic slug and mention its mobile
path in the spec comment; no central parity manifest is needed. Logged-out
smoke, navigation, and stream coverage correspond to
.maestro/logged-out/{smoke,tabs,stream}.yaml. OAuth setup corresponds to
.maestro/setup/oauth-login.yaml; authenticated chat/profile behavior is covered
on mobile by .maestro/logged-in/{chat-reply,chat-profile}.yaml.
The web app renders the desktop layout (a sidebar of nav links), not the
mobile tab bar, so tabs drives Home ↔ Settings through the sidebar.
go-live checks that the Live Dashboard (/live) requires login; this is
web-only coverage, with no current native Go Live scenario. Chat popout is also
web-only, as is chat-wheel-scroll (trackpad wheel input in the inverted
chat list), which also runs in a Firefox project because the bug it covers
was reported there. vod opens the VOD the harness publishes and checks both the node's
page title and the app's tab title; document titles have no native counterpart.
branding-favicons exercises authenticated light/dark favicon uploads, scheme
switches, persistence and deletion fallbacks, including the HTML before
hydration. It is web-only: native apps have no browser tab favicon, and branding
image uploads are already limited to the web.
stream also covers the narrow portrait web layout at 320 × 568, where the
live video sits above chat. It reveals the player chrome, exercises mute and
fullscreen entry/exit, and verifies that faded controls reveal instead of
accepting an unseen tap.
auto-publish-vods saves and reloads the automatic publishing preference,
checks that the stream form's Create tab and Privacy & Security share it,
including the popout form, and hides the control without VOD beta access or
outside the Create tab. The compact checkbox keeps persistent-consent details in
its tooltip. The native Maestro flow opens the shared form by deep link, enables
the preference there, and verifies persistence in settings after a relaunch.
The browser flow also holds a recording opt-out request to verify another privacy toggle cannot
overwrite it while saving. Deleting the node's settings record also withdraws
automatic publishing consent; the PDS-backed regression for that index deletion
lives in pkg/atproto/auto_publish_vod_test.go. It also injects a deletion failure
and checks that later deletions still run while the commit watermark advances
only after successful reprocessing.
syndication plays the test stream from the harness's second node, which pulls
it from the first over Media over QUIC (SERVER2_URL; see docs/moq.md). It is
web-only: node-to-node transfer has no mobile surface, and the mobile flows play
from the ingest node.
OAuth over real HTTPS #
oauth-login logs in the way a user does, through the node's OAuth proxy
and the local PDS's sign-in and consent pages. atproto OAuth will not run over
plain HTTP, with ports, or on .test-style names, and parts of it resolve
did:plc against a hardcoded https://plc.directory. So
hack/e2e-web-local.sh starts the harness with public DNS names for 127.0.0.1:
--https-pds-hostname localhost-pds.streamplace.network (plus a wildcard record
under it, for account handles) and
--https-station-hostname localhost-station.streamplace.team (the node's
broadcaster host: the app and the OAuth client). The two must be on different
registrable domains, as in production — the PDS rejects a same-site navigation
to its sign-in page. In that mode the harness:
- mints a throwaway CA and serves both on 127.0.0.1:443, routed by SNI (so it must be able to bind 443 — the container runs as root);
- publishes this build's lexicons into a local lexicon authority account (the
stand-in for the account behind
_lexicon.stream.place) and has the PDS resolve lexicons from it, so the OAuth permission set the app asks for (include:place.stream.authFull) is this branch's, not production's; - runs a small proxy that sends
plc.directoryto the local PLC; the node and the browser use it; - has each process trust the CA on its own (Go
SSL_CERT_DIR, NodeNODE_EXTRA_CA_CERTS, Chromium the leaf's SPKI) — nothing is installed system-wide; - additionally prints
SERVER_HTTPS_URL,PDS_HTTPS_URL,E2E_PROXY_URLandE2E_TLS_SPKI, which the config and the flow pick up.
Unauthenticated flows keep using the plain-HTTP SERVER_URL; OAuth and chat
popout tests establish their own login using the HTTPS URLs. Run with
E2E_HTTPS_PDS_HOSTNAME= to skip HTTPS; those tests then skip themselves.
Details are in pkg/cmd/e2e_https.go.
Run it locally #
# once: install the browser
pnpm --filter @streamplace/e2e-web install-browser
# build a streamplace binary that embeds the web app, then run the suite
make dev
hack/e2e-web-local.sh
hack/e2e-web-local.sh starts the harness, waits for SERVER_URL, runs the
flows, and tears the harness down. On a host without the cgo runtime, run it
inside the build container.
Artifacts on failure (traces, screenshots, video) land in test-results/ and a
report in playwright-report/ (pnpm --filter @streamplace/e2e-web report).