← Back to devlog

Proving it actually works: true end-to-end validation

There is a particular kind of confidence you get when a machine mimics a real user — picks up a file, drops it in the app, watches pixels appear on screen, zooms in, pans around, taps a base, downloads a PNG, opens the FASTA — and every assertion passes without a single hand-wave. That is what Step 5 delivers: a Playwright suite that acts exactly like a user would, across both desktop Chrome and an emulated iPad, against every real fixture we ship.

What was already there (and what was missing)

The prior four steps had meaningful test coverage, but it had gaps. The existing viewer.e2e.test.ts iterated all four fixtures and checked for a non-blank canvas, but it bundled everything into a single monolithic test — making failures hard to attribute to a specific fixture. Export tests checked only the suggested filename; nothing verified that the downloaded file was actually a valid PNG or contained real FASTA structure. The hover-tooltip test ran on both desktop and tablet even though tablets have no hover state. And while touch.e2e.test.ts covered pinch-zoom and tap on one fixture, that single-fixture tap assertion lived in a file that did a lot of other things.

Step 5 closes every one of those gaps in a single, explicitly annotated file: tests/e2e/step5-e2e.e2e.test.ts.

Per-fixture canvas validation

The new suite runs a dedicated test for each of the four real committed traces: 310.ab1, 3100.ab1, abcZ_F.scf, and the large 3730.ab1. Each test loads the file through the actual #file-input element (the same path a user would take), waits for the status to confirm Loaded, then reads the raw pixel data out of the canvas and sums every RGB channel. A blank canvas sums to zero; a rendered trace is always strictly positive. Both the desktop and tablet Playwright projects exercise this path, so we know the canvas paints correctly on a portrait iPad viewport too.

Zoom and pan with real assertions

Clicking Zoom + should narrow the visible base range. Clicking ← Pan should shift the start position. The Step 5 tests capture the position-readout text before and after each button click and assert the change with expect.poll — so even if there is a brief animation delay the assertion retries until the DOM reflects the new state. This runs against all four fixtures on both the desktop and tablet projects.

Hover tooltip — desktop only, intentionally skipped on tablet

The tooltip that shows peak amplitude and base identity when you mouse over the chromatogram only makes sense on a pointer device. On the emulated iPad there is no hover state, so the test guard is explicit: test.skip(isMobile, 'hover tooltip is a pointer/cursor interaction'). On desktop, the test sweeps the mouse across ten evenly-spaced columns in the canvas until the tooltip appears, then asserts that its text contains peak:. It is not enough to assert the tooltip is visible — we need to know it contains real data.

Tap-to-select — tablet only, intentionally skipped on desktop

The mirror image of the above: dispatching synthetic pointer events to simulate a finger tap is only meaningful on the tablet project. The guard is test.skip(!isMobile || browserName !== 'chromium', …). After the tap, two things must be true: the tooltip must contain peak:, and exactly one element with class .selected-base must exist in the sequence panel and contain a single valid IUPAC base character. This is a tighter assertion than the prior touch suite, which only checked for a count of one.

File-switch: does the canvas actually change?

Switching between fixtures is easy to test incorrectly — you can load file A, record the status text, load file B, assert the status says Loaded again, and call it a day. But that does not prove the canvas re-rendered with different content. The Step 5 test records a pixel sum for file A, then a pixel sum for file B, and asserts they differ. Since 310.ab1 and 3100.ab1 have different sample counts and different sequences, their rendered pixel sums are always distinct. A rendering regression that silently kept the old trace on screen would fail this check.

Export validation: full PNG signature and FASTA grammar

PNG export now goes beyond checking the filename. After waitForEvent('download') resolves, the test reads the downloaded file from disk using download.path() and checks all eight bytes of the full PNG signature (0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A). It then reads the IHDR chunk directly from the file bytes to assert that the exported image has non-zero width and height, and that the file is at least 67 bytes — the minimum for a structurally complete PNG (signature + IHDR + IDAT + IEND). You cannot fake that with a stubbed download.

FASTA export similarly reads the file content as UTF-8. The assertion checks that the text starts with a > header line, followed by one or more lines that match /^[ACGTNacgtn]+$/. A malformed export — missing the header, containing coordinates or metadata where the sequence should be, or producing an empty file — would fail immediately.

CI is green

All tests in the new file pass in CI under both Playwright projects. The Chromium browser install step was already wired into the CI workflow from Step 3; Step 5 adds no new dependencies or workflow changes. The full suite — lint, typecheck, Vitest unit tests, Playwright E2E on desktop and tablet, performance smoke — remains green.

What this proves, and what comes next

At this point the viewer is validated end-to-end with real fixtures, real interactions, and real assertions on real output files. Every major interaction path — load, zoom, pan, hover, tap, switch, export PNG, export FASTA — is covered by a test that a machine runs on every PR. The devlog is complete for this phase.

The north star from the project brief is full feature parity with professional Sanger viewers: reverse-complement, PHRED quality trimming, subsequence search with IUPAC ambiguity codes, multi-trace workspace, and high-resolution vector export. Each of those is its own small PR, its own devlog entry, its own green CI run. The foundation is solid enough to build on.