Your first contribution
From a fresh clone to a fixed bug in the engine in about ten minutes. No prior knowledge of the codebase, no system packages, nothing to configure.
Why start with a failing test?
The hardest part of a first contribution is often finding something to work on. We skip that step by using the web-platform-tests (WPT): the conformance suite that all browser vendors share. Every failing test in it is a specific, already-agreed statement about something our engine gets wrong. You don't have to invent a task, and you don't have to guess whether the work is wanted.
All you need is Rust and git.
1. Set up the engine and the tests
Clone the engine, then clone the part of WPT we test against, pinned to the commit the engine is measured against. The WPT checkout is sparse, so it's a few hundred MB rather than several GB.
# The engine
git clone https://github.com/gosub-io/gosub-engine.git
cd gosub-engine
# A sparse wpt checkout (the wpt/ directory is gitignored)
git clone --filter=blob:none --sparse \
https://github.com/web-platform-tests/wpt.git wpt
git -C wpt sparse-checkout set \
resources common css/css-syntax css/css-values css/support css/reference
git -C wpt checkout "$(cat tests/wpt/wpt-commit.txt)"
export WPT_ROOT="$PWD/wpt"
# Run a component. The first build takes about a minute, the run about 30 seconds.
cargo run --release -p gosub-wpt -- "$WPT_ROOT" css/css-valuesIf the last command prints a table of directories with bars next to them, you're ready. The pass rates will be low. That's expected, and it's exactly why there's so much to pick from.
css/css-values █░░░░░░░░░ 192/4516 4.3%
css/css-values/calc-size ███░░░░░░░ 38/120 31.7%
css/css-values/urls ███░░░░░░░ 39/126 31.0%The test runner, gosub-wpt, parses each test with our real HTML5 parser and runs its scripts against our real CSS engine. It isn't a full browser: there's no network and no window, which is what keeps it fast.
2. Find something to fix
Ask the engine what's worth picking up right now:
cargo run --release -p gosub-wpt -- "$WPT_ROOT" css/css-values --shortlist Partly passing, nearest to working first:
94.3% css/css-values/progress-invalid.html 33/35, 2 left
71.4% css/css-values/random-item-invalid.html 10/14, 4 left
53.2% css/css-values/tree-counting/calc-sibling... 25/47, 22 leftTake one from the top. A partly passing test means the engine already understands the feature and gets a detail wrong. That's an afternoon of work, not a project. If the list opens with a Crashes section, start there: a crash is a bug that a real web page could trigger.
Then run just that one file to see every failing assertion, and what the engine returned instead:
cargo run --release -p gosub-wpt -- "$WPT_ROOT" <the-file-you-picked>3. A worked example
This is a real bug from September 2026. It has probably been fixed by now, but the method is the same every time.
The file css/css-values/viewport-units-parsing.html had 9 subtests passing and 15 failing:
FAIL e.style['width'] = "1svh" should set the property value
- assert_not_equals: property should be set got disallowed value ""
FAIL e.style['width'] = "1lvmax" should set the property value
- assert_not_equals: property should be set got disallowed value ""Compare what passes with what fails. 1svw was accepted, but 1svh wasn't. So viewport units weren't missing altogether. Somewhere, a list included some of the spellings and left out others. Reading the results this way is the key step, and it works for almost every test.
Search for a value that works next to one that doesn't:
rg '"svw"' crates/gosub_css3/src/That search leads to LENGTH_UNITS in crates/gosub_css3/src/matcher/syntax_matcher.rs. The list had svw, lvw and dvw, but none of the matching h, i, b, min and max units, even though the code that computes the final values already supported them. Adding the missing units fixed the file: 24 of 24 passing.
One rule: fix the behaviour in the engine itself. The gosub_domjs crate only exists so the tests can talk to the engine. Hard-coding an answer there would turn the test green without fixing anything.
4. Prove it
Run your file again. It should pass now. Then compare the whole suite against the committed baseline:
cargo run --release -p gosub-wpt -- "$WPT_ROOT" --all --expect tests/wpt/expectations-css.txtThis run fails, and that's intended. You'll see an UNEXPECTED PASS line for each subtest you fixed. The baseline file records exactly which tests pass, so every improvement to the engine has to update it. Regenerate the baseline:
cargo run --release -p gosub-wpt -- "$WPT_ROOT" css/css-syntax css/css-values \
--write-expectations > tests/wpt/expectations-css.txtCommit your fix together with the regenerated baseline. The change in the baseline shows that your fix works.
5. Open a pull request
make test # fmt, clippy, smoke and unit testsA few things we ask of every pull request:
- Signed commits. Every commit must be signed before it can be merged. GitHub has a guide on setting this up.
- Keep it small. Small PRs get reviewed and merged much faster. One to three commits is ideal, so squash where it makes sense.
- Explain what you did. Add a short summary of what the PR changes and why. Nobody on the team knows every part of the codebase.
Where to go next
Run the shortlist again and take the next one. Once you're comfortable, the engine repository has more reading material:
- CONTRIBUTING.md - code style, modules, the Makefile and other ways to contribute.
- WPT quickstart - the source of this walkthrough, kept up to date alongside the code.
- Web-platform-tests - the full reference for the test harness.
- Documentation index - architecture, the tutorial, and every other page in the docs.
Not into CSS parsers? That's fine too. See the contribute page for everything else we need help with, from documentation to security research.
Stuck, or found something better to work on?
Tell us in the Zulip chat. We're happy to help you find your way around the engine.
- seafloor reached · resurface at ↑ 0 m -