playwright-checkpoint 0.3.0 → 0.4.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 +57 -0
- package/dist/{chunk-KG37WSYS.js → chunk-M3BRR3LT.js} +9 -3
- package/dist/{chunk-KG37WSYS.js.map → chunk-M3BRR3LT.js.map} +1 -1
- package/dist/{chunk-X5IPL32H.js → chunk-WXZOP7XI.js} +153 -35
- package/dist/chunk-WXZOP7XI.js.map +1 -0
- package/dist/{chunk-K5DX32TO.js → chunk-YUFXGGZM.js} +2 -2
- package/dist/cli/bin.cjs +2501 -2386
- package/dist/cli/bin.cjs.map +1 -1
- package/dist/cli/bin.js +3 -2
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/index.cjs +1405 -68
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +2 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +3 -2
- package/dist/{core-CD4jHGgI.d.cts → core-6gyzs35M.d.ts} +2 -1
- package/dist/{core-CZvnc0rE.d.ts → core-Dd3WLuTs.d.cts} +2 -1
- package/dist/core.cjs +8 -2
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +1 -1
- package/dist/{index-BjYQX_hK.d.ts → index-CvcgBzvl.d.ts} +1 -1
- package/dist/{index-Cabk31qi.d.cts → index-OQx9qcVO.d.cts} +1 -1
- package/dist/index.cjs +212 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +69 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +148 -34
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +4 -4
- package/dist/teardown.cjs +1409 -72
- package/dist/teardown.cjs.map +1 -1
- package/dist/teardown.js +3 -2
- package/dist/teardown.js.map +1 -1
- package/dist/{types-G7w4n8kR.d.cts → types-wX4eB9mb.d.cts} +16 -1
- package/dist/{types-G7w4n8kR.d.ts → types-wX4eB9mb.d.ts} +16 -1
- package/package.json +2 -1
- package/dist/chunk-X5IPL32H.js.map +0 -1
- /package/dist/{chunk-K5DX32TO.js.map → chunk-YUFXGGZM.js.map} +0 -0
package/dist/teardown.cjs
CHANGED
|
@@ -35,14 +35,14 @@ __export(teardown_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(teardown_exports);
|
|
37
37
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
38
|
-
var
|
|
38
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
39
39
|
|
|
40
40
|
// src/cli/index.ts
|
|
41
|
-
var
|
|
41
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
42
42
|
|
|
43
43
|
// src/report/index.ts
|
|
44
|
-
var
|
|
45
|
-
var
|
|
44
|
+
var import_promises16 = __toESM(require("fs/promises"), 1);
|
|
45
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
46
46
|
|
|
47
47
|
// src/report/html-reporter.ts
|
|
48
48
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
@@ -669,8 +669,1231 @@ var htmlReporter = {
|
|
|
669
669
|
};
|
|
670
670
|
|
|
671
671
|
// src/report/markdown-reporter.ts
|
|
672
|
+
var import_promises14 = __toESM(require("fs/promises"), 1);
|
|
673
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
674
|
+
|
|
675
|
+
// src/core.ts
|
|
676
|
+
var import_promises13 = __toESM(require("fs/promises"), 1);
|
|
677
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
678
|
+
|
|
679
|
+
// src/collectors/aria-snapshot.ts
|
|
672
680
|
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
673
681
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
682
|
+
function countSnapshotNodes(value) {
|
|
683
|
+
if (value == null) {
|
|
684
|
+
return 0;
|
|
685
|
+
}
|
|
686
|
+
if (Array.isArray(value)) {
|
|
687
|
+
return value.reduce((total, item) => total + countSnapshotNodes(item), 0);
|
|
688
|
+
}
|
|
689
|
+
if (typeof value !== "object") {
|
|
690
|
+
return 1;
|
|
691
|
+
}
|
|
692
|
+
const node = value;
|
|
693
|
+
const children = Array.isArray(node.children) ? node.children : [];
|
|
694
|
+
return 1 + children.reduce((total, child) => total + countSnapshotNodes(child), 0);
|
|
695
|
+
}
|
|
696
|
+
async function captureAriaSnapshot(page) {
|
|
697
|
+
try {
|
|
698
|
+
const root = page.locator(":root");
|
|
699
|
+
if (typeof root.ariaSnapshot === "function") {
|
|
700
|
+
const snapshot = await root.ariaSnapshot();
|
|
701
|
+
return snapshot ?? null;
|
|
702
|
+
}
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
if (typeof page.accessibility?.snapshot === "function") {
|
|
706
|
+
try {
|
|
707
|
+
const snapshot = await page.accessibility.snapshot({ interestingOnly: false });
|
|
708
|
+
return snapshot ?? null;
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
var ariaSnapshotCollector = {
|
|
716
|
+
name: "aria-snapshot",
|
|
717
|
+
defaultEnabled: false,
|
|
718
|
+
async collect(ctx) {
|
|
719
|
+
const snapshot = await captureAriaSnapshot(ctx.page);
|
|
720
|
+
const nodeCount = countSnapshotNodes(snapshot);
|
|
721
|
+
const outputPath = import_node_path2.default.join(ctx.checkpointDir, "aria-snapshot.json");
|
|
722
|
+
const data = {
|
|
723
|
+
snapshot,
|
|
724
|
+
nodeCount
|
|
725
|
+
};
|
|
726
|
+
await import_promises2.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
|
|
727
|
+
`, "utf8");
|
|
728
|
+
return {
|
|
729
|
+
data,
|
|
730
|
+
artifacts: [
|
|
731
|
+
{
|
|
732
|
+
name: "aria-snapshot",
|
|
733
|
+
path: outputPath,
|
|
734
|
+
contentType: "application/json"
|
|
735
|
+
}
|
|
736
|
+
],
|
|
737
|
+
summary: {
|
|
738
|
+
nodeCount
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// src/collectors/axe.ts
|
|
745
|
+
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
746
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
747
|
+
|
|
748
|
+
// src/page-utils.ts
|
|
749
|
+
async function settlePage(page) {
|
|
750
|
+
await page.waitForLoadState("domcontentloaded").catch(() => void 0);
|
|
751
|
+
await page.waitForLoadState("load", { timeout: 3e3 }).catch(() => void 0);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/collectors/axe.ts
|
|
755
|
+
var axeLoader = () => import("@axe-core/playwright");
|
|
756
|
+
var warnedAboutMissingAxe = false;
|
|
757
|
+
function warnOnce(message, error) {
|
|
758
|
+
if (warnedAboutMissingAxe) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
warnedAboutMissingAxe = true;
|
|
762
|
+
if (error instanceof Error) {
|
|
763
|
+
console.warn(`[playwright-checkpoint] ${message}`, error);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (error !== void 0) {
|
|
767
|
+
console.warn(`[playwright-checkpoint] ${message}`, String(error));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
console.warn(`[playwright-checkpoint] ${message}`);
|
|
771
|
+
}
|
|
772
|
+
function resolveAxeBuilder(module2) {
|
|
773
|
+
return module2.default ?? module2.AxeBuilder ?? null;
|
|
774
|
+
}
|
|
775
|
+
async function analyzeAccessibility(page, AxeBuilder) {
|
|
776
|
+
try {
|
|
777
|
+
await settlePage(page);
|
|
778
|
+
return await new AxeBuilder({ page }).analyze();
|
|
779
|
+
} catch {
|
|
780
|
+
await page.waitForTimeout(500);
|
|
781
|
+
await settlePage(page);
|
|
782
|
+
return await new AxeBuilder({ page }).analyze();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function skippedAxeResult(reason) {
|
|
786
|
+
return {
|
|
787
|
+
data: {
|
|
788
|
+
skipped: true,
|
|
789
|
+
reason,
|
|
790
|
+
violations: 0,
|
|
791
|
+
results: null
|
|
792
|
+
},
|
|
793
|
+
artifacts: [],
|
|
794
|
+
summary: {
|
|
795
|
+
violations: 0
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
var axeCollector = {
|
|
800
|
+
name: "axe",
|
|
801
|
+
defaultEnabled: true,
|
|
802
|
+
async collect(ctx) {
|
|
803
|
+
const timeoutBudgetMs = typeof ctx.config.timeoutMs === "number" ? ctx.config.timeoutMs : 5e3;
|
|
804
|
+
if (timeoutBudgetMs > 0) {
|
|
805
|
+
if (typeof ctx.adjustTimeout === "function") {
|
|
806
|
+
ctx.adjustTimeout(timeoutBudgetMs);
|
|
807
|
+
} else if (ctx.testInfo && typeof ctx.testInfo.setTimeout === "function") {
|
|
808
|
+
ctx.testInfo.setTimeout(ctx.testInfo.timeout + timeoutBudgetMs);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
let module2;
|
|
812
|
+
try {
|
|
813
|
+
module2 = await axeLoader();
|
|
814
|
+
} catch (error) {
|
|
815
|
+
warnOnce("Skipping axe collector because @axe-core/playwright is unavailable.", error);
|
|
816
|
+
return skippedAxeResult("@axe-core/playwright is unavailable");
|
|
817
|
+
}
|
|
818
|
+
const AxeBuilder = resolveAxeBuilder(module2);
|
|
819
|
+
if (!AxeBuilder) {
|
|
820
|
+
warnOnce("Skipping axe collector because @axe-core/playwright did not expose an AxeBuilder export.");
|
|
821
|
+
return skippedAxeResult("@axe-core/playwright did not expose AxeBuilder");
|
|
822
|
+
}
|
|
823
|
+
const results = await analyzeAccessibility(ctx.page, AxeBuilder);
|
|
824
|
+
const violations = results && typeof results === "object" && Array.isArray(results.violations) ? results.violations.length : 0;
|
|
825
|
+
const axePath = import_node_path3.default.join(ctx.checkpointDir, "axe.json");
|
|
826
|
+
await import_promises3.default.writeFile(axePath, `${JSON.stringify(results, null, 2)}
|
|
827
|
+
`, "utf8");
|
|
828
|
+
return {
|
|
829
|
+
data: {
|
|
830
|
+
skipped: false,
|
|
831
|
+
reason: null,
|
|
832
|
+
violations,
|
|
833
|
+
results
|
|
834
|
+
},
|
|
835
|
+
artifacts: [
|
|
836
|
+
{
|
|
837
|
+
name: "axe",
|
|
838
|
+
path: axePath,
|
|
839
|
+
contentType: "application/json"
|
|
840
|
+
}
|
|
841
|
+
],
|
|
842
|
+
summary: {
|
|
843
|
+
violations
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/collectors/console.ts
|
|
850
|
+
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
851
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
852
|
+
var consoleStates = /* @__PURE__ */ new WeakMap();
|
|
853
|
+
function getLocation(message) {
|
|
854
|
+
const location2 = message.location();
|
|
855
|
+
if (!location2.url && location2.lineNumber == null && location2.columnNumber == null) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
...location2.url ? { url: location2.url } : {},
|
|
860
|
+
...location2.lineNumber == null ? {} : { lineNumber: location2.lineNumber },
|
|
861
|
+
...location2.columnNumber == null ? {} : { columnNumber: location2.columnNumber }
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
var consoleCollector = {
|
|
865
|
+
name: "console",
|
|
866
|
+
defaultEnabled: true,
|
|
867
|
+
async setup({ page }) {
|
|
868
|
+
if (consoleStates.has(page)) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const entries = [];
|
|
872
|
+
const recordConsoleMessage = (message) => {
|
|
873
|
+
if (message.type() !== "error") {
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
entries.push({
|
|
877
|
+
type: message.type(),
|
|
878
|
+
text: message.text(),
|
|
879
|
+
location: getLocation(message),
|
|
880
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
881
|
+
});
|
|
882
|
+
};
|
|
883
|
+
const recordPageError = (error) => {
|
|
884
|
+
entries.push({
|
|
885
|
+
type: "pageerror",
|
|
886
|
+
text: error.message,
|
|
887
|
+
location: null,
|
|
888
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
889
|
+
});
|
|
890
|
+
};
|
|
891
|
+
page.on("console", recordConsoleMessage);
|
|
892
|
+
page.on("pageerror", recordPageError);
|
|
893
|
+
consoleStates.set(page, {
|
|
894
|
+
entries,
|
|
895
|
+
offset: 0,
|
|
896
|
+
recordConsoleMessage,
|
|
897
|
+
recordPageError
|
|
898
|
+
});
|
|
899
|
+
},
|
|
900
|
+
async collect(ctx) {
|
|
901
|
+
const state = consoleStates.get(ctx.page);
|
|
902
|
+
const checkpointEntries = state ? state.entries.slice(state.offset) : [];
|
|
903
|
+
if (state) {
|
|
904
|
+
state.offset = state.entries.length;
|
|
905
|
+
}
|
|
906
|
+
const outputPath = import_node_path4.default.join(ctx.checkpointDir, "console-errors.json");
|
|
907
|
+
await import_promises4.default.writeFile(outputPath, `${JSON.stringify(checkpointEntries, null, 2)}
|
|
908
|
+
`, "utf8");
|
|
909
|
+
return {
|
|
910
|
+
data: checkpointEntries,
|
|
911
|
+
artifacts: [
|
|
912
|
+
{
|
|
913
|
+
name: "console-errors",
|
|
914
|
+
path: outputPath,
|
|
915
|
+
contentType: "application/json"
|
|
916
|
+
}
|
|
917
|
+
],
|
|
918
|
+
summary: {
|
|
919
|
+
consoleErrorCount: checkpointEntries.length
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
},
|
|
923
|
+
async teardown({ page }) {
|
|
924
|
+
const state = consoleStates.get(page);
|
|
925
|
+
if (!state) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
page.off("console", state.recordConsoleMessage);
|
|
929
|
+
page.off("pageerror", state.recordPageError);
|
|
930
|
+
consoleStates.delete(page);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// src/collectors/dom-stats.ts
|
|
935
|
+
var import_promises5 = __toESM(require("fs/promises"), 1);
|
|
936
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
937
|
+
var domStatsCollector = {
|
|
938
|
+
name: "dom-stats",
|
|
939
|
+
defaultEnabled: false,
|
|
940
|
+
async collect(ctx) {
|
|
941
|
+
const stats = await ctx.page.evaluate(() => {
|
|
942
|
+
const allNodes = document.querySelectorAll("*");
|
|
943
|
+
const maxDepthFrom = (root) => {
|
|
944
|
+
if (!root) {
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
let maxDepth = 1;
|
|
948
|
+
const queue = [{ node: root, depth: 1 }];
|
|
949
|
+
while (queue.length > 0) {
|
|
950
|
+
const current = queue.shift();
|
|
951
|
+
if (!current) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
maxDepth = Math.max(maxDepth, current.depth);
|
|
955
|
+
for (const child of Array.from(current.node.children)) {
|
|
956
|
+
queue.push({ node: child, depth: current.depth + 1 });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return maxDepth;
|
|
960
|
+
};
|
|
961
|
+
const maybeGetEventListeners = globalThis.getEventListeners;
|
|
962
|
+
let eventListenerCount = null;
|
|
963
|
+
if (typeof maybeGetEventListeners === "function") {
|
|
964
|
+
eventListenerCount = 0;
|
|
965
|
+
const targets = [window, document, ...Array.from(allNodes)];
|
|
966
|
+
for (const target of targets) {
|
|
967
|
+
try {
|
|
968
|
+
const listeners = maybeGetEventListeners(target) ?? {};
|
|
969
|
+
for (const entries of Object.values(listeners)) {
|
|
970
|
+
eventListenerCount += Array.isArray(entries) ? entries.length : 0;
|
|
971
|
+
}
|
|
972
|
+
} catch {
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return {
|
|
977
|
+
nodeCount: allNodes.length,
|
|
978
|
+
maxDepth: maxDepthFrom(document.documentElement),
|
|
979
|
+
formCount: document.querySelectorAll("form").length,
|
|
980
|
+
imageCount: document.querySelectorAll("img").length,
|
|
981
|
+
scriptCount: document.querySelectorAll("script").length,
|
|
982
|
+
stylesheetCount: document.styleSheets.length,
|
|
983
|
+
eventListenerCount
|
|
984
|
+
};
|
|
985
|
+
});
|
|
986
|
+
const data = {
|
|
987
|
+
nodeCount: stats.nodeCount,
|
|
988
|
+
maxDepth: stats.maxDepth,
|
|
989
|
+
formCount: stats.formCount,
|
|
990
|
+
imageCount: stats.imageCount,
|
|
991
|
+
scriptCount: stats.scriptCount,
|
|
992
|
+
stylesheetCount: stats.stylesheetCount,
|
|
993
|
+
eventListenerCount: stats.eventListenerCount
|
|
994
|
+
};
|
|
995
|
+
const outputPath = import_node_path5.default.join(ctx.checkpointDir, "dom-stats.json");
|
|
996
|
+
await import_promises5.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
|
|
997
|
+
`, "utf8");
|
|
998
|
+
return {
|
|
999
|
+
data,
|
|
1000
|
+
artifacts: [
|
|
1001
|
+
{
|
|
1002
|
+
name: "dom-stats",
|
|
1003
|
+
path: outputPath,
|
|
1004
|
+
contentType: "application/json"
|
|
1005
|
+
}
|
|
1006
|
+
],
|
|
1007
|
+
summary: {
|
|
1008
|
+
nodeCount: data.nodeCount,
|
|
1009
|
+
maxDepth: data.maxDepth,
|
|
1010
|
+
formCount: data.formCount,
|
|
1011
|
+
imageCount: data.imageCount
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// src/collectors/forms.ts
|
|
1018
|
+
var import_promises6 = __toESM(require("fs/promises"), 1);
|
|
1019
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1020
|
+
var REDACTED = "[REDACTED]";
|
|
1021
|
+
var DEFAULT_REDACT_PATTERNS = ["password", "token", "secret", "api[_-]?key", "authorization", "bearer"];
|
|
1022
|
+
var EMAIL_LIKE_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1023
|
+
function toRegex(pattern) {
|
|
1024
|
+
const trimmed = pattern.trim();
|
|
1025
|
+
if (!trimmed) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
try {
|
|
1029
|
+
return new RegExp(trimmed, "i");
|
|
1030
|
+
} catch {
|
|
1031
|
+
return null;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function redactionRegexes(ctx) {
|
|
1035
|
+
const fromConfig = Array.isArray(ctx.config.redact) ? ctx.config.redact.filter((entry) => typeof entry === "string") : [];
|
|
1036
|
+
return [...DEFAULT_REDACT_PATTERNS, ...ctx.redact, ...fromConfig].map((pattern) => toRegex(pattern)).filter((value) => value instanceof RegExp);
|
|
1037
|
+
}
|
|
1038
|
+
function shouldRedactText(value, regexes) {
|
|
1039
|
+
if (EMAIL_LIKE_REGEX.test(value.trim())) {
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
return regexes.some((regex) => regex.test(value));
|
|
1043
|
+
}
|
|
1044
|
+
function fieldIdentifier(field) {
|
|
1045
|
+
return [field.type, field.name, field.id, field.label, field.placeholder].filter((value) => !!value).join(" ");
|
|
1046
|
+
}
|
|
1047
|
+
function redactValue(value) {
|
|
1048
|
+
if (value == null) {
|
|
1049
|
+
return value;
|
|
1050
|
+
}
|
|
1051
|
+
if (Array.isArray(value)) {
|
|
1052
|
+
return value.map(() => REDACTED);
|
|
1053
|
+
}
|
|
1054
|
+
return REDACTED;
|
|
1055
|
+
}
|
|
1056
|
+
function fieldNeedsRedaction(field, regexes) {
|
|
1057
|
+
if (shouldRedactText(fieldIdentifier(field), regexes)) {
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
if (field.value == null) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
if (Array.isArray(field.value)) {
|
|
1064
|
+
return field.value.some((entry) => shouldRedactText(entry, regexes));
|
|
1065
|
+
}
|
|
1066
|
+
return shouldRedactText(field.value, regexes);
|
|
1067
|
+
}
|
|
1068
|
+
var formsCollector = {
|
|
1069
|
+
name: "forms",
|
|
1070
|
+
defaultEnabled: false,
|
|
1071
|
+
async collect(ctx) {
|
|
1072
|
+
const rawFields = await ctx.page.evaluate(() => {
|
|
1073
|
+
const elements = Array.from(document.querySelectorAll("input, select, textarea"));
|
|
1074
|
+
const isVisible = (element) => {
|
|
1075
|
+
if (!(element instanceof HTMLElement)) {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
const inputType = element instanceof HTMLInputElement ? element.type.toLowerCase() : null;
|
|
1079
|
+
if (inputType === "hidden") {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
const style = window.getComputedStyle(element);
|
|
1086
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
const rect = element.getBoundingClientRect();
|
|
1090
|
+
return rect.width > 0 && rect.height > 0;
|
|
1091
|
+
};
|
|
1092
|
+
const readLabel = (element) => {
|
|
1093
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
1094
|
+
const fromLabels = element.labels && element.labels.length > 0 ? element.labels[0]?.textContent?.trim() : null;
|
|
1095
|
+
if (fromLabels) {
|
|
1096
|
+
return fromLabels;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return element.getAttribute("aria-label")?.trim() ?? null;
|
|
1100
|
+
};
|
|
1101
|
+
const readValue = (element) => {
|
|
1102
|
+
if (element instanceof HTMLSelectElement) {
|
|
1103
|
+
if (element.multiple) {
|
|
1104
|
+
return {
|
|
1105
|
+
value: Array.from(element.selectedOptions).map((option) => option.value),
|
|
1106
|
+
checked: null,
|
|
1107
|
+
type: "select-multiple"
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
value: element.value,
|
|
1112
|
+
checked: null,
|
|
1113
|
+
type: "select-one"
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
if (element instanceof HTMLTextAreaElement) {
|
|
1117
|
+
return {
|
|
1118
|
+
value: element.value,
|
|
1119
|
+
checked: null,
|
|
1120
|
+
type: "textarea"
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
if (element instanceof HTMLInputElement) {
|
|
1124
|
+
const inputType = element.type.toLowerCase();
|
|
1125
|
+
if (inputType === "checkbox" || inputType === "radio") {
|
|
1126
|
+
return {
|
|
1127
|
+
value: element.checked ? element.value || "on" : null,
|
|
1128
|
+
checked: element.checked,
|
|
1129
|
+
type: inputType
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
if (inputType === "file") {
|
|
1133
|
+
return {
|
|
1134
|
+
value: element.files ? Array.from(element.files).map((file) => file.name) : [],
|
|
1135
|
+
checked: null,
|
|
1136
|
+
type: inputType
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
value: element.value,
|
|
1141
|
+
checked: null,
|
|
1142
|
+
type: inputType || null
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
return {
|
|
1146
|
+
value: null,
|
|
1147
|
+
checked: null,
|
|
1148
|
+
type: null
|
|
1149
|
+
};
|
|
1150
|
+
};
|
|
1151
|
+
return elements.filter((element) => isVisible(element)).map((element) => {
|
|
1152
|
+
const { value, checked, type } = readValue(element);
|
|
1153
|
+
return {
|
|
1154
|
+
tagName: element.tagName.toLowerCase(),
|
|
1155
|
+
type,
|
|
1156
|
+
name: element.getAttribute("name"),
|
|
1157
|
+
id: element.getAttribute("id"),
|
|
1158
|
+
label: readLabel(element),
|
|
1159
|
+
placeholder: element.getAttribute("placeholder"),
|
|
1160
|
+
value,
|
|
1161
|
+
checked,
|
|
1162
|
+
disabled: element.disabled,
|
|
1163
|
+
required: element.required
|
|
1164
|
+
};
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
const regexes = redactionRegexes({ redact: ctx.redact, config: ctx.config });
|
|
1168
|
+
let redactedCount = 0;
|
|
1169
|
+
const fields = rawFields.map((field) => {
|
|
1170
|
+
const redacted = fieldNeedsRedaction(field, regexes);
|
|
1171
|
+
if (redacted) {
|
|
1172
|
+
redactedCount += 1;
|
|
1173
|
+
}
|
|
1174
|
+
return {
|
|
1175
|
+
...field,
|
|
1176
|
+
redacted,
|
|
1177
|
+
value: redacted ? redactValue(field.value) : field.value
|
|
1178
|
+
};
|
|
1179
|
+
});
|
|
1180
|
+
const data = {
|
|
1181
|
+
fieldCount: fields.length,
|
|
1182
|
+
redactedCount,
|
|
1183
|
+
fields
|
|
1184
|
+
};
|
|
1185
|
+
const outputPath = import_node_path6.default.join(ctx.checkpointDir, "form-state.json");
|
|
1186
|
+
await import_promises6.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
|
|
1187
|
+
`, "utf8");
|
|
1188
|
+
return {
|
|
1189
|
+
data,
|
|
1190
|
+
artifacts: [
|
|
1191
|
+
{
|
|
1192
|
+
name: "form-state",
|
|
1193
|
+
path: outputPath,
|
|
1194
|
+
contentType: "application/json"
|
|
1195
|
+
}
|
|
1196
|
+
],
|
|
1197
|
+
summary: {
|
|
1198
|
+
fieldCount: data.fieldCount,
|
|
1199
|
+
redactedCount: data.redactedCount
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// src/collectors/html.ts
|
|
1206
|
+
var import_promises7 = __toESM(require("fs/promises"), 1);
|
|
1207
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1208
|
+
async function readPageContent(page) {
|
|
1209
|
+
try {
|
|
1210
|
+
await settlePage(page);
|
|
1211
|
+
return await page.content();
|
|
1212
|
+
} catch {
|
|
1213
|
+
await page.waitForTimeout(500);
|
|
1214
|
+
await settlePage(page);
|
|
1215
|
+
return await page.content();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
var htmlCollector = {
|
|
1219
|
+
name: "html",
|
|
1220
|
+
defaultEnabled: true,
|
|
1221
|
+
async collect(ctx) {
|
|
1222
|
+
const htmlPath = import_node_path7.default.join(ctx.checkpointDir, "page.html");
|
|
1223
|
+
const html = await readPageContent(ctx.page);
|
|
1224
|
+
await import_promises7.default.writeFile(htmlPath, html, "utf8");
|
|
1225
|
+
return {
|
|
1226
|
+
data: {
|
|
1227
|
+
contentLength: html.length
|
|
1228
|
+
},
|
|
1229
|
+
artifacts: [
|
|
1230
|
+
{
|
|
1231
|
+
name: "html",
|
|
1232
|
+
path: htmlPath,
|
|
1233
|
+
contentType: "text/html"
|
|
1234
|
+
}
|
|
1235
|
+
],
|
|
1236
|
+
summary: {
|
|
1237
|
+
htmlPath: "page.html"
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
// src/collectors/metadata.ts
|
|
1244
|
+
var import_promises8 = __toESM(require("fs/promises"), 1);
|
|
1245
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
1246
|
+
function normalizeStructuredData(scriptContents) {
|
|
1247
|
+
const values = [];
|
|
1248
|
+
for (const content of scriptContents) {
|
|
1249
|
+
const value = content?.trim();
|
|
1250
|
+
if (!value) {
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
values.push(JSON.parse(value));
|
|
1255
|
+
} catch {
|
|
1256
|
+
values.push({
|
|
1257
|
+
parseError: "Invalid JSON-LD",
|
|
1258
|
+
raw: value
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return values;
|
|
1263
|
+
}
|
|
1264
|
+
var metadataCollector = {
|
|
1265
|
+
name: "metadata",
|
|
1266
|
+
defaultEnabled: true,
|
|
1267
|
+
async collect(ctx) {
|
|
1268
|
+
const metadata = await ctx.page.evaluate(() => {
|
|
1269
|
+
const meta = (selector) => document.querySelector(selector)?.getAttribute("content") ?? null;
|
|
1270
|
+
const canonicalLink = document.querySelector('link[rel="canonical"]');
|
|
1271
|
+
const html = document.documentElement;
|
|
1272
|
+
const structuredDataScripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map((script) => script.textContent ?? null);
|
|
1273
|
+
return {
|
|
1274
|
+
url: location.href,
|
|
1275
|
+
title: document.title,
|
|
1276
|
+
description: meta('meta[name="description"]'),
|
|
1277
|
+
openGraph: {
|
|
1278
|
+
title: meta('meta[property="og:title"]'),
|
|
1279
|
+
description: meta('meta[property="og:description"]'),
|
|
1280
|
+
image: meta('meta[property="og:image"]')
|
|
1281
|
+
},
|
|
1282
|
+
canonicalUrl: canonicalLink?.getAttribute("href") ?? null,
|
|
1283
|
+
lang: html.getAttribute("lang"),
|
|
1284
|
+
viewport: meta('meta[name="viewport"]'),
|
|
1285
|
+
structuredDataScripts
|
|
1286
|
+
};
|
|
1287
|
+
});
|
|
1288
|
+
const normalizedMetadata = {
|
|
1289
|
+
url: metadata.url,
|
|
1290
|
+
title: metadata.title,
|
|
1291
|
+
description: metadata.description,
|
|
1292
|
+
openGraph: metadata.openGraph,
|
|
1293
|
+
canonicalUrl: metadata.canonicalUrl,
|
|
1294
|
+
lang: metadata.lang,
|
|
1295
|
+
viewport: metadata.viewport,
|
|
1296
|
+
structuredData: normalizeStructuredData(metadata.structuredDataScripts)
|
|
1297
|
+
};
|
|
1298
|
+
const outputPath = import_node_path8.default.join(ctx.checkpointDir, "metadata.json");
|
|
1299
|
+
await import_promises8.default.writeFile(outputPath, `${JSON.stringify(normalizedMetadata, null, 2)}
|
|
1300
|
+
`, "utf8");
|
|
1301
|
+
return {
|
|
1302
|
+
data: normalizedMetadata,
|
|
1303
|
+
artifacts: [
|
|
1304
|
+
{
|
|
1305
|
+
name: "metadata",
|
|
1306
|
+
path: outputPath,
|
|
1307
|
+
contentType: "application/json"
|
|
1308
|
+
}
|
|
1309
|
+
],
|
|
1310
|
+
summary: {
|
|
1311
|
+
url: normalizedMetadata.url,
|
|
1312
|
+
title: normalizedMetadata.title,
|
|
1313
|
+
lang: normalizedMetadata.lang
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
// src/collectors/network.ts
|
|
1320
|
+
var import_promises9 = __toESM(require("fs/promises"), 1);
|
|
1321
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
1322
|
+
var networkStates = /* @__PURE__ */ new WeakMap();
|
|
1323
|
+
var networkCollector = {
|
|
1324
|
+
name: "network",
|
|
1325
|
+
defaultEnabled: true,
|
|
1326
|
+
async setup({ page }) {
|
|
1327
|
+
if (networkStates.has(page)) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const entries = [];
|
|
1331
|
+
const recordRequestFailure = (request) => {
|
|
1332
|
+
entries.push({
|
|
1333
|
+
kind: "requestfailed",
|
|
1334
|
+
url: request.url(),
|
|
1335
|
+
method: request.method(),
|
|
1336
|
+
status: null,
|
|
1337
|
+
statusText: null,
|
|
1338
|
+
failureText: request.failure()?.errorText ?? null,
|
|
1339
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1340
|
+
});
|
|
1341
|
+
};
|
|
1342
|
+
const recordHttpError = (response) => {
|
|
1343
|
+
if (response.status() < 400) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
entries.push({
|
|
1347
|
+
kind: "http-error",
|
|
1348
|
+
url: response.url(),
|
|
1349
|
+
method: response.request().method(),
|
|
1350
|
+
status: response.status(),
|
|
1351
|
+
statusText: response.statusText(),
|
|
1352
|
+
failureText: null,
|
|
1353
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1354
|
+
});
|
|
1355
|
+
};
|
|
1356
|
+
page.on("requestfailed", recordRequestFailure);
|
|
1357
|
+
page.on("response", recordHttpError);
|
|
1358
|
+
networkStates.set(page, {
|
|
1359
|
+
entries,
|
|
1360
|
+
offset: 0,
|
|
1361
|
+
recordRequestFailure,
|
|
1362
|
+
recordHttpError
|
|
1363
|
+
});
|
|
1364
|
+
},
|
|
1365
|
+
async collect(ctx) {
|
|
1366
|
+
const state = networkStates.get(ctx.page);
|
|
1367
|
+
const checkpointEntries = state ? state.entries.slice(state.offset) : [];
|
|
1368
|
+
if (state) {
|
|
1369
|
+
state.offset = state.entries.length;
|
|
1370
|
+
}
|
|
1371
|
+
const outputPath = import_node_path9.default.join(ctx.checkpointDir, "failed-requests.json");
|
|
1372
|
+
await import_promises9.default.writeFile(outputPath, `${JSON.stringify(checkpointEntries, null, 2)}
|
|
1373
|
+
`, "utf8");
|
|
1374
|
+
return {
|
|
1375
|
+
data: checkpointEntries,
|
|
1376
|
+
artifacts: [
|
|
1377
|
+
{
|
|
1378
|
+
name: "failed-requests",
|
|
1379
|
+
path: outputPath,
|
|
1380
|
+
contentType: "application/json"
|
|
1381
|
+
}
|
|
1382
|
+
],
|
|
1383
|
+
summary: {
|
|
1384
|
+
failedRequestCount: checkpointEntries.length
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
},
|
|
1388
|
+
async teardown({ page }) {
|
|
1389
|
+
const state = networkStates.get(page);
|
|
1390
|
+
if (!state) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
page.off("requestfailed", state.recordRequestFailure);
|
|
1394
|
+
page.off("response", state.recordHttpError);
|
|
1395
|
+
networkStates.delete(page);
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
// src/collectors/network-timing.ts
|
|
1400
|
+
var import_promises10 = __toESM(require("fs/promises"), 1);
|
|
1401
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
1402
|
+
var timingStates = /* @__PURE__ */ new WeakMap();
|
|
1403
|
+
function maybeDuration(start, end) {
|
|
1404
|
+
if (start <= 0 || end <= 0 || end < start) {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
return end - start;
|
|
1408
|
+
}
|
|
1409
|
+
function toNetworkRecord(response, timing) {
|
|
1410
|
+
return {
|
|
1411
|
+
url: response.url,
|
|
1412
|
+
status: response.status,
|
|
1413
|
+
statusText: response.statusText,
|
|
1414
|
+
resourceType: response.resourceType,
|
|
1415
|
+
timestamp: response.timestamp,
|
|
1416
|
+
durationMs: timing ? timing.duration : null,
|
|
1417
|
+
transferSize: timing ? timing.transferSize : null,
|
|
1418
|
+
encodedBodySize: timing ? timing.encodedBodySize : null,
|
|
1419
|
+
decodedBodySize: timing ? timing.decodedBodySize : null,
|
|
1420
|
+
nextHopProtocol: timing ? timing.nextHopProtocol || null : null,
|
|
1421
|
+
timing: {
|
|
1422
|
+
startTimeMs: timing ? timing.startTime : null,
|
|
1423
|
+
redirectMs: timing ? maybeDuration(timing.redirectStart, timing.redirectEnd) : null,
|
|
1424
|
+
dnsMs: timing ? maybeDuration(timing.domainLookupStart, timing.domainLookupEnd) : null,
|
|
1425
|
+
connectMs: timing ? maybeDuration(timing.connectStart, timing.connectEnd) : null,
|
|
1426
|
+
tlsMs: timing ? maybeDuration(timing.secureConnectionStart, timing.connectEnd) : null,
|
|
1427
|
+
requestMs: timing ? maybeDuration(timing.requestStart, timing.responseStart) : null,
|
|
1428
|
+
responseMs: timing ? maybeDuration(timing.responseStart, timing.responseEnd) : null
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
var networkTimingCollector = {
|
|
1433
|
+
name: "network-timing",
|
|
1434
|
+
defaultEnabled: false,
|
|
1435
|
+
async setup({ page }) {
|
|
1436
|
+
if (timingStates.has(page)) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
const responses = [];
|
|
1440
|
+
const recordResponse = (response) => {
|
|
1441
|
+
responses.push({
|
|
1442
|
+
url: response.url(),
|
|
1443
|
+
status: response.status(),
|
|
1444
|
+
statusText: response.statusText(),
|
|
1445
|
+
resourceType: response.request().resourceType(),
|
|
1446
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1447
|
+
});
|
|
1448
|
+
};
|
|
1449
|
+
page.on("response", recordResponse);
|
|
1450
|
+
timingStates.set(page, {
|
|
1451
|
+
responses,
|
|
1452
|
+
responseOffset: 0,
|
|
1453
|
+
resourceOffsetByUrl: /* @__PURE__ */ new Map(),
|
|
1454
|
+
recordResponse
|
|
1455
|
+
});
|
|
1456
|
+
},
|
|
1457
|
+
async collect(ctx) {
|
|
1458
|
+
const state = timingStates.get(ctx.page);
|
|
1459
|
+
const recentResponses = state ? state.responses.slice(state.responseOffset) : [];
|
|
1460
|
+
if (state) {
|
|
1461
|
+
state.responseOffset = state.responses.length;
|
|
1462
|
+
}
|
|
1463
|
+
const resourceTimings = await ctx.page.evaluate(() => {
|
|
1464
|
+
const entries = performance.getEntriesByType("resource");
|
|
1465
|
+
return entries.map((entry) => ({
|
|
1466
|
+
name: entry.name,
|
|
1467
|
+
duration: entry.duration,
|
|
1468
|
+
transferSize: entry.transferSize,
|
|
1469
|
+
encodedBodySize: entry.encodedBodySize,
|
|
1470
|
+
decodedBodySize: entry.decodedBodySize,
|
|
1471
|
+
nextHopProtocol: entry.nextHopProtocol,
|
|
1472
|
+
startTime: entry.startTime,
|
|
1473
|
+
redirectStart: entry.redirectStart,
|
|
1474
|
+
redirectEnd: entry.redirectEnd,
|
|
1475
|
+
domainLookupStart: entry.domainLookupStart,
|
|
1476
|
+
domainLookupEnd: entry.domainLookupEnd,
|
|
1477
|
+
connectStart: entry.connectStart,
|
|
1478
|
+
connectEnd: entry.connectEnd,
|
|
1479
|
+
secureConnectionStart: entry.secureConnectionStart,
|
|
1480
|
+
requestStart: entry.requestStart,
|
|
1481
|
+
responseStart: entry.responseStart,
|
|
1482
|
+
responseEnd: entry.responseEnd
|
|
1483
|
+
}));
|
|
1484
|
+
});
|
|
1485
|
+
const timingsByUrl = /* @__PURE__ */ new Map();
|
|
1486
|
+
for (const timing of resourceTimings) {
|
|
1487
|
+
const list = timingsByUrl.get(timing.name);
|
|
1488
|
+
if (list) {
|
|
1489
|
+
list.push(timing);
|
|
1490
|
+
} else {
|
|
1491
|
+
timingsByUrl.set(timing.name, [timing]);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const requests = recentResponses.map((response) => {
|
|
1495
|
+
if (!state) {
|
|
1496
|
+
return toNetworkRecord(response, null);
|
|
1497
|
+
}
|
|
1498
|
+
const list = timingsByUrl.get(response.url) ?? [];
|
|
1499
|
+
const currentOffset = state.resourceOffsetByUrl.get(response.url) ?? 0;
|
|
1500
|
+
const match = list[currentOffset] ?? null;
|
|
1501
|
+
if (match) {
|
|
1502
|
+
state.resourceOffsetByUrl.set(response.url, currentOffset + 1);
|
|
1503
|
+
}
|
|
1504
|
+
return toNetworkRecord(response, match);
|
|
1505
|
+
});
|
|
1506
|
+
const totalBytes = requests.reduce((total, request) => {
|
|
1507
|
+
if (typeof request.transferSize !== "number" || request.transferSize < 0) {
|
|
1508
|
+
return total;
|
|
1509
|
+
}
|
|
1510
|
+
return total + request.transferSize;
|
|
1511
|
+
}, 0);
|
|
1512
|
+
const slowestRequestMs = requests.reduce((slowest, request) => {
|
|
1513
|
+
if (typeof request.durationMs !== "number") {
|
|
1514
|
+
return slowest;
|
|
1515
|
+
}
|
|
1516
|
+
return Math.max(slowest, request.durationMs);
|
|
1517
|
+
}, 0);
|
|
1518
|
+
const data = {
|
|
1519
|
+
requestCount: requests.length,
|
|
1520
|
+
totalBytes,
|
|
1521
|
+
slowestRequestMs,
|
|
1522
|
+
requests
|
|
1523
|
+
};
|
|
1524
|
+
const outputPath = import_node_path10.default.join(ctx.checkpointDir, "network-timing.json");
|
|
1525
|
+
await import_promises10.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
|
|
1526
|
+
`, "utf8");
|
|
1527
|
+
return {
|
|
1528
|
+
data,
|
|
1529
|
+
artifacts: [
|
|
1530
|
+
{
|
|
1531
|
+
name: "network-timing",
|
|
1532
|
+
path: outputPath,
|
|
1533
|
+
contentType: "application/json"
|
|
1534
|
+
}
|
|
1535
|
+
],
|
|
1536
|
+
summary: {
|
|
1537
|
+
requestCount: data.requestCount,
|
|
1538
|
+
totalBytes: data.totalBytes,
|
|
1539
|
+
slowestRequestMs: data.slowestRequestMs
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
},
|
|
1543
|
+
async teardown({ page }) {
|
|
1544
|
+
const state = timingStates.get(page);
|
|
1545
|
+
if (!state) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
page.off("response", state.recordResponse);
|
|
1549
|
+
timingStates.delete(page);
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
// src/collectors/screenshot.ts
|
|
1554
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
1555
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1556
|
+
function readPngSize(buffer) {
|
|
1557
|
+
if (buffer.length < 24 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
if (buffer.toString("ascii", 12, 16) !== "IHDR") {
|
|
1561
|
+
return null;
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
width: buffer.readUInt32BE(16),
|
|
1565
|
+
height: buffer.readUInt32BE(20)
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
var screenshotCollector = {
|
|
1569
|
+
name: "screenshot",
|
|
1570
|
+
defaultEnabled: true,
|
|
1571
|
+
async collect(ctx) {
|
|
1572
|
+
const fullPage = ctx.options.fullPage ?? true;
|
|
1573
|
+
const screenshotPath = import_node_path11.default.join(ctx.checkpointDir, "page.png");
|
|
1574
|
+
const screenshotBuffer = await ctx.page.screenshot({ path: screenshotPath, fullPage });
|
|
1575
|
+
let highlightBounds = null;
|
|
1576
|
+
if (ctx.options.highlightSelector) {
|
|
1577
|
+
highlightBounds = await ctx.page.locator(ctx.options.highlightSelector).boundingBox().catch(() => null);
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
data: {
|
|
1581
|
+
fullPage,
|
|
1582
|
+
highlightBounds,
|
|
1583
|
+
highlightSelector: ctx.options.highlightSelector ?? null,
|
|
1584
|
+
imageSize: Buffer.isBuffer(screenshotBuffer) ? readPngSize(screenshotBuffer) : null
|
|
1585
|
+
},
|
|
1586
|
+
artifacts: [
|
|
1587
|
+
{
|
|
1588
|
+
name: "screenshot",
|
|
1589
|
+
path: screenshotPath,
|
|
1590
|
+
contentType: "image/png"
|
|
1591
|
+
}
|
|
1592
|
+
],
|
|
1593
|
+
summary: {
|
|
1594
|
+
screenshotPath: "page.png"
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
// src/collectors/storage.ts
|
|
1601
|
+
var import_promises11 = __toESM(require("fs/promises"), 1);
|
|
1602
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
1603
|
+
var REDACTED2 = "[REDACTED]";
|
|
1604
|
+
var DEFAULT_REDACT_PATTERNS2 = ["password", "token", "secret", "api[_-]?key", "authorization", "session", "email"];
|
|
1605
|
+
var EMAIL_LIKE_REGEX2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1606
|
+
function toRegex2(pattern) {
|
|
1607
|
+
const trimmed = pattern.trim();
|
|
1608
|
+
if (!trimmed) {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
try {
|
|
1612
|
+
return new RegExp(trimmed, "i");
|
|
1613
|
+
} catch {
|
|
1614
|
+
return null;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function buildRedactionRegexes(ctx) {
|
|
1618
|
+
const fromConfig = Array.isArray(ctx.config.redact) ? ctx.config.redact.filter((entry) => typeof entry === "string") : [];
|
|
1619
|
+
return [...DEFAULT_REDACT_PATTERNS2, ...ctx.redact, ...fromConfig].map((pattern) => toRegex2(pattern)).filter((value) => value instanceof RegExp);
|
|
1620
|
+
}
|
|
1621
|
+
function shouldRedact(identifier, value, regexes) {
|
|
1622
|
+
if (regexes.some((regex) => regex.test(identifier))) {
|
|
1623
|
+
return true;
|
|
1624
|
+
}
|
|
1625
|
+
if (!value) {
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
if (EMAIL_LIKE_REGEX2.test(value.trim())) {
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
return regexes.some((regex) => regex.test(value));
|
|
1632
|
+
}
|
|
1633
|
+
var storageCollector = {
|
|
1634
|
+
name: "storage",
|
|
1635
|
+
defaultEnabled: false,
|
|
1636
|
+
async collect(ctx) {
|
|
1637
|
+
const includeCookieValues = ctx.config.includeCookieValues === true;
|
|
1638
|
+
const includeLocalStorageValues = ctx.config.includeLocalStorageValues === true;
|
|
1639
|
+
const redactValues = ctx.config.redactValues !== false;
|
|
1640
|
+
const regexes = buildRedactionRegexes({ redact: ctx.redact, config: ctx.config });
|
|
1641
|
+
const cookies = await ctx.page.context().cookies();
|
|
1642
|
+
const localStorageEntries = await ctx.page.evaluate(
|
|
1643
|
+
() => Object.keys(localStorage).map((key) => ({
|
|
1644
|
+
key,
|
|
1645
|
+
value: localStorage.getItem(key) ?? ""
|
|
1646
|
+
}))
|
|
1647
|
+
);
|
|
1648
|
+
const normalizedCookies = cookies.map((cookie) => {
|
|
1649
|
+
const rawValue = includeCookieValues ? cookie.value : null;
|
|
1650
|
+
const redacted = redactValues && shouldRedact(cookie.name, rawValue, regexes);
|
|
1651
|
+
return {
|
|
1652
|
+
name: cookie.name,
|
|
1653
|
+
domain: cookie.domain,
|
|
1654
|
+
path: cookie.path,
|
|
1655
|
+
value: rawValue == null ? null : redacted ? REDACTED2 : rawValue,
|
|
1656
|
+
redacted,
|
|
1657
|
+
expires: cookie.expires,
|
|
1658
|
+
httpOnly: cookie.httpOnly,
|
|
1659
|
+
secure: cookie.secure,
|
|
1660
|
+
sameSite: cookie.sameSite
|
|
1661
|
+
};
|
|
1662
|
+
});
|
|
1663
|
+
const normalizedLocalStorage = localStorageEntries.map((entry) => {
|
|
1664
|
+
const rawValue = includeLocalStorageValues ? entry.value : null;
|
|
1665
|
+
const redacted = redactValues && shouldRedact(entry.key, rawValue, regexes);
|
|
1666
|
+
return {
|
|
1667
|
+
key: entry.key,
|
|
1668
|
+
value: rawValue == null ? null : redacted ? REDACTED2 : rawValue,
|
|
1669
|
+
redacted
|
|
1670
|
+
};
|
|
1671
|
+
});
|
|
1672
|
+
const data = {
|
|
1673
|
+
cookieCount: normalizedCookies.length,
|
|
1674
|
+
localStorageKeyCount: normalizedLocalStorage.length,
|
|
1675
|
+
cookies: normalizedCookies,
|
|
1676
|
+
localStorage: normalizedLocalStorage
|
|
1677
|
+
};
|
|
1678
|
+
const outputPath = import_node_path12.default.join(ctx.checkpointDir, "storage-state.json");
|
|
1679
|
+
await import_promises11.default.writeFile(outputPath, `${JSON.stringify(data, null, 2)}
|
|
1680
|
+
`, "utf8");
|
|
1681
|
+
return {
|
|
1682
|
+
data,
|
|
1683
|
+
artifacts: [
|
|
1684
|
+
{
|
|
1685
|
+
name: "storage-state",
|
|
1686
|
+
path: outputPath,
|
|
1687
|
+
contentType: "application/json"
|
|
1688
|
+
}
|
|
1689
|
+
],
|
|
1690
|
+
summary: {
|
|
1691
|
+
cookieCount: data.cookieCount,
|
|
1692
|
+
localStorageKeyCount: data.localStorageKeyCount
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
// src/collectors/web-vitals.ts
|
|
1699
|
+
var import_promises12 = __toESM(require("fs/promises"), 1);
|
|
1700
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
1701
|
+
var initializedPages = /* @__PURE__ */ new WeakSet();
|
|
1702
|
+
function rateMetric(value, thresholds) {
|
|
1703
|
+
if (value == null || Number.isNaN(value)) {
|
|
1704
|
+
return "unknown";
|
|
1705
|
+
}
|
|
1706
|
+
if (value <= thresholds.good) {
|
|
1707
|
+
return "good";
|
|
1708
|
+
}
|
|
1709
|
+
if (value <= thresholds.needsImprovement) {
|
|
1710
|
+
return "needs-improvement";
|
|
1711
|
+
}
|
|
1712
|
+
return "poor";
|
|
1713
|
+
}
|
|
1714
|
+
function metric(value, thresholds) {
|
|
1715
|
+
return {
|
|
1716
|
+
value,
|
|
1717
|
+
rating: rateMetric(value, thresholds)
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
async function captureWebVitals(page) {
|
|
1721
|
+
const raw = await page.evaluate(() => {
|
|
1722
|
+
const globalState = globalThis;
|
|
1723
|
+
const state = globalState.__e2eWebVitals ?? {
|
|
1724
|
+
cls: 0,
|
|
1725
|
+
fcp: null,
|
|
1726
|
+
lcp: null,
|
|
1727
|
+
inp: null
|
|
1728
|
+
};
|
|
1729
|
+
const navigation = performance.getEntriesByType("navigation")[0];
|
|
1730
|
+
return {
|
|
1731
|
+
cls: state.cls,
|
|
1732
|
+
fcp: state.fcp,
|
|
1733
|
+
lcp: state.lcp,
|
|
1734
|
+
inp: state.inp,
|
|
1735
|
+
ttfb: navigation ? navigation.responseStart : null,
|
|
1736
|
+
domContentLoaded: navigation ? navigation.domContentLoadedEventEnd : null,
|
|
1737
|
+
loadEvent: navigation ? navigation.loadEventEnd : null,
|
|
1738
|
+
url: location.href
|
|
1739
|
+
};
|
|
1740
|
+
});
|
|
1741
|
+
return {
|
|
1742
|
+
url: raw.url,
|
|
1743
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1744
|
+
cls: metric(raw.cls, { good: 0.1, needsImprovement: 0.25 }),
|
|
1745
|
+
fcpMs: metric(raw.fcp, { good: 1800, needsImprovement: 3e3 }),
|
|
1746
|
+
lcpMs: metric(raw.lcp, { good: 2500, needsImprovement: 4e3 }),
|
|
1747
|
+
inpMs: metric(raw.inp, { good: 200, needsImprovement: 500 }),
|
|
1748
|
+
ttfbMs: metric(raw.ttfb, { good: 800, needsImprovement: 1800 }),
|
|
1749
|
+
domContentLoadedMs: raw.domContentLoaded,
|
|
1750
|
+
loadEventMs: raw.loadEvent
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
var webVitalsCollector = {
|
|
1754
|
+
name: "web-vitals",
|
|
1755
|
+
defaultEnabled: true,
|
|
1756
|
+
async setup({ page }) {
|
|
1757
|
+
if (initializedPages.has(page)) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
initializedPages.add(page);
|
|
1761
|
+
await page.addInitScript(() => {
|
|
1762
|
+
const globalState = globalThis;
|
|
1763
|
+
if (!globalState.__e2eWebVitals) {
|
|
1764
|
+
globalState.__e2eWebVitals = {
|
|
1765
|
+
cls: 0,
|
|
1766
|
+
fcp: null,
|
|
1767
|
+
lcp: null,
|
|
1768
|
+
inp: null
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
const state = globalState.__e2eWebVitals;
|
|
1772
|
+
try {
|
|
1773
|
+
const paintObserver = new PerformanceObserver((entryList) => {
|
|
1774
|
+
for (const entry of entryList.getEntries()) {
|
|
1775
|
+
if (entry.name === "first-contentful-paint") {
|
|
1776
|
+
state.fcp = entry.startTime;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
paintObserver.observe({ type: "paint", buffered: true });
|
|
1781
|
+
} catch {
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
const lcpObserver = new PerformanceObserver((entryList) => {
|
|
1785
|
+
const entries = entryList.getEntries();
|
|
1786
|
+
const lastEntry = entries[entries.length - 1];
|
|
1787
|
+
if (lastEntry) {
|
|
1788
|
+
state.lcp = lastEntry.startTime;
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
|
|
1792
|
+
addEventListener("pagehide", () => lcpObserver.disconnect(), { once: true });
|
|
1793
|
+
} catch {
|
|
1794
|
+
}
|
|
1795
|
+
try {
|
|
1796
|
+
const clsObserver = new PerformanceObserver((entryList) => {
|
|
1797
|
+
for (const entry of entryList.getEntries()) {
|
|
1798
|
+
if (!entry.hadRecentInput) {
|
|
1799
|
+
state.cls += entry.value ?? 0;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
clsObserver.observe({ type: "layout-shift", buffered: true });
|
|
1804
|
+
addEventListener("pagehide", () => clsObserver.disconnect(), { once: true });
|
|
1805
|
+
} catch {
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
const inpObserver = new PerformanceObserver((entryList) => {
|
|
1809
|
+
for (const entry of entryList.getEntries()) {
|
|
1810
|
+
const duration = entry.duration ?? 0;
|
|
1811
|
+
if (state.inp == null || duration > state.inp) {
|
|
1812
|
+
state.inp = duration;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
inpObserver.observe({ type: "event", buffered: true, durationThreshold: 40 });
|
|
1817
|
+
addEventListener("pagehide", () => inpObserver.disconnect(), { once: true });
|
|
1818
|
+
} catch {
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
},
|
|
1822
|
+
async collect(ctx) {
|
|
1823
|
+
const snapshot = await captureWebVitals(ctx.page);
|
|
1824
|
+
const outputPath = import_node_path13.default.join(ctx.checkpointDir, "web-vitals.json");
|
|
1825
|
+
await import_promises12.default.writeFile(outputPath, `${JSON.stringify(snapshot, null, 2)}
|
|
1826
|
+
`, "utf8");
|
|
1827
|
+
return {
|
|
1828
|
+
data: snapshot,
|
|
1829
|
+
artifacts: [
|
|
1830
|
+
{
|
|
1831
|
+
name: "web-vitals",
|
|
1832
|
+
path: outputPath,
|
|
1833
|
+
contentType: "application/json"
|
|
1834
|
+
}
|
|
1835
|
+
],
|
|
1836
|
+
summary: {
|
|
1837
|
+
cls: snapshot.cls,
|
|
1838
|
+
fcp: snapshot.fcpMs,
|
|
1839
|
+
lcp: snapshot.lcpMs,
|
|
1840
|
+
inp: snapshot.inpMs,
|
|
1841
|
+
ttfb: snapshot.ttfbMs
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
},
|
|
1845
|
+
async teardown({ page }) {
|
|
1846
|
+
initializedPages.delete(page);
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
// src/collectors/builtin-collectors.ts
|
|
1851
|
+
var builtinCollectors = [
|
|
1852
|
+
screenshotCollector,
|
|
1853
|
+
htmlCollector,
|
|
1854
|
+
axeCollector,
|
|
1855
|
+
webVitalsCollector,
|
|
1856
|
+
consoleCollector,
|
|
1857
|
+
networkCollector,
|
|
1858
|
+
metadataCollector,
|
|
1859
|
+
ariaSnapshotCollector,
|
|
1860
|
+
domStatsCollector,
|
|
1861
|
+
formsCollector,
|
|
1862
|
+
storageCollector,
|
|
1863
|
+
networkTimingCollector
|
|
1864
|
+
];
|
|
1865
|
+
|
|
1866
|
+
// src/collectors/registry.ts
|
|
1867
|
+
var builtinCollectors2 = /* @__PURE__ */ new Map();
|
|
1868
|
+
var builtinsRegistered = false;
|
|
1869
|
+
function registerBuiltinCollector(collector) {
|
|
1870
|
+
builtinCollectors2.set(collector.name, collector);
|
|
1871
|
+
}
|
|
1872
|
+
function registerBuiltinCollectors(collectors) {
|
|
1873
|
+
if (builtinsRegistered) {
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
for (const collector of collectors) {
|
|
1877
|
+
registerBuiltinCollector(collector);
|
|
1878
|
+
}
|
|
1879
|
+
builtinsRegistered = true;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// src/core.ts
|
|
1883
|
+
registerBuiltinCollectors(builtinCollectors);
|
|
1884
|
+
function warn(message, error) {
|
|
1885
|
+
if (error instanceof Error) {
|
|
1886
|
+
console.warn(`[playwright-checkpoint] ${message}`, error);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (error !== void 0) {
|
|
1890
|
+
console.warn(`[playwright-checkpoint] ${message}`, String(error));
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
console.warn(`[playwright-checkpoint] ${message}`);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/report/markdown-reporter.ts
|
|
674
1897
|
function slugify2(value) {
|
|
675
1898
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "story";
|
|
676
1899
|
}
|
|
@@ -678,6 +1901,28 @@ function stripTags(value) {
|
|
|
678
1901
|
const stripped = value.replace(/\s+@[a-z0-9-]+/gi, " ").replace(/\s+/g, " ").trim();
|
|
679
1902
|
return stripped || value.trim() || "Untitled story";
|
|
680
1903
|
}
|
|
1904
|
+
function articleTitle(run) {
|
|
1905
|
+
const override = run.article?.title?.trim();
|
|
1906
|
+
return override || stripTags(run.title);
|
|
1907
|
+
}
|
|
1908
|
+
function articleDescription(run) {
|
|
1909
|
+
const description = run.article?.description?.trim();
|
|
1910
|
+
return description ? description : null;
|
|
1911
|
+
}
|
|
1912
|
+
function articleSlug(run) {
|
|
1913
|
+
const override = run.article?.slug?.trim();
|
|
1914
|
+
return slugify2(override || stripTags(run.title));
|
|
1915
|
+
}
|
|
1916
|
+
function uniqueArticleSlug(baseSlug, usedSlugs) {
|
|
1917
|
+
if (!usedSlugs.has(baseSlug)) {
|
|
1918
|
+
return baseSlug;
|
|
1919
|
+
}
|
|
1920
|
+
let index = 1;
|
|
1921
|
+
while (usedSlugs.has(`${baseSlug}-${index}`)) {
|
|
1922
|
+
index += 1;
|
|
1923
|
+
}
|
|
1924
|
+
return `${baseSlug}-${index}`;
|
|
1925
|
+
}
|
|
681
1926
|
function normalizeConfig(config) {
|
|
682
1927
|
return {
|
|
683
1928
|
storiesDir: typeof config.storiesDir === "string" ? config.storiesDir : ".",
|
|
@@ -688,7 +1933,8 @@ function normalizeConfig(config) {
|
|
|
688
1933
|
footer: typeof config.footer === "string" ? config.footer : void 0,
|
|
689
1934
|
frontmatter: config.frontmatter === true || config.frontmatter === false || config.frontmatter != null && typeof config.frontmatter === "object" && !Array.isArray(config.frontmatter) ? config.frontmatter : false,
|
|
690
1935
|
imagePathPrefix: typeof config.imagePathPrefix === "string" ? config.imagePathPrefix : void 0,
|
|
691
|
-
copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true
|
|
1936
|
+
copyScreenshots: typeof config.copyScreenshots === "boolean" ? config.copyScreenshots : true,
|
|
1937
|
+
requireExplicitStep: typeof config.requireExplicitStep === "boolean" ? config.requireExplicitStep : false
|
|
692
1938
|
};
|
|
693
1939
|
}
|
|
694
1940
|
function normalizeTags(tags) {
|
|
@@ -700,6 +1946,9 @@ function shouldIncludeRun(run, config) {
|
|
|
700
1946
|
const runTags = new Set(normalizeTags(run.tags));
|
|
701
1947
|
return includeTags.some((tag) => runTags.has(tag));
|
|
702
1948
|
}
|
|
1949
|
+
if (run.articles) {
|
|
1950
|
+
return true;
|
|
1951
|
+
}
|
|
703
1952
|
return run.checkpoints.some((checkpoint) => {
|
|
704
1953
|
const hasDescription = typeof checkpoint.description === "string" && checkpoint.description.trim().length > 0;
|
|
705
1954
|
return hasDescription || typeof checkpoint.step === "number";
|
|
@@ -724,7 +1973,7 @@ function choosePrimaryRun(runs, preferredProject) {
|
|
|
724
1973
|
})[0] ?? null;
|
|
725
1974
|
}
|
|
726
1975
|
function resolveArtifactPath2(run, artifactPath) {
|
|
727
|
-
return
|
|
1976
|
+
return import_node_path15.default.isAbsolute(artifactPath) ? artifactPath : import_node_path15.default.resolve(import_node_path15.default.dirname(run.sourceManifestPath), artifactPath);
|
|
728
1977
|
}
|
|
729
1978
|
function screenshotSourcePath(run, checkpoint) {
|
|
730
1979
|
const artifacts = checkpoint.collectors.screenshot?.artifacts ?? [];
|
|
@@ -768,21 +2017,21 @@ function breadcrumbLabel(url) {
|
|
|
768
2017
|
}
|
|
769
2018
|
function autoDescription(checkpoint) {
|
|
770
2019
|
const pageTitle = checkpoint.title.trim();
|
|
771
|
-
const
|
|
2020
|
+
const location2 = urlLabel(checkpoint.url);
|
|
772
2021
|
if (pageTitle) {
|
|
773
|
-
return `This step captures **${pageTitle}** at \`${
|
|
2022
|
+
return `This step captures **${pageTitle}** at \`${location2}\`.`;
|
|
774
2023
|
}
|
|
775
|
-
return `This step captures **${checkpoint.name}** at \`${
|
|
2024
|
+
return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
|
|
776
2025
|
}
|
|
777
2026
|
function markdownRelativePath(fromFile, toFile) {
|
|
778
|
-
const relativePath =
|
|
2027
|
+
const relativePath = import_node_path15.default.relative(import_node_path15.default.dirname(fromFile), toFile).split(import_node_path15.default.sep).join("/");
|
|
779
2028
|
if (relativePath.startsWith(".")) {
|
|
780
2029
|
return relativePath;
|
|
781
2030
|
}
|
|
782
2031
|
return `./${relativePath}`;
|
|
783
2032
|
}
|
|
784
2033
|
function rewriteImagePath(markdownFile, imageFile, outputDir, prefix) {
|
|
785
|
-
const relativePath =
|
|
2034
|
+
const relativePath = import_node_path15.default.relative(outputDir, imageFile).split(import_node_path15.default.sep).join("/");
|
|
786
2035
|
if (prefix) {
|
|
787
2036
|
return `${prefix.replace(/\/+$/g, "")}/${relativePath.replace(/^\/+/, "")}`;
|
|
788
2037
|
}
|
|
@@ -818,17 +2067,22 @@ async function materializeScreenshot(args) {
|
|
|
818
2067
|
if (!sourcePath) {
|
|
819
2068
|
return null;
|
|
820
2069
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
2070
|
+
const cachedTargetPath = args.screenshotCopies?.get(sourcePath);
|
|
2071
|
+
if (cachedTargetPath) {
|
|
2072
|
+
return rewriteImagePath(args.markdownFile, cachedTargetPath, args.outputDir, args.config.imagePathPrefix);
|
|
2073
|
+
}
|
|
2074
|
+
const extension = import_node_path15.default.extname(sourcePath) || ".png";
|
|
2075
|
+
const targetPath = import_node_path15.default.join(
|
|
823
2076
|
args.outputDir,
|
|
824
2077
|
args.config.screenshotsDir ?? "screenshots",
|
|
825
|
-
args.
|
|
826
|
-
`${
|
|
2078
|
+
args.screenshotDirSlug,
|
|
2079
|
+
`${args.screenshotFileSlug}${extension}`
|
|
827
2080
|
);
|
|
828
2081
|
try {
|
|
829
2082
|
if (args.config.copyScreenshots !== false) {
|
|
830
|
-
await
|
|
831
|
-
await
|
|
2083
|
+
await import_promises14.default.mkdir(import_node_path15.default.dirname(targetPath), { recursive: true });
|
|
2084
|
+
await import_promises14.default.copyFile(sourcePath, targetPath);
|
|
2085
|
+
args.screenshotCopies?.set(sourcePath, targetPath);
|
|
832
2086
|
args.writtenFiles.add(targetPath);
|
|
833
2087
|
return rewriteImagePath(args.markdownFile, targetPath, args.outputDir, args.config.imagePathPrefix);
|
|
834
2088
|
}
|
|
@@ -848,10 +2102,31 @@ function orderedCheckpoints(checkpoints) {
|
|
|
848
2102
|
});
|
|
849
2103
|
}
|
|
850
2104
|
async function buildSteps(args) {
|
|
851
|
-
|
|
2105
|
+
let checkpoints;
|
|
2106
|
+
if (args.stepNames && args.stepNames.length > 0) {
|
|
2107
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2108
|
+
for (const checkpoint of args.run.checkpoints) {
|
|
2109
|
+
if (byName.has(checkpoint.name)) {
|
|
2110
|
+
warn(`Duplicate checkpoint name "${checkpoint.name}" in "${args.run.title}". Using the latest capture for article generation.`);
|
|
2111
|
+
}
|
|
2112
|
+
byName.set(checkpoint.name, checkpoint);
|
|
2113
|
+
}
|
|
2114
|
+
checkpoints = args.stepNames.map((stepName) => {
|
|
2115
|
+
const checkpoint = byName.get(stepName);
|
|
2116
|
+
if (!checkpoint) {
|
|
2117
|
+
warn(`Markdown article step "${stepName}" was not captured in "${args.run.title}". Skipping step.`);
|
|
2118
|
+
return null;
|
|
2119
|
+
}
|
|
2120
|
+
return checkpoint;
|
|
2121
|
+
}).filter((checkpoint) => checkpoint !== null);
|
|
2122
|
+
} else {
|
|
2123
|
+
checkpoints = orderedCheckpoints(args.run.checkpoints).filter(
|
|
2124
|
+
(checkpoint) => !args.config.requireExplicitStep || typeof checkpoint.step === "number"
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
852
2127
|
const steps = [];
|
|
853
2128
|
for (const [index, checkpoint] of checkpoints.entries()) {
|
|
854
|
-
const order = typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
|
|
2129
|
+
const order = args.stepNames ? index + 1 : typeof checkpoint.step === "number" ? checkpoint.step : index + 1;
|
|
855
2130
|
steps.push({
|
|
856
2131
|
checkpoint,
|
|
857
2132
|
order,
|
|
@@ -860,12 +2135,13 @@ async function buildSteps(args) {
|
|
|
860
2135
|
imagePath: await materializeScreenshot({
|
|
861
2136
|
run: args.run,
|
|
862
2137
|
checkpoint,
|
|
863
|
-
|
|
864
|
-
|
|
2138
|
+
screenshotDirSlug: args.screenshotDirSlug,
|
|
2139
|
+
screenshotFileSlug: args.stepNames ? checkpoint.slug : `${String(order).padStart(2, "0")}-${slugify2(checkpoint.name)}`,
|
|
865
2140
|
outputDir: args.outputDir,
|
|
866
2141
|
markdownFile: args.markdownFile,
|
|
867
2142
|
config: args.config,
|
|
868
|
-
writtenFiles: args.writtenFiles
|
|
2143
|
+
writtenFiles: args.writtenFiles,
|
|
2144
|
+
screenshotCopies: args.screenshotCopies
|
|
869
2145
|
}),
|
|
870
2146
|
urlLabel: urlLabel(checkpoint.url),
|
|
871
2147
|
breadcrumbLabel: breadcrumbLabel(checkpoint.url),
|
|
@@ -876,13 +2152,14 @@ async function buildSteps(args) {
|
|
|
876
2152
|
}
|
|
877
2153
|
function renderMarkdown(args) {
|
|
878
2154
|
const frontmatterFields = args.config.frontmatter === true || typeof args.config.frontmatter === "object" ? {
|
|
879
|
-
title: args.title,
|
|
880
2155
|
project: args.run.project,
|
|
881
|
-
testId: args.run.testId,
|
|
882
2156
|
tags: args.run.tags,
|
|
2157
|
+
...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {},
|
|
2158
|
+
...args.article?.frontmatter ?? {},
|
|
2159
|
+
testId: args.run.testId,
|
|
883
2160
|
startedAt: args.run.startedAt,
|
|
884
2161
|
generatedAt: args.generatedAt,
|
|
885
|
-
|
|
2162
|
+
title: args.title
|
|
886
2163
|
} : null;
|
|
887
2164
|
const sections = args.steps.map((step) => {
|
|
888
2165
|
const lines = [`## Step ${step.order}: ${step.heading}`, ""];
|
|
@@ -902,6 +2179,7 @@ function renderMarkdown(args) {
|
|
|
902
2179
|
const parts = [
|
|
903
2180
|
frontmatterFields ? serializeFrontmatter(frontmatterFields) : "",
|
|
904
2181
|
`# ${args.title}`,
|
|
2182
|
+
args.description?.trim() ?? "",
|
|
905
2183
|
args.config.header ? args.config.header.trim() : "",
|
|
906
2184
|
sections,
|
|
907
2185
|
args.config.footer ? args.config.footer.trim() : ""
|
|
@@ -909,6 +2187,42 @@ function renderMarkdown(args) {
|
|
|
909
2187
|
return `${parts.join("\n\n")}
|
|
910
2188
|
`;
|
|
911
2189
|
}
|
|
2190
|
+
function resolveArticles(run) {
|
|
2191
|
+
const multiArticles = (run.articles ?? []).filter((article) => Array.isArray(article.steps));
|
|
2192
|
+
if (run.articles && multiArticles.length === 0) {
|
|
2193
|
+
warn(`Markdown reporter received an empty articles array for "${run.title}". Falling back to the default single-article output.`);
|
|
2194
|
+
}
|
|
2195
|
+
if (multiArticles.length === 0) {
|
|
2196
|
+
return [
|
|
2197
|
+
{
|
|
2198
|
+
title: articleTitle(run),
|
|
2199
|
+
description: articleDescription(run),
|
|
2200
|
+
slug: articleSlug(run),
|
|
2201
|
+
metadata: run.article,
|
|
2202
|
+
screenshotDirSlug: articleSlug(run)
|
|
2203
|
+
}
|
|
2204
|
+
];
|
|
2205
|
+
}
|
|
2206
|
+
const usedSlugs = /* @__PURE__ */ new Set();
|
|
2207
|
+
const screenshotDirSlug = slugify2(stripTags(run.title));
|
|
2208
|
+
return multiArticles.map((article, index) => {
|
|
2209
|
+
const fallbackSlug = `${screenshotDirSlug}-${index + 1}`;
|
|
2210
|
+
const baseSlug = slugify2(article.slug?.trim() || fallbackSlug);
|
|
2211
|
+
const uniqueSlug = uniqueArticleSlug(baseSlug, usedSlugs);
|
|
2212
|
+
if (uniqueSlug !== baseSlug) {
|
|
2213
|
+
warn(`Markdown article slug collision for "${article.title ?? run.title}" resolved as "${uniqueSlug}".`);
|
|
2214
|
+
}
|
|
2215
|
+
usedSlugs.add(uniqueSlug);
|
|
2216
|
+
return {
|
|
2217
|
+
title: article.title?.trim() || stripTags(run.title),
|
|
2218
|
+
description: article.description?.trim() || null,
|
|
2219
|
+
slug: uniqueSlug,
|
|
2220
|
+
metadata: article,
|
|
2221
|
+
stepNames: [...article.steps],
|
|
2222
|
+
screenshotDirSlug
|
|
2223
|
+
};
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
912
2226
|
var markdownReporter = {
|
|
913
2227
|
name: "markdown",
|
|
914
2228
|
description: "Generates one Markdown help article per captured story.",
|
|
@@ -920,37 +2234,56 @@ var markdownReporter = {
|
|
|
920
2234
|
const stories = groupByStory(context.runs);
|
|
921
2235
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
922
2236
|
const writtenFiles = /* @__PURE__ */ new Set();
|
|
2237
|
+
const usedStorySlugs = /* @__PURE__ */ new Set();
|
|
2238
|
+
const screenshotCopies = /* @__PURE__ */ new Map();
|
|
923
2239
|
let articleCount = 0;
|
|
924
2240
|
for (const [storyTitle, runs] of stories) {
|
|
925
2241
|
const primaryRun = choosePrimaryRun(runs, config.preferredProject);
|
|
926
2242
|
if (!primaryRun || !shouldIncludeRun(primaryRun, config)) {
|
|
927
2243
|
continue;
|
|
928
2244
|
}
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
markdownFile,
|
|
943
|
-
renderMarkdown({
|
|
944
|
-
title,
|
|
945
|
-
steps,
|
|
2245
|
+
for (const article of resolveArticles(primaryRun)) {
|
|
2246
|
+
let storySlug = article.slug;
|
|
2247
|
+
if (usedStorySlugs.has(storySlug)) {
|
|
2248
|
+
let index = 2;
|
|
2249
|
+
while (usedStorySlugs.has(`${article.slug}-${index}`)) {
|
|
2250
|
+
index += 1;
|
|
2251
|
+
}
|
|
2252
|
+
storySlug = `${article.slug}-${index}`;
|
|
2253
|
+
warn(`Markdown article slug collision for "${article.title || storyTitle}" resolved as "${storySlug}".`);
|
|
2254
|
+
}
|
|
2255
|
+
usedStorySlugs.add(storySlug);
|
|
2256
|
+
const markdownFile = import_node_path15.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
|
|
2257
|
+
const steps = await buildSteps({
|
|
946
2258
|
run: primaryRun,
|
|
2259
|
+
stepNames: article.stepNames,
|
|
2260
|
+
screenshotDirSlug: article.screenshotDirSlug,
|
|
2261
|
+
outputDir: context.outputDir,
|
|
2262
|
+
markdownFile,
|
|
947
2263
|
config,
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
2264
|
+
writtenFiles,
|
|
2265
|
+
screenshotCopies
|
|
2266
|
+
});
|
|
2267
|
+
if (steps.length === 0) {
|
|
2268
|
+
continue;
|
|
2269
|
+
}
|
|
2270
|
+
await import_promises14.default.mkdir(import_node_path15.default.dirname(markdownFile), { recursive: true });
|
|
2271
|
+
await import_promises14.default.writeFile(
|
|
2272
|
+
markdownFile,
|
|
2273
|
+
renderMarkdown({
|
|
2274
|
+
title: article.title,
|
|
2275
|
+
description: article.description,
|
|
2276
|
+
steps,
|
|
2277
|
+
run: primaryRun,
|
|
2278
|
+
article: article.metadata,
|
|
2279
|
+
config,
|
|
2280
|
+
generatedAt
|
|
2281
|
+
}),
|
|
2282
|
+
"utf8"
|
|
2283
|
+
);
|
|
2284
|
+
writtenFiles.add(markdownFile);
|
|
2285
|
+
articleCount += 1;
|
|
2286
|
+
}
|
|
954
2287
|
}
|
|
955
2288
|
return {
|
|
956
2289
|
files: [...writtenFiles],
|
|
@@ -960,8 +2293,8 @@ var markdownReporter = {
|
|
|
960
2293
|
};
|
|
961
2294
|
|
|
962
2295
|
// src/report/mdx-reporter.ts
|
|
963
|
-
var
|
|
964
|
-
var
|
|
2296
|
+
var import_promises15 = __toESM(require("fs/promises"), 1);
|
|
2297
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
965
2298
|
var DEFAULT_PROJECT_ORDER2 = ["desktop-light", "desktop-dark", "mobile-light", "mobile-dark"];
|
|
966
2299
|
function slugify3(value) {
|
|
967
2300
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "story";
|
|
@@ -1027,7 +2360,7 @@ function orderedCheckpoints2(checkpoints) {
|
|
|
1027
2360
|
});
|
|
1028
2361
|
}
|
|
1029
2362
|
function resolveArtifactPath3(run, artifactPath) {
|
|
1030
|
-
return
|
|
2363
|
+
return import_node_path16.default.isAbsolute(artifactPath) ? artifactPath : import_node_path16.default.resolve(import_node_path16.default.dirname(run.sourceManifestPath), artifactPath);
|
|
1031
2364
|
}
|
|
1032
2365
|
function screenshotSourcePath2(run, checkpoint) {
|
|
1033
2366
|
const artifacts = checkpoint.collectors.screenshot?.artifacts ?? [];
|
|
@@ -1061,21 +2394,21 @@ function urlLabel2(url) {
|
|
|
1061
2394
|
}
|
|
1062
2395
|
function autoDescription2(checkpoint) {
|
|
1063
2396
|
const pageTitle = checkpoint.title.trim();
|
|
1064
|
-
const
|
|
2397
|
+
const location2 = urlLabel2(checkpoint.url);
|
|
1065
2398
|
if (pageTitle) {
|
|
1066
|
-
return `This step captures **${pageTitle}** at \`${
|
|
2399
|
+
return `This step captures **${pageTitle}** at \`${location2}\`.`;
|
|
1067
2400
|
}
|
|
1068
|
-
return `This step captures **${checkpoint.name}** at \`${
|
|
2401
|
+
return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
|
|
1069
2402
|
}
|
|
1070
2403
|
function markdownRelativePath2(fromFile, toFile) {
|
|
1071
|
-
const relativePath =
|
|
2404
|
+
const relativePath = import_node_path16.default.relative(import_node_path16.default.dirname(fromFile), toFile).split(import_node_path16.default.sep).join("/");
|
|
1072
2405
|
if (relativePath.startsWith(".")) {
|
|
1073
2406
|
return relativePath;
|
|
1074
2407
|
}
|
|
1075
2408
|
return `./${relativePath}`;
|
|
1076
2409
|
}
|
|
1077
2410
|
function rewriteImagePath2(mdxFile, imageFile, outputDir, prefix) {
|
|
1078
|
-
const relativePath =
|
|
2411
|
+
const relativePath = import_node_path16.default.relative(outputDir, imageFile).split(import_node_path16.default.sep).join("/");
|
|
1079
2412
|
if (prefix) {
|
|
1080
2413
|
return `${prefix.replace(/\/+$/g, "")}/${relativePath.replace(/^\/+/, "")}`;
|
|
1081
2414
|
}
|
|
@@ -1150,8 +2483,8 @@ async function materializeScreenshot2(args) {
|
|
|
1150
2483
|
if (!sourcePath) {
|
|
1151
2484
|
return null;
|
|
1152
2485
|
}
|
|
1153
|
-
const extension =
|
|
1154
|
-
const targetPath =
|
|
2486
|
+
const extension = import_node_path16.default.extname(sourcePath) || ".png";
|
|
2487
|
+
const targetPath = import_node_path16.default.join(
|
|
1155
2488
|
args.outputDir,
|
|
1156
2489
|
args.config.screenshotsDir ?? "screenshots",
|
|
1157
2490
|
args.storySlug,
|
|
@@ -1159,8 +2492,8 @@ async function materializeScreenshot2(args) {
|
|
|
1159
2492
|
);
|
|
1160
2493
|
try {
|
|
1161
2494
|
if (args.config.copyScreenshots !== false) {
|
|
1162
|
-
await
|
|
1163
|
-
await
|
|
2495
|
+
await import_promises15.default.mkdir(import_node_path16.default.dirname(targetPath), { recursive: true });
|
|
2496
|
+
await import_promises15.default.copyFile(sourcePath, targetPath);
|
|
1164
2497
|
args.writtenFiles.add(targetPath);
|
|
1165
2498
|
return rewriteImagePath2(args.mdxFile, targetPath, args.outputDir, args.config.imagePathPrefix);
|
|
1166
2499
|
}
|
|
@@ -1290,7 +2623,7 @@ var mdxReporter = {
|
|
|
1290
2623
|
}
|
|
1291
2624
|
const title = stripTags2(storyTitle);
|
|
1292
2625
|
const storySlug = slugify3(title);
|
|
1293
|
-
const mdxFile =
|
|
2626
|
+
const mdxFile = import_node_path16.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.mdx`);
|
|
1294
2627
|
const steps = await buildSteps2({
|
|
1295
2628
|
runs,
|
|
1296
2629
|
primaryRun,
|
|
@@ -1300,8 +2633,8 @@ var mdxReporter = {
|
|
|
1300
2633
|
config,
|
|
1301
2634
|
writtenFiles
|
|
1302
2635
|
});
|
|
1303
|
-
await
|
|
1304
|
-
await
|
|
2636
|
+
await import_promises15.default.mkdir(import_node_path16.default.dirname(mdxFile), { recursive: true });
|
|
2637
|
+
await import_promises15.default.writeFile(
|
|
1305
2638
|
mdxFile,
|
|
1306
2639
|
renderMdx({
|
|
1307
2640
|
title,
|
|
@@ -1330,10 +2663,10 @@ var builtinReporterDefaults = {
|
|
|
1330
2663
|
mdx: false
|
|
1331
2664
|
};
|
|
1332
2665
|
async function walkFiles(directory) {
|
|
1333
|
-
const dirents = await
|
|
2666
|
+
const dirents = await import_promises16.default.readdir(directory, { withFileTypes: true });
|
|
1334
2667
|
const files = [];
|
|
1335
2668
|
for (const dirent of dirents) {
|
|
1336
|
-
const absolutePath =
|
|
2669
|
+
const absolutePath = import_node_path17.default.join(directory, dirent.name);
|
|
1337
2670
|
if (dirent.isDirectory()) {
|
|
1338
2671
|
files.push(...await walkFiles(absolutePath));
|
|
1339
2672
|
continue;
|
|
@@ -1345,7 +2678,7 @@ async function walkFiles(directory) {
|
|
|
1345
2678
|
return files;
|
|
1346
2679
|
}
|
|
1347
2680
|
function isCheckpointManifestFile(filePath) {
|
|
1348
|
-
const fileName =
|
|
2681
|
+
const fileName = import_node_path17.default.basename(filePath);
|
|
1349
2682
|
return fileName === "checkpoint-manifest.json" || fileName.startsWith("checkpoint-manifest-") && fileName.endsWith(".json");
|
|
1350
2683
|
}
|
|
1351
2684
|
function isCheckpointManifest(value) {
|
|
@@ -1363,6 +2696,8 @@ function toRunRecord(manifest, sourceManifestPath) {
|
|
|
1363
2696
|
project: manifest.project,
|
|
1364
2697
|
testId: manifest.testId,
|
|
1365
2698
|
title: manifest.title,
|
|
2699
|
+
...manifest.article ? { article: manifest.article } : {},
|
|
2700
|
+
...manifest.articles ? { articles: manifest.articles } : {},
|
|
1366
2701
|
tags: manifest.tags,
|
|
1367
2702
|
startedAt: manifest.startedAt,
|
|
1368
2703
|
checkpoints: manifest.checkpoints
|
|
@@ -1374,6 +2709,8 @@ function toManifest(run) {
|
|
|
1374
2709
|
project: run.project,
|
|
1375
2710
|
testId: run.testId,
|
|
1376
2711
|
title: run.title,
|
|
2712
|
+
...run.article ? { article: run.article } : {},
|
|
2713
|
+
...run.articles ? { articles: run.articles } : {},
|
|
1377
2714
|
tags: run.tags,
|
|
1378
2715
|
startedAt: run.startedAt,
|
|
1379
2716
|
checkpoints: run.checkpoints
|
|
@@ -1418,7 +2755,7 @@ async function loadRuns(testResultsDir) {
|
|
|
1418
2755
|
for (const manifestPath of manifestFiles) {
|
|
1419
2756
|
let rawManifest;
|
|
1420
2757
|
try {
|
|
1421
|
-
rawManifest = JSON.parse(await
|
|
2758
|
+
rawManifest = JSON.parse(await import_promises16.default.readFile(manifestPath, "utf8"));
|
|
1422
2759
|
} catch {
|
|
1423
2760
|
continue;
|
|
1424
2761
|
}
|
|
@@ -1469,20 +2806,20 @@ var DEFAULT_REPORT_OUTPUT_DIR = "report";
|
|
|
1469
2806
|
// src/teardown.ts
|
|
1470
2807
|
function resolveResultsDir(config) {
|
|
1471
2808
|
if (process.env.PLAYWRIGHT_CHECKPOINT_RESULTS_DIR) {
|
|
1472
|
-
return
|
|
2809
|
+
return import_node_path19.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_RESULTS_DIR);
|
|
1473
2810
|
}
|
|
1474
|
-
const defaultDir =
|
|
2811
|
+
const defaultDir = import_node_path19.default.resolve(process.cwd(), DEFAULT_RESULTS_DIR);
|
|
1475
2812
|
if (import_node_fs.default.existsSync(defaultDir)) {
|
|
1476
2813
|
return defaultDir;
|
|
1477
2814
|
}
|
|
1478
2815
|
const firstProjectOutputDir = config?.projects.find((project) => typeof project.outputDir === "string")?.outputDir;
|
|
1479
|
-
return
|
|
2816
|
+
return import_node_path19.default.resolve(firstProjectOutputDir ?? defaultDir);
|
|
1480
2817
|
}
|
|
1481
2818
|
function resolveOutputDir() {
|
|
1482
2819
|
if (process.env.PLAYWRIGHT_CHECKPOINT_REPORT_DIR) {
|
|
1483
|
-
return
|
|
2820
|
+
return import_node_path19.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_REPORT_DIR);
|
|
1484
2821
|
}
|
|
1485
|
-
return
|
|
2822
|
+
return import_node_path19.default.resolve(process.cwd(), DEFAULT_REPORT_OUTPUT_DIR);
|
|
1486
2823
|
}
|
|
1487
2824
|
async function globalTeardown(config) {
|
|
1488
2825
|
const testResultsDir = resolveResultsDir(config);
|