How waiting works
The number one rule of Dawn's design: waiting is delegated to Playwright, never reimplemented in PHP. Dusk waits by polling the DOM from PHP; Dawn never does. There are three mechanisms.
1. Native locator waits
waitFor, waitUntilMissing, waitForLink, waitForInput, whenAvailable compile the Dusk selector to a Playwright locator and call the engine's locator.waitFor() with the appropriate state (visible, hidden, attached) and your timeout:
$browser->waitFor('@chart', 10);
// → locator('[dusk="chart"]').first().waitFor(state: visible, timeout: 10000)On timeout you get a Dawn\Exceptions\TimeoutException with the Dusk-style message: Waited 10 seconds for selector [@chart].
2. In-browser condition promises
waitForText, waitForTextIn, waitUntilMissingText, waitForLocation, waitForRoute, waitUntil, waitUntilEnabled, waitUntilDisabled, waitForReload need conditions Playwright's locator states can't express (Dusk's case-sensitive scope-text matching, pathname-only URL comparison, arbitrary JS). For these, Dawn installs a single Promise inside the page that re-checks the condition on every animation frame and self-resolves at the deadline:
() => new Promise(resolve => {
const deadline = Date.now() + timeout;
const check = () => {
if (condition) return resolve(true);
if (Date.now() > deadline) return resolve(false);
requestAnimationFrame(check);
};
check();
})This is exactly the mechanism Playwright's own waitForFunction uses. One evaluate() call, no PHP-side timers. If a navigation destroys the page's execution context mid-wait - the normal case for waitForLocation after a click - Dawn re-arms the promise on the new document and keeps the original deadline. The PHP loop iterates only in reaction to navigations; it is not a timed poll.
waitForLocation preserves Dusk's exact semantics: pathname comparison for paths (query string ignored), protocol + host + pathname for full URLs.
3. The one exception: waitUsing()
$browser->waitUsing(10, 100, fn () => $order->fresh()->isShipped());Dusk's contract here is "wait until this arbitrary PHP closure returns truthy" - something Playwright cannot evaluate. Dawn implements it faithfully in one isolated, documented class (Dawn\Support\Waiter), and CI enforces that no sleeping exists anywhere else in the codebase. Prefer the native waits whenever the condition lives in the browser.
Actions auto-wait too
Every action (click, type, press, check, …) carries an explicit Playwright timeout equal to Browser::$waitSeconds (5 s by default, the same default Dusk uses for waits). Playwright waits for the element to be actionable - attached, visible, stable, enabled - before acting, so the classic Dusk race (click fires before the button renders) disappears without any code changes.
pause() deserves a note: Dusk sleeps the PHP process; Dawn resolves a setTimeout promise inside the browser's event loop. Same observable behaviour, no PHP-side sleep.