Excalidraw Onboarding LabChapter VII — Your first contribution: tests, CI gates, and PR norms0/6Contents
Chapter VII

Your first contribution: tests, CI gates, and PR norms

This module shows where this repo's actual quality bar lives: not in CONTRIBUTING.md (which just links out) but in package.json's yarn scripts and the two GitHub Actions workflows that gate a PR. You'll see the colocated-test convention through packages/excalidraw/tests/dragCreate.test.tsx — the existing test for the rectangle-drag trace from an earlier module — and learn how to scope a first contribution to a single Action, data loader, or test file the way this repo's own structure isolates them.


"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false","test:app": "vitest","test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .","test:other": "yarn prettier --list-different","test:typecheck": "tsc",
package.json · lines 6872
Figure I · Four separate gates, each a distinct concern: test:typecheck runs tsc with no --noEmit output, just type-checking; test:code runs eslint with --max-warnings=0, meaning a single warning fails the whole command, not just errors; test:other runs prettier in --list-different mode, which exits non-zero if any file isn't already formatted (it doesn't rewrite anything, unlike fix:other); test:app is the actual vitest test runner. test:all chains all four with &&, so it stops at the first failure — this is what you should run locally before opening a PR.
walkthrough

dragCreate.test.tsx: the rectangle-drawing trace, as a test

packages/excalidraw/tests/dragCreate.test.tsx · lines 171
lines 2–9

Instead of asserting on pixels, the test spies on renderInteractiveScene and renderStaticScene — the two renderer entry points from the drawing trace you already saw. A spy wraps a real function so calls still execute normally but get recorded; asserting on .mock.calls.length later verifies the render pipeline fired the expected number of times for a given interaction, without needing a real canvas snapshot.

lines 11–18

The test drives the exact same input sequence a user produces: click a tool button, then pointerDown/pointerMove/pointerUp on canvas.interactive — the interactive-layer canvas from the renderer. render(<Excalidraw />) mounts the full component tree from test-utils, so this exercises App's real pointer handlers, not a mock.

lines 20–23

h is a debug handle exposed by the app (window.h) giving direct read access to live editor state — here h.elements, the same elements array the actions and scene machinery from earlier modules operate on. Asserting x/width against the raw pointer coordinates (30,20)→(60,70) confirms newElement's geometry math end-to-end, not just that *something* got created.

import { Excalidraw } from "../index";import * as InteractiveScene from "../renderer/interactiveScene";import * as StaticScene from "../renderer/staticScene";const renderInteractiveScene = vi.spyOn(  InteractiveScene,  "renderInteractiveScene",);const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");it("rectangle", async () => {  const { getByToolName, container } = await render(<Excalidraw />);  const tool = getByToolName("rectangle");  fireEvent.click(tool);  const canvas = container.querySelector("canvas.interactive")!;  fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });  fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });  fireEvent.pointerUp(canvas);  expect(h.elements.length).toEqual(1);  expect(h.elements[0].type).toEqual("rectangle");  expect(h.elements[0].x).toEqual(30);  expect(h.elements[0].width).toEqual(30); // 60 - 30});
step 1 of 3
checkpoint

Check your understanding

Answered in place — nothing is graded, everything is explained. 0 / 3 passed

Why does this module treat package.json and the GitHub Actions workflows as the source of truth instead of CONTRIBUTING.md?

A PR fails its lint.yml check. Which of the following is NOT one of the commands lint.yml runs?

In dragCreate.test.tsx, why does the test spy on renderInteractiveScene and renderStaticScene instead of, say, inspecting canvas pixel output?