@piwitests/reporter 0.18.2 → 0.19.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/index.js CHANGED
@@ -40,7 +40,27 @@ __export(index_exports, {
40
40
  module.exports = __toCommonJS(index_exports);
41
41
 
42
42
  // src/public/reporter.ts
43
- var path13 = __toESM(require("path"));
43
+ var path14 = __toESM(require("path"));
44
+
45
+ // src/internal/config/desktop.ts
46
+ var fs = __toESM(require("fs"));
47
+ var os = __toESM(require("os"));
48
+ var path = __toESM(require("path"));
49
+ function defaultDesktopConfigPath() {
50
+ return path.join(os.homedir(), ".piwi", "desktop.json");
51
+ }
52
+ function readDesktopConfig(filePath) {
53
+ try {
54
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
55
+ const url = parsed?.url;
56
+ const token = parsed?.token;
57
+ if (typeof url !== "string" || !url) return null;
58
+ if (typeof token !== "string" || !token) return null;
59
+ return { url, token };
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
44
64
 
45
65
  // src/internal/config/env.ts
46
66
  var DEFAULTS = {
@@ -85,6 +105,7 @@ var PIWI_ENV_KEYS = {
85
105
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
86
106
  outputFile: "PIWI_OUTPUT_FILE"
87
107
  };
108
+ var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
88
109
  function readBool(val) {
89
110
  if (val === void 0) return void 0;
90
111
  return val === "true";
@@ -111,6 +132,10 @@ var ENV_FALLBACK_SPECS = [
111
132
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
112
133
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
113
134
  ];
135
+ var desktopDiscovered = false;
136
+ function usedDesktopDiscovery() {
137
+ return desktopDiscovered;
138
+ }
114
139
  function resolveOptions(raw) {
115
140
  const env = process.env;
116
141
  const mergedRaw = { ...raw };
@@ -123,6 +148,17 @@ function resolveOptions(raw) {
123
148
  mergedRaw[spec.option] = spec.kind === "number" ? Number(value) : value;
124
149
  }
125
150
  }
151
+ desktopDiscovered = false;
152
+ const serverUrlUnset = mergedRaw.serverUrl === void 0;
153
+ const apiKeyUnset = mergedRaw.apiKey === void 0 || mergedRaw.apiKey === null;
154
+ if (serverUrlUnset && apiKeyUnset) {
155
+ const desktop = readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath());
156
+ if (desktop) {
157
+ mergedRaw.serverUrl = desktop.url;
158
+ mergedRaw.apiKey = desktop.token;
159
+ desktopDiscovered = true;
160
+ }
161
+ }
126
162
  const opts = { ...DEFAULTS, ...mergedRaw };
127
163
  if (env[PIWI_ENV_KEYS.verbose] !== void 0) opts.verbose = env[PIWI_ENV_KEYS.verbose] === "true";
128
164
  return opts;
@@ -337,8 +373,8 @@ var HttpClient = class {
337
373
  };
338
374
 
339
375
  // src/internal/submit/uploader.ts
340
- var fs = __toESM(require("fs"));
341
- var path = __toESM(require("path"));
376
+ var fs2 = __toESM(require("fs"));
377
+ var path2 = __toESM(require("path"));
342
378
  var import_form_data2 = __toESM(require("form-data"));
343
379
 
344
380
  // src/internal/submit/serializer.ts
@@ -383,6 +419,8 @@ function toWireTestCase(tc) {
383
419
  suitePath: rest.suitePath ?? null,
384
420
  suiteConfig: rest.suiteConfig ?? null,
385
421
  testAnnotations: rest.testAnnotations ?? null,
422
+ tags: rest.tags ?? null,
423
+ testMeta: rest.testMeta ?? null,
386
424
  locatorSnapshots: rest.locatorSnapshots || null
387
425
  };
388
426
  }
@@ -521,8 +559,8 @@ var Uploader = class {
521
559
  if (traceInfo) {
522
560
  form.append("trace_hash", traceInfo.hash);
523
561
  if (includeTraceFile2) {
524
- form.append("trace", fs.createReadStream(traceInfo.tracePath), {
525
- filename: path.basename(traceInfo.tracePath)
562
+ form.append("trace", fs2.createReadStream(traceInfo.tracePath), {
563
+ filename: path2.basename(traceInfo.tracePath)
526
564
  });
527
565
  }
528
566
  }
@@ -534,7 +572,7 @@ var Uploader = class {
534
572
  )
535
573
  );
536
574
  for (const a of attachments) {
537
- form.append("attach_file", fs.createReadStream(a.path), { filename: a.originalName });
575
+ form.append("attach_file", fs2.createReadStream(a.path), { filename: a.originalName });
538
576
  }
539
577
  }
540
578
  return form;
@@ -587,7 +625,7 @@ var Uploader = class {
587
625
  )
588
626
  );
589
627
  for (const a of attachments) {
590
- form.append(`attach_file_${i}`, fs.createReadStream(a.path), { filename: a.originalName });
628
+ form.append(`attach_file_${i}`, fs2.createReadStream(a.path), { filename: a.originalName });
591
629
  attachmentCount++;
592
630
  }
593
631
  }
@@ -596,8 +634,8 @@ var Uploader = class {
596
634
  let traceCount = 0;
597
635
  for (const [i, tc] of testCases.entries()) {
598
636
  for (const tp of this.fileHandler.findTraceFiles(tc)) {
599
- if (fs.existsSync(tp)) {
600
- form.append(`trace_${i}`, fs.createReadStream(tp), { filename: path.basename(tp) });
637
+ if (fs2.existsSync(tp)) {
638
+ form.append(`trace_${i}`, fs2.createReadStream(tp), { filename: path2.basename(tp) });
601
639
  traceCount++;
602
640
  }
603
641
  }
@@ -607,40 +645,40 @@ var Uploader = class {
607
645
  };
608
646
 
609
647
  // src/internal/streaming/stream-buffer.ts
610
- var path2 = __toESM(require("path"));
611
- var os2 = __toESM(require("os"));
612
- var fs2 = __toESM(require("fs"));
648
+ var path3 = __toESM(require("path"));
649
+ var os3 = __toESM(require("os"));
650
+ var fs3 = __toESM(require("fs"));
613
651
 
614
652
  // src/internal/support/instance-id.ts
615
653
  var crypto = __toESM(require("crypto"));
616
- var os = __toESM(require("os"));
654
+ var os2 = __toESM(require("os"));
617
655
  function hashForProject(projectName) {
618
656
  return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
619
657
  }
620
658
  function computeInstanceId(projectName, runLabel) {
621
- const seed = runLabel ? `${projectName}|${runLabel}` : `${os.hostname()}|${projectName}`;
659
+ const seed = runLabel ? `${projectName}|${runLabel}` : `${os2.hostname()}|${projectName}`;
622
660
  return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 16);
623
661
  }
624
662
 
625
663
  // src/internal/streaming/stream-buffer.ts
626
664
  var StreamBuffer = class {
627
665
  constructor(projectName) {
628
- this.filePath = path2.join(os2.tmpdir(), `piwi-dashboard-stream-${hashForProject(projectName)}.jsonl`);
666
+ this.filePath = path3.join(os3.tmpdir(), `piwi-dashboard-stream-${hashForProject(projectName)}.jsonl`);
629
667
  }
630
668
  /** Append one or more events to the on-disk buffer */
631
669
  append(events) {
632
670
  if (events.length === 0) return;
633
671
  try {
634
672
  const lines = events.map((e) => JSON.stringify(e) + "\n").join("");
635
- fs2.appendFileSync(this.filePath, lines, "utf8");
673
+ fs3.appendFileSync(this.filePath, lines, "utf8");
636
674
  } catch {
637
675
  }
638
676
  }
639
677
  /** Load all buffered events from disk, clearing the file */
640
678
  load() {
641
679
  try {
642
- if (fs2.existsSync(this.filePath)) {
643
- const content = fs2.readFileSync(this.filePath, "utf8");
680
+ if (fs3.existsSync(this.filePath)) {
681
+ const content = fs3.readFileSync(this.filePath, "utf8");
644
682
  return content.split("\n").filter(Boolean).map((line) => JSON.parse(line));
645
683
  }
646
684
  } catch {
@@ -650,17 +688,17 @@ var StreamBuffer = class {
650
688
  /** Delete the buffer file from disk */
651
689
  clear() {
652
690
  try {
653
- if (fs2.existsSync(this.filePath)) fs2.unlinkSync(this.filePath);
691
+ if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
654
692
  } catch {
655
693
  }
656
694
  }
657
695
  /** Remove the buffer file if it is older than `maxAgeMs` (default 2 hours). Used on startup to discard orphaned data. */
658
696
  clearStale(maxAgeMs = 72e5) {
659
697
  try {
660
- if (fs2.existsSync(this.filePath)) {
661
- const stats = fs2.statSync(this.filePath);
698
+ if (fs3.existsSync(this.filePath)) {
699
+ const stats = fs3.statSync(this.filePath);
662
700
  if (Date.now() - stats.mtimeMs > maxAgeMs) {
663
- fs2.unlinkSync(this.filePath);
701
+ fs3.unlinkSync(this.filePath);
664
702
  }
665
703
  }
666
704
  } catch {
@@ -669,7 +707,7 @@ var StreamBuffer = class {
669
707
  };
670
708
 
671
709
  // src/internal/streaming/crash-recovery.ts
672
- var path3 = __toESM(require("path"));
710
+ var path4 = __toESM(require("path"));
673
711
 
674
712
  // src/internal/support/errors.ts
675
713
  function errorMessage(error) {
@@ -678,8 +716,8 @@ function errorMessage(error) {
678
716
  }
679
717
 
680
718
  // src/internal/streaming/crash-recovery.ts
681
- var os3 = __toESM(require("os"));
682
- var fs3 = __toESM(require("fs"));
719
+ var os4 = __toESM(require("os"));
720
+ var fs4 = __toESM(require("fs"));
683
721
  var CrashRecovery = class {
684
722
  /**
685
723
  * @param projectName Used to derive the temp-file name so recovery data is project-scoped.
@@ -687,12 +725,12 @@ var CrashRecovery = class {
687
725
  */
688
726
  constructor(projectName, logger = new Logger()) {
689
727
  this.logger = logger;
690
- this.filePath = path3.join(os3.tmpdir(), `piwi-dashboard-recovery-${hashForProject(projectName)}.json`);
728
+ this.filePath = path4.join(os4.tmpdir(), `piwi-dashboard-recovery-${hashForProject(projectName)}.json`);
691
729
  }
692
730
  /** Serialise the payload to a temp file for later retry */
693
731
  save(data) {
694
732
  try {
695
- fs3.writeFileSync(this.filePath, JSON.stringify(data), "utf8");
733
+ fs4.writeFileSync(this.filePath, JSON.stringify(data), "utf8");
696
734
  this.logger.info("Saved recovery data for later upload");
697
735
  } catch (error) {
698
736
  this.logger.error(`Failed to save recovery data: ${errorMessage(error)}`);
@@ -701,7 +739,7 @@ var CrashRecovery = class {
701
739
  /** Read the saved payload from disk, or return `null` if none exists */
702
740
  load() {
703
741
  try {
704
- if (fs3.existsSync(this.filePath)) return JSON.parse(fs3.readFileSync(this.filePath, "utf8"));
742
+ if (fs4.existsSync(this.filePath)) return JSON.parse(fs4.readFileSync(this.filePath, "utf8"));
705
743
  } catch {
706
744
  }
707
745
  return null;
@@ -709,7 +747,7 @@ var CrashRecovery = class {
709
747
  /** Delete the recovery file from disk */
710
748
  clear() {
711
749
  try {
712
- if (fs3.existsSync(this.filePath)) fs3.unlinkSync(this.filePath);
750
+ if (fs4.existsSync(this.filePath)) fs4.unlinkSync(this.filePath);
713
751
  } catch {
714
752
  }
715
753
  }
@@ -730,24 +768,24 @@ var CrashRecovery = class {
730
768
  };
731
769
 
732
770
  // src/internal/files/file-handler.ts
733
- var fs5 = __toESM(require("fs"));
734
- var path5 = __toESM(require("path"));
771
+ var fs6 = __toESM(require("fs"));
772
+ var path6 = __toESM(require("path"));
735
773
  var crypto2 = __toESM(require("crypto"));
736
774
 
737
775
  // src/internal/files/compression.ts
738
- var fs4 = __toESM(require("fs"));
739
- var path4 = __toESM(require("path"));
776
+ var fs5 = __toESM(require("fs"));
777
+ var path5 = __toESM(require("path"));
740
778
  var zlib = __toESM(require("zlib"));
741
779
  var import_node_util = require("util");
742
780
  var gzipAsync = (0, import_node_util.promisify)(zlib.gzip);
743
781
  async function compressDirectory(sourceDir) {
744
782
  const files = [];
745
783
  function collect(dir, baseDir = "") {
746
- for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
747
- const full = path4.join(dir, entry.name);
748
- const rel = path4.join(baseDir, entry.name);
784
+ for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
785
+ const full = path5.join(dir, entry.name);
786
+ const rel = path5.join(baseDir, entry.name);
749
787
  if (entry.isDirectory()) collect(full, rel);
750
- else if (entry.isFile()) files.push({ path: rel, content: fs4.readFileSync(full) });
788
+ else if (entry.isFile()) files.push({ path: rel, content: fs5.readFileSync(full) });
751
789
  }
752
790
  }
753
791
  collect(sourceDir);
@@ -785,19 +823,19 @@ var FileHandler = class {
785
823
  }
786
824
  /** Locate a Playwright HTML report directory containing `index.html`. Optionally override the search path. */
787
825
  findHTMLReportDirectory(customDir) {
788
- const possibleDirs = customDir ? [customDir, path5.join(process.cwd(), customDir)] : ["playwright-report", "./playwright-report", path5.join(process.cwd(), "playwright-report")];
826
+ const possibleDirs = customDir ? [customDir, path6.join(process.cwd(), customDir)] : ["playwright-report", "./playwright-report", path6.join(process.cwd(), "playwright-report")];
789
827
  for (const reportDir of possibleDirs) {
790
- if (fs5.existsSync(reportDir) && fs5.statSync(reportDir).isDirectory()) {
791
- if (fs5.existsSync(path5.join(reportDir, "index.html"))) return reportDir;
828
+ if (fs6.existsSync(reportDir) && fs6.statSync(reportDir).isDirectory()) {
829
+ if (fs6.existsSync(path6.join(reportDir, "index.html"))) return reportDir;
792
830
  }
793
831
  }
794
832
  return null;
795
833
  }
796
834
  /** Check if `dir` exists and is a directory; also checks `path.join(process.cwd(), dir)` */
797
835
  findReportDirectory(dir) {
798
- const candidates = [dir, path5.join(process.cwd(), dir)];
836
+ const candidates = [dir, path6.join(process.cwd(), dir)];
799
837
  for (const candidate of candidates) {
800
- if (fs5.existsSync(candidate) && fs5.statSync(candidate).isDirectory()) return candidate;
838
+ if (fs6.existsSync(candidate) && fs6.statSync(candidate).isDirectory()) return candidate;
801
839
  }
802
840
  return null;
803
841
  }
@@ -815,7 +853,7 @@ var FileHandler = class {
815
853
  const set = /* @__PURE__ */ new Set();
816
854
  if (testCase.attachments) {
817
855
  for (const a of testCase.attachments) {
818
- if (a.name === "trace" && a.path) set.add(path5.resolve(a.path));
856
+ if (a.name === "trace" && a.path) set.add(path6.resolve(a.path));
819
857
  }
820
858
  }
821
859
  return Array.from(set);
@@ -827,12 +865,12 @@ var FileHandler = class {
827
865
  for (const a of testCase.attachments) {
828
866
  if (a.name === "trace") continue;
829
867
  if (a.name && INTERNAL_ATTACHMENT_NAMES.has(a.name)) continue;
830
- if (a.path && fs5.existsSync(a.path)) {
868
+ if (a.path && fs6.existsSync(a.path)) {
831
869
  result.push({
832
870
  name: a.name || "attachment",
833
- path: path5.resolve(a.path),
871
+ path: path6.resolve(a.path),
834
872
  contentType: a.contentType || "application/octet-stream",
835
- originalName: path5.basename(a.path)
873
+ originalName: path6.basename(a.path)
836
874
  });
837
875
  }
838
876
  }
@@ -887,17 +925,17 @@ var FileHandler = class {
887
925
  const tracePaths = this.findTraceFiles(testCase);
888
926
  let lastPath = null;
889
927
  for (const tp of tracePaths) {
890
- if (fs5.existsSync(tp)) lastPath = tp;
928
+ if (fs6.existsSync(tp)) lastPath = tp;
891
929
  }
892
930
  if (!lastPath) return null;
893
931
  const hash = crypto2.createHash("sha256");
894
932
  await new Promise((resolve5, reject) => {
895
- fs5.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve5).on("error", reject);
933
+ fs6.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve5).on("error", reject);
896
934
  });
897
935
  return {
898
936
  tracePath: lastPath,
899
937
  hash: hash.digest("hex"),
900
- size: fs5.statSync(lastPath).size
938
+ size: fs6.statSync(lastPath).size
901
939
  };
902
940
  }
903
941
  /** Ask the server which trace hashes it already has, returning the set of hashes that are missing */
@@ -1115,7 +1153,7 @@ var MetadataCollector = class {
1115
1153
  };
1116
1154
 
1117
1155
  // src/internal/streaming/stream-manager.ts
1118
- var fs7 = __toESM(require("fs"));
1156
+ var fs8 = __toESM(require("fs"));
1119
1157
 
1120
1158
  // src/internal/support/limiter.ts
1121
1159
  function createLimiter(maxConcurrent) {
@@ -1140,18 +1178,18 @@ function createLimiter(maxConcurrent) {
1140
1178
  }
1141
1179
 
1142
1180
  // src/internal/support/setup-file.ts
1143
- var path6 = __toESM(require("path"));
1144
- var os4 = __toESM(require("os"));
1145
- var fs6 = __toESM(require("fs"));
1181
+ var path7 = __toESM(require("path"));
1182
+ var os5 = __toESM(require("os"));
1183
+ var fs7 = __toESM(require("fs"));
1146
1184
  function getSetupFilePath(projectName) {
1147
- return path6.join(os4.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
1185
+ return path7.join(os5.tmpdir(), `piwi-dashboard-setup-${hashForProject(projectName)}.json`);
1148
1186
  }
1149
1187
  function readSetupInfo(projectName) {
1150
1188
  const setupFile = getSetupFilePath(projectName);
1151
1189
  try {
1152
- if (fs6.existsSync(setupFile)) {
1153
- const info = JSON.parse(fs6.readFileSync(setupFile, "utf8"));
1154
- fs6.unlinkSync(setupFile);
1190
+ if (fs7.existsSync(setupFile)) {
1191
+ const info = JSON.parse(fs7.readFileSync(setupFile, "utf8"));
1192
+ fs7.unlinkSync(setupFile);
1155
1193
  if (info.projectName === projectName) return info;
1156
1194
  }
1157
1195
  } catch {
@@ -1470,7 +1508,7 @@ var StreamManager = class {
1470
1508
  }
1471
1509
  /** Schedule a live upload of trace and attachment files for a test case. Skips cases with no files. Concurrency is limited to 2 simultaneous uploads. */
1472
1510
  scheduleLiveUpload(tc) {
1473
- const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs7.existsSync(p));
1511
+ const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs8.existsSync(p));
1474
1512
  const hasAttachments = this.fileHandler.findAllAttachments(tc).length > 0;
1475
1513
  if (!hasTrace && !hasAttachments) return;
1476
1514
  const promise = (async () => {
@@ -1534,7 +1572,7 @@ var StreamManager = class {
1534
1572
  }
1535
1573
  };
1536
1574
 
1537
- // src/internal/collect/step-analyzer.ts
1575
+ // ../packages/core/src/step-analysis.ts
1538
1576
  function categorizeStep(title, pwCategory) {
1539
1577
  if (!title) return "other";
1540
1578
  if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
@@ -1595,8 +1633,8 @@ function collectStepMetrics(steps) {
1595
1633
  }
1596
1634
  function percentile(sortedArr, p) {
1597
1635
  if (sortedArr.length === 0) return 0;
1598
- const index = Math.ceil(p / 100 * sortedArr.length) - 1;
1599
- return sortedArr[Math.max(0, index)];
1636
+ const index = Math.min(sortedArr.length - 1, Math.max(0, Math.ceil(p / 100 * sortedArr.length) - 1));
1637
+ return sortedArr[index];
1600
1638
  }
1601
1639
  function computePerformanceSummary(testCases) {
1602
1640
  const durations = testCases.filter((tc) => tc.duration != null).map((tc) => tc.duration);
@@ -1671,15 +1709,15 @@ function extractWaitEvents(steps, insideWait = false) {
1671
1709
  }
1672
1710
 
1673
1711
  // src/internal/support/reporter-version.ts
1674
- var fs8 = __toESM(require("fs"));
1675
- var path7 = __toESM(require("path"));
1712
+ var fs9 = __toESM(require("fs"));
1713
+ var path8 = __toESM(require("path"));
1676
1714
  var cachedVersion = null;
1677
1715
  var CANDIDATE_OFFSETS = ["..", "../../.."];
1678
1716
  function findOwnPackageJson(fromDir) {
1679
1717
  for (const offset of CANDIDATE_OFFSETS) {
1680
1718
  try {
1681
- const pkgPath = path7.resolve(fromDir, offset, "package.json");
1682
- const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
1719
+ const pkgPath = path8.resolve(fromDir, offset, "package.json");
1720
+ const pkg = JSON.parse(fs9.readFileSync(pkgPath, "utf-8"));
1683
1721
  const { name, version } = pkg;
1684
1722
  if (name === "@piwitests/reporter" && typeof version === "string") return { version };
1685
1723
  } catch {
@@ -1694,11 +1732,11 @@ function getReporterVersion() {
1694
1732
  }
1695
1733
 
1696
1734
  // src/internal/support/source-snippet.ts
1697
- var fs9 = __toESM(require("fs"));
1698
- var path8 = __toESM(require("path"));
1735
+ var fs10 = __toESM(require("fs"));
1736
+ var path9 = __toESM(require("path"));
1699
1737
  function readSourceSnippet(file, declLine, context, failingLine) {
1700
1738
  try {
1701
- const content = fs9.readFileSync(file, "utf-8");
1739
+ const content = fs10.readFileSync(file, "utf-8");
1702
1740
  const lines = content.split("\n");
1703
1741
  const anchor = failingLine ?? declLine;
1704
1742
  const start = Math.max(0, anchor - context - 1);
@@ -1735,18 +1773,18 @@ function collectSourceFrames(errorText, testFile, declLine, opts = {}) {
1735
1773
  while ((m = stackRe.exec(errorText)) !== null && picked.length < maxFrames) {
1736
1774
  let abs;
1737
1775
  try {
1738
- abs = path8.resolve(m[1]);
1776
+ abs = path9.resolve(m[1]);
1739
1777
  } catch {
1740
1778
  continue;
1741
1779
  }
1742
- if (abs.includes(`${path8.sep}node_modules${path8.sep}`)) continue;
1743
- const rel = path8.relative(projectRoot, abs);
1744
- if (rel.startsWith("..") || path8.isAbsolute(rel)) continue;
1780
+ if (abs.includes(`${path9.sep}node_modules${path9.sep}`)) continue;
1781
+ const rel = path9.relative(projectRoot, abs);
1782
+ if (rel.startsWith("..") || path9.isAbsolute(rel)) continue;
1745
1783
  const line = parseInt(m[2], 10);
1746
1784
  if (!isNaN(line)) add(abs, line);
1747
1785
  }
1748
1786
  }
1749
- const absTest = path8.resolve(testFile);
1787
+ const absTest = path9.resolve(testFile);
1750
1788
  if (!picked.some((p) => p.absFile === absTest)) {
1751
1789
  add(absTest, extractFailingLine(errorText, testFile, declLine));
1752
1790
  }
@@ -1754,19 +1792,19 @@ function collectSourceFrames(errorText, testFile, declLine, opts = {}) {
1754
1792
  for (const p of picked.slice(0, maxFrames)) {
1755
1793
  const snippet = readSourceSnippet(p.absFile, p.line, context, p.line);
1756
1794
  if (snippet) {
1757
- frames.push({ file: path8.relative(projectRoot, p.absFile) || path8.basename(p.absFile), line: p.line, snippet });
1795
+ frames.push({ file: path9.relative(projectRoot, p.absFile) || path9.basename(p.absFile), line: p.line, snippet });
1758
1796
  }
1759
1797
  }
1760
1798
  return frames;
1761
1799
  }
1762
1800
  function extractFailingLine(errorText, testFile, declarationLine) {
1763
1801
  if (!errorText) return declarationLine;
1764
- const expectedFile = path8.resolve(testFile);
1802
+ const expectedFile = path9.resolve(testFile);
1765
1803
  const stackRe = /^\s+at (?:[^(]*\()?(.+?):(\d+):\d+\)?\s*$/gm;
1766
1804
  let m;
1767
1805
  while ((m = stackRe.exec(errorText)) !== null) {
1768
1806
  try {
1769
- const frameFile = path8.resolve(m[1]);
1807
+ const frameFile = path9.resolve(m[1]);
1770
1808
  if (frameFile === expectedFile) {
1771
1809
  const line = parseInt(m[2], 10);
1772
1810
  if (!isNaN(line)) return line;
@@ -1839,18 +1877,29 @@ function detectCliFileFilters(argv = process.argv) {
1839
1877
  }
1840
1878
 
1841
1879
  // src/public/global-setup.ts
1842
- var path9 = __toESM(require("path"));
1843
- var fs10 = __toESM(require("fs"));
1880
+ var path10 = __toESM(require("path"));
1881
+ var fs11 = __toESM(require("fs"));
1882
+
1883
+ // src/internal/support/run-mode.ts
1884
+ var PW_UI_FLAGS = ["--ui", "--ui-host", "--ui-port"];
1885
+ function isUiMode(argv = process.argv) {
1886
+ const args = argv.slice(2);
1887
+ const testIdx = args.indexOf("test");
1888
+ const rest = testIdx >= 0 ? args.slice(testIdx + 1) : args;
1889
+ return rest.some((tok) => PW_UI_FLAGS.some((flag) => tok === flag || tok.startsWith(`${flag}=`)));
1890
+ }
1891
+
1892
+ // src/public/global-setup.ts
1844
1893
  function createGlobalSetup(options, userSetup) {
1845
1894
  return async function globalSetupFn(config) {
1846
- const piwiReporterPath = path9.resolve(__dirname, "./index.js");
1895
+ const piwiReporterPath = path10.resolve(__dirname, "./index.js");
1847
1896
  let inlineReporterOptions = {};
1848
1897
  if (Array.isArray(config?.reporter)) {
1849
1898
  for (const r of config.reporter) {
1850
1899
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
1851
1900
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
1852
1901
  try {
1853
- return path9.resolve(require.resolve(r[0])) === piwiReporterPath;
1902
+ return path10.resolve(require.resolve(r[0])) === piwiReporterPath;
1854
1903
  } catch {
1855
1904
  return false;
1856
1905
  }
@@ -1863,6 +1912,11 @@ function createGlobalSetup(options, userSetup) {
1863
1912
  }
1864
1913
  const opts = resolveOptions({ ...inlineReporterOptions, ...options });
1865
1914
  const logger = new Logger(opts.verbose ?? false);
1915
+ if (isUiMode()) {
1916
+ logger.debug("UI mode detected \u2014 skipping run registration.");
1917
+ if (userSetup) return userSetup(config);
1918
+ return;
1919
+ }
1866
1920
  if (opts.enabled === false || !opts.serverUrl) {
1867
1921
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
1868
1922
  if (userSetup) return userSetup(config);
@@ -1872,7 +1926,7 @@ function createGlobalSetup(options, userSetup) {
1872
1926
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
1873
1927
  if (r[0].toLowerCase().includes("piwi")) return true;
1874
1928
  try {
1875
- return path9.resolve(require.resolve(r[0])) === piwiReporterPath;
1929
+ return path10.resolve(require.resolve(r[0])) === piwiReporterPath;
1876
1930
  } catch {
1877
1931
  return false;
1878
1932
  }
@@ -1904,7 +1958,7 @@ function createGlobalSetup(options, userSetup) {
1904
1958
  auth
1905
1959
  );
1906
1960
  if (response?.runId && response?.setupToken) {
1907
- fs10.writeFileSync(
1961
+ fs11.writeFileSync(
1908
1962
  getSetupFilePath(opts.projectName),
1909
1963
  JSON.stringify({
1910
1964
  runId: response.runId,
@@ -1922,8 +1976,8 @@ function createGlobalSetup(options, userSetup) {
1922
1976
  }
1923
1977
 
1924
1978
  // src/public/config-wrapper.ts
1925
- var path10 = __toESM(require("path"));
1926
- var fs11 = __toESM(require("fs"));
1979
+ var path11 = __toESM(require("path"));
1980
+ var fs12 = __toESM(require("fs"));
1927
1981
  var PIWI_MODULE = "@piwitests/reporter";
1928
1982
  function isPiwiReporterEntry(entry) {
1929
1983
  if (typeof entry === "string") return entry.toLowerCase().includes("piwi");
@@ -1941,11 +1995,11 @@ function injectReporter(reporter, piwiOptions) {
1941
1995
  }
1942
1996
  function resolveSetupModule() {
1943
1997
  const candidates = [
1944
- path10.join(__dirname, "global-setup-module.js"),
1945
- path10.join(__dirname, "..", "global-setup-module.js"),
1946
- path10.join(__dirname, "..", "global-setup-module.ts")
1998
+ path11.join(__dirname, "global-setup-module.js"),
1999
+ path11.join(__dirname, "..", "global-setup-module.js"),
2000
+ path11.join(__dirname, "..", "global-setup-module.ts")
1947
2001
  ];
1948
- return candidates.find((candidate) => fs11.existsSync(candidate)) ?? candidates[0];
2002
+ return candidates.find((candidate) => fs12.existsSync(candidate)) ?? candidates[0];
1949
2003
  }
1950
2004
  function wrapConfig(config, piwiOptions) {
1951
2005
  if (piwiOptions) applyOptionsToEnv(piwiOptions);
@@ -1962,7 +2016,7 @@ function wrapConfig(config, piwiOptions) {
1962
2016
  };
1963
2017
  }
1964
2018
 
1965
- // src/internal/collect/skip-classify.ts
2019
+ // ../packages/core/src/status-classify.ts
1966
2020
  function mergeAnnotations(test, result) {
1967
2021
  const out = [];
1968
2022
  const seen = /* @__PURE__ */ new Set();
@@ -1982,13 +2036,99 @@ function classifyStatus(rawStatus, annotations) {
1982
2036
  return intentional ? "skipped" : "didnotrun";
1983
2037
  }
1984
2038
 
2039
+ // ../packages/core/src/test-meta.ts
2040
+ var PIWI_ANNOTATION_PREFIX = "piwi:";
2041
+ var TEST_PRIORITIES = ["critical", "high", "medium", "low"];
2042
+ var MAX_TEST_TAGS = 20;
2043
+ var MAX_TEST_TAG_CHARS = 60;
2044
+ var MAX_TEST_META_CHARS = 120;
2045
+ var MAX_TEST_LINK_CHARS = 500;
2046
+ var PRIORITY_SET = new Set(TEST_PRIORITIES);
2047
+ function cleanString(value, maxChars) {
2048
+ if (typeof value !== "string") return void 0;
2049
+ const trimmed = value.trim();
2050
+ if (!trimmed) return void 0;
2051
+ return trimmed.slice(0, maxChars);
2052
+ }
2053
+ function normalizeTestTags(raw) {
2054
+ if (!Array.isArray(raw)) return [];
2055
+ const seen = /* @__PURE__ */ new Set();
2056
+ const out = [];
2057
+ for (const entry of raw) {
2058
+ if (typeof entry !== "string") continue;
2059
+ const tag = entry.trim().replace(/^@+/, "").trim().slice(0, MAX_TEST_TAG_CHARS);
2060
+ if (!tag || seen.has(tag)) continue;
2061
+ seen.add(tag);
2062
+ out.push(tag);
2063
+ if (out.length >= MAX_TEST_TAGS) break;
2064
+ }
2065
+ return out;
2066
+ }
2067
+ function normalizeLink(value) {
2068
+ const raw = cleanString(value, MAX_TEST_LINK_CHARS);
2069
+ if (!raw) return void 0;
2070
+ try {
2071
+ const { protocol } = new URL(raw);
2072
+ return protocol === "http:" || protocol === "https:" ? raw : void 0;
2073
+ } catch {
2074
+ return void 0;
2075
+ }
2076
+ }
2077
+ function parseTestMetadata(annotations) {
2078
+ if (!Array.isArray(annotations)) return null;
2079
+ const meta = {};
2080
+ for (const entry of annotations) {
2081
+ if (!entry || typeof entry !== "object") continue;
2082
+ const { type, description } = entry;
2083
+ if (typeof type !== "string") continue;
2084
+ const lower = type.trim().toLowerCase();
2085
+ if (!lower.startsWith(PIWI_ANNOTATION_PREFIX)) continue;
2086
+ const field = lower.slice(PIWI_ANNOTATION_PREFIX.length);
2087
+ switch (field) {
2088
+ case "owner": {
2089
+ const value = cleanString(description, MAX_TEST_META_CHARS);
2090
+ if (value) meta.owner = value;
2091
+ break;
2092
+ }
2093
+ case "priority": {
2094
+ const value = cleanString(description, MAX_TEST_META_CHARS)?.toLowerCase();
2095
+ if (value && PRIORITY_SET.has(value)) meta.priority = value;
2096
+ break;
2097
+ }
2098
+ case "feature": {
2099
+ const value = cleanString(description, MAX_TEST_META_CHARS);
2100
+ if (value) meta.feature = value;
2101
+ break;
2102
+ }
2103
+ case "link": {
2104
+ const value = normalizeLink(description);
2105
+ if (value) meta.link = value;
2106
+ break;
2107
+ }
2108
+ default:
2109
+ break;
2110
+ }
2111
+ }
2112
+ return Object.keys(meta).length > 0 ? meta : null;
2113
+ }
2114
+
2115
+ // src/internal/collect/test-meta.ts
2116
+ function collectTestTags(test) {
2117
+ return normalizeTestTags(test.tags);
2118
+ }
2119
+ function collectTestMetadata(annotations) {
2120
+ return parseTestMetadata(annotations);
2121
+ }
2122
+
1985
2123
  // src/internal/collect/error-text.ts
1986
- var path11 = __toESM(require("path"));
1987
- function buildErrorText(result) {
1988
- if (!result.errors || result.errors.length === 0) return null;
2124
+ var path12 = __toESM(require("path"));
2125
+
2126
+ // ../packages/core/src/error-text.ts
2127
+ function joinErrorMessages(errors) {
2128
+ if (!errors || errors.length === 0) return null;
1989
2129
  const seen = /* @__PURE__ */ new Set();
1990
2130
  const parts = [];
1991
- for (const err of result.errors) {
2131
+ for (const err of errors) {
1992
2132
  const msg = err.message ?? "";
1993
2133
  if (!msg) continue;
1994
2134
  const dedupeKey = msg.trim().slice(0, 200);
@@ -1996,21 +2136,27 @@ function buildErrorText(result) {
1996
2136
  seen.add(dedupeKey);
1997
2137
  parts.push(msg);
1998
2138
  }
1999
- let text = parts.join("\n---\n");
2139
+ return parts.join("\n---\n") || null;
2140
+ }
2141
+ function appendErrorLocation(text, location) {
2142
+ if (!location?.file || /\n\s+at\s/.test(text)) return text;
2143
+ return `${text}
2144
+ at ${location.file}:${location.line}:${location.column}`;
2145
+ }
2146
+
2147
+ // src/internal/collect/error-text.ts
2148
+ function buildErrorText(result) {
2149
+ const text = joinErrorMessages(result.errors);
2000
2150
  if (!text) return null;
2001
- const primary = result.error;
2002
- const loc = primary?.location;
2003
- if (loc?.file && !/\n\s+at\s/.test(text)) {
2004
- const rel = path11.relative(process.cwd(), loc.file).split(path11.sep).join("/");
2005
- text += `
2006
- at ${rel}:${loc.line}:${loc.column}`;
2007
- }
2008
- return text;
2151
+ const loc = result.error?.location;
2152
+ if (!loc?.file) return text;
2153
+ const rel = path12.relative(process.cwd(), loc.file).split(path12.sep).join("/");
2154
+ return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
2009
2155
  }
2010
2156
 
2011
2157
  // src/internal/support/ci-output.ts
2012
- var fs12 = __toESM(require("fs"));
2013
- var path12 = __toESM(require("path"));
2158
+ var fs13 = __toESM(require("fs"));
2159
+ var path13 = __toESM(require("path"));
2014
2160
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
2015
2161
  logger.info(`View run: ${output.runUrl}`);
2016
2162
  if (outputFile) writeOutputFile(outputFile, output, logger);
@@ -2019,9 +2165,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
2019
2165
  }
2020
2166
  function writeOutputFile(file, output, logger) {
2021
2167
  try {
2022
- const dir = path12.dirname(file);
2023
- if (dir && dir !== ".") fs12.mkdirSync(dir, { recursive: true });
2024
- fs12.writeFileSync(
2168
+ const dir = path13.dirname(file);
2169
+ if (dir && dir !== ".") fs13.mkdirSync(dir, { recursive: true });
2170
+ fs13.writeFileSync(
2025
2171
  file,
2026
2172
  JSON.stringify(
2027
2173
  {
@@ -2073,7 +2219,7 @@ function emitGitLabDotenv(output, env, logger) {
2073
2219
  if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
2074
2220
  if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
2075
2221
  try {
2076
- fs12.writeFileSync(file, lines.join("\n") + "\n");
2222
+ fs13.writeFileSync(file, lines.join("\n") + "\n");
2077
2223
  logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
2078
2224
  } catch (error) {
2079
2225
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
@@ -2081,7 +2227,7 @@ function emitGitLabDotenv(output, env, logger) {
2081
2227
  }
2082
2228
  function appendFileLines(file, lines, logger, label) {
2083
2229
  try {
2084
- fs12.appendFileSync(file, lines.join("\n") + "\n");
2230
+ fs13.appendFileSync(file, lines.join("\n") + "\n");
2085
2231
  } catch (error) {
2086
2232
  logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
2087
2233
  }
@@ -2298,7 +2444,7 @@ var RunSubmitter = class {
2298
2444
 
2299
2445
  // src/public/reporter.ts
2300
2446
  function testLocation(test) {
2301
- const relativeFilePath = path13.relative(process.cwd(), test.location.file).split(path13.sep).join("/");
2447
+ const relativeFilePath = path14.relative(process.cwd(), test.location.file).split(path14.sep).join("/");
2302
2448
  return `${relativeFilePath}:${test.location.line}:${test.location.column}`;
2303
2449
  }
2304
2450
  var PiwiDashboardReporter = class {
@@ -2327,6 +2473,7 @@ var PiwiDashboardReporter = class {
2327
2473
  this.setupSteps = [];
2328
2474
  this.options = resolveOptions(rawOptions);
2329
2475
  this.enabled = this.options.enabled !== false && !!this.options.serverUrl;
2476
+ this.viaDesktopApp = usedDesktopDiscovery();
2330
2477
  this.runLabel = this.options.runLabel || detectCiRunLabel();
2331
2478
  this.instanceId = computeInstanceId(this.options.projectName, this.runLabel);
2332
2479
  const logger = new Logger(this.options.verbose ?? false);
@@ -2363,6 +2510,9 @@ var PiwiDashboardReporter = class {
2363
2510
  this.logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
2364
2511
  return;
2365
2512
  }
2513
+ if (this.viaDesktopApp) {
2514
+ this.logger.info(`Using the desktop app running at ${this.options.serverUrl} (no serverUrl configured).`);
2515
+ }
2366
2516
  this.startTime = (/* @__PURE__ */ new Date()).toISOString();
2367
2517
  this.playwrightVersion = config.version;
2368
2518
  this.logger.info(
@@ -2473,6 +2623,7 @@ var PiwiDashboardReporter = class {
2473
2623
  const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
2474
2624
  const annotations = mergeAnnotations(test, result);
2475
2625
  const status = classifyStatus(result.status, annotations);
2626
+ const tags = collectTestTags(test);
2476
2627
  const testCase = {
2477
2628
  type: "complete",
2478
2629
  title: test.title,
@@ -2491,7 +2642,9 @@ var PiwiDashboardReporter = class {
2491
2642
  browser: this.metadataCollector.getBrowserConfig(test) || void 0,
2492
2643
  suitePath,
2493
2644
  suiteConfig,
2494
- testAnnotations: annotations.length ? annotations : null
2645
+ testAnnotations: annotations.length ? annotations : null,
2646
+ tags: tags.length ? tags : null,
2647
+ testMeta: collectTestMetadata(annotations)
2495
2648
  };
2496
2649
  if (result.status === "failed" || result.status === "timedOut") {
2497
2650
  const failingLine = extractFailingLine(testCase.error, test.location.file, test.location.line);
@@ -2551,6 +2704,8 @@ var PiwiDashboardReporter = class {
2551
2704
  for (const test of this.plannedTests) {
2552
2705
  if (this.reportedTestIds.has(test.id)) continue;
2553
2706
  const { suitePath, suiteConfig } = this.metadataCollector.getSuiteInfo(test);
2707
+ const declaredAnnotations = test.annotations ?? [];
2708
+ const tags = collectTestTags(test);
2554
2709
  const testCase = {
2555
2710
  type: "complete",
2556
2711
  title: test.title,
@@ -2567,7 +2722,9 @@ var PiwiDashboardReporter = class {
2567
2722
  browser: this.metadataCollector.getBrowserConfig(test) || void 0,
2568
2723
  suitePath,
2569
2724
  suiteConfig,
2570
- testAnnotations: test.annotations?.length ? test.annotations : null
2725
+ testAnnotations: declaredAnnotations.length ? declaredAnnotations : null,
2726
+ tags: tags.length ? tags : null,
2727
+ testMeta: collectTestMetadata(declaredAnnotations)
2571
2728
  };
2572
2729
  this.testCases.push(testCase);
2573
2730
  this.totalTests++;
@@ -2610,7 +2767,7 @@ var PiwiDashboardReporter = class {
2610
2767
  var import_node_zlib = require("zlib");
2611
2768
 
2612
2769
  // src/internal/capture/locator-healing.ts
2613
- var path14 = __toESM(require("path"));
2770
+ var path15 = __toESM(require("path"));
2614
2771
 
2615
2772
  // ../packages/core/src/locator-fingerprint.ts
2616
2773
  var PRESENT_SIMILARITY = 0.8;
@@ -3049,6 +3206,34 @@ var ACTION_METHODS = [
3049
3206
  "waitFor"
3050
3207
  ];
3051
3208
  var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
3209
+ var EXPECT_METHOD = "_expect";
3210
+ var EXPECT_CAPTURE_EXPRESSIONS = /* @__PURE__ */ new Set([
3211
+ "to.be.attached",
3212
+ "to.be.checked",
3213
+ "to.be.disabled",
3214
+ "to.be.editable",
3215
+ "to.be.empty",
3216
+ "to.be.enabled",
3217
+ "to.be.focused",
3218
+ "to.be.in.viewport",
3219
+ "to.be.readonly",
3220
+ "to.be.visible",
3221
+ "to.contain.class",
3222
+ "to.contain.text",
3223
+ "to.have.accessible.description",
3224
+ "to.have.accessible.error.message",
3225
+ "to.have.accessible.name",
3226
+ "to.have.attribute",
3227
+ "to.have.attribute.value",
3228
+ "to.have.class",
3229
+ "to.have.css",
3230
+ "to.have.id",
3231
+ "to.have.js.property",
3232
+ "to.have.role",
3233
+ "to.have.text",
3234
+ "to.have.value",
3235
+ "to.match.aria"
3236
+ ]);
3052
3237
  function extractAccessibleName(ariaSnapshot) {
3053
3238
  if (!ariaSnapshot) return null;
3054
3239
  const match = ariaSnapshot.match(/- \w+ "([^"]+)"/);
@@ -3135,6 +3320,7 @@ function suggestLocatorsFromAria(failed, ariaSnapshot) {
3135
3320
  function captureCallerLocation(stack = new Error().stack ?? "") {
3136
3321
  const lines = stack.split("\n");
3137
3322
  let prevWasCaptureModule = false;
3323
+ let selfFile = null;
3138
3324
  for (let i = 1; i < lines.length; i++) {
3139
3325
  const line = lines[i].trim();
3140
3326
  if (!line.startsWith("at")) continue;
@@ -3153,6 +3339,11 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
3153
3339
  prevWasCaptureModule = false;
3154
3340
  continue;
3155
3341
  }
3342
+ if (selfFile === null || file === selfFile) {
3343
+ selfFile ??= file;
3344
+ prevWasCaptureModule = /[\\/]locator-healing\.[a-z]+$/i.test(file);
3345
+ continue;
3346
+ }
3156
3347
  if (/[\\/]locator-healing\.[a-z]+$/i.test(file)) {
3157
3348
  prevWasCaptureModule = true;
3158
3349
  continue;
@@ -3167,10 +3358,10 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
3167
3358
  }
3168
3359
  let rel = file;
3169
3360
  try {
3170
- rel = path14.relative(process.cwd(), file);
3361
+ rel = path15.relative(process.cwd(), file);
3171
3362
  } catch {
3172
3363
  }
3173
- rel = rel.split(path14.sep).join("/");
3364
+ rel = rel.split(path15.sep).join("/");
3174
3365
  if (rel.startsWith("./")) rel = rel.slice(2);
3175
3366
  return `${rel}:${m[3]}:${m[4]}`;
3176
3367
  }
@@ -3213,7 +3404,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
3213
3404
  }
3214
3405
 
3215
3406
  // src/internal/capture/pick-on-failure.ts
3216
- var path15 = __toESM(require("path"));
3407
+ var path16 = __toESM(require("path"));
3217
3408
  var ANSI_RE = /\[[0-9;]*m/g;
3218
3409
  function endOfString(s, start) {
3219
3410
  const q = s[start];
@@ -3307,7 +3498,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE, "");
3307
3498
  const parsed = parseLeafLocatorExpression(line[1].trim());
3308
3499
  if (!parsed) continue;
3309
3500
  const loc = err.location;
3310
- const location = loc ? `${path15.relative(process.cwd(), loc.file).split(path15.sep).join("/")}:${loc.line}:${loc.column}` : null;
3501
+ const location = loc ? `${path16.relative(process.cwd(), loc.file).split(path16.sep).join("/")}:${loc.line}:${loc.column}` : null;
3311
3502
  return { method: parsed.method, args: parsed.args, location };
3312
3503
  }
3313
3504
  return null;
@@ -4072,6 +4263,7 @@ function createSink() {
4072
4263
  capturedLocators: [],
4073
4264
  capturePromises: [],
4074
4265
  failedLocators: [],
4266
+ expectCapturedLocations: /* @__PURE__ */ new Set(),
4075
4267
  lastActivePage: null,
4076
4268
  testInfo: null,
4077
4269
  stashedWebVitals: null,
@@ -4475,6 +4667,53 @@ function probeElementAttrs(el, arg) {
4475
4667
  ancestors
4476
4668
  };
4477
4669
  }
4670
+ function startElementCapture(sink, target, seq, callerLocation, used) {
4671
+ const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
4672
+ const settledProbe = probe.then(
4673
+ () => void 0,
4674
+ () => void 0
4675
+ );
4676
+ PENDING_PROBES.add(settledProbe);
4677
+ settledProbe.then(() => PENDING_PROBES.delete(settledProbe));
4678
+ sink.capturePromises.push(settledProbe);
4679
+ const resolveAttrs = (async () => {
4680
+ let deadline;
4681
+ try {
4682
+ const attrs = await Promise.race([
4683
+ probe,
4684
+ new Promise((_, reject) => {
4685
+ deadline = setTimeout(() => reject(new Error("locator capture timeout")), 500);
4686
+ })
4687
+ ]);
4688
+ const role = resolveAriaRole({ ...attrs, accessibleName: null });
4689
+ const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
4690
+ const aria = role || isFormField ? await ariaSnapshotBestEffort(target, 500) : null;
4691
+ const accessibleName = extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null });
4692
+ sink.capturedLocators[seq] = {
4693
+ location: callerLocation,
4694
+ used,
4695
+ // hasLabel/selectorCounts inform alternative generation only —
4696
+ // keep the stored element to the wire shape. rolePosition and
4697
+ // ancestors ARE wire fields: the server's renamed-element match
4698
+ // uses them at heal time.
4699
+ element: {
4700
+ tagName: attrs.tagName,
4701
+ attributes: attrs.attributes,
4702
+ textContent: attrs.textContent,
4703
+ accessibleName,
4704
+ center: attrs.center,
4705
+ ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
4706
+ ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
4707
+ },
4708
+ alternatives: generateAlternatives({ ...attrs, accessibleName })
4709
+ };
4710
+ } catch {
4711
+ } finally {
4712
+ clearTimeout(deadline);
4713
+ }
4714
+ })();
4715
+ sink.capturePromises.push(resolveAttrs);
4716
+ }
4478
4717
  function wrapLocator(page, locator, originMethod, originArgs) {
4479
4718
  return new Proxy(locator, {
4480
4719
  get(target, prop) {
@@ -4490,6 +4729,38 @@ function wrapLocator(page, locator, originMethod, originArgs) {
4490
4729
  return wrapLocator(page, next, originMethod, originArgs);
4491
4730
  };
4492
4731
  }
4732
+ if (prop === EXPECT_METHOD) {
4733
+ return async (...callArgs) => {
4734
+ const sink = currentSink;
4735
+ const expression = typeof callArgs[0] === "string" ? callArgs[0] : "";
4736
+ const isNot = Boolean(callArgs[1]?.isNot);
4737
+ if (!sink || isNot || !EXPECT_CAPTURE_EXPRESSIONS.has(expression)) {
4738
+ return fn.apply(target, callArgs);
4739
+ }
4740
+ sink.lastActivePage = page;
4741
+ const callerLocation = captureCallerLocation();
4742
+ const used = {
4743
+ method: originMethod,
4744
+ args: originArgs,
4745
+ raw: `${originMethod}(${JSON.stringify(originArgs)})`
4746
+ };
4747
+ const alreadyCaptured = callerLocation !== null && sink.expectCapturedLocations.has(callerLocation);
4748
+ let seq = -1;
4749
+ if (!alreadyCaptured) {
4750
+ seq = sink.capturedLocators.length;
4751
+ sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
4752
+ }
4753
+ const result = await fn.apply(target, callArgs);
4754
+ const matches = result?.matches;
4755
+ if (matches === true && !alreadyCaptured) {
4756
+ if (callerLocation) sink.expectCapturedLocations.add(callerLocation);
4757
+ startElementCapture(sink, target, seq, callerLocation, used);
4758
+ } else if (matches === false) {
4759
+ sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
4760
+ }
4761
+ return result;
4762
+ };
4763
+ }
4493
4764
  if (!ACTION_METHOD_SET.has(prop)) return original;
4494
4765
  return async (...callArgs) => {
4495
4766
  const sink = currentSink;
@@ -4515,51 +4786,7 @@ function wrapLocator(page, locator, originMethod, originArgs) {
4515
4786
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
4516
4787
  throw error;
4517
4788
  }
4518
- const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
4519
- const settledProbe = probe.then(
4520
- () => void 0,
4521
- () => void 0
4522
- );
4523
- PENDING_PROBES.add(settledProbe);
4524
- settledProbe.then(() => PENDING_PROBES.delete(settledProbe));
4525
- sink.capturePromises.push(settledProbe);
4526
- const resolveAttrs = (async () => {
4527
- let deadline;
4528
- try {
4529
- const attrs = await Promise.race([
4530
- probe,
4531
- new Promise((_, reject) => {
4532
- deadline = setTimeout(() => reject(new Error("locator capture timeout")), 500);
4533
- })
4534
- ]);
4535
- const role = resolveAriaRole({ ...attrs, accessibleName: null });
4536
- const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
4537
- const aria = role || isFormField ? await ariaSnapshotBestEffort(target, 500) : null;
4538
- const accessibleName = extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null });
4539
- sink.capturedLocators[seq] = {
4540
- location: callerLocation,
4541
- used,
4542
- // hasLabel/selectorCounts inform alternative generation only —
4543
- // keep the stored element to the wire shape. rolePosition and
4544
- // ancestors ARE wire fields: the server's renamed-element match
4545
- // uses them at heal time.
4546
- element: {
4547
- tagName: attrs.tagName,
4548
- attributes: attrs.attributes,
4549
- textContent: attrs.textContent,
4550
- accessibleName,
4551
- center: attrs.center,
4552
- ...attrs.rolePosition ? { rolePosition: attrs.rolePosition } : {},
4553
- ...attrs.ancestors && attrs.ancestors.length > 0 ? { ancestors: attrs.ancestors } : {}
4554
- },
4555
- alternatives: generateAlternatives({ ...attrs, accessibleName })
4556
- };
4557
- } catch {
4558
- } finally {
4559
- clearTimeout(deadline);
4560
- }
4561
- })();
4562
- sink.capturePromises.push(resolveAttrs);
4789
+ startElementCapture(sink, target, seq, callerLocation, used);
4563
4790
  return result;
4564
4791
  };
4565
4792
  }