@piwitests/reporter 0.60.0 → 0.61.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 CHANGED
@@ -932,6 +932,7 @@ var PIWI_ENV_KEYS = {
932
932
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
933
933
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
934
934
  coverageServer: "PIWI_COVERAGE_SERVER",
935
+ coverageCss: "PIWI_COVERAGE_CSS",
935
936
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
936
937
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
937
938
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -2320,6 +2321,8 @@ var STEPS_LIMITS = {
2320
2321
  /** Titles, names, texts, locators. */
2321
2322
  textLength: 500,
2322
2323
  viewports: 50,
2324
+ /** The options a manual step's instruction names. */
2325
+ options: 50,
2323
2326
  /** The widest and tallest viewport, in CSS pixels. */
2324
2327
  viewportSize: 1e4,
2325
2328
  /** The browser's zoom factors a viewport may name, as browsers allow them. */
@@ -2491,12 +2494,71 @@ function checkManual(c, v, path25) {
2491
2494
  }
2492
2495
  const description = c.text(v.description, `${path25}.description`, STEPS_LIMITS.valueLength, false);
2493
2496
  if (description != null && !description.trim()) c.fail(`${path25}.description`, "must not be empty");
2497
+ const instruction = readInstruction(v.instruction);
2494
2498
  return {
2495
2499
  description: description ?? "",
2496
2500
  location: c.text(v.location, `${path25}.location`, STEPS_LIMITS.textLength, true),
2497
- call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true)
2501
+ call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true),
2502
+ ...instruction ? { instruction } : {}
2498
2503
  };
2499
2504
  }
2505
+ var isText = (v) => typeof v === "string" && v.length <= STEPS_LIMITS.valueLength;
2506
+ var isTextOrNull = (v) => v == null || isText(v);
2507
+ var isCount = (v) => Number.isInteger(v) && v >= 0;
2508
+ var INSTRUCTION_FIELDS = {
2509
+ locator: isText,
2510
+ call: isText,
2511
+ matcher: isText,
2512
+ what: isText,
2513
+ expected: isTextOrNull,
2514
+ received: isTextOrNull,
2515
+ x: (v) => typeof v === "number" && Number.isFinite(v),
2516
+ y: (v) => typeof v === "number" && Number.isFinite(v),
2517
+ count: isCount,
2518
+ limit: isCount,
2519
+ options: (v) => Array.isArray(v) && v.length > 0 && v.length <= STEPS_LIMITS.options && v.every((o) => isText(o) || isCount(o))
2520
+ };
2521
+ var CHECK_VALUES = ["expected", "received"];
2522
+ var INSTRUCTION_KINDS = {
2523
+ goBack: [],
2524
+ goForward: [],
2525
+ reload: [],
2526
+ gotoUnknown: [],
2527
+ setContent: [],
2528
+ keyboardType: [],
2529
+ mouseAt: ["x", "y", "call"],
2530
+ mouseOrKeyboard: ["call"],
2531
+ failedCall: ["call"],
2532
+ locatorCall: ["locator", "call"],
2533
+ fillUnknown: ["locator"],
2534
+ fillPartial: ["locator"],
2535
+ pressUnknown: ["locator"],
2536
+ selectOptionUnknown: ["locator"],
2537
+ setInputFiles: ["locator"],
2538
+ dragTo: ["locator"],
2539
+ drop: ["locator"],
2540
+ focus: ["locator"],
2541
+ blur: ["locator"],
2542
+ dispatchEvent: ["locator"],
2543
+ scrollIntoViewIfNeeded: ["locator"],
2544
+ selectOptions: ["locator", "options"],
2545
+ expectPoll: ["matcher", ...CHECK_VALUES],
2546
+ expectValue: ["what", ...CHECK_VALUES],
2547
+ expectLocator: ["locator", "matcher", ...CHECK_VALUES],
2548
+ expectURL: CHECK_VALUES,
2549
+ leadingSteps: ["count", "limit"]
2550
+ };
2551
+ function readInstruction(v) {
2552
+ if (!isObject(v) || typeof v.kind !== "string" || !Object.prototype.hasOwnProperty.call(INSTRUCTION_KINDS, v.kind))
2553
+ return null;
2554
+ const kind = v.kind;
2555
+ const out = { kind };
2556
+ for (const field of INSTRUCTION_KINDS[kind]) {
2557
+ if (!INSTRUCTION_FIELDS[field](v[field])) return null;
2558
+ out[field] = v[field] ?? null;
2559
+ }
2560
+ return out;
2561
+ }
2500
2562
  function checkViewports(c, v, stepCount) {
2501
2563
  if (v == null) return [];
2502
2564
  if (!Array.isArray(v)) {
@@ -6308,7 +6370,7 @@ function attributeMatches(value, exact, actual) {
6308
6370
 
6309
6371
  // ../core/src/locator-break.ts
6310
6372
  var TEXT_KINDS = /* @__PURE__ */ new Set(["text", "translation", "literal"]);
6311
- function isText(anchor) {
6373
+ function isText2(anchor) {
6312
6374
  return TEXT_KINDS.has(anchor.kind);
6313
6375
  }
6314
6376
  function hasAttribute(anchor, names) {
@@ -6407,7 +6469,7 @@ function directSlots(call, testIds) {
6407
6469
  const hasTextSlot = (index) => {
6408
6470
  const hasText = optionsOf(call.args[index]).get("hasText");
6409
6471
  if (!hasText) return null;
6410
- return stringSlot(hasText, false, "text", isText, (next) => withOption(call, index, "hasText", next));
6472
+ return stringSlot(hasText, false, "text", isText2, (next) => withOption(call, index, "hasText", next));
6411
6473
  };
6412
6474
  const textOr = (attribute) => (a) => a.kind === "translation" || a.kind === "literal" || hasAttribute(a, [attribute]);
6413
6475
  switch (call.method) {
@@ -6420,16 +6482,16 @@ function directSlots(call, testIds) {
6420
6482
  opts.get("name"),
6421
6483
  exact,
6422
6484
  "name",
6423
- (a) => isText(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
6485
+ (a) => isText2(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
6424
6486
  (next) => withOption(call, 1, "name", next)
6425
6487
  )
6426
6488
  );
6427
6489
  break;
6428
6490
  case "getByText":
6429
- slots.push(argSlot("text", isText));
6491
+ slots.push(argSlot("text", isText2));
6430
6492
  break;
6431
6493
  case "getByLabel":
6432
- slots.push(argSlot("text", (a) => isText(a) || hasAttribute(a, ["aria-label"])));
6494
+ slots.push(argSlot("text", (a) => isText2(a) || hasAttribute(a, ["aria-label"])));
6433
6495
  break;
6434
6496
  case "getByPlaceholder":
6435
6497
  slots.push(argSlot("text", textOr("placeholder")));
@@ -202,6 +202,7 @@ var PIWI_ENV_KEYS = {
202
202
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
203
203
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
204
204
  coverageServer: "PIWI_COVERAGE_SERVER",
205
+ coverageCss: "PIWI_COVERAGE_CSS",
205
206
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
206
207
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
207
208
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -321,10 +322,14 @@ function resolveOptions(raw, configDir) {
321
322
  const coverage = { ...mergedRaw.coverage };
322
323
  const coverageBrowser = env[PIWI_ENV_KEYS.coverageBrowser];
323
324
  const coverageServer = env[PIWI_ENV_KEYS.coverageServer];
325
+ const coverageCss = env[PIWI_ENV_KEYS.coverageCss];
324
326
  if (coverage.browser === void 0 && coverageBrowser)
325
327
  coverage.browser = coverageBrowser;
326
328
  if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
327
- if (coverage.browser !== void 0 || coverage.server !== void 0) mergedRaw.coverage = coverage;
329
+ if (coverage.css === void 0 && coverageCss) coverage.css = coverageCss;
330
+ if (coverage.browser !== void 0 || coverage.server !== void 0 || coverage.css !== void 0) {
331
+ mergedRaw.coverage = coverage;
332
+ }
328
333
  const externalId = { ...mergedRaw.externalId };
329
334
  const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
330
335
  const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
@@ -736,6 +741,7 @@ function isListMode(argv = process.argv) {
736
741
  }
737
742
 
738
743
  // src/internal/support/base-url-check.ts
744
+ var net = __toESM(require("net"));
739
745
  var BASE_URL_CHECK_ATTEMPTS = 3;
740
746
  var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
741
747
  var BASE_URL_CHECK_DELAY_MS = 2e3;
@@ -757,9 +763,9 @@ function cliProjectNames(argv = process.argv) {
757
763
  }
758
764
  return names;
759
765
  }
760
- function wildcard(pattern) {
766
+ function wildcard(pattern, flags = "i") {
761
767
  const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
762
- return new RegExp(`^${escaped.join(".*")}$`, "i");
768
+ return new RegExp(`^${escaped.join(".*")}$`, flags);
763
769
  }
764
770
  function selectedProjects(projects, argv = process.argv, filtered) {
765
771
  let chosen;
@@ -769,7 +775,7 @@ function selectedProjects(projects, argv = process.argv, filtered) {
769
775
  } else {
770
776
  const names = cliProjectNames(argv);
771
777
  if (!names || names.length === 0) return projects;
772
- const patterns = names.map(wildcard);
778
+ const patterns = names.map((name) => wildcard(name));
773
779
  chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
774
780
  }
775
781
  if (!argv.includes("--no-deps")) {
@@ -791,23 +797,58 @@ function asProxy(value) {
791
797
  const proxy = value;
792
798
  return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
793
799
  }
794
- function baseUrlTargets(config, argv = process.argv) {
800
+ function exposeNetworkCovers(rule, url) {
801
+ if (!rule) return false;
802
+ let parsed;
803
+ try {
804
+ parsed = new URL(url);
805
+ } catch {
806
+ return false;
807
+ }
808
+ const host = parsed.hostname;
809
+ const port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
810
+ return rule.split(",").some((token) => {
811
+ const match = token.match(/^(.*?)(?::(\d+))?$/);
812
+ if (!match) return false;
813
+ if (match[2] !== void 0 && Number(match[2]) !== port) return false;
814
+ const tokenHost = match[1];
815
+ if (tokenHost === "<loopback>") {
816
+ return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "[::1]";
817
+ }
818
+ if (tokenHost === "*") return true;
819
+ if (net.isIP(tokenHost)) return host === tokenHost;
820
+ if (net.isIP(host)) return false;
821
+ return wildcard(tokenHost.startsWith(".") ? `*${tokenHost}` : tokenHost, "").test(host);
822
+ });
823
+ }
824
+ function reachedRemotely(use, url, env) {
825
+ if (use?.connectOptions === void 0) {
826
+ return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
827
+ }
828
+ const connect = use.connectOptions;
829
+ if (!connect || typeof connect !== "object" || typeof connect.wsEndpoint !== "string") return false;
830
+ return !exposeNetworkCovers(typeof connect.exposeNetwork === "string" ? connect.exposeNetwork : void 0, url);
831
+ }
832
+ function baseUrlTargets(config, argv = process.argv, env = process.env) {
795
833
  const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
796
834
  const filtered = config.projects?.length ? config.filteredProjects : void 0;
797
835
  const targets = /* @__PURE__ */ new Map();
798
836
  for (const project of selectedProjects(projects, argv, filtered)) {
799
837
  const url = project.use?.baseURL;
800
838
  if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
839
+ const remoteBrowser = reachedRemotely(project.use, url, env);
801
840
  const target = targets.get(url);
802
841
  if (target) {
803
842
  if (project.name) target.projects.push(project.name);
843
+ target.remoteBrowser ||= remoteBrowser;
804
844
  continue;
805
845
  }
806
846
  targets.set(url, {
807
847
  url,
808
848
  projects: project.name ? [project.name] : [],
809
849
  ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
810
- proxy: asProxy(project.use?.proxy)
850
+ proxy: asProxy(project.use?.proxy),
851
+ remoteBrowser
811
852
  });
812
853
  }
813
854
  return [...targets.values()];
@@ -848,14 +889,21 @@ async function checkBaseUrl(target, options = {}) {
848
889
  }
849
890
  return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
850
891
  }
892
+ function remoteBrowserNote(target) {
893
+ if (!target.remoteBrowser) return null;
894
+ const { host } = new URL(target.url);
895
+ return `The check ran from this machine's network, but a project's remote browser reaches ${host} from its own, since its exposeNetwork rule does not cover that address.`;
896
+ }
851
897
  async function checkBaseUrls(config, options = {}) {
852
- const targets = baseUrlTargets(config, options.argv);
898
+ const targets = baseUrlTargets(config, options.argv, options.env);
853
899
  const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
854
900
  const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
855
901
  if (down.length === 0) return targets;
856
902
  const lines = down.map(({ target, reason }) => {
857
903
  const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
858
- return ` ${target.url}${used}: ${reason}`;
904
+ const note = remoteBrowserNote(target);
905
+ return ` ${target.url}${used}: ${reason}${note ? `
906
+ ${note}` : ""}`;
859
907
  });
860
908
  const message = [
861
909
  `[Piwi Dashboard] The app under test did not answer, so no test ran:`,
@@ -1008,7 +1056,16 @@ function createGlobalSetup(options, userSetup) {
1008
1056
  }
1009
1057
  if (opts.checkBaseUrl === true) {
1010
1058
  const checked = await checkBaseUrls(config ?? {});
1011
- for (const target of checked) logger.debug(`Base URL answered: ${target.url}`);
1059
+ for (const target of checked) {
1060
+ const note = remoteBrowserNote(target);
1061
+ if (note) {
1062
+ logger.info(
1063
+ `Base URL answered: ${target.url}. ${note} A navigation failure from that browser can mean its machine cannot reach the app, not that the app is down.`
1064
+ );
1065
+ } else {
1066
+ logger.debug(`Base URL answered: ${target.url}`);
1067
+ }
1068
+ }
1012
1069
  }
1013
1070
  if (opts.enabled === false || !opts.serverUrl) {
1014
1071
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
package/dist/index.d.ts CHANGED
@@ -140,6 +140,19 @@ interface PiwiDashboardOptions {
140
140
  * `PIWI_COVERAGE_SERVER`.
141
141
  */
142
142
  server?: 'off' | 'files' | 'functions' | 'lines';
143
+ /**
144
+ * What the browser's style sheets record: `'rules'`, the rules that
145
+ * matched an element during each test, from Chromium's CSS rule usage, each
146
+ * as its first and last line in the application's style sheet (a `.css`
147
+ * file served as written, a Vite dev server's styles, or a sheet with a
148
+ * source map), sent where a script's functions are; `'off'` records
149
+ * nothing. A change to a rule then selects the tests that rendered it. It
150
+ * costs style recalculation time, a little on most pages and up to half
151
+ * the test's time on a page with large style sheets and frequent
152
+ * re-renders, so it is a choice of its own. Chromium only. Defaults to
153
+ * `'off'`. Can also be set with `PIWI_COVERAGE_CSS`.
154
+ */
155
+ css?: 'off' | 'rules';
143
156
  };
144
157
  /**
145
158
  * Match each test to its history on the dashboard by an external ID, the ID the test has in your own test
@@ -256,8 +269,11 @@ interface PiwiDashboardOptions {
256
269
  * `globalSetup`, so leave it off when that setup is what starts the app. Only
257
270
  * the projects the run selects are checked, with their dependencies: the ones
258
271
  * `--project` names, or every project but those declared `default: false`
259
- * (Playwright 1.64+). **Defaults to `false`**. Can
260
- * also be set with `PIWI_CHECK_BASE_URL=true`.
272
+ * (Playwright 1.64+). The check runs from this machine's network: for a
273
+ * project whose browser runs on another machine (`connectOptions`) and whose
274
+ * `exposeNetwork` rule does not cover the base URL's host and port, its message says
275
+ * so, since that browser reaches the app from its own network. **Defaults to
276
+ * `false`**. Can also be set with `PIWI_CHECK_BASE_URL=true`.
261
277
  */
262
278
  checkBaseUrl?: boolean;
263
279
  /**
@@ -525,6 +541,8 @@ declare class PiwiDashboardReporter {
525
541
  private resourcesFile;
526
542
  /** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
527
543
  private sampler;
544
+ /** Posts what the sampler and the censuses add while someone has the run open on the dashboard. */
545
+ private liveResources;
528
546
  /** Bytes of the files the tests attached, by kind. */
529
547
  private artifactBytes;
530
548
  /** The run's resource report for the dashboard, built in `onEnd`. */