@piwitests/reporter 0.63.0 → 0.64.0
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/README.md +1 -1
- package/dist/cli/index.js +62 -27
- package/dist/global-setup-module.js +61 -2
- package/dist/index.d.ts +21 -10
- package/dist/index.js +1833 -546
- package/dist/internal/capture/attachments.d.ts +1 -1
- package/dist/internal/capture/capture-fixtures.d.ts +130 -7
- package/dist/internal/capture/capture-fixtures.js +1900 -776
- package/dist/internal/capture/locator-healing.d.ts +212 -4
- package/dist/internal/capture/pick-on-failure.d.ts +184 -6
- package/dist/types-CGr9jJZ_.d.ts +87 -0
- package/dist/wire-7zsqlBoY.d.ts +48 -0
- package/package.json +2 -2
- package/templates/skills/write-the-missing-test/SKILL.md +6 -4
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ Most options can also be set via a `PIWI_*` environment variable (config wins ov
|
|
|
67
67
|
## Requirements
|
|
68
68
|
|
|
69
69
|
- Node.js 20 or higher (the reporter runs inside your test project — the dashboard *server* itself targets Node 22+, or use its Docker image)
|
|
70
|
-
- Playwright Test 1.
|
|
70
|
+
- Playwright Test 1.59 or higher (a few features need a later version, named in their docs)
|
|
71
71
|
- A running Piwi Dashboard server
|
|
72
72
|
|
|
73
73
|
## Contributing
|
package/dist/cli/index.js
CHANGED
|
@@ -1844,13 +1844,27 @@ function safeLocator(expr) {
|
|
|
1844
1844
|
if (!chain || !chainIsSafe(chain)) return null;
|
|
1845
1845
|
return { chain, text: renderLocatorChain(chain) };
|
|
1846
1846
|
}
|
|
1847
|
-
|
|
1847
|
+
var TEXT_CHECKS = /* @__PURE__ */ new Set(["toHaveText", "toContainText"]);
|
|
1848
|
+
function checkedText(step) {
|
|
1849
|
+
const assertion = step?.action === "assert" ? step.assertion : null;
|
|
1850
|
+
if (!assertion || !TEXT_CHECKS.has(assertion.matcher)) return null;
|
|
1851
|
+
const text = assertion.expected?.replace(/\s+/g, " ").trim();
|
|
1852
|
+
return text ? text : null;
|
|
1853
|
+
}
|
|
1854
|
+
function findsByText(chain, text) {
|
|
1855
|
+
const holds = (arg) => arg?.type === "string" && arg.value.replace(/\s+/g, " ").trim().includes(text);
|
|
1856
|
+
const option = (arg, key) => arg?.type === "object" ? arg.entries.find(([name]) => name === key)?.[1] : void 0;
|
|
1857
|
+
return chain.calls.some(
|
|
1858
|
+
(call) => call.method === "getByText" && holds(call.args[0]) || call.method === "getByRole" && holds(option(call.args[1], "name")) || call.args.some((arg) => holds(option(arg, "hasText")))
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
function chooseLocator(target, options, check) {
|
|
1848
1862
|
if (!target) return null;
|
|
1849
|
-
const
|
|
1863
|
+
const recorded = target.alternatives.flatMap((alt) => {
|
|
1850
1864
|
const safe = safeLocator(alt.locator);
|
|
1851
1865
|
return safe ? [safe] : [];
|
|
1852
1866
|
});
|
|
1853
|
-
if (
|
|
1867
|
+
if (recorded.length === 0) return null;
|
|
1854
1868
|
const levels = /* @__PURE__ */ new Map();
|
|
1855
1869
|
const levelOf = (c) => {
|
|
1856
1870
|
let level = levels.get(c.text);
|
|
@@ -1860,6 +1874,9 @@ function chooseLocator(target, options) {
|
|
|
1860
1874
|
}
|
|
1861
1875
|
return level;
|
|
1862
1876
|
};
|
|
1877
|
+
const text = checkedText(check);
|
|
1878
|
+
const apart = text ? recorded.filter((c) => !findsByText(c.chain, text) && levelOf(c) !== "brittle") : [];
|
|
1879
|
+
const candidates = apart.length ? apart : recorded;
|
|
1863
1880
|
const prefer = options.preferLocators;
|
|
1864
1881
|
if (prefer && prefer.size > 0) {
|
|
1865
1882
|
const known = candidates.find((c) => prefer.has(c.text) && levelOf(c) !== "brittle");
|
|
@@ -1940,7 +1957,7 @@ function urlForCode(url, ctx) {
|
|
|
1940
1957
|
return isPath && ctx.origin ? `${ctx.origin}${url}` : url;
|
|
1941
1958
|
}
|
|
1942
1959
|
function locatorForStep(step, index, ctx) {
|
|
1943
|
-
const chosen = chooseLocator(step.target, ctx.options);
|
|
1960
|
+
const chosen = chooseLocator(step.target, ctx.options, step);
|
|
1944
1961
|
if (!chosen) {
|
|
1945
1962
|
ctx.warnings.push({ step: index, code: "no-locator", message: "No locator was captured for this element." });
|
|
1946
1963
|
return `${ctx.page}.locator(${quote("/* no locator captured */")})`;
|
|
@@ -2407,19 +2424,11 @@ function renderSpec(session, options = {}) {
|
|
|
2407
2424
|
const needs = needsLines("");
|
|
2408
2425
|
return result([...needs, ...testOpening, ...testBody, `});`, ``], needs.length + testOpening.length);
|
|
2409
2426
|
}
|
|
2410
|
-
const
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
...importLines,
|
|
2416
|
-
``,
|
|
2417
|
-
...testOpening,
|
|
2418
|
-
...testBody,
|
|
2419
|
-
`});`,
|
|
2420
|
-
``
|
|
2421
|
-
];
|
|
2422
|
-
return result(lines, 1 + importLines.length + 1 + testOpening.length);
|
|
2427
|
+
const testModule = options.testImport?.trim() || "@playwright/test";
|
|
2428
|
+
const expectModule = options.expectImport?.trim() || testModule;
|
|
2429
|
+
const testImports = expectModule === testModule ? [`import { test, expect } from ${quote(testModule)};`] : [`import { test } from ${quote(testModule)};`, `import { expect } from ${quote(expectModule)};`];
|
|
2430
|
+
const lines = [...testImports, ...importLines, ``, ...testOpening, ...testBody, `});`, ``];
|
|
2431
|
+
return result(lines, testImports.length + importLines.length + 1 + testOpening.length);
|
|
2423
2432
|
}
|
|
2424
2433
|
|
|
2425
2434
|
// ../core/src/steps.ts
|
|
@@ -3178,14 +3187,23 @@ var DEFAULT_COVERAGE_EXCLUDES = [
|
|
|
3178
3187
|
"tests/",
|
|
3179
3188
|
"test/",
|
|
3180
3189
|
"__tests__/",
|
|
3190
|
+
"playwright.config.*",
|
|
3181
3191
|
"*Tests/",
|
|
3182
3192
|
"*.Tests/",
|
|
3183
3193
|
"obj/",
|
|
3184
3194
|
"bin/",
|
|
3185
3195
|
"node_modules/",
|
|
3196
|
+
"dist/",
|
|
3197
|
+
".next/",
|
|
3198
|
+
".nuxt/",
|
|
3199
|
+
".output/",
|
|
3200
|
+
".svelte-kit/",
|
|
3186
3201
|
"*.g.cs",
|
|
3187
3202
|
"*.Designer.cs",
|
|
3188
|
-
"*.generated.*"
|
|
3203
|
+
"*.generated.*",
|
|
3204
|
+
"*.gen.*",
|
|
3205
|
+
"__generated__/",
|
|
3206
|
+
"*.d.ts"
|
|
3189
3207
|
];
|
|
3190
3208
|
var DEFAULT_COVERAGE_SCOPE = { include: [], exclude: [...DEFAULT_COVERAGE_EXCLUDES] };
|
|
3191
3209
|
var MAX_SCOPE_PATTERNS = 200;
|
|
@@ -6825,15 +6843,22 @@ function diffImpactInput(files) {
|
|
|
6825
6843
|
var fs17 = __toESM(require("fs"));
|
|
6826
6844
|
var path21 = __toESM(require("path"));
|
|
6827
6845
|
var import_node_module2 = require("module");
|
|
6846
|
+
|
|
6847
|
+
// src/internal/support/playwright-version.ts
|
|
6848
|
+
function playwrightVersionAtLeast(version, minimum) {
|
|
6849
|
+
const have = version ? /^(\d+)\.(\d+)/.exec(version) : null;
|
|
6850
|
+
const want = /^(\d+)\.(\d+)/.exec(minimum);
|
|
6851
|
+
if (!have || !want) return false;
|
|
6852
|
+
const major = Number(have[1]);
|
|
6853
|
+
const wantMajor = Number(want[1]);
|
|
6854
|
+
return major > wantMajor || major === wantMajor && Number(have[2]) >= Number(want[2]);
|
|
6855
|
+
}
|
|
6856
|
+
|
|
6857
|
+
// src/cli/add-reporter.ts
|
|
6828
6858
|
var REPORTER_PACKAGE2 = "@piwitests/reporter";
|
|
6829
6859
|
var ADD_REPORTER_FLAG = "--add-reporter";
|
|
6830
|
-
var ADD_REPORTER_MIN = { major: 1, minor: 63 };
|
|
6831
6860
|
function playwrightSupportsAddReporter(version) {
|
|
6832
|
-
|
|
6833
|
-
if (!match) return false;
|
|
6834
|
-
const major = Number(match[1]);
|
|
6835
|
-
const minor = Number(match[2]);
|
|
6836
|
-
return major > ADD_REPORTER_MIN.major || major === ADD_REPORTER_MIN.major && minor >= ADD_REPORTER_MIN.minor;
|
|
6861
|
+
return playwrightVersionAtLeast(version, "1.63");
|
|
6837
6862
|
}
|
|
6838
6863
|
function configHasPiwiReporter(source) {
|
|
6839
6864
|
return new RegExp(REPORTER_PACKAGE2.replace(/[/\\]/g, "\\$&")).test(source);
|
|
@@ -7790,6 +7815,15 @@ function readOption9(argv, name) {
|
|
|
7790
7815
|
function escapeRegex2(s) {
|
|
7791
7816
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7792
7817
|
}
|
|
7818
|
+
function probePlaywrightArgs(playwrightArgs2, testTitles) {
|
|
7819
|
+
const given = (...flags) => playwrightArgs2.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
7820
|
+
const grep = testTitles.filter((t) => !!t).map(escapeRegex2).join("|");
|
|
7821
|
+
const args = [...playwrightArgs2];
|
|
7822
|
+
if (grep && !given("--grep", "-g")) args.push("--grep", grep);
|
|
7823
|
+
if (!given("--retries")) args.push("--retries=0");
|
|
7824
|
+
if (!given("--max-failures", "-x")) args.push("--max-failures=0");
|
|
7825
|
+
return args;
|
|
7826
|
+
}
|
|
7793
7827
|
async function runProbe(argv) {
|
|
7794
7828
|
const sep6 = argv.indexOf("--");
|
|
7795
7829
|
const own = sep6 < 0 ? argv : argv.slice(0, sep6);
|
|
@@ -7844,9 +7878,10 @@ async function runProbe(argv) {
|
|
|
7844
7878
|
const runOutputFile = path24.join(dir, "run.json");
|
|
7845
7879
|
fs20.writeFileSync(planFile, JSON.stringify(plan));
|
|
7846
7880
|
fs20.writeFileSync(resultsFile, "");
|
|
7847
|
-
const
|
|
7848
|
-
|
|
7849
|
-
|
|
7881
|
+
const pwArgs = probePlaywrightArgs(
|
|
7882
|
+
playwrightArgs2,
|
|
7883
|
+
plan.items.map((i) => i.testTitle)
|
|
7884
|
+
);
|
|
7850
7885
|
const childEnv = {
|
|
7851
7886
|
...env,
|
|
7852
7887
|
[PIWI_PROBE_ENV.flag]: "1",
|
|
@@ -225,7 +225,7 @@ var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
|
|
|
225
225
|
var PIWI_SHARD_ENV = "PIWI_SHARD";
|
|
226
226
|
function readBool(val) {
|
|
227
227
|
if (val === void 0) return void 0;
|
|
228
|
-
return
|
|
228
|
+
return ["true", "1", "yes", "on"].includes(val.trim().toLowerCase());
|
|
229
229
|
}
|
|
230
230
|
var ENV_FALLBACK_SPECS = [
|
|
231
231
|
{ option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
|
|
@@ -335,7 +335,7 @@ function resolveOptions(raw, configDir) {
|
|
|
335
335
|
externalId.onDuplicate = externalIdOnDuplicate;
|
|
336
336
|
if (Object.keys(externalId).length > 0) mergedRaw.externalId = externalId;
|
|
337
337
|
const opts = { ...DEFAULTS, ...mergedRaw };
|
|
338
|
-
if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = env[PIWI_ENV_KEYS.verbose]
|
|
338
|
+
if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = readBool(env[PIWI_ENV_KEYS.verbose]);
|
|
339
339
|
return opts;
|
|
340
340
|
}
|
|
341
341
|
|
|
@@ -690,6 +690,65 @@ function normalizeRef(ref) {
|
|
|
690
690
|
return ref.replace(/^refs\/heads\//, "");
|
|
691
691
|
}
|
|
692
692
|
|
|
693
|
+
// ../core/src/locator-chain.ts
|
|
694
|
+
var LOCATING_METHODS = /* @__PURE__ */ new Set([
|
|
695
|
+
"getByRole",
|
|
696
|
+
"getByTestId",
|
|
697
|
+
"getByText",
|
|
698
|
+
"getByLabel",
|
|
699
|
+
"getByPlaceholder",
|
|
700
|
+
"getByAltText",
|
|
701
|
+
"getByTitle",
|
|
702
|
+
"locator",
|
|
703
|
+
"frameLocator"
|
|
704
|
+
]);
|
|
705
|
+
var NARROWING_METHODS = /* @__PURE__ */ new Set([
|
|
706
|
+
"filter",
|
|
707
|
+
"first",
|
|
708
|
+
"last",
|
|
709
|
+
"nth",
|
|
710
|
+
"and",
|
|
711
|
+
"or",
|
|
712
|
+
"visible",
|
|
713
|
+
"contentFrame",
|
|
714
|
+
"owner"
|
|
715
|
+
]);
|
|
716
|
+
var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
|
|
717
|
+
|
|
718
|
+
// ../core/src/mask.ts
|
|
719
|
+
var SECRET_AUTOCOMPLETE = [
|
|
720
|
+
"current-password",
|
|
721
|
+
"new-password",
|
|
722
|
+
"one-time-code",
|
|
723
|
+
"cc-number",
|
|
724
|
+
"cc-csc"
|
|
725
|
+
];
|
|
726
|
+
var SECRET_FIELD_SELECTOR = [
|
|
727
|
+
'input[type="password" i]',
|
|
728
|
+
...SECRET_AUTOCOMPLETE.flatMap((token) => [
|
|
729
|
+
`input[autocomplete~="${token}" i]`,
|
|
730
|
+
`textarea[autocomplete~="${token}" i]`
|
|
731
|
+
])
|
|
732
|
+
].join(", ");
|
|
733
|
+
var SECRET_SHAPES = [
|
|
734
|
+
/\beyJ[\w-]{5,}\.[\w-]{5,}\.[\w-]*/g,
|
|
735
|
+
/\bBearer(?:\s+|%20)[\w.~+/-]{8,}=*/gi,
|
|
736
|
+
/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
|
|
737
|
+
/\bwhsec_[A-Za-z0-9]{20,}/g,
|
|
738
|
+
/\bsk-(?=[\w-]{0,200}\d)(?=[\w-]{0,200}[A-Z])[\w-]{32,}/g,
|
|
739
|
+
/\bglpat-[\w-]{20,}/g,
|
|
740
|
+
/\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
741
|
+
/\bSK[0-9a-f]{32}\b/g,
|
|
742
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
743
|
+
/\bAIza[\w-]{30,}/g,
|
|
744
|
+
/\bgh[opsu]_[A-Za-z0-9]{20,}/g,
|
|
745
|
+
/\bgithub_pat_\w{20,}/g,
|
|
746
|
+
/\bxox[abprs]-[\w-]{8,}/g
|
|
747
|
+
];
|
|
748
|
+
var WHOLE_SHAPES = SECRET_SHAPES.map(
|
|
749
|
+
(re) => new RegExp(`^(?:${re.source})$`, re.flags.replace("g", ""))
|
|
750
|
+
);
|
|
751
|
+
|
|
693
752
|
// ../core/src/live-view.ts
|
|
694
753
|
var LIVE_FRAME_BYTE_RATE = 256 * 1024;
|
|
695
754
|
var MAX_LIVE_FRAME = 192 * 1024;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,11 +2,8 @@ import { FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright
|
|
|
2
2
|
import { PlaywrightTestConfig, Locator, TestType, Page, Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '@playwright/test';
|
|
3
3
|
export { PlaywrightTestConfig } from '@playwright/test';
|
|
4
4
|
export { PiwiFixtures, PiwiWorkerFixtures, extendPiwiFixtures, piwiFixtures } from './internal/capture/capture-fixtures.js';
|
|
5
|
-
import '
|
|
6
|
-
import '
|
|
7
|
-
import '@piwitests/core/wire';
|
|
8
|
-
import '@piwitests/core/run-channel';
|
|
9
|
-
import '@piwitests/core/code-reach';
|
|
5
|
+
import './types-CGr9jJZ_.js';
|
|
6
|
+
import './wire-7zsqlBoY.js';
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Options for configuring the Piwi Dashboard reporter.
|
|
@@ -59,8 +56,9 @@ interface PiwiDashboardOptions {
|
|
|
59
56
|
collectPerformanceMetrics?: boolean;
|
|
60
57
|
/**
|
|
61
58
|
* Capture per-action locator snapshots that power failure-time healing
|
|
62
|
-
* suggestions. Adds a small per-action cost (one DOM read,
|
|
63
|
-
* snapshot)
|
|
59
|
+
* suggestions. Adds a small per-action cost (one DOM read, rarely two,
|
|
60
|
+
* sometimes an ARIA snapshot) and one DOM read per new document in the test
|
|
61
|
+
* worker. Defaults to `true`; automatically disabled
|
|
64
62
|
* when `collectPerformanceMetrics` is `false` (the reporter discards the data
|
|
65
63
|
* in that case anyway). Can also be forced off with `PIWI_CAPTURE_LOCATORS=false`.
|
|
66
64
|
*/
|
|
@@ -324,7 +322,7 @@ interface PiwiDashboardOptions {
|
|
|
324
322
|
*/
|
|
325
323
|
maxStreamBufferBytes?: number;
|
|
326
324
|
/**
|
|
327
|
-
* Fail the run when any test was flaky (
|
|
325
|
+
* Fail the run when any test was flaky (one attempt failed and another passed). Forwarded
|
|
328
326
|
* to Playwright's native `failOnFlakyTests` config option (Playwright 1.52+)
|
|
329
327
|
* when the reporter is installed via `wrapConfig`, so a flaky-only run exits
|
|
330
328
|
* non-zero without any server round-trip. Defaults to `false`. Can also be
|
|
@@ -505,6 +503,10 @@ declare class PiwiDashboardReporter {
|
|
|
505
503
|
private plannedTests;
|
|
506
504
|
/** Ids of tests that actually reported via `onTestEnd`, to find the ones that never ran. */
|
|
507
505
|
private reportedTestIds;
|
|
506
|
+
/** The spec files said to have run without the capture fixtures in a run that can stop (`noteUncaptured`). */
|
|
507
|
+
private uncapturedSpecs;
|
|
508
|
+
/** The live steps begun and not ended, by step, sent again when someone opens the run (`onWatched`). */
|
|
509
|
+
private openSteps;
|
|
508
510
|
/** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
|
|
509
511
|
private attemptsByTest;
|
|
510
512
|
/** The folder test files are named relative to: the Playwright config's, set in `onBegin`. */
|
|
@@ -583,8 +585,8 @@ declare class PiwiDashboardReporter {
|
|
|
583
585
|
* Name the run's live view on the instance for the workers, which Playwright starts after this hook with this
|
|
584
586
|
* environment: the key the instance gave the global setup, for a project that turned its live view on, for this run
|
|
585
587
|
* alone. A CI run's workers stream with it at once; a run outside CI's hold it until a person shares the run (Share
|
|
586
|
-
* live), and such a run without a key names why instead, for the surfaces that would share it
|
|
587
|
-
* stream starts, which takes the setup file.
|
|
588
|
+
* live), and such a run without a key names why instead, for the surfaces that would share it; a CI run without one
|
|
589
|
+
* says why in its log (`ciLiveViewNote`). Read before the stream starts, which takes the setup file.
|
|
588
590
|
*/
|
|
589
591
|
private shareLiveView;
|
|
590
592
|
/**
|
|
@@ -607,6 +609,15 @@ declare class PiwiDashboardReporter {
|
|
|
607
609
|
* human watches; the meaningful readout is the assertion around them.
|
|
608
610
|
*/
|
|
609
611
|
private static isLiveStep;
|
|
612
|
+
/**
|
|
613
|
+
* In a run that can stop (breakpoints, or a run channel to an editor or the desktop app), say of the first spec file
|
|
614
|
+
* whose test made Playwright calls without the capture fixtures, where the stops happen, that it never stops at a
|
|
615
|
+
* breakpoint nor at Pause, which nothing else says; the others are named in one line as the run ends
|
|
616
|
+
* (`noteOtherUncaptured`).
|
|
617
|
+
*/
|
|
618
|
+
private noteUncaptured;
|
|
619
|
+
/** The spec files past the first that ran without the capture fixtures, in one line (`noteUncaptured`). */
|
|
620
|
+
private noteOtherUncaptured;
|
|
610
621
|
/** Playwright reporter hook: called when a step (including hook/fixture) begins */
|
|
611
622
|
onStepBegin(test: TestCase | undefined, _result: TestResult | undefined, step: any): void;
|
|
612
623
|
/** Playwright reporter hook: called when a step (including hook/fixture) ends */
|