taqwright

Custom reporters

Taqwright has no reporting layer of its own — it forwards the reporter option straight to Playwright's runner. So any Playwright reporter works unchanged, including a fully custom one you write. And because taqwright's device trace and screen recording are attached as standard Playwright attachments, your custom reporter can pull them into whatever report format you want.

Overview

When you run npx taqwright test, taqwright locates your taqwright.config.ts and spawns Playwright's own runner against it. defineConfig() returns a real Playwright TestConfig and passes reporter through verbatim — there is no whitelist, no wrapper, and no forced reporter. The built-in presets (list, html, json, junit, blob, github, …) are covered in Running & debugging → Reports. This page is about writing your own.

What "custom report" means here. A custom reporter is a small module that implements Playwright's Reporter interface. Playwright calls your hooks as tests run; you decide what to emit — a bespoke HTML dashboard, a Slack message, a CSV, a row in your own DB. It's the same mechanism Playwright's own html / json reporters use.

Writing a custom reporter

Implement the Reporter interface from @playwright/test/reporter. The three hooks you'll almost always want are onBegin, onTestEnd, and onEnd:

reporters/my-reporter.ts
import type {
  Reporter, FullConfig, Suite, TestCase, TestResult, FullResult,
} from '@playwright/test/reporter';

export default class MyReporter implements Reporter {
  private rows: Array<Record<string, unknown>> = [];

  onBegin(config: FullConfig, suite: Suite) {
    console.log(`Starting ${suite.allTests().length} tests`);
  }

  onTestEnd(test: TestCase, result: TestResult) {
    this.rows.push({
      title: test.titlePath().join(' › '),
      project: test.parent.project()?.name,
      status: result.status,                 // 'passed' | 'failed' | 'timedOut' | 'skipped'
      durationMs: result.duration,
      retry: result.retry,
      error: result.error?.message,
      attachments: result.attachments.map((a) => a.name),
    });
  }

  async onEnd(result: FullResult) {
    const fs = await import('node:fs/promises');
    await fs.writeFile(
      'reports/custom.json',
      JSON.stringify({ status: result.status, tests: this.rows }, null, 2),
    );
    console.log(`Custom report written — overall: ${result.status}`);
  }
}

Every Playwright reporter hook is available (onStdOut, onStdErr, onStepBegin/onStepEnd, onError, printsToStdio, …). taqwright doesn't intercept any of them — your reporter sees exactly what a Playwright web reporter would: standard TestCase / TestResult objects, the Playwright-managed result.status, and the full attachment list.

Wiring it in

Reference the reporter module from taqwright.config.ts. Use the array tuple form[modulePath] or [modulePath, options] — and stack it alongside any built-ins:

taqwright.config.ts
import { defineConfig, Platform } from 'taqwright';

export default defineConfig({
  reporter: [
    ['list'],                                  // keep a console reporter
    ['./reporters/my-reporter.ts'],            // your custom reporter
    ['./reporters/my-reporter.ts', { slackWebhook: process.env.SLACK_URL }],
  ],
  projects: [ /* ... */ ],
});
Use the array form, not a bare string. taqwright's reporter type is presets | Array<[string] | [string, unknown]>. The bare-string custom-module form (reporter: './reporters/my-reporter.ts') is not in the type union, so TypeScript will flag it even though Playwright would accept it at runtime. Always wrap a custom reporter in the tuple form: reporter: [['./reporters/my-reporter.ts']].

You can also pass it per-run on the CLI — taqwright test forwards --reporter to Playwright:

npx taqwright test --reporter=./reporters/my-reporter.ts
# or stack with a preset:
npx taqwright test --reporter=list,./reporters/my-reporter.ts

Consuming taqwright's trace & video

This is the part unique to taqwright. When trace or video is enabled, taqwright writes its artifacts as standard Playwright attachments on the test result — so a custom reporter reads them straight off result.attachments, no taqwright import required:

Attachment namecontentTypeWhat it is
taqwright-tracetext/htmlSelf-contained per-action timeline (screenshots + page-source) as one trace.html.
taqwright-videovideo/mp4Full-run on-device screen recording, screen.mp4.

They are deliberately not named trace / video — Playwright's own HTML report reserves those names for its browser .zip traces and recordings. Each attachment is the normal Playwright shape ({ name, contentType, path }, occasionally body), so you can copy, embed, or link them however your report needs:

reporters/my-reporter.ts (excerpt)
onTestEnd(test: TestCase, result: TestResult) {
  for (const att of result.attachments) {
    if (att.name === 'taqwright-trace' && att.path) {
      // e.g. copy next to your report and link it
      this.traceFor[test.id] = att.path;            // .../trace.html (text/html)
    }
    if (att.name === 'taqwright-video' && att.path) {
      this.videoFor[test.id] = att.path;            // .../screen.mp4 (video/mp4)
    }
  }
}
Retention follows the artifact mode. An attachment only exists when the run's outcome matches the configured mode ('on' always; 'on-failure' / 'retain-on-failure' only for failed tests). Always guard on att.path being present — don't assume every test result carries the trace/video.

TypeScript & dependencies