Waiting and retrying
How geowright waits for the map without a fixed sleep, and why tests synchronise on retrying assertions.
The library has no fixed sleep of its own, and neither do its tests. A test waits for the map to be idle where that means something, and otherwise lets its assertions retry.
Idle, done properly
MapLibre emits idle only from its render loop, at the end of a frame with nothing left to do. If the map is already idle, a plain once('idle') never fires, because nothing will draw another frame. waitForIdle() subscribes first and then calls the public triggerRepaint(): one forced frame either fires idle at once or schedules the render that eventually will. areTilesLoaded() is checked on top.
Idle is not enough
Idle cannot see the app's own asynchronous work: between a click and the fetch-then-setData a real handler runs, the map is idle the whole time. So retrying assertions are what a test synchronises on, like Playwright's web-first assertions. Each poll waits for a bounded idle first, and that is all idle is for.
Absence needs a settled map
A verdict of absence (a .not, a count, “not visible”) is taken only from a poll in which the map was idle, or had been seen never to become idle with nothing else still loading. Without that rule, .not.toHaveFeature could pass on a half-loaded map, a moment before the feature appeared.
A poll that cannot be decided is neither true nor false: the map is not registered, a named layer does not exist, several features match where one is required, the map has not settled, the camera is moving. Such a poll never ends the retry, and if it is the last one, the assertion fails whichever way it is written, and says why.
Maps that never become idle
Some apps never idle on purpose: an animated canvas or video source, a source replaced every frame, a camera that keeps rotating. waitForIdle() then times out and says which of these it saw; assertions and actions work without it. For a feature that never stops moving, click({ moving: 'allow' }) leads the pointer and checks the press when it arrives.
Timeouts that follow the test
The first idle after a navigation is a page load, so waitForIdle() defaults to two thirds of the test's timeout, and an action to a third of it (at least 10 s); test.slow() scales both. Matchers use Playwright's expect timeout. An action's timeout is wall-clock until its input is sent, and real input is never abandoned half-way.
How geowright's own suite stays repeatable
No network reaches the outside world during a test. Styles, data and raster tiles are answered from fixtures through page.route, and vector tiles are cut from GeoJSON on the fly with the same libraries a real tile pipeline uses, so an app's real tile URLs run against generated tiles. A request nobody answers fails the test and names the URL.
Rendering uses the software-GL flags MapLibre's own continuous integration uses. The flake bar is --repeat-each=20 with no retries configured anywhere: a failure is a failure. The complete suite of 349 tests passed 6,980 of 6,980 executions at that bar, with 0 flakes and 0 retries, and has passed it repeatedly as it grew. Testing evidence has the rest.
Read more
- TroubleshootingA map that never becomes idle, and pages that are slow to settle.
- Assert and actRetrying assertions and auto-waiting actions in a test.
- Testing evidenceThe apps geowright is tested against, and the repeat-run bar.