Appearance
Testing
There are three layers of tests. Each one catches problems the others miss.
| Command | What it runs | Takes |
|---|---|---|
npm test | Unit tests: rules engine, dice derivation, match equity, rate limits, mail, SPF merging, error scrubbing, backups, analysis, record formats | seconds |
npm run test:e2e | Node clients talking to a real server over WebSockets: full matches, analysis, puzzles | ~1 min |
npm run test:browser | Playwright playing through the real UI in real browsers, on five devices, with video | ~10 min |
Browser tests
bash
npx playwright install chromium webkit
npm run test:browser
npm run test:browser:reporte2e/serve.mjs starts a throwaway server with its own temporary data directory and relaxed rate limits. It never touches your real database.
What gets played
Every spec runs on every device project:
| Spec | Covers |
|---|---|
local.spec.js | Two players on one device, a full match. Tests confirm-before-send, undo after one checker, swapping dice, playing to the end, and verifying the record. Runs again with confirm turned off. |
bots.spec.js | Full matches against the easy, medium and hard bots. |
online.spec.js | Two separate browsers: one posts a public seek, the other accepts, and both play to the end and verify. Also a private challenge link. |
cube-resign.spec.js | Double, take, the cube moving to the taker, and resigning through the dialog, which scores level × cube. |
skins.spec.js | Walnut, slate and ivory through play and a reload. |
panels.spec.js | Every lobby panel, the settings and account dialogs, and the privacy and terms pages. |
| Project | Browser | Viewport |
|---|---|---|
desktop | Chromium | 1920×1080 |
laptop | Chromium | 1366×768 |
phone | WebKit (iPhone 15) | portrait, touch |
phone-landscape | WebKit (iPhone 15) | landscape, touch |
tablet | WebKit (iPad) | portrait, touch |
How it plays
e2e/driver.js clicks what a person would click: the real SVG hit areas on the board and the real buttons. It uses Playwright's actionability checks, so a control covered by something else fails the test. Nothing is triggered from JavaScript. Every click has a time limit, because controls legitimately disappear when the server answers.
What counts as broken
Each match is checked against layout invariants as it's played, not just at the end. A match fails if:
- the page scrolls in either direction (the game screen is a fixed viewport)
- a closed dialog is still painted
- the board overflows the viewport, is under 200px wide, or doesn't have 24 points
- the two dice show different owners, or the dark player's dice aren't dark
- the board changes size during the match
- a visible control on the top layer is off screen (after scrolling its panel, if it has one), or a tap at its centre lands on something else
- on a touch device, a control is under 40px tall
- text in a player card overlaps (name, pips, clock) or spills out of the card
- the page throws an error or logs
console.error
Recordings
Videos, screenshots and traces go to e2e/results/, and the HTML report to e2e/report/. Both folders are git-ignored. To review a run quickly, turn each video into a contact sheet:
bash
for v in e2e/results/*/video.webm; do
ffmpeg -loglevel error -y -i "$v" -vf "fps=1/2,scale=400:-1,tile=5x4" -frames:v 1 "${v%.webm}-sheet.png"
doneProblems these tests found
- The play button could be pressed twice while a play was still on its way to the server. There is now an in-flight lock.
- On a shared device, the double button could flash on for a moment mid-move. The two seats' snapshots were briefly out of step; they are now compared by event count.
- A Content Security Policy error on every page: the inline script that sets the skin was blocked.
- Touch targets under 40px: fairness, log, resign, close and accept.
- On phones, the play, undo and resign buttons wrapped to two rows and took height from the board, so the board resized every turn.
- Hover styles stuck on touch screens, which left "resign" glowing red after a tap.
- On landscape phones, play/undo/resign and take/pass ran off the bottom of the screen, and the resign dialog needed scrolling.
- An empty game screen flashed for a network round trip before the first position arrived.
- In timed matches on phones, the clock was drawn over the player names, which were cut to one letter. The score now stacks, and the opponent's name truncates instead of overflowing.
- The account dialog on phones had half-width fields, and opening it brought up the keyboard.