name: tests on: push: pull_request: jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: # The game targets Node >=20 (see package.json engines); run the suite on # the current LTS lines so a version-specific regression can't slip through. node-version: [20, 22] name: tests (node ${{ matrix.node-version }}) steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node-version }} # No dependencies to install — the game and its test suite run on the Node # standard library alone, which is the whole point. Just run the tests. - name: Run the engine test suite run: npm test # CONTRIBUTING.md's release checklist requires `npm run typecheck` (tsc -p jsconfig.json) # to pass, but no CI step ever ran it. TypeScript stays out of package.json — this repo is # explicitly "No dependencies, no build step" — so fetch the compiler on-demand via npx # instead of installing it. `--package typescript` names the package explicitly so this # works correctly on a clean runner regardless of any ambient global `tsc`, unlike a bare # `npx tsc` (which depends on npx's registry-matching behavior). # # THE VERSION IS PINNED, and that part is load-bearing. Unpinned, `--package typescript` # resolves to whatever is newest on the registry that morning, so a green commit can go red # overnight with no code change and a real regression is indistinguishable from a compiler # upgrade. That is not hypothetical here: TypeScript 7 reported 21 errors on a tree that # TypeScript 5.7 reported 2 on, and CI stayed red across two merges to main. Pinning the # INVOCATION keeps the compiler out of package.json exactly as the note above intends — # it is still not a dependency, it is just no longer a moving target. Bump this # deliberately, in its own commit, so a compiler upgrade is reviewable on its own. - name: Type-check the annotated files run: npx --yes --package typescript@5.7.2 -- tsc -p jsconfig.json # The suite above is headless and dependency-free, which is the whole point of it — and it means # nothing in it can see a page that parses, imports cleanly, and then throws on load. With no # build step that failure is a blank white screen, and the way it has been caught here before is # a human opening index.html. Demonstrated: a bad draw call in renderUnits.js leaves all 2502 # tests passing and takes the game down on the first frame. # # So: one job that boots the real page in real Chromium, starts a match, clicks, and fails on any # uncaught error. Playwright is fetched at run time via npx and never enters package.json — # exactly the precedent the type-check step set for TypeScript. It is a separate job rather than # a matrix step because it tests the BROWSER, not the Node version; running it twice would just # cost two Chromium downloads to learn the same thing. smoke: runs-on: ubuntu-latest name: browser smoke test steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 22 # --no-save: Playwright is installed for this checkout only and never enters package.json, # so the repo keeps its zero-dependency promise. `npx --package playwright -- node ...` is # the tidier-looking version of this and does NOT work — npx puts the package's BIN on PATH # but not its module on a child node process's resolution path, so the script's own require # fails. That shipped broken once; this is the verified form. # # The FULL playwright package, not playwright-core: only the full one carries the CLI that # downloads browsers. Pinned for the same reason the compiler is — a gate that moves # underneath you cannot tell a regression from an upgrade. Chromium only; the bare # `playwright install` pulls three browsers this never launches. - name: Install Playwright and Chromium (never saved to package.json) run: | npm install --no-save playwright@1.56.1 npx playwright install --with-deps chromium - name: Boot the game in a real browser run: npm run smoke