Skip to main content
skill Claude Code CX Codex Cursor GitHub Copilot

playwright-test-results — Claude Code skill: use and install

Query Playwright CI results in DuckDB to find flaky, failing, and slow tests, with details for runs, SHAs, and pull requests.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add microsoft/playwright --skill playwright-test-results -a claude-code
Open on GitHub

What playwright-test-results does

This skill works with an aggregated DuckDB database containing Playwright CI test results. It lets an agent inspect tests whose final verdict changes between runs, calculate failure rates, find slow checks, and filter results by run, SHA, or pull request. Flakiness is derived from final statuses after retries, while intentional failures can be excluded with expected_status = 'passed'.

From the repository root, run npm ci, then refresh the database with GITHUB_TOKEN=$(gh auth token) node utils/test-results-db/cli.ts update --lookback-days 3. Queries use the bundled @duckdb/node-api package, so a separate DuckDB installation is not needed after npm ci. The database keeps a recent window of runs, which is a reason to check its available history before making long-term comparisons.

What it helps with

  • find tests that flip verdicts across CI runs
  • calculate failure rates by test and project
  • exclude intentional test.fail() results from failure lists
  • rank slow Playwright tests from CI data
  • inspect results for a run, SHA, or pull request

Example prompts for the agent

  • Update the Playwright results database with `GITHUB_TOKEN=$(gh auth token) node utils/test-results-db/cli.ts update --lookback-days 3`, then find the most flaky tests.
  • List tests tagged `@slow` and sort them by run count.
  • Show the retry statuses and final verdict for a test identified by `project_name`, `file`, and `test_title`.

How to install playwright-test-results

1Skills CLI (Vercel)

npx skills add microsoft/playwright --skill playwright-test-results -a claude-code

2Manually: put the files in place

~/.claude/skills/playwright-test-results/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/playwright-test-results/

From the same set3

The whole set →

Similar

ce-debug EveryInc Finds the root cause of a bug from trigger to symptom and applies a test-first fix. Useful when investigating broken behavior or failing CI logs. Claude Code CX Codex Cursor +1 3,500installs ce-simplify-code EveryInc Simplifies code after implementation while keeping its behavior, and removes tests or comments with little value. Use it before review or for a repository audit. Claude Code CX Codex Cursor +1 3,400installs spring-boot-engineer https://github.com/Jeffallan Generates Spring Boot 3.x code, JPA repositories, REST controllers, and Spring Security 6 configurations. Useful when writing Java microservices and reactive endpoints. Claude Code CX Codex Cursor +1 8,400installs export-download-debugging nexu-io Diagnoses and resolves failed downloads, broken File System Access API calls, and 0 KB file exports in browsers and Electron shells. Claude Code CX Codex Cursor +1 1,800installs woocommerce-backend-dev woocommerce Skill guides you through WooCommerce backend PHP development following project standards. Use it before creating new classes, modifying backend code, or writing unit tests. Claude Code CX Codex Cursor +1 2,300installs bun-development sickn33 This skill guides JavaScript and TypeScript work with the Bun runtime, from new projects and Node.js migrations to Bun-specific troubleshooting. Claude Code CX Codex Cursor +1 2,800installs improve-codebase-architecture Matt Pocock This skill scans a codebase for architectural friction, then builds a visual HTML report with refactoring candidates. Use it before a deeper architecture review. Claude Code 1,163,035installs design-taste-frontend Leonxlnx This skill directs the design of public pages and work screens, then checks the result in a browser through screenshots. Use it when an interface feels generic or unfinished. Claude Code CX Codex Cursor +1 673,100installs

Questions about playwright-test-results

How do I install playwright-test-results in Claude Code?

Run npx skills add microsoft/playwright --skill playwright-test-results -a claude-code, run the same command again, and check ~/.claude/skills/playwright-test-results/SKILL.md.

What does playwright-test-results do?

It queries Playwright CI results from DuckDB. The skill covers flaky tests, failures, slow tests, and results grouped by runs, SHAs, and pull requests.

What is required before querying the database?

From the repository root, run npm ci, then refresh the database with GITHUB_TOKEN=$(gh auth token) node utils/test-results-db/cli.ts update --lookback-days 3. Queries use @duckdb/node-api.

Source text

SKILL.md · Apache-2.0
---
name: playwright-test-results
description: Query Playwright CI test results from the aggregated DuckDB database. Answers questions about flaky tests, failure rates, slow tests, and per-run/SHA/PR results without hunting through GitHub artifacts.
user_invocable: true
---

# Playwright Test Results (DuckDB)

A single DuckDB file holds recent Playwright CI test results, so you can answer
questions about failures, flakiness, and slow tests with plain SQL. It is
refreshed every few hours.

## Get the database

Download the latest snapshot and top it up with the newest runs:

```bash
npm ci                       # first time only, from the repo root
GITHUB_TOKEN=$(gh auth token) node utils/test-results-db/cli.ts update --lookback-days 3
```

`update` downloads the snapshot first when there is no local database, then
ingests only the artifacts the snapshot lacks. Never delete the database to
rebuild it: every artifact costs one GitHub API request, the budget is shared
by every workflow in the repository, and a rebuild of a few days exhausts it.
To only refresh the snapshot without ingesting, run `download`.

Query it through the bundled `@duckdb/node-api` binding — no separate DuckDB
install needed, it ships in `node_modules` after `npm ci`:

```bash
node --input-type=module -e '
import { DuckDBInstance } from "@duckdb/node-api";
const conn = await (await DuckDBInstance.create("utils/test-results-db/test-results.duckdb")).connect();
console.table((await conn.runAndReadAll(process.argv[1])).getRowObjectsJson());
' "SELECT count(*) FROM test_results"
```

Integer columns come back as strings (JSON-safe), so do ranking and filtering
in SQL, not in JS.

## Schema

Single table `test_results`, one row per test result (**one row per retry**).
The columns are inferred from the parquet the reporter emits
(`tests/config/parquetReporter.ts`), plus two trailing columns this CLI adds:

| Column | Meaning |
| --- | --- |
| `run_id`, `run_attempt` | GitHub Actions run identity |
| `run_started_at` | when the run started |
| `workflow_name` | e.g. `tests 1` / `tests 2` / `tests others` / `MCP` |
| `event` | `push` / `pull_request` |
| `head_sha`, `head_branch`, `pr_number` | what was tested |
| `bot_name` | e.g. `chromium-ubuntu-22.04-node20`, `webkit-macos-15-large` — the CI bot. **OS and arch are encoded here**; there is no separate os column. |
| `project_name` | CI project = browser + suite, e.g. `chromium-page`, `webkit-library`, `playwright-test` |
| `test_title` | title path within the file, joined by ` › ` (`describe › test`) |
| `file`, `line`, `column_number` | source location (file is relative to repo root) |
| `expected_status` | `passed` / `skipped` / ... |
| `status` | actual result: `passed` / `failed` / `timedOut` / `skipped` / `interrupted` |
| `retry` | 0 = first attempt |
| `result_started_at` | when this attempt started |
| `duration_ms` | result duration |
| `error_message` | all errors joined, ANSI-stripped (NULL when none) |
| `tags` | **list** of strings, e.g. `['@slow', '@flaky']` (use list functions / `list_contains`) |
| `annotations` | list of `{type, description}` structs, e.g. `[{'type': 'skip', 'description': 'flaky on CI'}]` (empty list when none) |
| `artifact_id` | the GitHub artifact this row came from (dedupe key) |
| `ingested_at` | debug only — when this row was imported |

Notes:
- **A test is identified by `(project_name, file, test_title)`** — group on that
  tuple. (Playwright's `test_id` hash is deliberately not stored; those three
  columns are its pre-image.)
- **Flakiness is derived**, not stored. The signal that matters most is
  **cross-run**: a test whose *final* verdict (after retries) flips between
  runs — green in some, red in others. A separate **within-run** flake is a
  test a retry rescued inside a single run (`failed`→`passed`).
- **Real failures vs intentional ones:** filter `expected_status = 'passed'`.
  Tests marked `test.fail()` record `status='failed'` *with*
  `expected_status='failed'` and would otherwise dominate any "most failing" list.
- The db is size-capped by **run count**: the oldest whole runs are evicted over
  time, so it holds a recent window, not full history.

## Example queries

Group tests by `(project_name, file, test_title)` and (for failure/flakiness)
scope to `expected_status = 'passed'` so intentional `test.fail()` tests don't
skew the results.

**Flaky across runs** — the test's final verdict flips between runs (this is
what makes a red CI run ambiguous). `least(failed_runs, passed_runs)` ranks
genuinely bimodal tests above both always-broken and one-off failures:

```sql
WITH per_run AS (
  SELECT project_name, file, test_title, run_id, run_attempt,
         arg_max(status, retry) AS final_status,
         any_value(expected_status) AS expected
  FROM test_results
  GROUP BY project_name, file, test_title, run_id, run_attempt)
SELECT project_name, test_title,
       count(*) AS runs,
       count(*) FILTER (WHERE final_status IN ('failed','timedOut')) AS failed_runs,
       count(*) FILTER (WHERE final_status = 'passed') AS passed_runs,
       round(100.0 * count(*) FILTER (WHERE final_status IN ('failed','timedOut'))
             / count(*), 1) AS fail_pct
FROM per_run
WHERE expected = 'passed'
GROUP BY project_name, test_title
HAVING failed_runs > 0 AND passed_runs > 0 AND runs >= 10
ORDER BY least(failed_runs, passed_runs) DESC, failed_runs DESC
LIMIT 20;
```

**Filter by tag** (`tags` is a list, not a string):

```sql
SELECT project_name, test_title, count(*) AS runs
FROM test_results
WHERE list_contains(tags, '@slow')
GROUP BY project_name, test_title
ORDER BY runs DESC
LIMIT 20;
```

## Generate a linked emoji run history

For a compact result that drops straight into a GitHub comment, render each
final run verdict as a linked square. Edit the four test identity fields, then
run:

```bash
node --input-type=module <<'EOF'
import { DuckDBInstance } from "@duckdb/node-api";

const repository = "microsoft/playwright";
const test = {
  projectName: "firefox-library",
  file: "library/proxy.spec.ts",
  testTitle: "should exclude patterns",
  botName: "firefox-macos-15-large",
};

const conn = await (await DuckDBInstance.create(
  "utils/test-results-db/test-results.duckdb"
)).connect();
const result = await conn.runAndReadAll(`
  WITH per_run AS (
    SELECT run_id, run_attempt,
           any_value(run_started_at) AS run_started_at,
           arg_max(status, retry) AS final_status,
           arg_max(expected_status, retry) AS expected_status,
           list(status ORDER BY retry) AS attempt_statuses
    FROM test_results
    WHERE project_name = $projectName
      AND file = $file
      AND test_title = $testTitle
      AND bot_name = $botName
    GROUP BY run_id, run_attempt
  )
  SELECT run_id, run_attempt, final_status, attempt_statuses
  FROM per_run
  WHERE expected_status = 'passed'
    AND final_status IN ('passed', 'failed', 'timedOut')
  ORDER BY run_started_at, run_id, run_attempt
`, test);

const markdown = result.getRowObjectsJson().map(row => {
  const rescued = row.final_status === "passed" &&
    row.attempt_statuses.some(status => status === "failed" || status === "timedOut");
  const emoji = rescued ? "🟧" : row.final_status === "passed" ? "🟩" : "🟥";
  const url = `https://github.com/${repository}/actions/runs/${row.run_id}/attempts/${row.run_attempt}`;
  return `[${emoji}](${url})`;
}).join("");

console.log(markdown);
EOF
```

The output is Markdown:

```markdown
[🟩](https://github.com/microsoft/playwright/actions/runs/123/attempts/1)[🟧](https://github.com/microsoft/playwright/actions/runs/456/attempts/1)[🟥](https://github.com/microsoft/playwright/actions/runs/789/attempts/1)
```

Each square is one workflow run attempt, oldest first. Green means passed,
orange means a retry rescued an earlier failure, and red means failed or timed
out. `arg_max(status, retry)` picks the final verdict after retries, while
grouping by `(run_id, run_attempt)` keeps retries from turning into extra
squares. The `/attempts/<n>` URL links to the exact rerun that produced the
result.

## Fetching the full detail

The db stores per-result summaries. For the full step tree / attachments / stdio,
fetch the original blob report for that run, if the run uploaded one. A row
identifies it by `run_id` + `bot_name`: the run's blob artifact is named
`blob-report-<bot_name>`.

```bash
# List the run's blob artifacts and find the one for this bot_name:
gh api /repos/microsoft/playwright/actions/runs/<run_id>/artifacts \
  --jq '.artifacts[] | select(.name | startswith("blob-report")) | {id, name}'

# Download it (name == "blob-report-<bot_name>"):
gh api /repos/microsoft/playwright/actions/artifacts/<artifact_id>/zip > blob.zip
```

Blob and parquet artifacts have a 7-day retention, so this works only for recent
runs; the db itself retains summaries longer (until run-count eviction).

© microsoft · Apache-2.0 · microsoft/playwright

See also

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor