@piwitests/reporter 0.44.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.
@@ -83,9 +83,12 @@ var DEFAULTS = {
83
83
  // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
84
84
  captureCodeReach: false,
85
85
  captureServerTraces: true,
86
+ captureResources: true,
87
+ leakCheck: "report",
86
88
  sampleAriaOnPass: true,
87
89
  uploadManifest: true,
88
90
  defaultCapture: true,
91
+ checkBaseUrl: false,
89
92
  streaming: true,
90
93
  streamingBatchSize: 5,
91
94
  streamingBatchDelay: 2e3,
@@ -123,9 +126,12 @@ var PIWI_ENV_KEYS = {
123
126
  captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
124
127
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
125
128
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
129
+ captureResources: "PIWI_CAPTURE_RESOURCES",
130
+ leakCheck: "PIWI_LEAK_CHECK",
126
131
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
127
132
  uploadManifest: "PIWI_UPLOAD_MANIFEST",
128
133
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
134
+ checkBaseUrl: "PIWI_CHECK_BASE_URL",
129
135
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
130
136
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
131
137
  outputFile: "PIWI_OUTPUT_FILE",
@@ -168,9 +174,12 @@ var ENV_FALLBACK_SPECS = [
168
174
  { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
169
175
  { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
170
176
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
177
+ { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
178
+ { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
171
179
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
172
180
  { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
173
181
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
182
+ { option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
174
183
  { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
175
184
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
176
185
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
@@ -613,6 +622,131 @@ function isListMode(argv = process.argv) {
613
622
  return rest.some((tok) => PW_LIST_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
614
623
  }
615
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
+
616
750
  // src/internal/manifest/collect.ts
617
751
  var fs4 = __toESM(require("fs"));
618
752
  var path4 = __toESM(require("path"));
@@ -712,6 +846,10 @@ function createGlobalSetup(options, userSetup) {
712
846
  if (userSetup) return userSetup(config);
713
847
  return;
714
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
+ }
715
853
  if (opts.enabled === false || !opts.serverUrl) {
716
854
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
717
855
  if (userSetup) return userSetup(config);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
2
  import { PlaywrightTestConfig, Locator, TestType, Page, Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions } from '@playwright/test';
3
3
  export { PlaywrightTestConfig } from '@playwright/test';
4
- export { PiwiFixtures, extendPiwiFixtures, piwiFixtures } from './internal/capture/capture-fixtures.js';
4
+ export { PiwiFixtures, PiwiWorkerFixtures, extendPiwiFixtures, piwiFixtures } from './internal/capture/capture-fixtures.js';
5
5
  import '@piwitests/picker-dom';
6
6
  import '@piwitests/core/wire';
7
7
  import '@piwitests/core/code-reach';
@@ -100,6 +100,35 @@ interface PiwiDashboardOptions {
100
100
  * `false`. Can also be forced off with `PIWI_CAPTURE_SERVER_TRACES=false`.
101
101
  */
102
102
  captureServerTraces?: boolean;
103
+ /**
104
+ * Keep a ledger of the browsers, contexts, pages and API request contexts each
105
+ * test opens, and list at the end of the run the ones left open after the
106
+ * scope that created them ended (a context a test never closed, a `beforeAll`
107
+ * context no `afterAll` closes, a browser a test launched itself), the pages
108
+ * opened and never used, and the pages, listeners and route handlers piling up
109
+ * on a page or context that lives across tests. Each finding names the line or
110
+ * the fixture that opened the object. Rides the capture fixtures and costs a
111
+ * few bookkeeping steps per test. Also samples the run's processes and the
112
+ * machine, with or without the fixtures, and prints what the run cost: CPU by
113
+ * process, time spent waiting for a CPU, peak memory and disk. The findings,
114
+ * the run's cost and each test's go to the dashboard with the run, on its
115
+ * Resources tab. Defaults to `true`. Set to `false` (or
116
+ * `PIWI_CAPTURE_RESOURCES=false`) to turn both off.
117
+ */
118
+ captureResources?: boolean;
119
+ /**
120
+ * What the capture fixtures do when a test leaves open a browser, context,
121
+ * page or API request context that the test itself opened (its body or its
122
+ * `beforeEach`/`afterEach` hooks; a fixture's objects are the fixture's):
123
+ * `'report'` lists it in the end-of-run summary, `'fail'` also fails the test
124
+ * with the line that opened it, and `'close'` closes it when the test ends and
125
+ * still lists it. A `beforeAll` object left open is only ever reported. Each
126
+ * test is judged when it ends, so a test that hands what it opened to the next
127
+ * one fails under `'fail'` and loses it under `'close'`: open such objects in
128
+ * `beforeAll` and close them in `afterAll`. Defaults to `'report'`. Can also
129
+ * be set with `PIWI_LEAK_CHECK`.
130
+ */
131
+ leakCheck?: 'report' | 'fail' | 'close';
103
132
  /**
104
133
  * Sample the ARIA snapshot at the end of a *passing* test, so a later failure
105
134
  * can be diffed against the page as it last looked when green. Rate-limited by
@@ -136,6 +165,20 @@ interface PiwiDashboardOptions {
136
165
  * to opt out and let Playwright's own defaults stand.
137
166
  */
138
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;
139
182
  /**
140
183
  * Open Piwi's own failure-time overlay on the failing page — for inspecting
141
184
  * the page and picking a locator for any element (click an element → confirm
@@ -299,6 +342,10 @@ interface PiwiDashboardOptions {
299
342
  * The server-run ID and a one-time token are written to a temp file so the
300
343
  * reporter instance can pick them up during the streaming handshake.
301
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
+ *
302
349
  * @param options Piwi Dashboard options (uses `serverUrl`, `projectName`, …).
303
350
  * @param userSetup An existing global setup to chain after the Piwi registration.
304
351
  */
@@ -375,6 +422,20 @@ declare class PiwiDashboardReporter {
375
422
  private filterDetails;
376
423
  /** Configured `maxFailures` (0 = unlimited) — disambiguates an interrupted run's unrun reason. */
377
424
  private maxFailures;
425
+ /** The resource censuses the capture fixtures attached, one per test attempt. */
426
+ private resourceCensuses;
427
+ /** Lifecycle steps of the tests that ran without the fixtures. */
428
+ private lifecycleTallies;
429
+ /** The user fixtures set up by the tests that left a page unused, by test id. */
430
+ private fixturesByTest;
431
+ /** The file workers append their shutdown census to, set in `onBegin`. */
432
+ private resourcesFile;
433
+ /** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
434
+ private sampler;
435
+ /** Bytes of the files the tests attached, by kind. */
436
+ private artifactBytes;
437
+ /** The run's resource report for the dashboard, built in `onEnd`. */
438
+ private resourceReport;
378
439
  private httpClient;
379
440
  private uploader;
380
441
  private fileHandler;
@@ -408,6 +469,17 @@ declare class PiwiDashboardReporter {
408
469
  onStepEnd(test: TestCase | undefined, _result: TestResult | undefined, step: any): void;
409
470
  /** Playwright reporter hook: called when an individual test finishes */
410
471
  onTestEnd(test: TestCase, result: TestResult): void;
472
+ /**
473
+ * Add up the files a test attached, and keep its resource census; for a test
474
+ * that ran without the capture fixtures, tally the browsers and contexts its
475
+ * steps opened and closed.
476
+ */
477
+ private collectResources;
478
+ /**
479
+ * Print what the run left open or opened for nothing, from the censuses and
480
+ * the workers' shutdown file, then what the run cost the machine.
481
+ */
482
+ private reportResources;
411
483
  /**
412
484
  * Synthesize `didnotrun` cases for tests Playwright planned but never reported
413
485
  * (no `onTestEnd`) — typically because `maxFailures` cut the run short. These
@@ -504,6 +576,48 @@ declare function extendPiwiAi<TestArgs extends FixtureArgs, WorkerArgs extends F
504
576
  page: Page & PiwiAi;
505
577
  }, WorkerArgs>;
506
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
+
507
621
  /**
508
622
  * Public API of `@piwitests/reporter`.
509
623
  *
@@ -513,4 +627,4 @@ declare function extendPiwiAi<TestArgs extends FixtureArgs, WorkerArgs extends F
513
627
  * exported from this file, it isn't part of the supported API.
514
628
  */
515
629
 
516
- 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 };