Playwright's report opens empty

npx playwright test finishes, playwright-report/index.html exists, you double-click it — and get the header, the theme toggle, and no tests.

The cause

Playwright's reporter is a single-page app. For small runs it inlines a base64 payload; past a threshold it splits results into sibling files and fetches them at runtime. Either way it needs a real origin: file:// pages have an opaque origin, and fetch against sibling files from there is blocked by the same-origin policy.

Console says: a CORS error against file:///…, or Failed to fetch.

Attachments and traces make it worse — traces are zip archives the viewer streams, so even a report whose test list loads can fail the moment you click a trace.

Locally: use the server Playwright ships

npx playwright show-report            # serves ./playwright-report over HTTP
npx playwright show-report path/to/report

Or configure it to open on failure:

// playwright.config.ts
reporter: [["html", { open: "on-failure" }]],

Both do the same thing — bind a local HTTP server so the fetches resolve.

In CI: the zip is the problem

The standard recipe uploads the report as an artifact:

- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: playwright-report
    path: playwright-report/

GitHub returns artifacts as zip downloads — there is no viewer, by design (why). So the reviewer downloads it, unzips it, double-clicks index.html, and lands back on the blank page at the top of this article. Two failure modes stacked on each other.

GitLab's artifact browser can serve a single HTML file, but its own CSP and path rules break the multi-file report; Jenkins' HTML Publisher fights its Content-Security-Policy for anything with scripts.

Publish the directory instead

Upload playwright-report/ as a unit. index.html becomes the report body; the data/ payloads, trace zips, screenshots and videos upload alongside it, and their relative references are rewritten to the uploaded copies. The report loads over HTTPS, the trace viewer streams, and the whole thing is a link.

- name: Publish report
  if: always() # red builds are the ones with a reader
  run: |
    curl -sS -X PATCH \
      "https://commareports.com/api/v1/reports/${{ vars.COMMA_REPORT_ID }}" \
      -H "Authorization: Bearer ${{ secrets.COMMA_API_TOKEN }}" \
      -H "Content-Type: application/json" \
      --data @<(jq -Rs '{html: .}' playwright-report/index.html)

The multi-file call — the one that carries traces and videos — is in publish from CI.

Limits: entry HTML 5 MB; assets 25 MB per file, 250 MB and 500 files per report. Videos are the usual ceiling; video: 'retain-on-failure' in the config keeps the payload sane.

The part that changes review

A flaky test argument is unresolvable in a chat thread. On a published report, the reviewer selects the failing assertion and comments on it — the thread stays anchored to that test across revisions, so the next person who hits the same flake reads the previous conclusion instead of re-deriving it. See comment on HTML.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history.

Publish a Playwright report →

Related