@piwitests/reporter 0.45.0 → 0.46.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/dist/cli/index.js +530 -249
- package/dist/global-setup-module.js +132 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +693 -104
- package/dist/internal/capture/capture-fixtures.js +187 -77
- package/dist/internal/capture/locator-healing.js +9 -7
- package/dist/internal/capture/pick-on-failure.js +130 -37
- package/package.json +1 -1
- package/templates/skills/apply-locator-healing/SKILL.md +2 -0
- package/templates/skills/investigate-failure/SKILL.md +5 -1
- package/templates/skills/write-the-missing-test/SKILL.md +9 -2
|
@@ -88,6 +88,7 @@ var DEFAULTS = {
|
|
|
88
88
|
sampleAriaOnPass: true,
|
|
89
89
|
uploadManifest: true,
|
|
90
90
|
defaultCapture: true,
|
|
91
|
+
checkBaseUrl: false,
|
|
91
92
|
streaming: true,
|
|
92
93
|
streamingBatchSize: 5,
|
|
93
94
|
streamingBatchDelay: 2e3,
|
|
@@ -130,6 +131,7 @@ var PIWI_ENV_KEYS = {
|
|
|
130
131
|
sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
|
|
131
132
|
uploadManifest: "PIWI_UPLOAD_MANIFEST",
|
|
132
133
|
defaultCapture: "PIWI_DEFAULT_CAPTURE",
|
|
134
|
+
checkBaseUrl: "PIWI_CHECK_BASE_URL",
|
|
133
135
|
inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
|
|
134
136
|
pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
|
|
135
137
|
outputFile: "PIWI_OUTPUT_FILE",
|
|
@@ -177,6 +179,7 @@ var ENV_FALLBACK_SPECS = [
|
|
|
177
179
|
{ option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
|
|
178
180
|
{ option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
|
|
179
181
|
{ option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
|
|
182
|
+
{ option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
|
|
180
183
|
{ option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
|
|
181
184
|
{ option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
|
|
182
185
|
{ option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
|
|
@@ -619,6 +622,131 @@ function isListMode(argv = process.argv) {
|
|
|
619
622
|
return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
|
|
620
623
|
}
|
|
621
624
|
|
|
625
|
+
// src/internal/support/base-url-check.ts
|
|
626
|
+
var BASE_URL_CHECK_ATTEMPTS = 3;
|
|
627
|
+
var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
|
|
628
|
+
var BASE_URL_CHECK_DELAY_MS = 2e3;
|
|
629
|
+
var GATEWAY_ANSWERS = {
|
|
630
|
+
502: "bad gateway",
|
|
631
|
+
503: "service unavailable",
|
|
632
|
+
504: "gateway timeout"
|
|
633
|
+
};
|
|
634
|
+
function cliProjectNames(argv = process.argv) {
|
|
635
|
+
let names = null;
|
|
636
|
+
for (let i = 0; i < argv.length; i++) {
|
|
637
|
+
const arg = argv[i];
|
|
638
|
+
if (arg.startsWith("--project=")) {
|
|
639
|
+
(names ??= []).push(arg.slice("--project=".length));
|
|
640
|
+
} else if (arg === "--project") {
|
|
641
|
+
names ??= [];
|
|
642
|
+
while (i + 1 < argv.length && !argv[i + 1].startsWith("-")) names.push(argv[++i]);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return names;
|
|
646
|
+
}
|
|
647
|
+
function wildcard(pattern) {
|
|
648
|
+
const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
649
|
+
return new RegExp(`^${escaped.join(".*")}$`, "i");
|
|
650
|
+
}
|
|
651
|
+
function selectedProjects(projects, argv = process.argv) {
|
|
652
|
+
const names = cliProjectNames(argv);
|
|
653
|
+
if (!names || names.length === 0) return projects;
|
|
654
|
+
const patterns = names.map(wildcard);
|
|
655
|
+
const chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
|
|
656
|
+
if (!argv.includes("--no-deps")) {
|
|
657
|
+
const byName = new Map(projects.map((p) => [p.name ?? "", p]));
|
|
658
|
+
const queue = [...chosen];
|
|
659
|
+
while (queue.length > 0) {
|
|
660
|
+
for (const dep of queue.shift().dependencies ?? []) {
|
|
661
|
+
const project = byName.get(dep);
|
|
662
|
+
if (project && !chosen.has(project)) {
|
|
663
|
+
chosen.add(project);
|
|
664
|
+
queue.push(project);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return projects.filter((p) => chosen.has(p));
|
|
670
|
+
}
|
|
671
|
+
function asProxy(value) {
|
|
672
|
+
const proxy = value;
|
|
673
|
+
return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
|
|
674
|
+
}
|
|
675
|
+
function baseUrlTargets(config, argv = process.argv) {
|
|
676
|
+
const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
|
|
677
|
+
const targets = /* @__PURE__ */ new Map();
|
|
678
|
+
for (const project of selectedProjects(projects, argv)) {
|
|
679
|
+
const url = project.use?.baseURL;
|
|
680
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
|
|
681
|
+
const target = targets.get(url);
|
|
682
|
+
if (target) {
|
|
683
|
+
if (project.name) target.projects.push(project.name);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
targets.set(url, {
|
|
687
|
+
url,
|
|
688
|
+
projects: project.name ? [project.name] : [],
|
|
689
|
+
ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
|
|
690
|
+
proxy: asProxy(project.use?.proxy)
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return [...targets.values()];
|
|
694
|
+
}
|
|
695
|
+
function requestError(error) {
|
|
696
|
+
const first = errorMessage(error).split("\n")[0].trim();
|
|
697
|
+
return first.replace(/^apiRequestContext\.\w+:\s*/, "");
|
|
698
|
+
}
|
|
699
|
+
var playwrightProbe = async (target, timeoutMs) => {
|
|
700
|
+
const { request } = await import("@playwright/test");
|
|
701
|
+
const context = await request.newContext({ ignoreHTTPSErrors: target.ignoreHTTPSErrors, proxy: target.proxy });
|
|
702
|
+
try {
|
|
703
|
+
const response = await context.get(target.url, { timeout: timeoutMs, maxRedirects: 0, failOnStatusCode: false });
|
|
704
|
+
return { status: response.status() };
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return { error: requestError(error) };
|
|
707
|
+
} finally {
|
|
708
|
+
await context.dispose().catch(() => {
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
async function checkBaseUrl(target, options = {}) {
|
|
713
|
+
const attempts = Math.max(1, options.attempts ?? BASE_URL_CHECK_ATTEMPTS);
|
|
714
|
+
const timeoutMs = options.timeoutMs ?? BASE_URL_CHECK_TIMEOUT_MS;
|
|
715
|
+
const delayMs = options.delayMs ?? BASE_URL_CHECK_DELAY_MS;
|
|
716
|
+
const probe = options.probe ?? playwrightProbe;
|
|
717
|
+
let reason = "";
|
|
718
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
719
|
+
const outcome = await probe(target, timeoutMs);
|
|
720
|
+
if ("status" in outcome) {
|
|
721
|
+
const gateway = GATEWAY_ANSWERS[outcome.status];
|
|
722
|
+
if (!gateway) return null;
|
|
723
|
+
reason = `${outcome.status} ${gateway}`;
|
|
724
|
+
} else {
|
|
725
|
+
reason = outcome.error;
|
|
726
|
+
}
|
|
727
|
+
if (attempt < attempts) await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
728
|
+
}
|
|
729
|
+
return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
|
|
730
|
+
}
|
|
731
|
+
async function checkBaseUrls(config, options = {}) {
|
|
732
|
+
const targets = baseUrlTargets(config, options.argv);
|
|
733
|
+
const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
|
|
734
|
+
const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
|
|
735
|
+
if (down.length === 0) return targets;
|
|
736
|
+
const lines = down.map(({ target, reason }) => {
|
|
737
|
+
const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
|
|
738
|
+
return ` ${target.url}${used}: ${reason}`;
|
|
739
|
+
});
|
|
740
|
+
const message = [
|
|
741
|
+
`[Piwi Dashboard] The app under test did not answer, so no test ran:`,
|
|
742
|
+
...lines,
|
|
743
|
+
`Bring the environment back and run again, or set checkBaseUrl: false (PIWI_CHECK_BASE_URL=false) to run the tests anyway.`
|
|
744
|
+
].join("\n");
|
|
745
|
+
const error = new Error(message);
|
|
746
|
+
error.stack = message;
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
|
|
622
750
|
// src/internal/manifest/collect.ts
|
|
623
751
|
var fs4 = __toESM(require("fs"));
|
|
624
752
|
var path4 = __toESM(require("path"));
|
|
@@ -718,6 +846,10 @@ function createGlobalSetup(options, userSetup) {
|
|
|
718
846
|
if (userSetup) return userSetup(config);
|
|
719
847
|
return;
|
|
720
848
|
}
|
|
849
|
+
if (opts.checkBaseUrl === true) {
|
|
850
|
+
const checked = await checkBaseUrls(config ?? {});
|
|
851
|
+
for (const target of checked) logger.debug(`Base URL answered: ${target.url}`);
|
|
852
|
+
}
|
|
721
853
|
if (opts.enabled === false || !opts.serverUrl) {
|
|
722
854
|
logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
|
|
723
855
|
if (userSetup) return userSetup(config);
|
package/dist/index.d.ts
CHANGED
|
@@ -165,6 +165,20 @@ interface PiwiDashboardOptions {
|
|
|
165
165
|
* to opt out and let Playwright's own defaults stand.
|
|
166
166
|
*/
|
|
167
167
|
defaultCapture?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Check that each `baseURL` the run's projects use answers before any worker
|
|
170
|
+
* starts, and stop the run with a message naming the ones that do not, so a
|
|
171
|
+
* suite never runs against an environment that is down. Any HTTP answer
|
|
172
|
+
* counts (a redirect or a 404 included) except a 502, 503 or 504 from a
|
|
173
|
+
* gateway; each base URL gets three tries of 10 seconds, 2 seconds apart,
|
|
174
|
+
* sent with the project's `ignoreHTTPSErrors` and `proxy`. A stopped run
|
|
175
|
+
* runs no test and reaches no dashboard. The check runs in the reporter's
|
|
176
|
+
* global setup, after `webServer` has started and before the config's own
|
|
177
|
+
* `globalSetup`, so leave it off when that setup is what starts the app. Only
|
|
178
|
+
* the projects `--project` selects are checked. **Defaults to `false`**. Can
|
|
179
|
+
* also be set with `PIWI_CHECK_BASE_URL=true`.
|
|
180
|
+
*/
|
|
181
|
+
checkBaseUrl?: boolean;
|
|
168
182
|
/**
|
|
169
183
|
* Open Piwi's own failure-time overlay on the failing page — for inspecting
|
|
170
184
|
* the page and picking a locator for any element (click an element → confirm
|
|
@@ -328,6 +342,10 @@ interface PiwiDashboardOptions {
|
|
|
328
342
|
* The server-run ID and a one-time token are written to a temp file so the
|
|
329
343
|
* reporter instance can pick them up during the streaming handshake.
|
|
330
344
|
*
|
|
345
|
+
* With `checkBaseUrl`, every `baseURL` the run's projects use is checked first,
|
|
346
|
+
* and the setup throws, stopping the run before any worker starts, when one
|
|
347
|
+
* does not answer.
|
|
348
|
+
*
|
|
331
349
|
* @param options Piwi Dashboard options (uses `serverUrl`, `projectName`, …).
|
|
332
350
|
* @param userSetup An existing global setup to chain after the Piwi registration.
|
|
333
351
|
*/
|
|
@@ -558,6 +576,48 @@ declare function extendPiwiAi<TestArgs extends FixtureArgs, WorkerArgs extends F
|
|
|
558
576
|
page: Page & PiwiAi;
|
|
559
577
|
}, WorkerArgs>;
|
|
560
578
|
|
|
579
|
+
/** How the code a recording writes looks, for everyone working in the repository. */
|
|
580
|
+
interface PiwiCodegenConfig {
|
|
581
|
+
/**
|
|
582
|
+
* After a step that leads to another page, wait for that page's URL and for the next step's element before going
|
|
583
|
+
* on. `true` by default. A call to one of your own functions waits for its pages itself either way.
|
|
584
|
+
*/
|
|
585
|
+
pageWaits?: boolean;
|
|
586
|
+
/**
|
|
587
|
+
* `literal` (default): typed values are written in the code, and only passwords are read from the environment.
|
|
588
|
+
* `env`: every typed value is read from an environment variable named after its field.
|
|
589
|
+
*/
|
|
590
|
+
values?: 'literal' | 'env';
|
|
591
|
+
/**
|
|
592
|
+
* What those environment variables start with: `E2E_` by default, as in `E2E_PASSWORD`. Upper-case letters, digits
|
|
593
|
+
* and underscores, starting with a letter.
|
|
594
|
+
*/
|
|
595
|
+
envPrefix?: string;
|
|
596
|
+
/**
|
|
597
|
+
* `page` wraps the steps of each page in `await test.step('<path>', …)`, in a test (not in a page object or a
|
|
598
|
+
* helper); `none` (default) does not.
|
|
599
|
+
*/
|
|
600
|
+
testSteps?: 'none' | 'page';
|
|
601
|
+
/** Tags a new test gets, such as `@recorded`; a missing `@` is added. */
|
|
602
|
+
tags?: string[];
|
|
603
|
+
/** Annotations a new test gets, such as `{ type: 'piwi:owner', description: '@shop-team' }`. */
|
|
604
|
+
annotations?: Array<{
|
|
605
|
+
type: string;
|
|
606
|
+
description?: string;
|
|
607
|
+
}>;
|
|
608
|
+
}
|
|
609
|
+
/** Piwi's section of a Playwright config, `'@piwi'`. */
|
|
610
|
+
interface PiwiConfig {
|
|
611
|
+
/** How the code a recording writes looks. */
|
|
612
|
+
codegen?: PiwiCodegenConfig;
|
|
613
|
+
}
|
|
614
|
+
declare module '@playwright/test' {
|
|
615
|
+
interface Config<TestArgs, WorkerArgs> {
|
|
616
|
+
/** Piwi's options, read from the repository by the editor extensions: how the code a recording writes looks. */
|
|
617
|
+
'@piwi'?: PiwiConfig;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
561
621
|
/**
|
|
562
622
|
* Public API of `@piwitests/reporter`.
|
|
563
623
|
*
|
|
@@ -567,4 +627,4 @@ declare function extendPiwiAi<TestArgs extends FixtureArgs, WorkerArgs extends F
|
|
|
567
627
|
* exported from this file, it isn't part of the supported API.
|
|
568
628
|
*/
|
|
569
629
|
|
|
570
|
-
export { type AiMode, type AiOnMiss, type PiwiAi, type PiwiDashboardOptions, PiwiDashboardReporter, type ResolveSelectionOptions, type ResolveSelectionResult, createGlobalSetup, PiwiDashboardReporter as default, extendPiwiAi, piwiAiFixtures, resolveSelection, wrapConfig };
|
|
630
|
+
export { type AiMode, type AiOnMiss, type PiwiAi, type PiwiCodegenConfig, type PiwiConfig, type PiwiDashboardOptions, PiwiDashboardReporter, type ResolveSelectionOptions, type ResolveSelectionResult, createGlobalSetup, PiwiDashboardReporter as default, extendPiwiAi, piwiAiFixtures, resolveSelection, wrapConfig };
|