Skip to content

Testing

There are three layers of tests. Each one catches problems the others miss.

CommandWhat it runsTakes
npm testUnit tests: rules engine, dice derivation, match equity, rate limits, mail, SPF merging, error scrubbing, backups, analysis, record formatsseconds
npm run test:e2eNode clients talking to a real server over WebSockets: full matches, analysis, puzzles~1 min
npm run test:browserPlaywright 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:report

e2e/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:

SpecCovers
local.spec.jsTwo 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.jsFull matches against the easy, medium and hard bots.
online.spec.jsTwo 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.jsDouble, take, the cube moving to the taker, and resigning through the dialog, which scores level × cube.
skins.spec.jsWalnut, slate and ivory through play and a reload.
panels.spec.jsEvery lobby panel, the settings and account dialogs, and the privacy and terms pages.
ProjectBrowserViewport
desktopChromium1920×1080
laptopChromium1366×768
phoneWebKit (iPhone 15)portrait, touch
phone-landscapeWebKit (iPhone 15)landscape, touch
tabletWebKit (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"
done

Problems 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.

Rules engine, fairness protocol, verifier, analysis and worker: MIT. Server and client: AGPL-3.0-or-later.