@piwitests/reporter 0.45.0 → 0.47.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.
@@ -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 };