@qartisan/playwright-loupe 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/README.md +22 -6
- package/dist/config/resolve.js +3 -1
- package/dist/config/types.d.ts +6 -1
- package/dist/config/validate.js +9 -2
- package/dist/core/build.js +21 -0
- package/dist/core/categories.d.ts +4 -5
- package/dist/core/categories.js +75 -34
- package/dist/core/scope.d.ts +10 -0
- package/dist/core/scope.js +34 -4
- package/dist/core/signature.d.ts +3 -2
- package/dist/core/signature.js +46 -5
- package/dist/renderer/index.html +3 -2
- package/dist/shared/color.d.ts +47 -0
- package/dist/shared/color.js +180 -0
- package/dist/shared/palette.d.ts +37 -0
- package/dist/shared/palette.js +91 -0
- package/licenses/DM-Sans-OFL.txt +93 -0
- package/package.json +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,46 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Before 1.0, minor versions may contain breaking changes. The JSON report has its own version (`schemaVersion`), see [`docs/schema/v1.md`](docs/schema/v1.md#versioning-policy).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.0.3] - 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **HTML report application shell.** The renderer is rebuilt with Preact and `@preact/signals`, still one self-contained `index.html` that works from `file://` (both libraries are bundled into it: the package keeps zero runtime dependencies).
|
|
12
|
+
- Header with the run summary: status, counts per outcome, duration, branch, commit and environment. A partial run shows a banner with the reasons it was detected.
|
|
13
|
+
- Collapsible sidebar with a test tree: projects → files → describe blocks → tests, single-child levels merged, a status icon per test (passed, failed, flaky, skipped) and failure counts per node. Keyboard navigation (arrows, Home / End, Enter), the current test highlighted and revealed. Collapsed nodes are not rendered, so large suites stay responsive.
|
|
14
|
+
- Hash routes that work from `file://`: `#/` (overview), `#/errors`, `#/health`, `#/group/<signature>`, `#/test/<id>`. The URL is the single source of navigation state.
|
|
15
|
+
- Light and dark themes: the system theme by default, or the `theme` option; the viewer's choice is remembered in the browser when storage is available.
|
|
16
|
+
- The test page is a placeholder for now.
|
|
17
|
+
- **Dashboard, in three views** reached from fixed sidebar entries. Widgets with nothing to show are hidden, except the verdict.
|
|
18
|
+
- **Overview** (`#/`): the verdict (failed, flaky or passed, a stacked bar of the outcomes with their counts, pass rate and duration; a run with run errors is never green), run errors when there are any, compact error categories (one bar per category, each opening that category in the Errors view, `#/errors/category/<name>`), and project cards (counts, pass rate, test time; a click reveals the project in the sidebar).
|
|
19
|
+
- **Pass rate**, one definition everywhere: passed / (total − skipped), labeled "pass rate", with a tooltip saying skipped tests are excluded.
|
|
20
|
+
- **Errors** (`#/errors`): run errors, every error group (most failed tests first, then most flaky), and the categories in detail, each expanding to its groups. Error groups are listed here only. The sidebar entry shows a warning marker and the count when the run has run errors. The former `#/run-errors` route redirects here.
|
|
21
|
+
- **Suite health** (`#/health`): one widget per dimension. Up to 8 values, one stacked bar per value (passed / failed / flaky / skipped) with the total; more values, a grid of tiles colored by worst status with its icon, problem values first, details on hover and keyboard focus, and a summary line (`38 values · 36 all passed · 2 with failures`). The exact table expands from every dimension widget. Tag hygiene: values outside the declared ones and values differing only by case (`API` / `api`) are flagged, never merged; a hint points to `values` when a dimension declares none. Then the ten slowest tests, the flaky tests with the error of their failing attempt, and the skipped tests with their reason.
|
|
22
|
+
- An error group row shows its category, its failed / flaky split, the first affected test (`+N others`) and the line that tells the error apart: the `Locator:` line or the HTTP response status rather than a generic `expect(locator).toBeVisible() failed`.
|
|
23
|
+
- **Error group pages** (`#/group/<signature>`): category, signature, failed / flaky split, a full sample message and every affected test with the attempts where the error occurred. Groups are computed in the renderer from the existing report data.
|
|
24
|
+
- **Visual identity.** New light and dark palettes, "Loupe" wordmark in the header, a "Loupe — a reporter for Playwright" footer with the version, and DM Sans (regular and semibold, latin) embedded in the HTML so the report keeps working offline. The font is under the SIL Open Font License, shipped in `licenses/DM-Sans-OFL.txt`. Contrast (4.5:1 for text, 3:1 for borders and icons) is checked by the test suite in both themes. The HTML report is about 52 kB gzip, 29 kB of which are the fonts.
|
|
25
|
+
- **Category colors from a palette.** `categories.rules[].color` now also accepts a palette name: `blue`, `cyan`, `teal`, `violet`, `magenta`, `orange`, `brown`, `gold` (see [Category palette](docs/config.md#category-palette)). An unknown name is a config error listing the valid names. `#rrggbb` colors still work; the HTML report derives light and dark shades from them that meet the contrast rules.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Error signatures change again: failures are grouped differently from 0.0.2.** The signature now uses only the stable part of Playwright messages. A strict mode violation loses its element list and count (`resolved to <n> elements`), so a test and its retry get the same signature when the page gained an item between attempts (`.first()` vs `.nth(1)`, `li:nth-child(13)` vs `14`). A locator assertion keeps only its `Error:` lines, `Locator:`, and `Expected…:` / `Received…:` lines: the timeout and screenshot pixel counts no longer count. Different locators, actions or expected values still give different signatures. The call log, stack and code frame were already left out. The JSON shape is unchanged: `schemaVersion` stays `1`. See [Error signature](docs/schema/v1.md#error-signature).
|
|
30
|
+
- **Built-in category colors changed** to the new palette, without reds or greens and with no two alike (they were near-duplicate indigos, and Assertion was red like a failure): Ambiguous locator violet `#8656b7`, Locator not found magenta `#ad4681`, Rate limited teal `#04837c`, Server error orange `#ab5803`, Network cyan `#0a7da1`, Assertion blue `#466cc8`, Timeout brown `#745f43`, Uncategorized `#6b6b6b`. New values, same shape: `schemaVersion` stays `1`. `CategoryDefinition.color` is now documented as a reference color that renderers may adapt per theme.
|
|
31
|
+
- **Categories without a configured color** take, in config rule order, the first palette color no other category of the report uses (gold, never used by a built-in category, comes last). Colors used to be assigned over every rule, so with the built-in categories present a configured category could share a color with one shown next to it. The same config with the same categories present always gives the same colors; set `color` for a color that never changes.
|
|
32
|
+
- **New built-in error category: Ambiguous locator**, evaluated first, before "Locator not found". It matches Playwright's `strict mode violation` (a locator resolving to several elements), which was "Uncategorized" or "Assertion" before. See [Error categories](docs/schema/v1.md#error-categories).
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **Run scope with `--no-deps`.** Playwright keeps `project.dependencies` in the config reporters get, so with `--project x --no-deps` the dependencies of `x` counted as running and the run could be reported `full` while a configured project was left out. They no longer count: the run is `partial` with the `project-filter` reason.
|
|
37
|
+
- **Run scope with `default: false` projects** (Playwright 1.64+). The reference for a full run is now the set of default projects: a plain run is `full` even though the non-default projects did not run, and `--project nightly` alone is `partial` because the default projects are missing. See [Default projects](docs/schema/v1.md#default-projects).
|
|
38
|
+
- **Run scope with several `--project` values.** `--project a b` selects two projects, as in Playwright; `b` was taken for a file filter and the run reported a `file-filter` reason.
|
|
39
|
+
|
|
40
|
+
## [0.0.2] - 2026-10-15
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Error signatures change: failures are grouped differently from 0.0.1.** Normalization now also replaces e-mail addresses with `<email>` and cuid-like and nanoid-like identifiers (20+ characters mixing letters and digits) with `<id>`, so the same failure for different users or records gets one signature. A signature computed by 0.0.1 for a message containing such values no longer matches. The JSON shape is unchanged: `schemaVersion` stays `1`. See [Error signature](docs/schema/v1.md#error-signature).
|
|
45
|
+
- **New built-in error categories**, evaluated after "Locator not found" and before "Network", "Assertion" and "Timeout": **Rate limited** (`Too Many Requests`, `rate limit`, a 429 in an HTTP context such as `status 429`, `HTTP 429` or `Response status: 429`) and **Server error** (a 500–599 status in the same contexts, or with its reason phrase such as `502 Bad Gateway`). Errors from `failOnStatusCode` and failed `expect(response).toBeOK()` that were "Uncategorized" or "Assertion" now land in these categories. A bare number (`429ms`, `Received: 500`) does not match. See [Error categories](docs/schema/v1.md#error-categories).
|
|
46
|
+
- The package description now lists only what exists today.
|
|
8
47
|
|
|
9
48
|
## [0.0.1] - 2026-10-13
|
|
10
49
|
|
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# playwright-loupe
|
|
2
|
-
Loupe —
|
|
2
|
+
Loupe — an HTML reporter for Playwright: a dashboard that shows what failed and why, error groups by normalized signature, tests grouped by project, file and describe block, and dimensions from tags and annotations. Native Playwright APIs only.
|
|
3
3
|
|
|
4
4
|
> **Status: work in progress — the JSON schema will change before 1.0.**
|
|
5
5
|
|
|
6
|
+

|
|
7
|
+
|
|
6
8
|
Loupe is a reporter for Playwright, not affiliated with Microsoft.
|
|
7
9
|
|
|
8
10
|
## Installation
|
|
@@ -29,10 +31,24 @@ export default defineConfig({
|
|
|
29
31
|
|
|
30
32
|
After a run, open `loupe-report/index.html`. The raw data is in `loupe-report/report.json`, with attachments in `loupe-report/data/`. Its format is documented in [`docs/schema/v1.md`](docs/schema/v1.md), with a [JSON Schema](docs/schema/report-v1.schema.json) and TypeScript types (`import type { LoupeReport } from '@qartisan/playwright-loupe'`).
|
|
31
33
|
|
|
32
|
-
Loupe requires `@playwright/test` 1.52 or later.
|
|
33
|
-
|
|
34
34
|
A run that broke outside its tests (a spec file that fails to import, a failing global setup) is reported in `run.errors`, even when no test ran.
|
|
35
35
|
|
|
36
|
+
## The report
|
|
37
|
+
|
|
38
|
+
The report is a single self-contained HTML file. It works offline, straight from `file://`, in light and dark themes.
|
|
39
|
+
|
|
40
|
+
- **Overview:** verdict with pass rate, run errors first when the run broke outside its tests, error categories, project cards.
|
|
41
|
+
- **Errors:** every error group, built from normalized error signatures, with the failed / flaky split and links to the affected tests.
|
|
42
|
+
- **Suite health:** dimensions from tags and annotations (with hints for unknown or inconsistent values), slowest, skipped and flaky tests.
|
|
43
|
+
- **Sidebar:** projects, files, `describe` blocks and tests, with status icons, failure counts and keyboard navigation.
|
|
44
|
+
|
|
45
|
+
### Coming next
|
|
46
|
+
|
|
47
|
+
- Search and filters (name, suite, status, tag), kept in the URL.
|
|
48
|
+
- Test detail: error, steps, and attachments per attempt (screenshots, video, trace), copyable rerun command and AI prompt.
|
|
49
|
+
|
|
50
|
+
History and flakiness tracking are planned for a later phase; the JSON model is already prepared for them.
|
|
51
|
+
|
|
36
52
|
## Configuration
|
|
37
53
|
|
|
38
54
|
Put a `loupe.config.ts` next to `playwright.config.ts`:
|
|
@@ -44,7 +60,7 @@ import { defineConfig } from '@qartisan/playwright-loupe';
|
|
|
44
60
|
export default defineConfig({
|
|
45
61
|
title: 'My suite',
|
|
46
62
|
categories: {
|
|
47
|
-
rules: [{ name: 'Infra', match: [/ECONNRESET/, /ECONNREFUSED/] }],
|
|
63
|
+
rules: [{ name: 'Infra', match: [/ECONNRESET/, /ECONNREFUSED/], color: 'teal' }],
|
|
48
64
|
},
|
|
49
65
|
dimensions: {
|
|
50
66
|
definitions: [
|
|
@@ -78,7 +94,7 @@ Node.js 22.12 is the minimum because ES module config files are loaded with `req
|
|
|
78
94
|
| `npm run typecheck` | Type-checks the core, tests, fixtures and renderer. |
|
|
79
95
|
| `npm test` | Builds, then runs the Vitest unit tests and the integration tests (fixture suite, and a sharded run merged with `merge-reports`). |
|
|
80
96
|
| `npm run fixtures` | Builds, then runs the fixture suite into `loupe-report/`. Its failures are intentional, see [`fixtures/README.md`](fixtures/README.md). |
|
|
81
|
-
| `npm run sample:update` |
|
|
82
|
-
| `npm run dev:renderer` | Vite dev server for the renderer, fed with `src/renderer/dev/sample.json`, with hot reload. |
|
|
97
|
+
| `npm run sample:update` | Builds, then runs the fixture suite with `fixtures/sample.config.ts` and writes its report to `src/renderer/dev/sample.json`: failures, flaky and skipped tests, a partial run and a run error. |
|
|
98
|
+
| `npm run dev:renderer` | Vite dev server for the renderer, fed with `src/renderer/dev/sample.json`, with hot reload. No Playwright run needed for UI work. |
|
|
83
99
|
|
|
84
100
|
Tests that drive a browser need Chromium: `npx playwright install chromium`.
|
package/dist/config/resolve.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { paletteReference } from '../shared/palette.js';
|
|
2
3
|
import { validateConfig } from './validate.js';
|
|
3
4
|
export const DEFAULT_OUTPUT_FOLDER = 'loupe-report';
|
|
4
5
|
export const DEFAULT_TITLE = 'Loupe report';
|
|
@@ -68,7 +69,8 @@ function resolveRules(config) {
|
|
|
68
69
|
name: rule.name,
|
|
69
70
|
match: (Array.isArray(rule.match) ? rule.match : [rule.match]).map(stateless),
|
|
70
71
|
on: rule.on ?? 'all',
|
|
71
|
-
color:
|
|
72
|
+
// A palette name becomes its reference color: the report only carries #rrggbb colors.
|
|
73
|
+
color: rule.color === undefined ? null : rule.color.startsWith('#') ? rule.color : paletteReference(rule.color),
|
|
72
74
|
}));
|
|
73
75
|
}
|
|
74
76
|
function resolveDimensions(config) {
|
package/dist/config/types.d.ts
CHANGED
|
@@ -17,7 +17,11 @@ export interface CategoryRuleConfig {
|
|
|
17
17
|
match: RegExp | RegExp[];
|
|
18
18
|
/** What the patterns are tested against. Default: `'all'`. */
|
|
19
19
|
on?: CategoryTarget;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* `#rrggbb`, or the name of a palette color: `blue`, `cyan`, `teal`,
|
|
22
|
+
* `violet`, `magenta`, `orange`, `brown`, `gold`. Default: the first
|
|
23
|
+
* palette color no other category of the report uses, in rule order.
|
|
24
|
+
*/
|
|
21
25
|
color?: string;
|
|
22
26
|
}
|
|
23
27
|
export interface CategoriesConfig {
|
|
@@ -75,6 +79,7 @@ export interface ResolvedCategoryRule {
|
|
|
75
79
|
name: string;
|
|
76
80
|
match: RegExp[];
|
|
77
81
|
on: CategoryTarget;
|
|
82
|
+
/** `#rrggbb` (palette names are resolved to their reference color); null when not configured. */
|
|
78
83
|
color: string | null;
|
|
79
84
|
}
|
|
80
85
|
export type ResolvedDimension = {
|
package/dist/config/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PALETTE_NAMES } from '../shared/palette.js';
|
|
1
2
|
import { CATEGORY_MODES, CATEGORY_TARGETS, SCREENSHOT_VIEWS, THEMES } from './types.js';
|
|
2
3
|
const COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
3
4
|
/** What a tag prefix cannot contain: `@layer:api` has the prefix `layer`. */
|
|
@@ -102,10 +103,16 @@ function boolean(ctx) {
|
|
|
102
103
|
fail(ctx, path, 'a boolean', value, errors);
|
|
103
104
|
};
|
|
104
105
|
}
|
|
106
|
+
/** A `#rrggbb` color or the name of a palette color (`teal`). */
|
|
105
107
|
function color(ctx) {
|
|
106
108
|
return (value, path, errors) => {
|
|
107
|
-
if (typeof value
|
|
108
|
-
|
|
109
|
+
if (typeof value === 'string' && (COLOR.test(value) || PALETTE_NAMES.includes(value)))
|
|
110
|
+
return;
|
|
111
|
+
const expected = `a #rrggbb color or a palette color (${PALETTE_NAMES.map((name) => `"${name}"`).join(', ')})`;
|
|
112
|
+
const suggestion = typeof value === 'string' && !value.startsWith('#') ? suggestKey(value, PALETTE_NAMES) : null;
|
|
113
|
+
fail(ctx, path, expected, value, errors);
|
|
114
|
+
if (suggestion)
|
|
115
|
+
errors[errors.length - 1] += `. Did you mean "${suggestion}"?`;
|
|
109
116
|
};
|
|
110
117
|
}
|
|
111
118
|
function regexps(ctx) {
|
package/dist/core/build.js
CHANGED
|
@@ -87,6 +87,26 @@ function toAnnotation(annotation) {
|
|
|
87
87
|
function isDefaultGrep(grep) {
|
|
88
88
|
return grep instanceof RegExp && grep.source === '.*' && grep.flags === '';
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Names of the projects with `default: false` (Playwright 1.64+). Only
|
|
92
|
+
* Playwright's internal config has the flag, under a private symbol; undefined
|
|
93
|
+
* when it is missing (older versions, merge-reports) or has another shape.
|
|
94
|
+
*/
|
|
95
|
+
function nonDefaultProjects(config) {
|
|
96
|
+
const symbol = Object.getOwnPropertySymbols(config).find((candidate) => candidate.description === 'configInternalSymbol');
|
|
97
|
+
const internal = symbol ? config[symbol] : undefined;
|
|
98
|
+
const projects = internal?.projects;
|
|
99
|
+
if (!Array.isArray(projects))
|
|
100
|
+
return undefined;
|
|
101
|
+
const names = [];
|
|
102
|
+
for (const project of projects) {
|
|
103
|
+
if (typeof project.default !== 'boolean' || typeof project.project?.name !== 'string')
|
|
104
|
+
return undefined;
|
|
105
|
+
if (!project.default)
|
|
106
|
+
names.push(project.project.name);
|
|
107
|
+
}
|
|
108
|
+
return names;
|
|
109
|
+
}
|
|
90
110
|
/** `test.only` / `describe.only` leave Playwright's internal `_only` flag on the node or an ancestor. */
|
|
91
111
|
function isFocused(test) {
|
|
92
112
|
for (let node = test; node; node = node.parent) {
|
|
@@ -263,6 +283,7 @@ export async function buildReport(config, suite, result, options) {
|
|
|
263
283
|
teardown: project.teardown,
|
|
264
284
|
})),
|
|
265
285
|
filteredProjects: extra.filteredProjects?.map((project) => project.name),
|
|
286
|
+
nonDefaultProjects: nonDefaultProjects(config),
|
|
266
287
|
runProjects: projects.map((project) => project.name),
|
|
267
288
|
only: suite.allTests().some(isFocused),
|
|
268
289
|
});
|
|
@@ -15,17 +15,16 @@ export interface ErrorText {
|
|
|
15
15
|
type Target = Exclude<CategoryTarget, 'all'>;
|
|
16
16
|
export interface CategoryRule {
|
|
17
17
|
name: string;
|
|
18
|
-
/** Null: a color is picked from the palette. */
|
|
18
|
+
/** `#rrggbb`. Null: a color is picked from the palette. */
|
|
19
19
|
color: string | null;
|
|
20
20
|
match: RegExp[];
|
|
21
21
|
targets: readonly Target[];
|
|
22
22
|
}
|
|
23
|
-
export declare const UNCATEGORIZED_COLOR
|
|
23
|
+
export declare const UNCATEGORIZED_COLOR: string;
|
|
24
24
|
export declare const BUILT_IN_RULES: readonly CategoryRule[];
|
|
25
25
|
/**
|
|
26
|
-
* Colors for categories without one
|
|
27
|
-
*
|
|
28
|
-
* are skipped, and the palette wraps around when exhausted.
|
|
26
|
+
* Colors for categories without one: the reference colors of the palette, in
|
|
27
|
+
* palette order (see `src/shared/palette.ts`).
|
|
29
28
|
*/
|
|
30
29
|
export declare const PALETTE: readonly string[];
|
|
31
30
|
export interface CategoriesOptions {
|
package/dist/core/categories.js
CHANGED
|
@@ -1,18 +1,54 @@
|
|
|
1
|
+
import { NEUTRAL_ENTRY, PALETTE_ENTRIES, paletteReference } from '../shared/palette.js';
|
|
1
2
|
const ALL_TARGETS = ['message', 'stack', 'step'];
|
|
2
|
-
export const UNCATEGORIZED_COLOR =
|
|
3
|
+
export const UNCATEGORIZED_COLOR = NEUTRAL_ENTRY.reference;
|
|
4
|
+
/**
|
|
5
|
+
* An HTTP status in context: `status 429`, `status code: 429`, `"status": 429`,
|
|
6
|
+
* `statusCode=429`, `HTTP 429`, `HTTP/1.1 429`, `Response status: 429`, or
|
|
7
|
+
* `← 429`, the response line of Playwright's fetch log.
|
|
8
|
+
*/
|
|
9
|
+
function httpStatus(code) {
|
|
10
|
+
return new RegExp(`(?:\\b(?:status(?:[ _]?code)?["']?\\s*[:=]?|HTTP(?:/\\d(?:\\.\\d)?)?:?)|\u2190)\\s*${code}(?!\\w|\\.\\d)`, 'i');
|
|
11
|
+
}
|
|
3
12
|
export const BUILT_IN_RULES = [
|
|
13
|
+
{
|
|
14
|
+
// Before "Locator not found", "Assertion" and "Timeout": a locator matching
|
|
15
|
+
// several elements fails strict mode, through an action or an expect.
|
|
16
|
+
name: 'Ambiguous locator',
|
|
17
|
+
color: paletteReference('violet'),
|
|
18
|
+
match: [/\bstrict mode violation\b/i],
|
|
19
|
+
targets: ['message', 'stack'],
|
|
20
|
+
},
|
|
4
21
|
{
|
|
5
22
|
// Before "Assertion" and "Timeout": a missing element surfaces as a failed
|
|
6
23
|
// or timed-out expect/action, and "not found" is the useful diagnosis.
|
|
7
24
|
name: 'Locator not found',
|
|
8
|
-
color: '
|
|
25
|
+
color: paletteReference('magenta'),
|
|
9
26
|
match: [/element\(s\) not found/i, /resolved to 0 elements/i],
|
|
10
27
|
targets: ['message', 'stack'],
|
|
11
28
|
},
|
|
29
|
+
{
|
|
30
|
+
// Before "Network", "Assertion" and "Timeout": the status is the diagnosis,
|
|
31
|
+
// whether it surfaces through `failOnStatusCode`, `toBeOK()` or a thrown
|
|
32
|
+
// error. A bare 429 is not enough (`429ms`, `Received: 429`): it needs an
|
|
33
|
+
// HTTP context.
|
|
34
|
+
name: 'Rate limited',
|
|
35
|
+
color: paletteReference('teal'),
|
|
36
|
+
match: [/\bToo Many Requests\b/i, /\brate[- ]?limit/i, httpStatus('429')],
|
|
37
|
+
targets: ['message', 'stack'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Server error',
|
|
41
|
+
color: paletteReference('orange'),
|
|
42
|
+
match: [
|
|
43
|
+
httpStatus('5\\d\\d'),
|
|
44
|
+
/\b5\d\d (?:Internal Server Error|Not Implemented|Bad Gateway|Service Unavailable|Gateway Time-?out|HTTP Version Not Supported)\b/i,
|
|
45
|
+
],
|
|
46
|
+
targets: ['message', 'stack'],
|
|
47
|
+
},
|
|
12
48
|
{
|
|
13
49
|
// Before "Timeout" so connection timeouts (ETIMEDOUT) count as network.
|
|
14
50
|
name: 'Network',
|
|
15
|
-
color: '
|
|
51
|
+
color: paletteReference('cyan'),
|
|
16
52
|
match: [
|
|
17
53
|
/\bnet::ERR_[A-Z_]+/,
|
|
18
54
|
/\bNS_ERROR_[A-Z_]+/,
|
|
@@ -26,34 +62,22 @@ export const BUILT_IN_RULES = [
|
|
|
26
62
|
// Before "Timeout": a retrying expect that never matched reports its own
|
|
27
63
|
// timeout but is an assertion failure.
|
|
28
64
|
name: 'Assertion',
|
|
29
|
-
color: '
|
|
65
|
+
color: paletteReference('blue'),
|
|
30
66
|
match: [/\bexpect\(/, /^\s*Expected\b.*:/m, /\bAssertionError\b/],
|
|
31
67
|
targets: ['message', 'stack'],
|
|
32
68
|
},
|
|
33
69
|
{
|
|
34
70
|
name: 'Timeout',
|
|
35
|
-
color: '
|
|
71
|
+
color: paletteReference('brown'),
|
|
36
72
|
match: [/\bTest timeout of \d+ms exceeded/i, /\bTimeout \d+ms exceeded/i, /\btimed out\b/i],
|
|
37
73
|
targets: ['message', 'stack'],
|
|
38
74
|
},
|
|
39
75
|
];
|
|
40
76
|
/**
|
|
41
|
-
* Colors for categories without one
|
|
42
|
-
*
|
|
43
|
-
* are skipped, and the palette wraps around when exhausted.
|
|
77
|
+
* Colors for categories without one: the reference colors of the palette, in
|
|
78
|
+
* palette order (see `src/shared/palette.ts`).
|
|
44
79
|
*/
|
|
45
|
-
export const PALETTE =
|
|
46
|
-
'#2563eb',
|
|
47
|
-
'#16a34a',
|
|
48
|
-
'#db2777',
|
|
49
|
-
'#ca8a04',
|
|
50
|
-
'#0d9488',
|
|
51
|
-
'#9333ea',
|
|
52
|
-
'#ea580c',
|
|
53
|
-
'#4f46e5',
|
|
54
|
-
'#65a30d',
|
|
55
|
-
'#be123c',
|
|
56
|
-
];
|
|
80
|
+
export const PALETTE = PALETTE_ENTRIES.map((entry) => entry.reference);
|
|
57
81
|
function matches(rule, error) {
|
|
58
82
|
return rule.targets.some((target) => {
|
|
59
83
|
const text = error[target];
|
|
@@ -80,26 +104,43 @@ export function createCategorizer(options) {
|
|
|
80
104
|
if (color)
|
|
81
105
|
colors.set(name, color);
|
|
82
106
|
}
|
|
83
|
-
const taken = new Set([...colors.values(), UNCATEGORIZED_COLOR].map((color) => color.toLowerCase()));
|
|
84
|
-
let next = 0;
|
|
85
|
-
for (const name of names) {
|
|
86
|
-
if (colors.has(name))
|
|
87
|
-
continue;
|
|
88
|
-
const free = PALETTE.slice(next).concat(PALETTE.slice(0, next)).find((color) => !taken.has(color));
|
|
89
|
-
const color = free ?? PALETTE[next % PALETTE.length];
|
|
90
|
-
next = (PALETTE.indexOf(color) + 1) % PALETTE.length;
|
|
91
|
-
taken.add(color);
|
|
92
|
-
colors.set(name, color);
|
|
93
|
-
}
|
|
94
107
|
const builtInNames = new Set(builtIns.map((rule) => rule.name));
|
|
108
|
+
/**
|
|
109
|
+
* Colors of the categories in `used` (names in rule order). Explicit colors
|
|
110
|
+
* (configured or built-in) never change. Each category without one takes,
|
|
111
|
+
* in config rule order, the first palette color that no category of the
|
|
112
|
+
* report uses, in palette order (gold last). Once every color is taken,
|
|
113
|
+
* colors repeat in palette order. The result only depends on the config and
|
|
114
|
+
* on which categories the report contains.
|
|
115
|
+
*/
|
|
116
|
+
function assignColors(used) {
|
|
117
|
+
const assigned = new Map();
|
|
118
|
+
const taken = new Set([UNCATEGORIZED_COLOR]);
|
|
119
|
+
for (const name of used) {
|
|
120
|
+
const color = colors.get(name);
|
|
121
|
+
if (color) {
|
|
122
|
+
assigned.set(name, color);
|
|
123
|
+
taken.add(color.toLowerCase());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let repeated = 0;
|
|
127
|
+
for (const name of used) {
|
|
128
|
+
if (assigned.has(name))
|
|
129
|
+
continue;
|
|
130
|
+
const color = PALETTE.find((candidate) => !taken.has(candidate)) ?? PALETTE[repeated++ % PALETTE.length];
|
|
131
|
+
taken.add(color);
|
|
132
|
+
assigned.set(name, color);
|
|
133
|
+
}
|
|
134
|
+
return assigned;
|
|
135
|
+
}
|
|
95
136
|
return {
|
|
96
137
|
categorize(error) {
|
|
97
138
|
return rules.find((rule) => matches(rule, error))?.name ?? options.uncategorized;
|
|
98
139
|
},
|
|
99
140
|
definitions(used) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
const usedNames = names.filter((name) => used.has(name));
|
|
142
|
+
const assigned = assignColors(usedNames);
|
|
143
|
+
const definitions = usedNames.map((name) => ({ name, color: assigned.get(name), builtIn: builtInNames.has(name) }));
|
|
103
144
|
if (used.has(options.uncategorized)) {
|
|
104
145
|
definitions.push({ name: options.uncategorized, color: UNCATEGORIZED_COLOR, builtIn: true });
|
|
105
146
|
}
|
package/dist/core/scope.d.ts
CHANGED
|
@@ -8,8 +8,13 @@ import type { RunScope } from '../schema/v1.js';
|
|
|
8
8
|
* command line (`config.argv` from 1.61, `process.argv` before).
|
|
9
9
|
* - `--project` is visible as `config.filteredProjects` from 1.64; earlier, the
|
|
10
10
|
* command line plus the projects present in the suite are used.
|
|
11
|
+
* - `--no-deps` leaves `project.dependencies` / `teardown` untouched in the
|
|
12
|
+
* config reporters get: only the command line tells that they did not run.
|
|
11
13
|
* - A `grep` / `grepInvert` in the config file is visible in the config.
|
|
12
14
|
* - `test.only` is only visible through Playwright's internal `_only` flag.
|
|
15
|
+
* - `default: false` on a project (1.64+) is only visible through Playwright's
|
|
16
|
+
* internal config; a plain run already leaves those projects out of
|
|
17
|
+
* `config.filteredProjects`.
|
|
13
18
|
*
|
|
14
19
|
* Limits are documented in docs/schema/v1.md ("Run scope").
|
|
15
20
|
*/
|
|
@@ -38,6 +43,11 @@ export interface ScopeInput {
|
|
|
38
43
|
projects: readonly ProjectRef[];
|
|
39
44
|
/** `config.filteredProjects` names when Playwright exposes them (1.64+); empty under merge-reports. */
|
|
40
45
|
filteredProjects: readonly string[] | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Names of the projects with `default: false` (Playwright 1.64+), read from
|
|
48
|
+
* Playwright's internal config; undefined when it is not available.
|
|
49
|
+
*/
|
|
50
|
+
nonDefaultProjects: readonly string[] | undefined;
|
|
41
51
|
/** Names of the projects that have a suite in this run. */
|
|
42
52
|
runProjects: readonly string[];
|
|
43
53
|
/** A `test.only` / `describe.only` is in effect. */
|
package/dist/core/scope.js
CHANGED
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
* command line (`config.argv` from 1.61, `process.argv` before).
|
|
8
8
|
* - `--project` is visible as `config.filteredProjects` from 1.64; earlier, the
|
|
9
9
|
* command line plus the projects present in the suite are used.
|
|
10
|
+
* - `--no-deps` leaves `project.dependencies` / `teardown` untouched in the
|
|
11
|
+
* config reporters get: only the command line tells that they did not run.
|
|
10
12
|
* - A `grep` / `grepInvert` in the config file is visible in the config.
|
|
11
13
|
* - `test.only` is only visible through Playwright's internal `_only` flag.
|
|
14
|
+
* - `default: false` on a project (1.64+) is only visible through Playwright's
|
|
15
|
+
* internal config; a plain run already leaves those projects out of
|
|
16
|
+
* `config.filteredProjects`.
|
|
12
17
|
*
|
|
13
18
|
* Limits are documented in docs/schema/v1.md ("Run scope").
|
|
14
19
|
*/
|
|
@@ -49,8 +54,17 @@ const VALUE_OPTIONS = new Set([
|
|
|
49
54
|
'--ui-host',
|
|
50
55
|
'--ui-port',
|
|
51
56
|
]);
|
|
57
|
+
/**
|
|
58
|
+
* Variadic options (`--project <name...>`): like commander, they take every
|
|
59
|
+
* following token up to the next one starting with '-'. `--project=a` takes one.
|
|
60
|
+
*/
|
|
61
|
+
const VARIADIC_OPTIONS = new Set(['--project']);
|
|
52
62
|
/** Options with an optional value: like commander, they take the next token unless it starts with '-'. */
|
|
53
63
|
const OPTIONAL_VALUE_OPTIONS = new Set(['--only-changed', '-u', '--update-snapshots']);
|
|
64
|
+
/** What commander takes for an option rather than a value: '-' followed by anything. */
|
|
65
|
+
function isOptionLike(arg) {
|
|
66
|
+
return arg.length > 1 && arg.startsWith('-');
|
|
67
|
+
}
|
|
54
68
|
/** Parses `playwright test` arguments from a full argv; null when the command is not `test`. */
|
|
55
69
|
export function parseTestCommand(argv) {
|
|
56
70
|
const args = argv.slice(2);
|
|
@@ -78,9 +92,13 @@ export function parseTestCommand(argv) {
|
|
|
78
92
|
command.options.add(name);
|
|
79
93
|
if (name !== arg)
|
|
80
94
|
continue;
|
|
81
|
-
if (
|
|
95
|
+
if (VARIADIC_OPTIONS.has(name)) {
|
|
96
|
+
while (args[i + 1] !== undefined && !isOptionLike(args[i + 1]))
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
else if (VALUE_OPTIONS.has(name))
|
|
82
100
|
i++;
|
|
83
|
-
else if (OPTIONAL_VALUE_OPTIONS.has(name) && args[i + 1] !== undefined && !args[i + 1]
|
|
101
|
+
else if (OPTIONAL_VALUE_OPTIONS.has(name) && args[i + 1] !== undefined && !isOptionLike(args[i + 1]))
|
|
84
102
|
i++;
|
|
85
103
|
}
|
|
86
104
|
return command;
|
|
@@ -104,12 +122,24 @@ function withDependencies(selected, projects) {
|
|
|
104
122
|
}
|
|
105
123
|
return result;
|
|
106
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The reference for a full run is the set of default projects: a project with
|
|
127
|
+
* `default: false` only runs when selected with --project, and its absence
|
|
128
|
+
* never makes a run partial.
|
|
129
|
+
*/
|
|
107
130
|
function hasProjectFilter(input, command) {
|
|
108
131
|
const all = input.projects.map((project) => project.name);
|
|
109
132
|
// merge-reports exposes an empty list: a real run always selects at least one project.
|
|
110
133
|
if (input.filteredProjects && input.filteredProjects.length > 0) {
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
// Without the internal flags, a plain `playwright test` already ran every default project.
|
|
135
|
+
if (!input.nonDefaultProjects && command && !command.options.has('--project'))
|
|
136
|
+
return false;
|
|
137
|
+
// With --no-deps, dependencies and teardowns of the selected projects do not run.
|
|
138
|
+
const running = command?.options.has('--no-deps')
|
|
139
|
+
? new Set(input.filteredProjects)
|
|
140
|
+
: withDependencies(input.filteredProjects, input.projects);
|
|
141
|
+
const nonDefault = new Set(input.nonDefaultProjects ?? []);
|
|
142
|
+
return all.some((name) => !nonDefault.has(name) && !running.has(name));
|
|
113
143
|
}
|
|
114
144
|
if (!command?.options.has('--project'))
|
|
115
145
|
return false;
|
package/dist/core/signature.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ export interface NormalizeOptions {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Normalizes an error message so the same failure produces the same text
|
|
10
|
-
* across tests and runs: ANSI codes and the call log are removed,
|
|
11
|
-
*
|
|
10
|
+
* across tests, retries and runs: ANSI codes and the call log are removed,
|
|
11
|
+
* only the stable part of Playwright messages is kept, volatile values are
|
|
12
|
+
* replaced with placeholders and whitespace is collapsed.
|
|
12
13
|
*/
|
|
13
14
|
export declare function normalizeErrorMessage(message: string, options?: NormalizeOptions): string;
|
|
14
15
|
/** First 12 hex chars of sha256 of the normalized message. Independent of the test. */
|
package/dist/core/signature.js
CHANGED
|
@@ -6,6 +6,34 @@ export function stripAnsi(text) {
|
|
|
6
6
|
}
|
|
7
7
|
/** Playwright appends the action log after a line starting with "Call log:". */
|
|
8
8
|
const CALL_LOG = /^[ \t]*Call log:/m;
|
|
9
|
+
/**
|
|
10
|
+
* A strict mode violation lists the matching elements, one per line:
|
|
11
|
+
* ` 1) <li>Order 13</li> aka locator('li').first()`. Their positions and
|
|
12
|
+
* content change when the page gains an item, so the list is dropped and the
|
|
13
|
+
* count becomes `<n>`. Playwright ends a long list with a `...` line.
|
|
14
|
+
*/
|
|
15
|
+
const STRICT_MODE = /\bstrict mode violation\b/;
|
|
16
|
+
const STRICT_MODE_COUNT = /(\bstrict mode violation:.*?\bresolved to )\d+( elements?\b)/g;
|
|
17
|
+
const ENUMERATED_ELEMENT = /^[ \t]*(?:\d+\) <.*|\.\.\.)[ \t]*$/;
|
|
18
|
+
/**
|
|
19
|
+
* Lines of a locator assertion that identify the failure: the `Error:` lines
|
|
20
|
+
* (the matcher line, and `Error: element(s) not found` or a strict mode
|
|
21
|
+
* violation under it), a matcher line without `Error:` (after a custom
|
|
22
|
+
* message), `Locator:`, and `Expected…:` / `Received…:`. The rest (timeout,
|
|
23
|
+
* screenshot pixel counts, diff bodies, element lists) varies between runs.
|
|
24
|
+
*/
|
|
25
|
+
const LOCATOR_LINE = /^[ \t]*Locator:/m;
|
|
26
|
+
const STABLE_LINE = /^[ \t]*(?:Error:|Locator:|(?:Expected|Received)\b[^:\n]*:|expect(?:\.\w+)*\()/;
|
|
27
|
+
/** The stable part of a Playwright message (the call log is already cut). See "Error signature" in docs/schema/v1.md. */
|
|
28
|
+
function stablePart(text) {
|
|
29
|
+
let lines = text.split('\n');
|
|
30
|
+
if (STRICT_MODE.test(text)) {
|
|
31
|
+
lines = lines.filter((line) => !ENUMERATED_ELEMENT.test(line)).map((line) => line.replace(STRICT_MODE_COUNT, '$1<n>$2'));
|
|
32
|
+
}
|
|
33
|
+
if (LOCATOR_LINE.test(text))
|
|
34
|
+
lines = lines.filter((line) => STABLE_LINE.test(line));
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
9
37
|
/**
|
|
10
38
|
* `merge-reports` prefixes the run errors of a blob whose config has a `tag`
|
|
11
39
|
* with `(<tags>) `, tags joined by spaces. Config tags always start with `@`.
|
|
@@ -16,13 +44,17 @@ export function stripMergeTagPrefix(text) {
|
|
|
16
44
|
return text.replace(MERGE_TAG_PREFIX, '');
|
|
17
45
|
}
|
|
18
46
|
/**
|
|
19
|
-
* Ordered replacements. The order matters: URLs before
|
|
20
|
-
*
|
|
21
|
-
*
|
|
47
|
+
* Ordered replacements. The order matters: URLs before e-mails (a URL may
|
|
48
|
+
* contain `user@host`) and paths (a URL contains slashes), e-mails and paths
|
|
49
|
+
* before identifiers, identifiers before durations, hex and numbers (so each
|
|
50
|
+
* is replaced as a whole), hex before numbers. See "Error signature" in
|
|
51
|
+
* docs/schema/v1.md.
|
|
22
52
|
*/
|
|
23
53
|
const REPLACEMENTS = [
|
|
24
54
|
[/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi, '<uuid>'],
|
|
25
55
|
[/\b[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>()[\]{}]*[^\s'"`<>()[\]{}.,;:!?]/gi, '<url>'],
|
|
56
|
+
// Local part, then a domain with at least one dot and an alphabetic TLD.
|
|
57
|
+
[/(?<![\w.%+-])[\w.%+-]+@[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}(?![\w-])/gi, '<email>'],
|
|
26
58
|
// ISO 8601 date with optional time and zone.
|
|
27
59
|
[/\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:[.,]\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?(?![\w:])/g, '<date>'],
|
|
28
60
|
// Time of day.
|
|
@@ -32,6 +64,13 @@ const REPLACEMENTS = [
|
|
|
32
64
|
// Absolute POSIX paths with at least two segments, and Windows drive paths.
|
|
33
65
|
[/(?<![\w.~:/\\-])\/(?:[\w.@+~-]+\/)+[\w.@+~-]+\/?/g, '<path>'],
|
|
34
66
|
[/\b[A-Za-z]:[\\/](?:[^\\/\s'"`:*?<>|]+[\\/])*[^\\/\s'"`:*?<>|]+/g, '<path>'],
|
|
67
|
+
// nanoid-like: 20+ chars of [A-Za-z0-9_-] with a lowercase letter, an
|
|
68
|
+
// uppercase letter and a digit. Mixed case keeps kebab-case and snake_case
|
|
69
|
+
// names out; a digit keeps camelCase words out.
|
|
70
|
+
[/(?<![\w-])(?=[\w-]*[a-z])(?=[\w-]*[A-Z])(?=[\w-]*\d)[\w-]{20,}(?![\w-])/g, '<id>'],
|
|
71
|
+
// cuid-like: 20+ lowercase letters and digits, with a digit and a letter
|
|
72
|
+
// beyond a-f (pure hex strings stay <hex>).
|
|
73
|
+
[/(?<![0-9A-Za-z])(?=[a-z0-9]*\d)(?=[a-z0-9]*[g-z])[a-z0-9]{20,}(?![0-9A-Za-z])/g, '<id>'],
|
|
35
74
|
[/\b\d+(?:\.\d+)?\s?(?:ms|milliseconds?|s|secs?|seconds?|m|mins?|minutes?|h|hours?)\b/gi, '<duration>'],
|
|
36
75
|
// 8+ hex chars with at least one letter (pure digit runs fall through to <n>).
|
|
37
76
|
[/(?<![0-9A-Za-z])(?=[0-9a-f]*[a-f])[0-9a-f]{8,}(?![0-9A-Za-z])/gi, '<hex>'],
|
|
@@ -39,8 +78,9 @@ const REPLACEMENTS = [
|
|
|
39
78
|
];
|
|
40
79
|
/**
|
|
41
80
|
* Normalizes an error message so the same failure produces the same text
|
|
42
|
-
* across tests and runs: ANSI codes and the call log are removed,
|
|
43
|
-
*
|
|
81
|
+
* across tests, retries and runs: ANSI codes and the call log are removed,
|
|
82
|
+
* only the stable part of Playwright messages is kept, volatile values are
|
|
83
|
+
* replaced with placeholders and whitespace is collapsed.
|
|
44
84
|
*/
|
|
45
85
|
export function normalizeErrorMessage(message, options = {}) {
|
|
46
86
|
let text = stripAnsi(message);
|
|
@@ -49,6 +89,7 @@ export function normalizeErrorMessage(message, options = {}) {
|
|
|
49
89
|
const callLog = CALL_LOG.exec(text);
|
|
50
90
|
if (callLog)
|
|
51
91
|
text = text.slice(0, callLog.index);
|
|
92
|
+
text = stablePart(text);
|
|
52
93
|
for (const [pattern, placeholder] of REPLACEMENTS) {
|
|
53
94
|
text = text.replace(pattern, placeholder);
|
|
54
95
|
}
|