Architecture
Your test (unchanged Dusk body)
│
▼
Dawn\Browser ─ Dusk's fluent API, one class per concern trait
│ (elements, mouse, waits, assertions, URLs, auth, JS)
▼
Dawn\ElementResolver ─ Dusk selector semantics → Playwright locators
│
▼
playwright-php/playwright ─ PHP ↔ Node JSON-RPC over stdio
│
▼
Playwright (Node) ─ Chromium / Firefox / WebKitThe pieces
Dawn\Browser holds a Playwright Page and an ElementResolver carrying the current scope prefix. Every fluent method translates to locator/page calls; unknown methods hit __call, try registered macros (Dawn's Browser is Macroable, like Dusk's), then throw UnsupportedDuskMethod.
Dawn\ElementResolver is pure translation - it never touches the DOM. @name → [dusk="name"], page-element aliases, scope prefixes, and Dusk's field/button candidate orders compiled into CSS selector lists. Locators resolve at action time, so Playwright's auto-wait covers the find as well as the act.
Dawn\Engine\Engine owns the lifecycle: one Node sidecar and one launched browser per PHP process, a browser context per Browser instance, everything shut down at process exit.
Dawn\TestCase / ProvidesBrowser mirror Dusk's lifecycle: browse() with as many Browser arguments as your closure declares, the primary browser persisted across tests in a class, failure screenshots and console logs captured automatically.
Dawn\DawnServiceProvider registers the /_dawn/* auth routes (non-production only).
src/compat.php aliases Laravel\Dusk\Browser → Dawn\Browser eagerly (PHP skips autoloading for parameter type checks) and the base classes lazily, only when laravel/dusk is absent.
Design rules
- No WebDriver. No
Facebook\WebDrivertypes, no W3C endpoint. - No PHP-side waiting - see How waiting works. CI greps the codebase to enforce it;
Dawn\Support\Waiter(backingwaitUsing()) is the single sanctioned exception. - Dusk is the spec. Resolution orders, assertion messages, timeout message formats and route mechanics are copied from laravel/dusk source; divergences are documented, never silent.
- Point-in-time assertions. Reads (
innerText, values, checked state) execute immediately viaevaluate(), exactly like Dusk'sgetText()- waiting stays in the explicitwaitFor*calls where your suite already puts it.