Recommended Free Tools
Short answer: Reporter.onEnd(result) receives a FullResult for the entire Playwright test run, so result.duration is the run’s elapsed time in milliseconds. It is not a collection of individual test results. To obtain each test attempt’s duration, read result.duration in onTestEnd(test, result), where the second argument is a completed TestResult. Save those records and summarize them later in onEnd.
The two duration fields have different scopes
Playwright uses the same property name, duration, on two different result types. The callback determines what the number means.
| What you need | Callback | Object | Meaning of duration |
|---|---|---|---|
| Total elapsed run time | onEnd(result) |
FullResult |
Test-run duration in milliseconds |
| One test attempt’s run time | onTestEnd(test, result) |
TestResult |
Running time for that completed attempt, in milliseconds |
Therefore, code such as onEnd(result) { console.log(result.duration); } is working as designed: it prints one suite-level value. The callback is invoked once after the run, not once for every test.
Why onEnd does not include individual tests
The Reporter API separates lifecycle events by scope. onTestEnd fires whenever a test attempt finishes and supplies the completed TestResult. onEnd fires once after the run and supplies a FullResult containing run status, start time and total duration.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
This design lets a reporter process a test as soon as it completes, while reserving onEnd for final run-level work such as writing a summary, closing a stream or publishing an aggregate. The full-run object is not intended to be a replacement for the per-test callback, so there is no automatically populated array of TestResult objects to read from onEnd.
What “duration” means in each callback
- In
onEnd: the elapsed duration of the complete test run, represented in milliseconds. - In
onTestEnd: the elapsed running time for the specific test attempt represented by thatTestResult.
Do not rename the run-level value to “test duration” in a report. Label it “run duration” or “suite duration” so consumers do not mistake it for an individual test timing.
Collect each test duration in a custom reporter
Record the per-attempt value in onTestEnd, then use the retained records in onEnd. This TypeScript reporter follows the current API shape and keeps the retry number beside every duration.
import type {
FullResult,
Reporter,
TestCase,
TestResult,
} from '@playwright/test/reporter';
class TimingReporter implements Reporter {
private attempts: Array<{
title: string;
retry: number;
durationMs: number;
}> = [];
onTestEnd(test: TestCase, result: TestResult) {
this.attempts.push({
title: test.title,
retry: result.retry,
durationMs: result.duration,
});
}
onEnd(result: FullResult) {
console.log(JSON.stringify({
runDurationMs: result.duration,
attempts: this.attempts,
}, null, 2));
}
}
export default TimingReporter;
Save the file, for example as timing-reporter.ts, and register it in your Playwright configuration:
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteRank #2
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['./timing-reporter.ts']],
});
The example emits one record for every completed attempt. The title shown is the test title supplied by the API; for a production report, you may also retain the test’s file, project and location so identically named tests remain distinguishable.
Retries change what “one test duration” means
A retry is a separate TestResult. Its retry property identifies the attempt, and its duration measures only that attempt. A test that fails once and passes on retry can therefore produce two timing records.
| Reporting goal | Recommended calculation | Why |
|---|---|---|
| Diagnose flaky behavior | Keep every record keyed by test and retry |
Shows how long the failing and succeeding attempts took |
| Show time spent on a logical test | Sum all attempts for that test | Represents the execution cost, including retries |
| Show the final user-visible outcome | Use the last attempt’s duration | Matches the attempt that determined the final status |
| Track performance regressions | Keep attempts separate and aggregate outside the reporter | Averages and percentiles should not hide retry behavior |
Playwright does not impose one aggregation policy. Choose one explicitly and document it in the report schema. Never silently collapse retries into a single number if consumers need to investigate flakiness.
Example aggregation after collection
const byTitle = new Map<string, number>();
for (const attempt of this.attempts) {
byTitle.set(
attempt.title,
(byTitle.get(attempt.title) ?? 0) + attempt.durationMs,
);
}
This sums attempts. Replace the addition with a last-attempt assignment or retain an array when your report has a different definition of duration.
Use the built-in JSON reporter when you do not need a custom hook
Playwright includes a json reporter and supports running multiple reporters together. It can be simpler to produce a machine-readable artifact and let a later step extract timing data.
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['list'],
['json', { outputFile: 'test-results.json' }],
],
});
The exact serialized shape is version-sensitive. Inspect the JSON generated by the Playwright version installed in your project and verify whether your consumer needs run-level statistics, individual results or both. A type declaration or example from another Playwright release is not proof that every release emits identical fields.
When a custom reporter is still preferable
- You need a streaming event as each attempt completes.
- You must apply a defined retry aggregation policy.
- You need to add organization-specific fields or send records to another system.
- You want one final document that contains both the retained attempts and
FullResult.duration.
Troubleshooting missing or confusing durations
“result.duration is undefined in onEnd”
Check that the imported type and callback signature match the Playwright package actually installed. In current Reporter API documentation, onEnd receives FullResult, whose duration is defined in milliseconds. A mismatched local type, an incorrectly named method or a wrapper around the reporter can make the object appear different.
“I get one number, but I need one number per test”
Move the per-test read to onTestEnd(test, result). Push the completed record into an array or write it to a stream, then read that collection from onEnd.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteRank #4
“The report has duplicate tests”
Those entries may be retries. Keep result.retry and, ideally, the test file or location. Decide whether your report is attempt-level or logical-test-level before deduplicating.
“The JSON artifact does not contain the field I expected”
Confirm whether you are inspecting a run statistic or an individual result, and check the installed Playwright version. Reporter output can vary in verbosity and schema between versions; do not infer callback behavior solely from a different version’s artifact.
“My final summary is incomplete”
Ensure records are captured in onTestEnd before the final emission in onEnd. If your reporter writes asynchronously, complete that work according to the reporter API contract and test the behavior in the same Playwright version used in CI.
“Durations look wrong when tests run in parallel”
Use stable identifiers rather than array position to associate records with tests. A completion callback reflects when an attempt finished; a report that requires source order should sort by file and location after collection.
Performance and reliability considerations
- Memory: retaining every attempt is inexpensive for a small suite but can grow with large, highly retried runs. Stream records to a file or datastore if the collection is large.
- Units: keep values as integers in milliseconds internally. Convert to seconds only at presentation time to avoid rounding away short tests.
- Run versus test timing: the sum of test durations need not equal the full-run duration. Parallel workers overlap, and run-level timing has a different scope.
- Schema stability: version your custom output and include the Playwright package version when artifacts are consumed by another service.
- Failure handling: emit the run-level status and any records received before an interruption; do not treat an absent record as a zero-duration test.
Validation checklist
- Print
result.durationfromonEndand label it as total run duration. - Print
result.durationfromonTestEndand verify that each completed attempt appears. - Run a suite with a deliberate retry and confirm two records with different
retryvalues. - Compare your JSON or custom artifact with the installed Playwright version, not a copied example from another release.
- Document whether dashboards show each attempt, the sum of attempts or the final attempt.
Or skip the browser setup
If your next step is to capture a visual image or PDF of a test report page rather than build browser automation for that capture, ScreenshotNeo provides a single HTTP request. It removes cookie or consent banners, newsletter popups and chat widgets before the shot; bot checks, blank pages, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots.
See the ScreenshotNeo API documentation for authentication and options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Replace the example URL with the report page you are authorized to capture. Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
Frequently Asked Questions
Does the installed Playwright version matter for reporter output?
Yes. Hook types and serialized JSON fields are version-sensitive. Check the package version in the project and inspect an artifact generated by that exact version.
Can a single report show both run time and test times?
Yes. Store attempt records from onTestEnd, then emit them together with FullResult.duration from onEnd.
Why might the sum of test durations exceed or fall below the run duration?
They measure different scopes. Tests can overlap across workers, while the run value is one elapsed duration for the complete run.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

