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.