@qartisan/playwright-loupe 0.1.0 → 0.1.2
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 +33 -0
- package/README.md +19 -3
- package/dist/config/resolve.d.ts +12 -1
- package/dist/config/resolve.js +34 -2
- package/dist/config/types.d.ts +22 -0
- package/dist/config/types.js +1 -0
- package/dist/config/validate.d.ts +2 -0
- package/dist/config/validate.js +35 -1
- package/dist/core/build.d.ts +4 -1
- package/dist/core/build.js +28 -18
- package/dist/core/categories.js +4 -13
- package/dist/core/http.d.ts +21 -0
- package/dist/core/http.js +64 -0
- package/dist/core/redact.d.ts +101 -0
- package/dist/core/redact.js +254 -0
- package/dist/core/reporter.d.ts +2 -0
- package/dist/core/reporter.js +6 -0
- package/dist/core/signature.js +9 -1
- package/dist/renderer/index.html +9 -9
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ 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
|
+
## [0.1.2] - 2026-10-21
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
|
|
11
|
+
- **Secrets are redacted from the report.** Reports are shared (CI artifacts, links), but they could contain secrets in clear: the value typed by `fill` into a password field is part of Playwright's step title (`Fill "<value>"`), and the call log of a failing API call prints the request and response headers, session cookies and bearer tokens included. Loupe now replaces them with `[redacted]` in `report.json`, and therefore in `index.html`, before writing them and before computing error signatures and categories. No change to your tests.
|
|
12
|
+
- **Input steps** (on by default): the value of `fill`, `type`, `pressSequentially`, `keyboard.type` and `keyboard.insertText` steps whose target has a secret-looking name, read from the step subtitle (Playwright 1.63+) or, on every version, from the source line of the call. That fallback also covers `keyboard.*` and neutral locators when the code names the value (`fill(password)`). The same value is redacted where Playwright repeats it in that attempt's errors (`fill("[redacted]")` in call logs, assertion lines).
|
|
13
|
+
- **HTTP headers** (on by default): `cookie`, `set-cookie`, `authorization`, `proxy-authorization` and any header with a secret-looking name, in call logs (`- cookie: [redacted]`).
|
|
14
|
+
- **URL parameters** (on by default): query and fragment parameters with a secret-looking name, wherever URLs appear (navigation steps, call logs, error messages, stdout), keeping the name: `?token=[redacted]`, `#access_token=[redacted]`.
|
|
15
|
+
- **Environment values** (opt-in): the values of the variables listed in the new `redact.env` option, or added through `LOUPE_REDACT_ENV`, everywhere in the report. Values shorter than 6 characters and unset variables are skipped with a warning naming the variable.
|
|
16
|
+
- New `redact` option (`patterns`, `mode`, `env`), validated at startup; one list of secret-looking names for input steps, headers and URL parameters, with whole-word matching (`auth` matches `x-auth-token`, not `Author`). The environment can add names, never remove them.
|
|
17
|
+
- **Not redacted**: attachments (traces, screenshots and videos contain typed values: treat a report with traces as private), snippets (source code from your repository), attachment file names. See [Secret redaction](docs/config.md#secret-redaction). Reports written by earlier versions are not rewritten.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Error signatures keep HTTP status codes.** A status code (100–599) in an HTTP context is no longer replaced with `<n>`, so `Unable to delete the member with API with error 401` and `… with error 500` are two error groups instead of one. Other numbers are normalized as before. This changes how signatures are computed, not the JSON shape (`schemaVersion` stays 1): signatures of messages with an HTTP status change, so groups do not match across this upgrade for those errors. See [Error signature](docs/schema/v1.md#error-signature).
|
|
22
|
+
- **Wider HTTP context, shared by categories and signatures.** A status code now also counts as an HTTP status after `error` (`with error 503`, `Error: 503`) and before its standard reason phrase (`401 Unauthorized`, previously only for 5xx). Categories change accordingly: an error such as `… with error 503` is now **Server error** and `… error 429` **Rate limited**, where they could be Uncategorized (or Assertion, Timeout) before. See [HTTP context](docs/schema/v1.md#http-context).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Docs: `Step.subtitle` is provided from Playwright **1.63**, not 1.61, and before that the target of an action is not always in the title: Playwright 1.53 to 1.62 leave it out (`Click`, `Fill "<value>"`). Only 1.52 writes it there (`locator.getByRole('button').click`). The report itself is unchanged.
|
|
27
|
+
|
|
28
|
+
## [0.1.1] - 2026-10-18
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Tags and dimension values in the Tests view.** Each row shows, under the test title, what the test covers as small neutral chips: its dimension values first (dimensions in config order, annotation-based ones included, then values in the test's order; the chip shows the value, `api`, and its tooltip and accessible name the dimension, "Layer: api"), then its free tags with their `@`, sorted. As in the filters, a tag a dimension takes appears only as that dimension's value. Each chip is a link that adds its value to the filters, keeping the others and the sort; a chip whose value is already filtered is shown active (inverted, with a ×) and removes it ("Remove filter Layer: api"). Both add a history entry, so Back undoes them. A row shows up to four chips, then a "+N" button (`aria-expanded`) that shows the rest in place; that state is not in the URL. Active chips are never hidden behind "+N": they are always shown, the other slots filled in the normal order, and "+N" counts only the chips actually hidden. The chips of each test are computed once per report. No chip line for a test without tags or dimensions. Other annotations stay in the test detail.
|
|
33
|
+
- `npm run perf:sample`: writes 5,000-test reports from the renderer sample (`loupe-perf/`, gitignored) and, with `-- --measure`, times the Tests view (opening, filtering, sorting, searching) in Chromium, optionally against another renderer build (`--renderer`), so performance measurements are reproducible.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Search covers tags and dimension values.** The search (`q`, in the sidebar) now also matches every raw tag of a test, those a dimension takes included (`smoke`, `@smoke` and `layer:api` all match), and its dimension values, annotation-based ones included (`BUG-42`). Same rules as before: case- and accent-insensitive, every word must match, as substrings.
|
|
38
|
+
- **Trace command: absolute path over `file://`.** When the report is opened from disk, "Copy trace command" copies the absolute path of the trace, computed from the report's own location (`npx playwright show-trace '/home/me/app/loupe-report/data/…/trace.zip'`), so the command works from any directory. Its help now says to run it from your project root, so it uses the project's own Playwright: the same rule as the rerun commands. The path is decoded (spaces, accented characters) before POSIX quoting; a Windows drive path loses the leading `/` of its URL form (commands stay POSIX-only). Over `http(s)` and for a `file://` URL with a host (a network share), the disk path is unknown: the command keeps the path relative to the report folder and the former help. The "Download trace" link is unchanged.
|
|
39
|
+
|
|
7
40
|
## [0.1.0] - 2026-10-18
|
|
8
41
|
|
|
9
42
|
### Added
|
package/README.md
CHANGED
|
@@ -45,14 +45,14 @@ The report is a single self-contained HTML file. It works offline, straight from
|
|
|
45
45
|
- **Screenshot comparisons**: a failed `toHaveScreenshot` / `toMatchSnapshot` gives one card per image name, before the other screenshots, with four modes: Diff, Side by side (expected | actual, at one scale), Slider (actual over expected, a divider to drag or move with the arrow keys) and Overlay (actual over expected, its opacity to set). Only the modes the images allow are offered; Diff is the default when there is a diff image. The chosen mode applies to every comparison until the report is closed. Each image's size is shown, with a warning when expected and actual differ: slider and overlay align them top-left. "Open in viewer" shows the same modes in the viewer below.
|
|
46
46
|
- **Screenshots** as thumbnails. A click (or Enter / Space) opens them in a viewer, after the comparisons: fitted to the window, or at actual size by clicking the image; previous / next with the buttons or `←` / `→`; "Open original" in a new tab. Escape, the close button or a click outside closes it, back on the same thumbnail. A missing file shows its name and path.
|
|
47
47
|
- **Videos** play inline. A missing or unplayable file shows its path instead of an empty player.
|
|
48
|
-
- **Traces**: "Copy trace command" copies `npx playwright show-trace <path>`, to run from the report folder (the one containing `index.html`)
|
|
48
|
+
- **Traces**: "Copy trace command" copies `npx playwright show-trace <path>`, to run from your project root in a POSIX shell, like the rerun commands. When the report is opened from disk (`file://`), the path is the absolute path of the trace, so the command works from any directory; when the report is served over HTTP, the disk path is unknown and the path is relative to the report folder (the one containing `index.html`): run it from there. The trace viewer needs an HTTP server, so it cannot open from `file://`; the command uses the project's own Playwright, which must be at least the version that ran the tests (an older trace viewer shows an empty screen on a newer trace). The card says which version, and has a "Download trace" link.
|
|
49
49
|
- Other files are listed as links; small text bodies inline, collapsed.
|
|
50
50
|
- **Copy buttons:** rerun commands (this test, the failed tests of its file, the failed tests of the current list) and an AI prompt describing the failure, in English. Both are only copied to the clipboard; nothing is sent anywhere. Projects failing on the same lines share one command (`--project=a --project=b`); otherwise each project gets its own, joined with `&&`.
|
|
51
51
|
- The commands target **POSIX shells** (bash, zsh; not `cmd.exe`) and run from the Playwright root directory: paths in the report are relative to it.
|
|
52
52
|
- A `file:line` argument targets the line where the test is declared and reruns every test declared on that line. A command is only valid while the file has not changed since the run.
|
|
53
53
|
- **Suite health:** dimensions from tags and annotations (with hints for unknown or inconsistent values), slowest, skipped and flaky tests.
|
|
54
|
-
- **Tests:** every test in one sortable list (by default in declaration order, as in the sidebar: project, file, line; the Status header sorts failed, flaky, passed, skipped), filtered by status, error category, project, tag and dimension, with the count each filter would give. Header counts, error categories and dimension values open it pre-filtered. Large lists show their first 500 rows, with "Show 500 more" and "Show all".
|
|
55
|
-
- **Sidebar:** search (`/` to focus), projects, folders, files, `describe` blocks and tests, with status icons, failure counts and keyboard navigation. The folder shared by every file is not repeated.
|
|
54
|
+
- **Tests:** every test in one sortable list (by default in declaration order, as in the sidebar: project, file, line; the Status header sorts failed, flaky, passed, skipped), filtered by status, error category, project, tag and dimension, with the count each filter would give. Header counts, error categories and dimension values open it pre-filtered. Large lists show their first 500 rows, with "Show 500 more" and "Show all". Under each title, chips show what the test covers: its dimension values first (dimensions in config order, the value alone, `api`, with the dimension in the tooltip), then its other tags (`@smoke`), sorted. A tag a dimension takes appears only as that dimension's value. A chip adds its value to the filters, keeping the others and the sort; an active chip (with a ×) removes it, and Back undoes either. Up to four chips per row, active chips always among them; "+N" shows the rest.
|
|
55
|
+
- **Sidebar:** search (`/` to focus; every word must match, ignoring case and accents, the title path, file, project, tags or dimension values: `smoke`, `api`, `layer:api`), projects, folders, files, `describe` blocks and tests, with status icons, failure counts and keyboard navigation. The folder shared by every file is not repeated.
|
|
56
56
|
- **Filters in the URL:** search, filters and sort live in the link, so a filtered view can be shared or bookmarked, even from `file://`. A project without a `name` (Playwright's default project) cannot be selected in the project filter: an empty name cannot be written in the URL. Give your projects a name to filter by them. Each tag is filterable in one place only: a tag a dimension is built from (`@layer:ui` with a `layer` dimension, configured or automatic) is in that dimension's filter (Layer: `ui`), not in the tag filter, so the two can never be combined into an empty result. Unprefixed tags (`@smoke`) and prefixed tags no dimension uses stay in the tag filter.
|
|
57
57
|
|
|
58
58
|
### Keyboard shortcuts
|
|
@@ -103,6 +103,21 @@ Options can also be passed inline (`['@qartisan/playwright-loupe', { title: 'Nig
|
|
|
103
103
|
|
|
104
104
|
Every option, the lookup rules and the environment variables are documented in [`docs/config.md`](docs/config.md).
|
|
105
105
|
|
|
106
|
+
### Security
|
|
107
|
+
|
|
108
|
+
Reports are meant to be shared, so Loupe removes secrets from `report.json` and `index.html` before writing them, replacing each with `[redacted]`:
|
|
109
|
+
|
|
110
|
+
- **typed values** of `fill`, `type`, `pressSequentially` and `keyboard.*` steps on secret-looking fields (a password field, `getByLabel('Password')`, or a source line that names the value, `fill(password)`), and wherever Playwright repeats them in that attempt's errors;
|
|
111
|
+
- **HTTP header values** printed in call logs (`cookie`, `set-cookie`, `authorization`, and any header with a secret-looking name);
|
|
112
|
+
- **URL parameters** with a secret-looking name, wherever URLs appear (navigation steps, call logs, error messages): `?token=[redacted]`, `&access_token=[redacted]`;
|
|
113
|
+
- the **values of the environment variables you list** in `redact.env` (names only, never values), everywhere in the report. It is also the fallback for what the names above miss: a secret in a URL parameter with a neutral name (`?sig=…`), or printed by your code.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
redact: { env: ['E2E_PASSWORD', 'API_TOKEN'] },
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Attachments are not altered**: traces, screenshots and videos contain typed values and visible secrets, so a report with traces must be treated as private. Snippets (source excerpts around errors) are left as they are: they come from your repository. See [Secret redaction](docs/config.md#secret-redaction).
|
|
120
|
+
|
|
106
121
|
## Compatibility
|
|
107
122
|
|
|
108
123
|
| | Supported | Tested in CI |
|
|
@@ -123,6 +138,7 @@ Node.js 22.12 is the minimum because ES module config files are loaded with `req
|
|
|
123
138
|
| `npm test` | Builds, then runs the Vitest unit tests and the integration tests (fixture suite, and a sharded run merged with `merge-reports`). |
|
|
124
139
|
| `npm run fixtures` | Builds, then runs the fixture suite into `loupe-report/`. Its failures are intentional, see [`fixtures/README.md`](fixtures/README.md). |
|
|
125
140
|
| `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. Its attachment files go to `src/renderer/dev/data/` (gitignored). |
|
|
141
|
+
| `npm run perf:sample` | Builds, then writes two 5,000-test reports to `loupe-perf/` (gitignored), from the renderer sample: `sample` (its own tags) and `heavy` (7 to 8 chips per row). Add `-- --measure` to time opening, filtering, sorting and searching the Tests view in Chromium (median of 7 runs), and `--renderer <index.html>` to time another renderer build, for a before / after comparison. |
|
|
126
142
|
| `npm run dev:renderer` | Vite dev server for the renderer, fed with `src/renderer/dev/sample.json`, with hot reload; it serves the sample's attachments at `data/`. No Playwright run needed for UI work. |
|
|
127
143
|
|
|
128
144
|
Tests that drive a browser need Chromium: `npx playwright install chromium`.
|
package/dist/config/resolve.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ type Env = Readonly<Record<string, string | undefined>>;
|
|
|
3
3
|
export declare const DEFAULT_OUTPUT_FOLDER = "loupe-report";
|
|
4
4
|
export declare const DEFAULT_TITLE = "Loupe report";
|
|
5
5
|
export declare const DEFAULT_UNCATEGORIZED = "Uncategorized";
|
|
6
|
-
/** Environment variables mapped onto config options. `LOUPE_CONFIG` and `
|
|
6
|
+
/** Environment variables mapped onto config options. `LOUPE_CONFIG`, `LOUPE_RUN_SCOPE` and `LOUPE_REDACT_ENV` are handled apart. */
|
|
7
7
|
export declare const ENV_OPTIONS: {
|
|
8
8
|
readonly LOUPE_OUTPUT_FOLDER: 'outputFolder';
|
|
9
9
|
readonly LOUPE_TITLE: 'title';
|
|
@@ -22,6 +22,17 @@ export declare class LoupeConfigError extends Error {
|
|
|
22
22
|
export declare function stripPlaywrightKeys(options: Record<string, unknown>): Record<string, unknown>;
|
|
23
23
|
/** The environment layer: options set through `LOUPE_*` variables. Empty variables are ignored. */
|
|
24
24
|
export declare function envLayer(env: Env): LoupeConfig;
|
|
25
|
+
/** Comma-separated variable names added to `redact.env`. */
|
|
26
|
+
export declare const REDACT_ENV = "LOUPE_REDACT_ENV";
|
|
27
|
+
/**
|
|
28
|
+
* Names from `LOUPE_REDACT_ENV`, with a problem for each invalid one (its text
|
|
29
|
+
* is not echoed: it may be a value pasted by mistake). The environment can only
|
|
30
|
+
* add names to `redact.env`, never remove or disable redaction.
|
|
31
|
+
*/
|
|
32
|
+
export declare function redactEnvNames(env: Env): {
|
|
33
|
+
names: string[];
|
|
34
|
+
problems: string[];
|
|
35
|
+
};
|
|
25
36
|
/**
|
|
26
37
|
* Merges `layer` over `base`: nested objects (`categories`, `dimensions`,
|
|
27
38
|
* `screenshots`, `aiPrompt`) merge key by key; arrays and other values replace. An
|
package/dist/config/resolve.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { paletteReference } from '../shared/palette.js';
|
|
3
|
-
import { validateConfig } from './validate.js';
|
|
3
|
+
import { ENV_NAME, validateConfig } from './validate.js';
|
|
4
4
|
export const DEFAULT_OUTPUT_FOLDER = 'loupe-report';
|
|
5
5
|
export const DEFAULT_TITLE = 'Loupe report';
|
|
6
6
|
export const DEFAULT_UNCATEGORIZED = 'Uncategorized';
|
|
7
|
-
/** Environment variables mapped onto config options. `LOUPE_CONFIG` and `
|
|
7
|
+
/** Environment variables mapped onto config options. `LOUPE_CONFIG`, `LOUPE_RUN_SCOPE` and `LOUPE_REDACT_ENV` are handled apart. */
|
|
8
8
|
export const ENV_OPTIONS = {
|
|
9
9
|
LOUPE_OUTPUT_FOLDER: 'outputFolder',
|
|
10
10
|
LOUPE_TITLE: 'title',
|
|
@@ -42,6 +42,30 @@ export function envLayer(env) {
|
|
|
42
42
|
}
|
|
43
43
|
return layer;
|
|
44
44
|
}
|
|
45
|
+
/** Comma-separated variable names added to `redact.env`. */
|
|
46
|
+
export const REDACT_ENV = 'LOUPE_REDACT_ENV';
|
|
47
|
+
/**
|
|
48
|
+
* Names from `LOUPE_REDACT_ENV`, with a problem for each invalid one (its text
|
|
49
|
+
* is not echoed: it may be a value pasted by mistake). The environment can only
|
|
50
|
+
* add names to `redact.env`, never remove or disable redaction.
|
|
51
|
+
*/
|
|
52
|
+
export function redactEnvNames(env) {
|
|
53
|
+
const raw = envValue(env, REDACT_ENV);
|
|
54
|
+
if (raw === undefined)
|
|
55
|
+
return { names: [], problems: [] };
|
|
56
|
+
const names = [];
|
|
57
|
+
const problems = [];
|
|
58
|
+
raw.split(',').forEach((part, index) => {
|
|
59
|
+
const name = part.trim();
|
|
60
|
+
if (name === '')
|
|
61
|
+
return;
|
|
62
|
+
if (ENV_NAME.test(name))
|
|
63
|
+
names.push(name);
|
|
64
|
+
else
|
|
65
|
+
problems.push(`${REDACT_ENV}: item ${index + 1} must be an environment variable name (letters, digits, _), got a string that is not a variable name (value not shown)`);
|
|
66
|
+
});
|
|
67
|
+
return { names, problems };
|
|
68
|
+
}
|
|
45
69
|
function isObject(value) {
|
|
46
70
|
return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof RegExp);
|
|
47
71
|
}
|
|
@@ -86,9 +110,11 @@ function resolveDimensions(config) {
|
|
|
86
110
|
export function resolveConfig(layers) {
|
|
87
111
|
const fileSource = layers.file ? path.basename(layers.file.path) : '';
|
|
88
112
|
const env = envLayer(layers.env);
|
|
113
|
+
const redactEnv = redactEnvNames(layers.env);
|
|
89
114
|
const problems = [
|
|
90
115
|
...(layers.file ? validateConfig(layers.file.config, fileSource) : []),
|
|
91
116
|
...validateConfig(layers.inline, 'reporter options', { allowConfigFile: true }),
|
|
117
|
+
...redactEnv.problems,
|
|
92
118
|
];
|
|
93
119
|
if (problems.length > 0)
|
|
94
120
|
throw new LoupeConfigError(problems);
|
|
@@ -115,5 +141,11 @@ export function resolveConfig(layers) {
|
|
|
115
141
|
screenshots: { defaultView: merged.screenshots?.defaultView ?? 'side-by-side' },
|
|
116
142
|
theme: merged.theme ?? 'system',
|
|
117
143
|
aiPrompt: { includeStack: merged.aiPrompt?.includeStack ?? true },
|
|
144
|
+
redact: {
|
|
145
|
+
mode: merged.redact?.mode ?? 'extend',
|
|
146
|
+
patterns: (merged.redact?.patterns ?? []).map((pattern) => (pattern instanceof RegExp ? stateless(pattern) : pattern)),
|
|
147
|
+
// LOUPE_REDACT_ENV adds to the configured names: a higher layer never removes one.
|
|
148
|
+
env: [...new Set([...(merged.redact?.env ?? []), ...redactEnv.names])],
|
|
149
|
+
},
|
|
118
150
|
};
|
|
119
151
|
}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const CATEGORY_MODES: readonly ['extend', 'replace'];
|
|
6
6
|
export type CategoryMode = (typeof CATEGORY_MODES)[number];
|
|
7
|
+
export declare const REDACT_MODES: readonly ['extend', 'replace'];
|
|
8
|
+
export type RedactMode = (typeof REDACT_MODES)[number];
|
|
7
9
|
export declare const CATEGORY_TARGETS: readonly ['message', 'stack', 'step', 'all'];
|
|
8
10
|
export type CategoryTarget = (typeof CATEGORY_TARGETS)[number];
|
|
9
11
|
export declare const SCREENSHOT_VIEWS: readonly ['side-by-side', 'slider', 'overlay', 'diff'];
|
|
@@ -53,6 +55,18 @@ export interface DimensionsConfig {
|
|
|
53
55
|
auto?: boolean;
|
|
54
56
|
definitions?: DimensionConfig[];
|
|
55
57
|
}
|
|
58
|
+
export interface RedactConfig {
|
|
59
|
+
/**
|
|
60
|
+
* Secret-looking names, for input step targets and source lines, and for
|
|
61
|
+
* HTTP header names. A string matches whole words (`auth` matches
|
|
62
|
+
* `x-auth-token`, not `Author`); a RegExp is tested as is.
|
|
63
|
+
*/
|
|
64
|
+
patterns?: Array<string | RegExp>;
|
|
65
|
+
/** `extend` (default): `patterns` add to the default names. `replace`: only `patterns`; `[]` turns name-based redaction off. */
|
|
66
|
+
mode?: RedactMode;
|
|
67
|
+
/** Names of environment variables whose values are redacted everywhere in the report. Names only, never values. */
|
|
68
|
+
env?: string[];
|
|
69
|
+
}
|
|
56
70
|
export interface LoupeConfig {
|
|
57
71
|
/** Folder the report is written to, relative to the Playwright config file. Default: `loupe-report`. */
|
|
58
72
|
outputFolder?: string;
|
|
@@ -72,6 +86,8 @@ export interface LoupeConfig {
|
|
|
72
86
|
/** Include each error's stack trace in the copyable AI prompt. Default: `true`. */
|
|
73
87
|
includeStack?: boolean;
|
|
74
88
|
};
|
|
89
|
+
/** Secret redaction in report.json and index.html. `LOUPE_REDACT_ENV` adds variable names. */
|
|
90
|
+
redact?: RedactConfig;
|
|
75
91
|
}
|
|
76
92
|
/** Inline reporter options: a config, plus the path of the config file. */
|
|
77
93
|
export interface LoupeReporterOptions extends LoupeConfig {
|
|
@@ -121,4 +137,10 @@ export interface ResolvedConfig {
|
|
|
121
137
|
aiPrompt: {
|
|
122
138
|
includeStack: boolean;
|
|
123
139
|
};
|
|
140
|
+
/** RegExps without the `g` and `y` flags; `env` merged from every layer, without duplicates. */
|
|
141
|
+
redact: {
|
|
142
|
+
mode: RedactMode;
|
|
143
|
+
patterns: Array<string | RegExp>;
|
|
144
|
+
env: string[];
|
|
145
|
+
};
|
|
124
146
|
}
|
package/dist/config/types.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* reporter options. Every option is optional; see docs/config.md.
|
|
4
4
|
*/
|
|
5
5
|
export const CATEGORY_MODES = ['extend', 'replace'];
|
|
6
|
+
export const REDACT_MODES = ['extend', 'replace'];
|
|
6
7
|
export const CATEGORY_TARGETS = ['message', 'stack', 'step', 'all'];
|
|
7
8
|
export const SCREENSHOT_VIEWS = ['side-by-side', 'slider', 'overlay', 'diff'];
|
|
8
9
|
export const THEMES = ['system', 'light', 'dark'];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** An environment variable name, as `redact.env` and `LOUPE_REDACT_ENV` take them. */
|
|
2
|
+
export declare const ENV_NAME: RegExp;
|
|
1
3
|
/** Optimal string alignment distance: Levenshtein plus adjacent transpositions (`titel` → `title` is 1). */
|
|
2
4
|
export declare function editDistance(a: string, b: string): number;
|
|
3
5
|
/** The closest known key, when it is close enough to be a typo. */
|
package/dist/config/validate.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { PALETTE_NAMES } from '../shared/palette.js';
|
|
2
|
-
import { CATEGORY_MODES, CATEGORY_TARGETS, SCREENSHOT_VIEWS, THEMES } from './types.js';
|
|
2
|
+
import { CATEGORY_MODES, CATEGORY_TARGETS, REDACT_MODES, SCREENSHOT_VIEWS, THEMES } from './types.js';
|
|
3
3
|
const COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
4
|
+
/** An environment variable name, as `redact.env` and `LOUPE_REDACT_ENV` take them. */
|
|
5
|
+
export const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4
6
|
/** What a tag prefix cannot contain: `@layer:api` has the prefix `layer`. */
|
|
5
7
|
const INVALID_PREFIX = /[@:\s]/;
|
|
6
8
|
function describe(value) {
|
|
@@ -151,6 +153,37 @@ function values(ctx) {
|
|
|
151
153
|
});
|
|
152
154
|
};
|
|
153
155
|
}
|
|
156
|
+
/** Secret-looking names: non-empty strings or RegExps. An empty list is valid (with `mode: 'replace'`, it turns them off). */
|
|
157
|
+
function namePatterns(ctx) {
|
|
158
|
+
return (value, path, errors) => {
|
|
159
|
+
if (!Array.isArray(value))
|
|
160
|
+
return fail(ctx, path, 'an array of strings or RegExps', value, errors);
|
|
161
|
+
value.forEach((item, index) => {
|
|
162
|
+
if (item instanceof RegExp)
|
|
163
|
+
return;
|
|
164
|
+
if (typeof item !== 'string' || item.trim() === '')
|
|
165
|
+
fail(ctx, `${path}[${index}]`, 'a non-empty string or a RegExp', item, errors);
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** Environment variable names. A value with other characters is most likely a secret pasted by mistake: it is not echoed. */
|
|
170
|
+
function envNames(ctx) {
|
|
171
|
+
return (value, path, errors) => {
|
|
172
|
+
if (!Array.isArray(value))
|
|
173
|
+
return fail(ctx, path, 'an array of environment variable names', value, errors);
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
value.forEach((item, index) => {
|
|
176
|
+
if (typeof item !== 'string' || !ENV_NAME.test(item)) {
|
|
177
|
+
const got = typeof item === 'string' ? 'a string that is not a variable name (value not shown)' : describe(item);
|
|
178
|
+
errors.push(`${ctx.source}: ${path}[${index}] must be an environment variable name (letters, digits, _), got ${got}`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (seen.has(item))
|
|
182
|
+
errors.push(`${ctx.source}: ${path}[${index}] repeats the variable ${item}`);
|
|
183
|
+
seen.add(item);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
}
|
|
154
187
|
function tagPrefix(ctx) {
|
|
155
188
|
return (value, path, errors) => {
|
|
156
189
|
if (typeof value !== 'string' || value === '' || INVALID_PREFIX.test(value)) {
|
|
@@ -228,6 +261,7 @@ export function validateConfig(value, source, options = {}) {
|
|
|
228
261
|
screenshots: object({ defaultView: oneOf(SCREENSHOT_VIEWS, ctx) }, ctx),
|
|
229
262
|
theme: oneOf(THEMES, ctx),
|
|
230
263
|
aiPrompt: object({ includeStack: boolean(ctx) }, ctx),
|
|
264
|
+
redact: object({ patterns: namePatterns(ctx), mode: oneOf(REDACT_MODES, ctx), env: envNames(ctx) }, ctx),
|
|
231
265
|
};
|
|
232
266
|
if (options.allowConfigFile)
|
|
233
267
|
fields['configFile'] = nonEmptyString(ctx);
|
package/dist/core/build.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { FullConfig, FullResult, Suite, TestError, TestStep } from '@playwr
|
|
|
2
2
|
import type { LoupeReport, RunScope } from '../schema/v1.js';
|
|
3
3
|
import { type CategoriesOptions, type ErrorText } from './categories.js';
|
|
4
4
|
import { type DimensionsOptions } from './dimensions.js';
|
|
5
|
+
import { type RedactOptions } from './redact.js';
|
|
5
6
|
/** Folder, inside the report folder, that holds the copied attachments. */
|
|
6
7
|
export declare const DATA_FOLDER = "data";
|
|
7
8
|
export interface BuildOptions {
|
|
@@ -20,12 +21,14 @@ export interface BuildOptions {
|
|
|
20
21
|
error: TestError;
|
|
21
22
|
project: string | null;
|
|
22
23
|
}>;
|
|
24
|
+
/** Secret redaction; default: the built-in names, no env values. */
|
|
25
|
+
redact?: RedactOptions;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Titles of the failing steps leading to `error`, outermost first: the path to
|
|
26
29
|
* the deepest step that ended with the same message. Empty when no step did.
|
|
27
30
|
*/
|
|
28
|
-
export declare function failingSteps(steps: readonly TestStep[], error: TestError): string[];
|
|
31
|
+
export declare function failingSteps(steps: readonly TestStep[], error: TestError, title?: (step: TestStep) => string): string[];
|
|
29
32
|
/**
|
|
30
33
|
* Text an error is categorized on. `steps`: titles of the failing steps, outermost first.
|
|
31
34
|
* Run errors lose the `merge-reports` tag prefix, so rules match the same text merged or not.
|
package/dist/core/build.js
CHANGED
|
@@ -8,6 +8,7 @@ import { detectEnvironment, detectGit } from './metadata.js';
|
|
|
8
8
|
import { collectOutput } from './output.js';
|
|
9
9
|
import { esmImportLocation, isUsableLocation } from './runErrors.js';
|
|
10
10
|
import { detectScopeReasons, resolveScope } from './scope.js';
|
|
11
|
+
import { DEFAULT_REDACT, createRedactor, mapReportStrings } from './redact.js';
|
|
11
12
|
import { errorSignature, stripAnsi, stripMergeTagPrefix } from './signature.js';
|
|
12
13
|
/** Folder, inside the report folder, that holds the copied attachments. */
|
|
13
14
|
export const DATA_FOLDER = 'data';
|
|
@@ -119,20 +120,20 @@ function isFocused(test) {
|
|
|
119
120
|
* Titles of the failing steps leading to `error`, outermost first: the path to
|
|
120
121
|
* the deepest step that ended with the same message. Empty when no step did.
|
|
121
122
|
*/
|
|
122
|
-
export function failingSteps(steps, error) {
|
|
123
|
+
export function failingSteps(steps, error, title = (step) => step.title) {
|
|
123
124
|
const message = error.message ?? error.value;
|
|
124
125
|
if (message === undefined)
|
|
125
126
|
return [];
|
|
126
127
|
for (const step of [...steps].reverse()) {
|
|
127
128
|
if (!step.error || (step.error.message ?? step.error.value) !== message)
|
|
128
129
|
continue;
|
|
129
|
-
return [step
|
|
130
|
+
return [title(step), ...failingSteps(step.steps, error, title)];
|
|
130
131
|
}
|
|
131
132
|
// A step may fail with a wrapped message while one of its children has the exact one.
|
|
132
133
|
for (const step of [...steps].reverse()) {
|
|
133
|
-
const inner = failingSteps(step.steps, error);
|
|
134
|
+
const inner = failingSteps(step.steps, error, title);
|
|
134
135
|
if (inner.length > 0)
|
|
135
|
-
return [step
|
|
136
|
+
return [title(step), ...inner];
|
|
136
137
|
}
|
|
137
138
|
return [];
|
|
138
139
|
}
|
|
@@ -151,6 +152,7 @@ export async function buildReport(config, suite, result, options) {
|
|
|
151
152
|
const rootDir = config.rootDir;
|
|
152
153
|
const store = new AttachmentStore(options.outputFolder);
|
|
153
154
|
const categorizer = createCategorizer(options.categories ?? DEFAULT_CATEGORIES);
|
|
155
|
+
const redactor = createRedactor(options.redact ?? DEFAULT_REDACT);
|
|
154
156
|
const dimensionSet = createDimensions(options.dimensions ?? DEFAULT_DIMENSIONS);
|
|
155
157
|
const categories = new Set();
|
|
156
158
|
const usedDimensions = new Set();
|
|
@@ -161,10 +163,11 @@ export async function buildReport(config, suite, result, options) {
|
|
|
161
163
|
/**
|
|
162
164
|
* `steps`: titles of the failing steps leading to the error, outermost first.
|
|
163
165
|
* `runError`: the `merge-reports` tag prefix is ignored by categories and the signature, kept in `message` and `stack`.
|
|
166
|
+
* `redact`: applied to the message and the stack before the signature and the category, so they never depend on a secret.
|
|
164
167
|
*/
|
|
165
|
-
const toError = (error, steps = [], location = error.location, runError = false) => {
|
|
166
|
-
const message = stripAnsi(error.message ?? error.value ?? '');
|
|
167
|
-
const stack = error.stack ? stripAnsi(error.stack) : null;
|
|
168
|
+
const toError = (error, redact, steps = [], location = error.location, runError = false) => {
|
|
169
|
+
const message = redact(stripAnsi(error.message ?? error.value ?? ''));
|
|
170
|
+
const stack = error.stack ? redact(stripAnsi(error.stack)) : null;
|
|
168
171
|
const category = categorizer.categorize(errorText(message, stack, steps, runError));
|
|
169
172
|
categories.add(category);
|
|
170
173
|
return {
|
|
@@ -176,17 +179,21 @@ export async function buildReport(config, suite, result, options) {
|
|
|
176
179
|
category,
|
|
177
180
|
};
|
|
178
181
|
};
|
|
179
|
-
const toStep = (step, parents) => {
|
|
180
|
-
const
|
|
182
|
+
const toStep = (step, parents, redact) => {
|
|
183
|
+
const title = redact.title(step);
|
|
184
|
+
const chain = [...parents, title];
|
|
185
|
+
// `subtitle` exists from Playwright 1.63. Before, the target of an action is in
|
|
186
|
+
// the title on 1.52 (`locator.getByRole('button').click`), and nowhere on
|
|
187
|
+
// 1.53–1.62 (`Click`, `Fill "<value>"`).
|
|
188
|
+
const subtitle = step.subtitle;
|
|
181
189
|
return {
|
|
182
|
-
title
|
|
183
|
-
|
|
184
|
-
subtitle: step.subtitle ?? null,
|
|
190
|
+
title,
|
|
191
|
+
subtitle: subtitle === undefined ? null : redact.text(subtitle),
|
|
185
192
|
category: step.category,
|
|
186
193
|
durationMs: toMs(step.duration),
|
|
187
|
-
error: step.error ? toError(step.error, chain) : null,
|
|
194
|
+
error: step.error ? toError(step.error, redact.text, chain) : null,
|
|
188
195
|
location: toLocation(step.location),
|
|
189
|
-
steps: step.steps.map((child) => toStep(child, chain)),
|
|
196
|
+
steps: step.steps.map((child) => toStep(child, chain, redact)),
|
|
190
197
|
};
|
|
191
198
|
};
|
|
192
199
|
const toAttempt = async (attempt, folder) => {
|
|
@@ -194,14 +201,15 @@ export async function buildReport(config, suite, result, options) {
|
|
|
194
201
|
for (const attachment of attempt.attachments) {
|
|
195
202
|
attachments.push(await store.add(`${folder}/${attempt.retry}`, attachment));
|
|
196
203
|
}
|
|
204
|
+
const redact = redactor.attempt(attempt.steps);
|
|
197
205
|
return {
|
|
198
206
|
index: attempt.retry,
|
|
199
207
|
status: attempt.status,
|
|
200
208
|
startedAt: attempt.startTime.toISOString(),
|
|
201
209
|
durationMs: toMs(attempt.duration),
|
|
202
210
|
workerIndex: attempt.workerIndex,
|
|
203
|
-
errors: attempt.errors.map((error) => toError(error, failingSteps(attempt.steps, error))),
|
|
204
|
-
steps: attempt.steps.map((step) => toStep(step, [])),
|
|
211
|
+
errors: attempt.errors.map((error) => toError(error, redact.text, failingSteps(attempt.steps, error, redact.title))),
|
|
212
|
+
steps: attempt.steps.map((step) => toStep(step, [], redact)),
|
|
205
213
|
attachments,
|
|
206
214
|
annotations: attempt.annotations.map(toAnnotation),
|
|
207
215
|
stdout: collectOutput(attempt.stdout),
|
|
@@ -292,9 +300,9 @@ export async function buildReport(config, suite, result, options) {
|
|
|
292
300
|
for (const { error, project } of options.runErrors ?? []) {
|
|
293
301
|
const message = stripAnsi(error.message ?? error.value ?? '');
|
|
294
302
|
const location = isUsableLocation(error.location) ? error.location : ((await esmImportLocation(message)) ?? undefined);
|
|
295
|
-
errors.push({ ...toError(error, [], location, true), project });
|
|
303
|
+
errors.push({ ...toError(error, redactor.text, [], location, true), project });
|
|
296
304
|
}
|
|
297
|
-
|
|
305
|
+
const report = {
|
|
298
306
|
schemaVersion: 1,
|
|
299
307
|
generator: { name: 'playwright-loupe', version: options.generatorVersion },
|
|
300
308
|
run: {
|
|
@@ -318,4 +326,6 @@ export async function buildReport(config, suite, result, options) {
|
|
|
318
326
|
categories: categorizer.definitions(categories),
|
|
319
327
|
dimensions: dimensionSet.definitions(usedDimensions),
|
|
320
328
|
};
|
|
329
|
+
// URL parameters and env values (redact.env) are redacted in every other string: test titles, annotations, outputs, attachment bodies...
|
|
330
|
+
return redactor.rewritesStrings ? mapReportStrings(report, redactor.strings) : report;
|
|
321
331
|
}
|
package/dist/core/categories.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { NEUTRAL_ENTRY, PALETTE_ENTRIES, paletteReference } from '../shared/palette.js';
|
|
2
|
+
import { httpStatus } from './http.js';
|
|
2
3
|
const ALL_TARGETS = ['message', 'stack', 'step'];
|
|
3
4
|
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
|
-
}
|
|
12
5
|
export const BUILT_IN_RULES = [
|
|
13
6
|
{
|
|
14
7
|
// Before "Locator not found", "Assertion" and "Timeout": a locator matching
|
|
@@ -30,7 +23,7 @@ export const BUILT_IN_RULES = [
|
|
|
30
23
|
// Before "Network", "Assertion" and "Timeout": the status is the diagnosis,
|
|
31
24
|
// whether it surfaces through `failOnStatusCode`, `toBeOK()` or a thrown
|
|
32
25
|
// error. A bare 429 is not enough (`429ms`, `Received: 429`): it needs an
|
|
33
|
-
// HTTP context.
|
|
26
|
+
// HTTP context (src/core/http.ts), shared with error signatures.
|
|
34
27
|
name: 'Rate limited',
|
|
35
28
|
color: paletteReference('teal'),
|
|
36
29
|
match: [/\bToo Many Requests\b/i, /\brate[- ]?limit/i, httpStatus('429')],
|
|
@@ -39,10 +32,8 @@ export const BUILT_IN_RULES = [
|
|
|
39
32
|
{
|
|
40
33
|
name: 'Server error',
|
|
41
34
|
color: paletteReference('orange'),
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/\b5\d\d (?:Internal Server Error|Not Implemented|Bad Gateway|Service Unavailable|Gateway Time-?out|HTTP Version Not Supported)\b/i,
|
|
45
|
-
],
|
|
35
|
+
// In an HTTP context, reason phrase included: `500 Internal Server Error` (failOnStatusCode).
|
|
36
|
+
match: [httpStatus('5\\d\\d')],
|
|
46
37
|
targets: ['message', 'stack'],
|
|
47
38
|
},
|
|
48
39
|
{
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP status codes in context, shared by the "Rate limited" and "Server
|
|
3
|
+
* error" categories and by error signatures, so both agree on what an HTTP
|
|
4
|
+
* status is. See "Error categories" and "Error signature" in docs/schema/v1.md.
|
|
5
|
+
*
|
|
6
|
+
* A code is in an HTTP context when it is:
|
|
7
|
+
* - preceded by `status`, `status code`, `statusCode`, `"status":`, `HTTP`,
|
|
8
|
+
* `HTTP/1.1`, `error`, or `←` (the response line of Playwright's fetch log),
|
|
9
|
+
* with an optional `:` or `=`: `status 429`, `Response status: 503`,
|
|
10
|
+
* `HTTP 500`, `statusCode=502`, `with error 401`;
|
|
11
|
+
* - or followed by its reason phrase: `401 Unauthorized`, `500 Internal Server Error`.
|
|
12
|
+
*
|
|
13
|
+
* A bare number is not enough: `Timeout 429ms exceeded` and `Expected: 200
|
|
14
|
+
* Received: 500` have no HTTP context.
|
|
15
|
+
*/
|
|
16
|
+
/** Any status code from 100 to 599. */
|
|
17
|
+
export declare const STATUS_CODE = "[1-5]\\d\\d";
|
|
18
|
+
/** Source of a pattern matching `code` (a regex source such as `429` or `5\\d\\d`) in an HTTP context, case-insensitive. */
|
|
19
|
+
export declare function httpStatusSource(code: string): string;
|
|
20
|
+
/** `code` in an HTTP context. */
|
|
21
|
+
export declare function httpStatus(code: string): RegExp;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP status codes in context, shared by the "Rate limited" and "Server
|
|
3
|
+
* error" categories and by error signatures, so both agree on what an HTTP
|
|
4
|
+
* status is. See "Error categories" and "Error signature" in docs/schema/v1.md.
|
|
5
|
+
*
|
|
6
|
+
* A code is in an HTTP context when it is:
|
|
7
|
+
* - preceded by `status`, `status code`, `statusCode`, `"status":`, `HTTP`,
|
|
8
|
+
* `HTTP/1.1`, `error`, or `←` (the response line of Playwright's fetch log),
|
|
9
|
+
* with an optional `:` or `=`: `status 429`, `Response status: 503`,
|
|
10
|
+
* `HTTP 500`, `statusCode=502`, `with error 401`;
|
|
11
|
+
* - or followed by its reason phrase: `401 Unauthorized`, `500 Internal Server Error`.
|
|
12
|
+
*
|
|
13
|
+
* A bare number is not enough: `Timeout 429ms exceeded` and `Expected: 200
|
|
14
|
+
* Received: 500` have no HTTP context.
|
|
15
|
+
*/
|
|
16
|
+
const PREFIX = `(?:\\b(?:status(?:[ _]?code)?["']?\\s*[:=]?|HTTP(?:/\\d(?:\\.\\d)?)?:?|error\\s*[:=]?)|←)`;
|
|
17
|
+
/** Standard reason phrases (RFC 9110, plus 429). */
|
|
18
|
+
const REASON_PHRASES = [
|
|
19
|
+
'Continue',
|
|
20
|
+
'Switching Protocols',
|
|
21
|
+
'OK',
|
|
22
|
+
'Created',
|
|
23
|
+
'Accepted',
|
|
24
|
+
'No Content',
|
|
25
|
+
'Moved Permanently',
|
|
26
|
+
'Found',
|
|
27
|
+
'See Other',
|
|
28
|
+
'Not Modified',
|
|
29
|
+
'Temporary Redirect',
|
|
30
|
+
'Permanent Redirect',
|
|
31
|
+
'Bad Request',
|
|
32
|
+
'Unauthorized',
|
|
33
|
+
'Payment Required',
|
|
34
|
+
'Forbidden',
|
|
35
|
+
'Not Found',
|
|
36
|
+
'Method Not Allowed',
|
|
37
|
+
'Not Acceptable',
|
|
38
|
+
'Request Time-?out',
|
|
39
|
+
'Conflict',
|
|
40
|
+
'Gone',
|
|
41
|
+
'Precondition Failed',
|
|
42
|
+
'Payload Too Large',
|
|
43
|
+
'Content Too Large',
|
|
44
|
+
'Unsupported Media Type',
|
|
45
|
+
'Unprocessable Entity',
|
|
46
|
+
'Unprocessable Content',
|
|
47
|
+
'Too Many Requests',
|
|
48
|
+
'Internal Server Error',
|
|
49
|
+
'Not Implemented',
|
|
50
|
+
'Bad Gateway',
|
|
51
|
+
'Service Unavailable',
|
|
52
|
+
'Gateway Time-?out',
|
|
53
|
+
'HTTP Version Not Supported',
|
|
54
|
+
];
|
|
55
|
+
/** Any status code from 100 to 599. */
|
|
56
|
+
export const STATUS_CODE = '[1-5]\\d\\d';
|
|
57
|
+
/** Source of a pattern matching `code` (a regex source such as `429` or `5\\d\\d`) in an HTTP context, case-insensitive. */
|
|
58
|
+
export function httpStatusSource(code) {
|
|
59
|
+
return `${PREFIX}\\s*${code}(?!\\w|\\.\\d)|\\b${code} (?:${REASON_PHRASES.join('|')})\\b`;
|
|
60
|
+
}
|
|
61
|
+
/** `code` in an HTTP context. */
|
|
62
|
+
export function httpStatus(code) {
|
|
63
|
+
return new RegExp(httpStatusSource(code), 'i');
|
|
64
|
+
}
|