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