@piwitests/reporter 0.18.1 → 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/README.md +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +205 -0
- package/dist/global-setup-module.js +59 -12
- package/dist/index.d.ts +10 -2
- package/dist/index.js +389 -162
- package/dist/internal/capture/capture-fixtures.js +115 -45
- package/dist/internal/capture/locator-healing.d.ts +29 -1
- package/dist/internal/capture/locator-healing.js +38 -0
- package/package.json +8 -3
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
|
|
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
|
|
341
|
-
var
|
|
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",
|
|
525
|
-
filename:
|
|
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",
|
|
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}`,
|
|
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 (
|
|
600
|
-
form.append(`trace_${i}`,
|
|
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
|
|
611
|
-
var
|
|
612
|
-
var
|
|
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
|
|
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}` : `${
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
643
|
-
const content =
|
|
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 (
|
|
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 (
|
|
661
|
-
const stats =
|
|
698
|
+
if (fs3.existsSync(this.filePath)) {
|
|
699
|
+
const stats = fs3.statSync(this.filePath);
|
|
662
700
|
if (Date.now() - stats.mtimeMs > maxAgeMs) {
|
|
663
|
-
|
|
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
|
|
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
|
|
682
|
-
var
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
734
|
-
var
|
|
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
|
|
739
|
-
var
|
|
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
|
|
747
|
-
const full =
|
|
748
|
-
const rel =
|
|
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:
|
|
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,
|
|
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 (
|
|
791
|
-
if (
|
|
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,
|
|
836
|
+
const candidates = [dir, path6.join(process.cwd(), dir)];
|
|
799
837
|
for (const candidate of candidates) {
|
|
800
|
-
if (
|
|
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(
|
|
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 &&
|
|
868
|
+
if (a.path && fs6.existsSync(a.path)) {
|
|
831
869
|
result.push({
|
|
832
870
|
name: a.name || "attachment",
|
|
833
|
-
path:
|
|
871
|
+
path: path6.resolve(a.path),
|
|
834
872
|
contentType: a.contentType || "application/octet-stream",
|
|
835
|
-
originalName:
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
1144
|
-
var
|
|
1145
|
-
var
|
|
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
|
|
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 (
|
|
1153
|
-
const info = JSON.parse(
|
|
1154
|
-
|
|
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) =>
|
|
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
|
-
//
|
|
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[
|
|
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
|
|
1675
|
-
var
|
|
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 =
|
|
1682
|
-
const pkg = JSON.parse(
|
|
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
|
|
1698
|
-
var
|
|
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 =
|
|
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 =
|
|
1776
|
+
abs = path9.resolve(m[1]);
|
|
1739
1777
|
} catch {
|
|
1740
1778
|
continue;
|
|
1741
1779
|
}
|
|
1742
|
-
if (abs.includes(`${
|
|
1743
|
-
const rel =
|
|
1744
|
-
if (rel.startsWith("..") ||
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
1843
|
-
var
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1926
|
-
var
|
|
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
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
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) =>
|
|
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
|
-
//
|
|
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
|
|
1987
|
-
|
|
1988
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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
|
|
2013
|
-
var
|
|
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 =
|
|
2023
|
-
if (dir && dir !== ".")
|
|
2024
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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 =
|
|
3361
|
+
rel = path15.relative(process.cwd(), file);
|
|
3171
3362
|
} catch {
|
|
3172
3363
|
}
|
|
3173
|
-
rel = rel.split(
|
|
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
|
|
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 ? `${
|
|
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
|
-
|
|
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
|
}
|