Parallel and cross-browser
Both work today, on the recordings you already have. Nothing is recorded again, and nothing here spends tokens.
Run in parallel
npx saffron run --workers 4
or "workers": 4 in saffron.config.json. Each worker replays one
scenario at a time in its own browser context, so cookies and storage
never leak between scenarios.
What happens to a scenario that cannot simply replay (no cache yet, a
stale cache, or a failure): it is set aside, and after the parallel phase
it runs again with the agent, exactly as it would without
--workers: a heal starts from the page where its own replay failed. The
cost is one extra replay for those scenarios; a fully cached suite pays
nothing.
Record and heal in parallel
npx saffron run --agent-workers 3
or "agentWorkers": 3 in saffron.config.json. By default one agent
session runs at a time. With --agent-workers N, up to N run at once,
each in a browser of its own: an agent attaches to a whole browser, so two
sessions never see each other's tabs. The scenarios that share one
recording (the rows of an outline) stay together on one worker, in order:
the first records, the rest replay what it filed.
| Cost | Each session is paid for, and N at once spend N times as fast. The run's total is what it would have been one at a time; the bill just arrives sooner. Keep it at 1 unless recording time matters. |
| What runs in parallel | Only the scenarios that need the agent, after the replay phase. Combine it with --workers for both. |
| Browsers | Recording needs a Chromium-family browser, so under firefox or webkit this setting does nothing. |
Where the time went
Every run reports its timing. The terminal prints the three slowest scenarios, and, with more than one worker, how busy each worker was:
workers over 41.2s: 1 92% busy · 2 88% busy · 3 90% busy · 4 61% busy · agent 23% busy
slowest: Checkout with a saved card 8.1s · Search by brand 6.4s · Sign in 3.9s
The report has the same in a timing panel: each worker's runs, its
busy time and its share of the pass it worked in, then the five slowest
scenarios. A worker well below the others is waiting for work: the suite
has a few long scenarios, and splitting one of them up does more than
another worker. Merged shards list each machine's workers by shard. In
latest.json the same figures are under timing (wallMs, and per
worker runs, busyMs and spanMs), and each scenario names its
worker. With --no-agent, or in Firefox and WebKit, a red from the
parallel phase is final: no agent could change it, so it is not run again.
| How many workers | Start with the number of CPU cores. Browsers are the cost, not Saffron. |
| Order | Scenarios start in file order and finish in any order. The report keeps file order. |
--no-agent |
Combine them in CI: --workers 4 --no-agent is the fast, zero-token gate. |
| Shared test data | Parallel scenarios share your application. Two scenarios that edit the same record will interfere, the same as in any parallel runner. Give them their own data with {unique:name} (§16), seed what they only read with setup (§17), or keep them in one scenario. |
Split a suite across CI machines
--workers is capped by one machine's cores. To go further, give the
suite to several machines and put the results back together afterwards:
npx saffron run --no-agent --shard 1/4 # on the first machine
npx saffron run --no-agent --shard 2/4 # on the second, and so on
npx saffron report --merge # once, after all four
Each shard runs a fixed part of the suite. The split is by scenario, not by
file: the scenarios are sorted and every fourth one goes to the same
shard, so one large feature file is spread over all four machines. The
rows of a Scenario Outline share one recording, so they stay together on
one shard, and a large outline is placed first so the load stays even.
Every machine works out the same split from the same checkout, so each
scenario runs exactly once. --shard combines with --workers and --filter.
To keep each feature file on one machine instead, add --shard-by file
(or "shardBy": "file" in the config):
npx saffron run --no-agent --shard 2/4 --shard-by file
Use it when the scenarios of a file share something expensive or fragile: a Background that seeds data, an account the application only lets log in from one place at a time, a file written to run in order. Whole files go to the machine with the fewest scenarios so far, largest file first, so the machines still get a similar load, but one very large file can make its machine the slowest. Every shard must use the same mode; the merge refuses a mix.
A shard does not write latest.json or add to history.jsonl: a quarter
of a run is not a run, and the trends would read it as one. It writes
.saffron/reports/shard-2-of-4.json (and an HTML view of its own part)
instead. saffron report --merge reads every shard-*-of-*.json under
.saffron/, or under the files and folders you name (searched
recursively, wherever a CI artifact download put them), and writes
latest.json, latest.html and one history line, as if the suite had run
on one machine: the same totals, the same scenario order, the same trends.
The merge refuses, with exit code 2, anything that would not add up to the whole run:
| A shard is missing | Named: missing shard 2/4. A merge without it would under-count the run. |
| A shard was interrupted | Named, with how many of its scenarios did not run. A shard stopped with Ctrl-C writes its report of what finished, marked interrupted; run that shard again. saffron import still takes what such a shard holds, and the imported report is marked interrupted. |
| The same shard twice | Both files are named. When a re-run wrote a shard again, the latest attempt's report is merged and the earlier one is named and left out. |
| Shards ran different suites | Every shard must run the same paths and --filter. |
| Shards ran against different base URLs | A staging shard and a production shard are two runs, not one. Merge one environment at a time. |
| A shard's history is malformed | Each history line in a shard file is checked field by field (numbers are numbers, the status is green, yellow or red) before it is merged, so a hand-edited or tampered CI artifact cannot put markup into the dashboard. |
| Shards come from different CI runs | Saffron records the run from GITHUB_RUN_ID, CI_PIPELINE_ID (GitLab), BUILD_BUILDID (Azure) or CIRCLE_WORKFLOW_ID, or SAFFRON_RUN_ID when you set it. |
Merging the same shards again rewrites the report but does not count the
run twice. Re-running one shard, say after a flaky machine, and merging
again replaces that run's history line with the new result, so the trends
show what the run finally was. In CI the run is identified by the CI run
id, the suite and the base URL, so this works whichever shard was re-run.
Outside CI, set SAFFRON_RUN_ID to something stable for the same effect. A shard with nothing to run (more machines than scenarios) still
writes its report, so the merge knows the set is complete.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- run: npm ci && npx playwright install chromium --with-deps
- run: npx saffron run --no-agent --workers 2 --shard ${{ matrix.shard }}/4
- uses: actions/upload-artifact@v4
if: always()
with:
name: saffron-shard-${{ matrix.shard }}
path: |
.saffron/reports/shard-*.json
.saffron/artifacts/
report:
runs-on: ubuntu-latest
needs: test
if: always()
steps:
- uses: actions/checkout@v4
- run: npm ci
- uses: actions/download-artifact@v4
with:
pattern: saffron-shard-*
path: .saffron
merge-multiple: true
- run: npx saffron report --merge
- uses: actions/upload-artifact@v4
with:
name: saffron-report
path: .saffron/reports/latest.*
| The build verdict | Each shard exits 1 on red, like any run, so the test jobs are the gate. The merge exits 0 when it wrote the report. |
| History | Commit history.jsonl from the merge job, not from the shards, if you keep trends in git. |
| Screenshots | The merged HTML embeds the failure screenshots it finds under .saffron/artifacts/. The example uploads them with each shard report and downloads everything back into .saffron/; scenarios never share a folder, so the shards cannot overwrite each other's. Without them the report is complete apart from the pictures. |
| Setup and teardown | They run on every shard, and receive SAFFRON_SHARD (2/4), so a seed script can keep the machines' data apart (§17). |
| Recording under shards | Works like any run: a shard with the agent enabled records or heals its own scenarios and files proposals on that machine. In CI, --no-agent is the usual choice. |
Run on another browser
npx saffron run --browser chrome
npx saffron run --no-agent --browser firefox
or "browser": "firefox" in saffron.config.json. Five browsers are
supported:
--browser |
What it is | Replay | Record and heal | Where it comes from |
|---|---|---|---|---|
chromium (default) |
The open-source Chromium build | Yes | Yes | Installed with Saffron |
chrome |
Google Chrome | Yes | Yes | The copy installed on the machine |
msedge |
Microsoft Edge | Yes | Yes | The copy installed on the machine |
firefox |
Firefox (Gecko engine) | Yes | No | npx playwright install firefox |
webkit |
WebKit, the engine behind Safari | Yes | No | npx playwright install webkit |
Recordings are browser-neutral: targets are roles, names and labels, not
engine-specific selectors. Record once, replay on every engine.
Recording and healing need a Chromium-family browser (chromium, chrome
or msedge), because the agent attaches to the browser over CDP. Under
Firefox or WebKit a scenario that needs the agent fails with that
instruction instead of a confusing error, so a red there means "record
this on Chromium first" or a genuine cross-browser difference in your app.
Google Chrome and Microsoft Edge
npx saffron run --browser chrome
npx saffron run --browser msedge
chromium is the open-source build Saffron ships with. chrome and
msedge are the branded browsers your users actually have, with their own
codecs, policies and release schedule. Use them when "it works in
Chromium" is not enough evidence for your team. Everything works on them,
recording and healing included, so you can also make one of them your
everyday browser:
{ "browser": "chrome" }
| Where the browser comes from | The copy already installed on the machine. Saffron does not download it. On a CI image without it, npx playwright install chrome or npx playwright install msedge adds it. |
| When it is missing | The run stops before the first scenario, exit code 2, and names the browser that is not installed. |
| Recordings | The same cache files. A scenario recorded on chromium replays on chrome and msedge, and the other way round. |
Several browsers in one run
npx saffron run --browser chromium,firefox,webkit --workers 4
or "browser": ["chromium", "firefox", "webkit"] in the config. The
suite runs in each browser, one after another, into one report: every
scenario has a row per browser, marked with its browser, in the terminal,
the report, the history and the dashboard.
| Order | Chromium-family browsers go first, whatever the order you gave: they record and heal, so Firefox and WebKit then replay what they filed instead of failing for want of a recording. |
| Before anything runs | Every browser starts first. One that is missing stops the run, exit 2, before any recording is touched. |
| Evidence | Each browser keeps its own screenshots and traces, under .saffron/artifacts/<feature>/<scenario>/<browser>/. saffron trace lists them as feature:scenario@browser. |
| Dashboard | A scenario stands at its worst browser, among the browsers the last run used: move from one browser to another and the one you left no longer counts. A scenario red in Firefox and green in Chromium is a cross-browser bug, not a flaky test: the dashboard compares each browser with itself. |
| Running one browser again | Every result names its browser, matrix or not. saffron run --browser chromium (or one scenario of it, features/checkout.saffron:Pay) replaces Chromium's rows and leaves Firefox's and WebKit's as they were, in the report and on the dashboard. A result from before Saffron recorded browsers counts as any browser's, and the next one replaces it. |
| For tools | Rows in latest.json, history.jsonl and the lastRun.attention list of saffron status --json (and the MCP project_status tool) carry browser, and so do a CI run's rows in saffron report --run <id> --json. Both list the run's browsers when its rows span several, and the browser labels show only then. In VS Code and JetBrains, Last Run and CI Runs label such rows with their browser, and Open Replay opens that browser's execution alone. |
With --shard |
Refused: give each shard job one --browser. A shard records its browser, and saffron report --merge refuses shards of different browsers: merge each browser's shards on their own, and each gets its own history line. A CI matrix (below) runs the browsers side by side, which a single machine cannot. |
Locally, one command and one report is what this is for. In CI, a matrix of jobs is faster.
Devices and viewports
npx saffron run --device "iPhone 15"
npx saffron run --viewport 1280x720
npx saffron run --browser chromium,webkit --device "Pixel 7"
or "device" and "viewport" in the config. A device is one of
Playwright's profiles ("iPhone 15", "Pixel 7", "iPad Pro 11",
"Desktop Chrome"...): it sets the viewport, the user agent, touch and the
pixel ratio for every scenario, the agent's recordings and heals included.
A viewport sets the size alone, and wins over the device's when both are
given. A name Playwright does not know exits 2 with the closest names.
Recordings are not tied to a profile. A scenario recorded on the desktop
replays under a phone profile, and passes where the page works the same,
or fails at the step where it does not (a menu behind a hamburger
button, say): that red is the finding. Record under the profile instead
when the phone flow differs on purpose. Playwright does not emulate
mobile devices in Firefox, so a mobile profile with --browser firefox
stops the run, exit 2; give Firefox a --viewport instead.
A cross-browser matrix in CI
strategy:
matrix:
browser: [chromium, chrome, msedge, firefox, webkit]
steps:
- run: npm ci
- run: npx playwright install ${{ matrix.browser }} --with-deps
- run: npx saffron run --no-agent --strict --workers 4 --browser ${{ matrix.browser }}
Five jobs, zero tokens, no credentials: replay needs no AI provider.