@piwitests/reporter 0.54.0 → 0.56.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.
@@ -35,7 +35,7 @@ __export(global_setup_module_exports, {
35
35
  module.exports = __toCommonJS(global_setup_module_exports);
36
36
 
37
37
  // src/public/global-setup.ts
38
- var path5 = __toESM(require("path"));
38
+ var path7 = __toESM(require("path"));
39
39
 
40
40
  // src/internal/support/errors.ts
41
41
  function errorMessage(error) {
@@ -44,7 +44,7 @@ function errorMessage(error) {
44
44
  }
45
45
 
46
46
  // src/public/global-setup.ts
47
- var fs5 = __toESM(require("fs"));
47
+ var fs6 = __toESM(require("fs"));
48
48
 
49
49
  // src/internal/config/desktop.ts
50
50
  var fs = __toESM(require("fs"));
@@ -274,11 +274,15 @@ var HttpClient = class {
274
274
  * @param logger Prefixed logger for verbose diagnostics.
275
275
  * @param timeout Socket inactivity timeout in ms (default 30s), so a hung
276
276
  * server fails fast rather than stalling the reporter.
277
+ * @param uploadTimeout Socket inactivity timeout of a multipart upload in ms
278
+ * (default 10 min): the dashboard answers one only once it
279
+ * has stored every report and trace in it.
277
280
  */
278
- constructor(serverUrl, logger = new Logger(), timeout = 3e4) {
281
+ constructor(serverUrl, logger = new Logger(), timeout = 3e4, uploadTimeout = 10 * 60 * 1e3) {
279
282
  this.serverUrl = serverUrl;
280
283
  this.logger = logger;
281
284
  this.timeout = timeout;
285
+ this.uploadTimeout = uploadTimeout;
282
286
  /** Requests in flight, so `close` can abort them. */
283
287
  this.inFlight = /* @__PURE__ */ new Set();
284
288
  /** Set by `close`: every later request fails with this reason. */
@@ -426,7 +430,7 @@ var HttpClient = class {
426
430
  /** Send a multipart form-data POST request. Used for report and trace uploads. */
427
431
  async postFormData(pathname, form, auth) {
428
432
  const headers = form.getHeaders();
429
- const res = await this.request("POST", pathname, { headers, form, auth });
433
+ const res = await this.request("POST", pathname, { headers, form, auth, timeout: this.uploadTimeout });
430
434
  if (res.status < 200 || res.status >= 300) {
431
435
  this.logger.debugError(`Response: ${res.text}`);
432
436
  const detail = parseErrorMessage(res.text);
@@ -443,7 +447,7 @@ var HttpClient = class {
443
447
  }
444
448
  /** Unified request core: transport, headers, auth, response accumulation, timeout. */
445
449
  request(method, pathname, opts) {
446
- return new Promise((resolve2, reject) => {
450
+ return new Promise((resolve3, reject) => {
447
451
  if (this.closedReason !== null) {
448
452
  reject(new Error(this.closedReason));
449
453
  return;
@@ -466,7 +470,7 @@ var HttpClient = class {
466
470
  data += chunk;
467
471
  });
468
472
  res.on("end", () => {
469
- resolve2({ status: res.statusCode ?? 0, text: data, headers: res.headers });
473
+ resolve3({ status: res.statusCode ?? 0, text: data, headers: res.headers });
470
474
  });
471
475
  res.on("error", reject);
472
476
  res.on("close", () => {
@@ -479,8 +483,9 @@ var HttpClient = class {
479
483
  this.inFlight.add(req);
480
484
  req.on("close", () => this.inFlight.delete(req));
481
485
  req.on("error", reject);
482
- req.setTimeout(this.timeout, () => {
483
- req.destroy(new Error(`Request to ${pathname} timed out after ${this.timeout}ms`));
486
+ const timeout = opts.timeout ?? this.timeout;
487
+ req.setTimeout(timeout, () => {
488
+ req.destroy(new Error(`Request to ${pathname} timed out after ${timeout}ms`));
484
489
  });
485
490
  if (opts.form) {
486
491
  opts.form.on("error", (error) => req.destroy(error));
@@ -556,6 +561,16 @@ function resolveShardInfo(config, env = process.env) {
556
561
 
557
562
  // src/internal/collect/metadata-collector.ts
558
563
  var import_node_child_process = require("child_process");
564
+
565
+ // src/internal/support/test-paths.ts
566
+ var path2 = __toESM(require("path"));
567
+ function configDirFromConfig(config) {
568
+ if (config?.configFile) return path2.dirname(config.configFile);
569
+ if (config?.rootDir) return config.rootDir;
570
+ return process.cwd();
571
+ }
572
+
573
+ // src/internal/collect/metadata-collector.ts
559
574
  var DETACHED_HEAD = "HEAD";
560
575
  function resolveScmBranch(env, gitBranch) {
561
576
  const pick = (...names) => {
@@ -585,22 +600,22 @@ function normalizeRef(ref) {
585
600
  }
586
601
 
587
602
  // src/internal/support/setup-file.ts
588
- var path2 = __toESM(require("path"));
603
+ var path3 = __toESM(require("path"));
589
604
  var os3 = __toESM(require("os"));
590
605
  var fs2 = __toESM(require("fs"));
591
606
  function getSetupFilePath(projectName) {
592
- return path2.join(os3.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
607
+ return path3.join(os3.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
593
608
  }
594
609
 
595
610
  // src/internal/support/aria-sampling.ts
596
- var path3 = __toESM(require("path"));
611
+ var path4 = __toESM(require("path"));
597
612
  var os4 = __toESM(require("os"));
598
613
  var fs3 = __toESM(require("fs"));
599
614
  function ariaSampleIdentity(filePath, title) {
600
615
  return `${filePath}\0${title}`;
601
616
  }
602
617
  function getAriaSampleFilePath(projectName) {
603
- return path3.join(os4.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
618
+ return path4.join(os4.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
604
619
  }
605
620
  function writeAriaSampleFile(projectName, identities) {
606
621
  try {
@@ -713,8 +728,8 @@ function requestError(error) {
713
728
  return first.replace(/^apiRequestContext\.\w+:\s*/, "");
714
729
  }
715
730
  var playwrightProbe = async (target, timeoutMs) => {
716
- const { request } = await import("@playwright/test");
717
- const context = await request.newContext({ ignoreHTTPSErrors: target.ignoreHTTPSErrors, proxy: target.proxy });
731
+ const { request: request3 } = await import("@playwright/test");
732
+ const context = await request3.newContext({ ignoreHTTPSErrors: target.ignoreHTTPSErrors, proxy: target.proxy });
718
733
  try {
719
734
  const response = await context.get(target.url, { timeout: timeoutMs, maxRedirects: 0, failOnStatusCode: false });
720
735
  return { status: response.status() };
@@ -740,7 +755,7 @@ async function checkBaseUrl(target, options = {}) {
740
755
  } else {
741
756
  reason = outcome.error;
742
757
  }
743
- if (attempt < attempts) await new Promise((resolve2) => setTimeout(resolve2, delayMs));
758
+ if (attempt < attempts) await new Promise((resolve3) => setTimeout(resolve3, delayMs));
744
759
  }
745
760
  return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
746
761
  }
@@ -765,7 +780,13 @@ async function checkBaseUrls(config, options = {}) {
765
780
 
766
781
  // src/internal/manifest/collect.ts
767
782
  var fs4 = __toESM(require("fs"));
768
- var path4 = __toESM(require("path"));
783
+ var path5 = __toESM(require("path"));
784
+
785
+ // src/internal/transport/backend-request.ts
786
+ var http2 = __toESM(require("http"));
787
+ var https2 = __toESM(require("https"));
788
+
789
+ // src/internal/manifest/collect.ts
769
790
  var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
770
791
  var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
771
792
  var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
@@ -793,7 +814,7 @@ function hasInstrumentationHeader(headers) {
793
814
  return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
794
815
  }
795
816
  function readCommittedManifest(configDir) {
796
- const file = path4.join(configDir, COMMITTED_MANIFEST_FILE);
817
+ const file = path5.join(configDir, COMMITTED_MANIFEST_FILE);
797
818
  try {
798
819
  if (!fs4.existsSync(file)) return null;
799
820
  return parseManifestJson(fs4.readFileSync(file, "utf8"));
@@ -801,34 +822,20 @@ function readCommittedManifest(configDir) {
801
822
  return null;
802
823
  }
803
824
  }
804
- function configDirFromConfig(config) {
805
- if (config?.configFile) return path4.dirname(config.configFile);
806
- if (config?.rootDir) return config.rootDir;
807
- return process.cwd();
808
- }
809
- function baseUrlFromConfig(config) {
810
- const top = config?.use?.baseURL;
811
- if (typeof top === "string" && top) return top;
812
- for (const project of config?.projects ?? []) {
813
- const url = project?.use?.baseURL;
814
- if (typeof url === "string" && url) return url;
815
- }
816
- return null;
817
- }
818
825
  var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
826
+ var MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
819
827
  function instrumentationManifestUrls(baseUrl) {
820
828
  const base = new URL(baseUrl);
821
829
  const root = new URL(INSTRUMENTATION_MANIFEST_PATH, base).toString();
822
- const path6 = base.pathname.replace(/\/+$/, "");
823
- return path6 ? [new URL(`${path6}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
830
+ const path8 = base.pathname.replace(/\/+$/, "");
831
+ return path8 ? [new URL(`${path8}${INSTRUMENTATION_MANIFEST_PATH}`, base).toString(), root] : [root];
824
832
  }
825
833
  async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
826
834
  try {
827
835
  const probe = await fetchImpl(baseUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
828
836
  if (!hasInstrumentationHeader(probe.headers)) return null;
829
837
  for (const manifestUrl of instrumentationManifestUrls(baseUrl)) {
830
- const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
831
- const manifest = res.ok ? parseManifestJson(await res.text()) : null;
838
+ const manifest = await fetchManifestAt(manifestUrl, fetchImpl);
832
839
  if (manifest) return manifest;
833
840
  }
834
841
  return null;
@@ -836,18 +843,55 @@ async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
836
843
  return null;
837
844
  }
838
845
  }
846
+ async function fetchManifestAt(manifestUrl, fetchImpl) {
847
+ const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
848
+ return res.ok ? parseManifestJson(await res.text()) : null;
849
+ }
850
+
851
+ // src/internal/manifest/run-manifests.ts
852
+ async function lookupProjectId(httpClient, auth, projectName) {
853
+ const menu = await httpClient.getJSON("/api/projects/menu", auth);
854
+ return menu?.items?.find(
855
+ (p) => p.name.toLowerCase() === projectName.toLowerCase()
856
+ )?.id;
857
+ }
858
+ async function uploadManifest(httpClient, auth, projectId, upload, logger) {
859
+ await httpClient.putJSON(`/api/projects/${projectId}/surface/manifest`, upload, auth).catch((e) => logger.debug(`Manifest upload (${upload.source}) skipped: ${errorMessage(e)}`));
860
+ }
861
+
862
+ // src/internal/collect/source-checksums.ts
863
+ var crypto2 = __toESM(require("crypto"));
864
+ var fs5 = __toESM(require("fs"));
865
+ var path6 = __toESM(require("path"));
866
+
867
+ // src/internal/collect/server-coverage.ts
868
+ var DEFAULT_COLLECTOR_LIMITS = {
869
+ batchTests: 25,
870
+ batchDelayMs: 1e3,
871
+ callTimeoutMs: 1e4,
872
+ runTimeoutMs: 3e4,
873
+ maxIdsPerCall: 1e4,
874
+ maxBodyBytes: 1024 * 1024 - 4096,
875
+ maxIdsPerTest: 5e4,
876
+ concurrency: 2
877
+ };
878
+ var MAX_ANSWER_BYTES = 64 * 1024 * 1024;
879
+ function baseUrlsFromConfig(config) {
880
+ const urls = [config?.use?.baseURL, ...(config?.projects ?? []).map((project) => project?.use?.baseURL)];
881
+ return [...new Set(urls.filter((url) => typeof url === "string" && url !== ""))];
882
+ }
839
883
 
840
884
  // src/public/global-setup.ts
841
885
  function createGlobalSetup(options, userSetup) {
842
886
  return async function globalSetupFn(config) {
843
- const piwiReporterPath = path5.resolve(__dirname, "./index.js");
887
+ const piwiReporterPath = path7.resolve(__dirname, "./index.js");
844
888
  let inlineReporterOptions = {};
845
889
  if (Array.isArray(config?.reporter)) {
846
890
  for (const r of config.reporter) {
847
891
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
848
892
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
849
893
  try {
850
- return path5.resolve(require.resolve(r[0])) === piwiReporterPath;
894
+ return path7.resolve(require.resolve(r[0])) === piwiReporterPath;
851
895
  } catch {
852
896
  return false;
853
897
  }
@@ -883,7 +927,7 @@ function createGlobalSetup(options, userSetup) {
883
927
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
884
928
  if (r[0].toLowerCase().includes("piwi")) return true;
885
929
  try {
886
- return path5.resolve(require.resolve(r[0])) === piwiReporterPath;
930
+ return path7.resolve(require.resolve(r[0])) === piwiReporterPath;
887
931
  } catch {
888
932
  return false;
889
933
  }
@@ -913,7 +957,7 @@ function createGlobalSetup(options, userSetup) {
913
957
  auth
914
958
  );
915
959
  if (response?.runId && response?.setupToken) {
916
- fs5.writeFileSync(
960
+ fs6.writeFileSync(
917
961
  getSetupFilePath(opts.projectName),
918
962
  JSON.stringify({
919
963
  runId: response.runId,
@@ -926,14 +970,11 @@ function createGlobalSetup(options, userSetup) {
926
970
  if (opts.projectName) clearAriaSampleFile(opts.projectName);
927
971
  const wantsAria = opts.sampleAriaOnPass !== false && !!opts.projectName;
928
972
  const committedManifest = opts.uploadManifest !== false ? readCommittedManifest(configDirFromConfig(config)) : null;
929
- const manifestBaseUrl = opts.uploadManifest !== false ? baseUrlFromConfig(config) : null;
930
- const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || !!manifestBaseUrl);
973
+ const manifestBaseUrls = opts.uploadManifest !== false ? baseUrlsFromConfig(config) : [];
974
+ const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || manifestBaseUrls.length > 0);
931
975
  let projectId;
932
976
  if ((wantsAria || wantsManifest) && opts.projectName) {
933
- const menu = await httpClient.getJSON("/api/projects/menu", auth);
934
- projectId = menu?.items?.find(
935
- (p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
936
- )?.id;
977
+ projectId = await lookupProjectId(httpClient, auth, opts.projectName);
937
978
  }
938
979
  if (wantsAria && projectId != null) {
939
980
  const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
@@ -947,20 +988,18 @@ function createGlobalSetup(options, userSetup) {
947
988
  if (wantsManifest && projectId != null) {
948
989
  const branch = resolveScmBranch(process.env) ?? null;
949
990
  if (committedManifest) {
950
- await httpClient.putJSON(
951
- `/api/projects/${projectId}/surface/manifest`,
991
+ await uploadManifest(
992
+ httpClient,
993
+ auth,
994
+ projectId,
952
995
  { source: "committed", branch, manifest: committedManifest },
953
- auth
954
- ).catch((e) => logger.debug(`Manifest upload (committed) skipped: ${errorMessage(e)}`));
996
+ logger
997
+ );
955
998
  }
956
- if (manifestBaseUrl) {
957
- const declared = await fetchInstrumentationManifest(manifestBaseUrl);
958
- if (declared) {
959
- await httpClient.putJSON(
960
- `/api/projects/${projectId}/surface/manifest`,
961
- { source: "instrumentation", branch, manifest: declared },
962
- auth
963
- ).catch((e) => logger.debug(`Manifest upload (instrumentation) skipped: ${errorMessage(e)}`));
999
+ const declared = await Promise.all(manifestBaseUrls.map((url) => fetchInstrumentationManifest(url)));
1000
+ for (const manifest of declared) {
1001
+ if (manifest) {
1002
+ await uploadManifest(httpClient, auth, projectId, { source: "instrumentation", branch, manifest }, logger);
964
1003
  }
965
1004
  }
966
1005
  }
package/dist/index.d.ts CHANGED
@@ -90,8 +90,14 @@ interface PiwiDashboardOptions {
90
90
  * repository root, which is always tried last. Defaults to the Playwright
91
91
  * config's directory. Set it to the dev server's root when that is not the
92
92
  * config's directory (Nuxt: `['app']`).
93
+ *
94
+ * An object gives directories per origin instead, for an application whose
95
+ * tests reach several dev servers that serve modules at the same paths (one per
96
+ * portal): a script resolves against the directories of its own origin, else
97
+ * of the origin of the page that loaded it, then those under `'*'`
98
+ * (`{ 'http://localhost:3001': 'portals/admin', '*': ['app'] }`).
93
99
  */
94
- codeReachRoots?: string[];
100
+ codeReachRoots?: string[] | Record<string, string | string[]>;
95
101
  /**
96
102
  * Record, per test, which source code ran, in the browser and in the
97
103
  * backend, and send it with the test as its coverage. Rides the capture
@@ -102,10 +108,13 @@ interface PiwiDashboardOptions {
102
108
  /**
103
109
  * What the browser side records: `'files'`, the application files whose
104
110
  * functions ran, from Chromium's JavaScript coverage, which is what
105
- * `captureCodeReach: true` records; `'off'` records nothing. Only paths are
106
- * sent. Defaults to `'off'`. Can also be set with `PIWI_COVERAGE_BROWSER`.
111
+ * `captureCodeReach: true` records; `'functions'` adds the start and end
112
+ * line of each function that ran, read through the dev server's modules or
113
+ * the bundle's source maps; `'off'` records nothing. Paths, and line numbers
114
+ * with `'functions'`, are sent, never source code or hit counts. Defaults to
115
+ * `'off'`. Can also be set with `PIWI_COVERAGE_BROWSER`.
107
116
  */
108
- browser?: 'off' | 'files';
117
+ browser?: 'off' | 'files' | 'functions';
109
118
  /**
110
119
  * What the server side records, from a backend built with Piwi's coverage
111
120
  * instrumentation (ASP.NET Core: `-p:PiwiCoverage=true`), which names each
@@ -170,10 +179,11 @@ interface PiwiDashboardOptions {
170
179
  */
171
180
  sampleAriaOnPass?: boolean;
172
181
  /**
173
- * Upload the application's declared surface at run start: a committed
174
- * `piwi.manifest.json` next to the Playwright config, and — when the base URL's
175
- * first response carries an instrumentation header — the instrumentation
176
- * package's `/__piwi/manifest`. A route or page the manifest declares that no
182
+ * Upload the application's declared surface: at run start, a committed
183
+ * `piwi.manifest.json` next to the Playwright config and the instrumentation
184
+ * package's `/__piwi/manifest` of each `baseURL` whose first response carries an
185
+ * instrumentation header; as the run's last test ends, the manifest of every
186
+ * other origin whose responses carried one. A route or page the manifest declares that no
177
187
  * test reaches becomes a "declared, never hit" gap. Defaults to `true`; set to
178
188
  * `false` (or `PIWI_UPLOAD_MANIFEST=false`) to never upload.
179
189
  */
@@ -441,6 +451,8 @@ declare class PiwiDashboardReporter {
441
451
  private reportedTestIds;
442
452
  /** Per-test attempt history, keyed by `test.id`; snapshotted onto every attempt's payload. */
443
453
  private attemptsByTest;
454
+ /** The folder test files are named relative to: the Playwright config's, set in `onBegin`. */
455
+ private testPathBase;
444
456
  /** Derived in `onBegin`, once the shard is known. */
445
457
  private instanceId;
446
458
  private shardInfo;
@@ -476,8 +488,10 @@ declare class PiwiDashboardReporter {
476
488
  private readonly coverageSettings;
477
489
  /** Each test's coverage, when browser or server coverage is on; set in `onBegin`. */
478
490
  private coverage;
479
- /** Tells when the run's last test ended, while server coverage is on. */
491
+ /** Tells when the run's last test ended, while server coverage needs it. */
480
492
  private lastTestWatch;
493
+ /** The manifests of the instrumented origins the run reaches beyond its base URLs. */
494
+ private manifests;
481
495
  /** Result events that stream once their test's server coverage is in. */
482
496
  private coverageSends;
483
497
  private httpClient;
@@ -492,6 +506,13 @@ declare class PiwiDashboardReporter {
492
506
  static wrapConfig: typeof wrapConfig;
493
507
  static createGlobalSetup: typeof createGlobalSetup;
494
508
  constructor(rawOptions?: Record<string, any>);
509
+ /**
510
+ * Playwright reporter hook: whether this reporter writes the run's progress to
511
+ * the terminal. It does not (it prints only its own `[Piwi Dashboard]` lines),
512
+ * so Playwright keeps its own terminal reporter when Piwi is the only one
513
+ * configured.
514
+ */
515
+ printsToStdio(): boolean;
495
516
  /** Playwright reporter hook: called once at the start of the test run */
496
517
  onBegin(config: FullConfig, suite: Suite): void;
497
518
  /** Playwright reporter hook: called when an individual test begins */