## Launch Checklist This PR increases coverage by adding unit tests to lib folde, replace the skipped end to end placeholder with actual tests and adds more end to end tests. This was mostly done by AI (Claude opus 4.8) and I reviewed it and requested changes where needed. - [x] Briefly describe the changes in this PR. - [x] Link to related issues. - [x] Include before/after visuals or gifs if this PR includes visual changes. - [x] Write tests for all new functionality. - [x] Add an entry to `CHANGELOG.md` under the `## main` section. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
5.0 KiB
Maputnik is a MapLibre style editor written using React and TypeScript.
To get started, install all npm packages:
npm install
Verify code correctness by running ESLint:
npm run lint
Or try fixing lint issues with:
npm run lint -- --fix
The project type checked and built with:
npm run build
Install the Playwright browser (first time only):
npx playwright install --with-deps chromium
Then run the end-to-end tests (Playwright starts the dev server automatically):
npm run test
Run the unit tests with Vitest:
npm run test-unit
Pull Requests
- Pull requests should update
CHANGELOG.mdwith a short description of the change.
Testing
Prefer end-to-end tests
Most of this codebase is React components, and they are only reachable from an end-to-end test. E2E coverage is the primary signal.
Reach for a unit test only for pure logic that e2e cannot cheaply reach (parsers, sorting, watchers, stores). Before writing one, check whether e2e already covers the file — a unit test that duplicates existing e2e coverage adds test code and almost no coverage:
npx nyc report --reporter=text --include="src/libs/style.ts"
Do not merge the Vitest (v8) and e2e (istanbul) coverage reports locally. They produce conflicting statement maps for the same files and the combined percentage is meaningless. Codecov merges the two uploads server-side; that is the number to trust. Locally, read them separately:
- e2e:
npx playwright testthennpx nyc report --reporter=text-summary(reads.nyc_output/) - unit:
npx vitest run --coverage(writescoverage/)
E2E layering
Three layers, and the boundaries matter:
e2e/playwright-helper.ts— generic, app-agnostic browser actions. The only file allowed to import@playwright/test(besidese2e/utils/fixtures.ts).e2e/maputnik-driver.ts— domain actions (layers, filters, functions, the style). Knows nothing aboutpageor Playwright.e2e/modal-driver.ts— actions scoped to a modal, exposed aswhen.modal.*.
Specs get a driver at describe scope and assert fluently:
describe("layer editor", () => {
const { given, get, when, then } = new MaputnikDriver();
...
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [{ id, type: "fill" }] });
});
New UI interactions belong in a driver, not inline in a spec.
Writing assertions
shouldDeepNestedIncludeis a recursive partial match (toMatchObject): nested objects are matched as subsets, arrays and primitives must match exactly (including array length).- Assert against the whole style, not an extracted slice. Avoid
get.styleFromLocalStorage().then(style => style.layers.find(...))— it moves test logic into the test. Compare the real object instead. Query.then()is lazy and returns a newQuery, not a Promise.await get.styleFromLocalStorage()hangs forever. Use.get()to await it directly, or pass the Query tothen(...).
One behaviour per test
If a test needs comments narrating "and now this…", it is several tests. Split it,
and hoist the shared setup into a nested describe + beforeEach.
Test ids
Test ids use the data-wd-key attribute and are read via get.elementByTestId.
The Input* components already accept data-wd-key and render it on the real
<input>; the Field* wrappers forward it through their {...props} spread. So
passing data-wd-key to a Field* component is usually enough. Do not also
add it to Block/Fieldset — the id then matches two elements and locators fail
in strict mode.
Note InputNumber renders <key>-text and <key>-range when allowRange is set,
and <key> otherwise.
Input commit semantics (common source of "the value didn't save")
InputStringonly fires itsonChangeon blur or Enter. Typing alone firesonInput. A driver that callsfill()must then callblur(), or the value never reaches the style.InputNumbercommits on every change; no blur needed.- The autocomplete inputs (layer source, add-layer source) are controlled
downshift comboboxes. Keystroke typing is dropped/reordered —
{selectall}then typingrasteryields"exampleaster". Usefill(), which dispatches a single input event, then pick from the filtered menu. - CodeMirror auto-closes brackets and quotes, and types over its own closers, so inserting a well-formed JSON fragment stays well-formed. To break JSON on purpose, insert a bare word.
Fixtures
Style fixtures live in e2e/fixtures/. A new one must be registered in two places
in maputnik-driver.ts: the list in given.setupMockBackedResponses and the
styleFileByKey map in when.setStyle.
Verify a new test can fail
A test that passes for the wrong reason is worse than no test. After writing one, mutate the expected value and confirm it fails. This has caught real mistakes (e.g. a driver that never committed its input, so the assertion was matching a value written by the previous step).