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.