Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Run Dyno tests
description: Run one suite and retain its console and JUnit output.
inputs:
suite:
description: core, browser, execution, reporter, package, capture, or native.
description: core, browser, execution, project, reporter, package, capture, or native.
required: true
browser:
description: Playwright engine for browser suites.
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
fail-fast: false
matrix:
engine: [chromium, firefox, webkit]
suite: [browser, execution, reporter, capture, native]
suite: [browser, execution, project, reporter, capture, native]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -95,7 +95,7 @@ jobs:
fail-fast: false
matrix:
engine: [chromium, firefox, webkit]
suite: [browser, execution, reporter, capture, native]
suite: [browser, execution, project, reporter, capture, native]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
9 changes: 6 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Signatures: [browser types](../types/index.d.ts), [configuration](../types/confi
| `Interactor.scope` | Bind an independent root that resolves again after navigation. Create scopes before chaining; a missing root fails instead of searching elsewhere. |
| `Interactor.fill` | Replace an editable input or textarea value and dispatch input/change events. Empty strings and numeric inputs work. Use upstream `type` when individual keystrokes matter. |
| `Interactor.upload` | Select files and dispatch input/change events. Pass no files to clear the selection. File bytes can come from a project fixture endpoint. |
| `Interactor.nativePress` | Send a real key to the focused app element. Requires managed keyboard support; ordinary Interactor `press` uses synthetic events. |
| `Interactor.nativePress` | Send a real key to the focused app element when browser defaults matter, such as Tab navigation. Requires managed keyboard support. Use upstream `press` for app-defined keyboard handlers; it runs directly in the browser. |
| `Interactor.nativeMove` | Move the native pointer in app viewport coordinates. Useful for moving away from a hovered element. |
| `Interactor.nativeHover` | Move the native pointer over the resolved element. An optional position is relative to its bounding box in CSS pixels. |
| `defineInteractor` | Define reusable component actions and assertions. Keep definitions in your app’s test support or beside the component, then bind them to the test’s `I`. Share definitions, never bound instances. |
Expand All @@ -56,8 +56,8 @@ Signatures: [browser types](../types/index.d.ts), [configuration](../types/confi
| --- | --- |
| `Application` | Navigate and inspect your app from a test. Tests have their own context and read the app DOM directly. Fresh documents don't reset browser storage or backend data. |
| `Application.goto` | Navigate to a same-origin app path and wait for its document bridge. Then use Interactor to wait for the UI you care about. |
| `Application.request` | Fetch a same-origin URL using the browser session and test cancellation signal. Returns a standard Response. Use this for your app's test-only fixture endpoints; keep database factories in your project. |
| `Application.setViewport` | Resize the actual managed viewport and wait for layout. Each dimension must be 1–8192 CSS pixels. Manual runs require the requested dimensions to already match. |
| `Application.request` | Fetch options pass through, including `credentials: 'omit'` for API setup that should not change the browser session. Same-origin cookies are included by default. Dyno owns the cancellation signal. |
| `Application.setViewport` | Resize the actual managed viewport and wait for layout. Each dimension must be 1–16384 CSS pixels. Manual runs require the requested dimensions to already match. |
| `Application.screenshot` | Save an exact PNG of the app, excluding the reporter. Full-page and element captures are supported. Required capture fails when unavailable; explicitly optional capture can skip. |
| `Application.pause` | Open the reporter and suspend the execution deadline until Continue. Requires `--ui` or `--manual`. App timers and network work keep running; cleanup cannot pause. |
| `Application.document` | Read the current app document after navigation is ready. Resolve elements again after navigation. |
Expand Down Expand Up @@ -93,6 +93,9 @@ Signatures: [browser types](../types/index.d.ts), [configuration](../types/confi
| `NativeHoverOptions` | Optionally choose a point inside the target element. Without it, Dyno chooses a visible point that receives pointer input. |
| `NativeDialogOptions` | Accept or dismiss the next dialog opened by the callback. Optionally require a message and supply prompt text. |
| `NativeConsoleMessage` | A captured console entry with its sequence ID, type and text. |
| `NativeBrowser.setColorScheme` | Choose the app system color scheme: light, dark, or no-preference. Cleanup restores the original preference. |
| `NativeBrowser.blockExternalRequests` | Block app-page HTTP requests outside the gateway origin. Data and blob URLs remain available. Does not cover service workers or WebSockets; resets between tests. |
| `NativeBrowser.blockedRequests` | Read an immutable list of up to 1,000 recent URLs blocked by the external-request guard. |

## Timings

Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Every command shares cancellation, including browser installation. Shutdown canc

The exported `Interactor` and each test's `I` use the same app-aware class. Its public Interactor extension points supply app-realm events/files, form replacement, rendered assertions, and dynamic scopes. Scopes create independent contexts with the same assertion settings and signal; they resolve their roots again after navigation. Interactor.js still owns selectors, chaining, retries, and stability checks. Apps keep domain page objects and fixture I/O.

`defineInteractor({ actions, assertions })` registers native extensions on a private subclass with its own assertion class. Its returned function binds that definition to an existing Interactor and optional selector. Definitions never register methods on `I` or siblings. Binding and `scope()` share one context-copy path; neither caches app elements. The small subclass isolates native registration rather than introducing another interaction engine.
`defineInteractor({ actions, assertions })` registers actions and assertions through Interactor's public API on a private subclass with its own assertion class. Its returned function binds that definition to an existing Interactor and optional selector. Definitions never register methods on `I` or siblings. Binding and `scope()` share one context-copy path; neither caches app elements. The small subclass keeps registrations independent.

## App and test windows

Expand Down Expand Up @@ -129,7 +129,7 @@ CLI lifecycle intervals use a separate session clock. Each report saves a runnin

## Native browser operations

Rare native keys, pointer movement, permissions, media, offline transport, and virtual authenticators use the existing runtime message, controller queue, authenticated gateway, and owned worker. Interactor resolves DOM targets locally. No exposed SDK binding, driver selector, or generic evaluation API enters the app.
Interactor owns clicks, typing, app keyboard handlers, and scrolling through local actions. The managed input surface is limited to real keys for browser defaults and pointer movement for CSS hover. Permissions, media, offline transport, and virtual authenticators use the same runtime message, controller queue, authenticated gateway, and owned worker. Interactor resolves DOM targets locally. No exposed SDK binding, driver selector, or generic evaluation API enters the app.

Requests carry execution and document identity. The worker checks the original page bridge immediately before dispatch. Geometry stays tied to that document; keys may navigate within their execution. Cancellation rejects queued requests and late results, but cannot undo delivered input. The checks detect races rather than making browser input atomic.

Expand Down
7 changes: 5 additions & 2 deletions docs/browsers.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ npx dyno --browser webkit

Runs never download browsers. On Linux, `dyno install --with-deps` also installs [system dependencies](https://playwright.dev/docs/browsers#install-system-dependencies) and may need administrator access. Use `dyno browsers` to see downloaded builds and paths.

Headless Chromium uses Playwright's default headless shell. On macOS, Chromium `--ui`, Chrome, Edge and custom Chromium executables reject captures taller than 16,384 physical pixels. Full Chromium can repeat content beyond that height while reporting valid dimensions. Use default headless Chromium for long-page images. This is a conservative support limit; some custom builds may work beyond it.

## Choose a browser

| Browser | How to run |
Expand All @@ -36,15 +38,16 @@ Use `app.native.capabilities` to choose browser-specific coverage explicitly. Re
| Feature | Chromium / Chrome / Edge | Firefox | WebKit | Manual URL |
| --- | --- | --- | --- | --- |
| Native keys and pointer | Yes | Yes | Yes | Unavailable |
| App HTTP request guard | Yes | Yes | Yes | Unavailable |
| Dialogs and browser console | Yes | Yes | Yes | Unavailable |
| Reduced motion | Yes | Yes | Yes | Unavailable |
| Color scheme and reduced motion | Yes | Yes | Yes | Unavailable |
| Clipboard permission grants | Read/write | Unavailable | Read only | Unavailable |
| Virtual authenticator | Yes | Unavailable | Unavailable | Unavailable |
| App network offline | Yes | Unavailable | Unavailable | Unavailable |

Native state resets between tests. Cookies, storage, and backend cleanup remain your project's responsibility. Chromium offline keeps companion HTTP connected, but can change its `navigator.onLine` too.

Set `browser.locale` and `browser.timezoneId` when creating managed contexts. `browser.deviceScaleFactor` accepts 1, 2, or 3. A mobile-sized viewport still uses a desktop browser; it doesn't emulate a phone's input or OS.
Set `browser.locale` and `browser.timezoneId` when creating managed contexts. `browser.deviceScaleFactor` accepts finite values from 1 to 3, including fractional ratios. Set `browser.hasTouch` for touch input, `browser.isMobile` for mobile viewport behavior, and `browser.userAgent` when needed. Firefox does not support `isMobile`. These are browser context settings; they do not reproduce a phone’s OS or hardware.

## Custom executables

Expand Down
7 changes: 4 additions & 3 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
| Package | `npm run test:package` | Fresh tarball and checkout installs, init, and a real Chromium test on Node 26 |
| Browser | `npm run test:browser -- firefox` | Real app, gateway, viewport, and process workflows in Chromium, Firefox, and WebKit on Linux and macOS, using Node 24 |
| Execution | `npm run test:execution -- firefox` | Native concurrency, ownership, queue replay, seeded order, suite hooks, cleanup, pause, watch and replacement workflows in all three engines on Linux and macOS |
| Project | `npm run test:project -- firefox` | Project resource acquisition, fixture transport and per-execution preparation in all three engines on Linux and macOS |
| Reporter | `npm run test:reporter -- firefox` | Inspection, output, timings, reruns, and portable saved reports in all three engines on Linux and macOS |
| Native | `npm run test:native -- firefox` | Native defaults, capabilities, state restoration, dialogs, diagnostics, and cancellation in all three engines on Linux and macOS |
| Capture | `npm run test:capture` | Actual pixels and capture lifecycle in Chromium, Firefox, and WebKit on Linux and macOS, using Node 24 |
Expand All @@ -21,11 +22,11 @@ node bin/dyno.js install --browser firefox
npm run test:browser -- firefox
```

Tests live under `tests/core`, `tests/package`, `tests/browser`, `tests/execution`, `tests/reporter`, `tests/capture`, and `tests/native`. The runner discovers every `.test.js` file in the selected directory and rejects test files outside these suites. Keep browser-free checks in core and browser workflows in the relevant feature suite. Tests use real CLI, server, and Playwright browser boundaries; these groups only decide where they run.
Tests live under `tests/core`, `tests/package`, `tests/browser`, `tests/execution`, `tests/project`, `tests/reporter`, `tests/capture`, and `tests/native`. The runner discovers every `.test.js` file in the selected directory and rejects test files outside these suites. Keep browser-free checks in core and browser workflows in the relevant feature suite. Tests use real CLI, server, and Playwright browser boundaries; these groups only decide where they run.

Core checks and actionlint workflow validation run first. Browser and package suites run in parallel once core passes. Documentation runs separately. Browser, execution, reporter, native, and capture suites have separate jobs so each complete suite has its own budget. Each test job has a hard five-minute cap, including setup. Browser and execution checks run up to three independent files together; each owns its app, gateway, browser profiles, and fixture directory. Tests inside a file stay sequential. Reporter, native, capture, and package suites stay sequential. The macOS process-group check runs once in core instead of repeating for each browser. There are no retries or extra sleeps.
Core checks and actionlint workflow validation run first. Browser and package suites run in parallel once core passes. Documentation runs separately. Browser, execution, project, reporter, native, and capture suites have separate jobs so each complete suite has its own budget. Each test job has a hard five-minute cap, including setup. Browser and execution checks run up to three independent files together; each owns its app, gateway, browser profiles, and fixture directory. Tests inside a file stay sequential. Project, reporter, native, capture, and package suites stay sequential. The macOS process-group check runs once in core instead of repeating for each browser. There are no retries or extra sleeps.

Execution and reporter checks have their own suites because the combined macOS Firefox browser suite reached 288–294 seconds, leaving no room for setup under the five-minute cap. Every test and engine/OS combination remains covered. Product suites and benchmarks keep their configured execution; this grouping only changes Dyno's own CI.
Execution and reporter checks have their own suites because the combined macOS Firefox browser suite reached 288–294 seconds. Project setup and preparation now have their own suite after execution exhausted the same cap. Every test and engine/OS combination remains covered. Product suites and benchmarks keep their configured execution; this grouping only changes Dyno's own CI.

## Results and setup

Expand Down
14 changes: 11 additions & 3 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ await I.find(/Contact saved/).rendered();
await I.find('$(input[name=email])').value('rob@example.com');
```

`rendered()` checks displayed content, including below the fold. `visible()` also requires it to be in the viewport. `expectTimeout` sets the assertion deadline.
`rendered()` checks displayed content, including below the fold. `visible()` also requires it to be in the viewport. Assertions retry until they match, then continue immediately. `expectTimeout` sets the assertion deadline.

For changing browser state, use an assertion callback:

Expand Down Expand Up @@ -263,7 +263,9 @@ Dyno gives each test 10 seconds, including hooks and document startup. Interacto
| `runTimeout` | `60000` | Headless browser opening and results deadline, after app readiness |
| `host`, `port` | `127.0.0.1`, `7357` | Gateway address; port 0 chooses a free port |
| `browser.name`, `path`, `args` | `chromium`, Playwright build, `[]` | Managed target, optional executable, and launch arguments |
| `browser.deviceScaleFactor` | `1` | Playwright pixel ratio: 1, 2, or 3 |
| `browser.deviceScaleFactor` | `1` | Playwright pixel ratio from 1 to 3 |
| `browser.isMobile`, `browser.hasTouch` | `false` | Emulate mobile viewport behavior and touch input |
| `browser.userAgent` | Browser default | Override the managed browser user agent |
| `viewport` | Browser size; managed: 1280 × 800 | Required app viewport dimensions |
| `reportDir` | `.dyno/reports` | Saved JSON, JUnit, HTML, and copied images per browser run |
| `artifactsDir` | `.dyno/artifacts` | PNG output directory |
Expand Down Expand Up @@ -606,7 +608,11 @@ Manual runs use the actual window. A browser can't reliably resize its own tab,

## Native browser actions

Use native input when you're testing the browser's default behavior, such as Tab navigation or CSS hover. Keep ordinary clicks, fields, and assertions in Interactor.
Interactor's `click`, `type`, `press`, and assertions run directly in the browser. Use `press` for app-defined keyboard handlers, such as chart arrows or search shortcuts. These don't need a driver.

Use a custom Interactor action with the element's `scrollBy` or `scrollTo` for scrolling. App scroll handlers and lazy loading still run in the real browser.

Use native input when you're testing browser defaults that synthetic events don't reproduce, such as Tab navigation or CSS hover.

```js
await I.find('Name').then.focus();
Expand All @@ -618,6 +624,8 @@ await I.nativeMove({ x: 0, y: 0 });

These work with scopes and custom interactors. `nativePress` uses the focused element; it doesn't focus a selector for you. Hover scrolls to a visible point that receives pointer input. Stale or covered targets fail. After an action navigates, assert on the destination with Interactor.

Use `setColorScheme('dark' | 'light' | 'no-preference')` for system appearance. `blockExternalRequests()` blocks app-page HTTP requests outside the gateway origin and records up to 1,000 recent URLs; `blockedRequests()` returns them. It doesn't block WebSockets, service-worker traffic, or companion requests. Both settings reset between tests.

### Handle a dialog

```js
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"test:core": "node scripts/test.js core",
"test:browser": "node scripts/test.js browser",
"test:execution": "node scripts/test.js execution",
"test:project": "node scripts/test.js project",
"test:reporter": "node scripts/test.js reporter",
"test:package": "node scripts/test.js package",
"test:capture": "node scripts/test.js capture",
Expand All @@ -54,7 +55,7 @@
"@jridgewell/trace-mapping": "0.3.31",
"commander": "15.0.0",
"esbuild": "0.28.2",
"interactor.js": "3.6.2",
"interactor.js": "git+https://github.com/wwilsman/interactor.js.git#d22e523485359a924cedfbc32ee3912c80319dbb",
"parse5": "8.0.0",
"playwright-core": "1.64.1",
"pngjs": "7.0.0"
Expand Down
Loading
Loading