Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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 that TestResult.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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

  1. Print result.duration from onEnd and label it as total run duration.
  2. Print result.duration from onTestEnd and verify that each completed attempt appears.
  3. Run a suite with a deliberate retry and confirm two records with different retry values.
  4. Compare your JSON or custom artifact with the installed Playwright version, not a copied example from another release.
  5. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.