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
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 import_node_path6 = __toESM(require("path"), 1);
38
+ var import_node_path19 = __toESM(require("path"), 1);
39
39
 
40
40
  // src/cli/index.ts
41
- var import_node_path5 = __toESM(require("path"), 1);
41
+ var import_node_path18 = __toESM(require("path"), 1);
42
42
 
43
43
  // src/report/index.ts
44
- var import_promises4 = __toESM(require("fs/promises"), 1);
45
- var import_node_path4 = __toESM(require("path"), 1);
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 import_node_path2.default.isAbsolute(artifactPath) ? artifactPath : import_node_path2.default.resolve(import_node_path2.default.dirname(run.sourceManifestPath), artifactPath);
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 location = urlLabel(checkpoint.url);
2020
+ const location2 = urlLabel(checkpoint.url);
772
2021
  if (pageTitle) {
773
- return `This step captures **${pageTitle}** at \`${location}\`.`;
2022
+ return `This step captures **${pageTitle}** at \`${location2}\`.`;
774
2023
  }
775
- return `This step captures **${checkpoint.name}** at \`${location}\`.`;
2024
+ return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
776
2025
  }
777
2026
  function markdownRelativePath(fromFile, toFile) {
778
- const relativePath = import_node_path2.default.relative(import_node_path2.default.dirname(fromFile), toFile).split(import_node_path2.default.sep).join("/");
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 = import_node_path2.default.relative(outputDir, imageFile).split(import_node_path2.default.sep).join("/");
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 extension = import_node_path2.default.extname(sourcePath) || ".png";
822
- const targetPath = import_node_path2.default.join(
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.storySlug,
826
- `${String(args.stepOrder).padStart(2, "0")}-${slugify2(args.checkpoint.name)}${extension}`
2078
+ args.screenshotDirSlug,
2079
+ `${args.screenshotFileSlug}${extension}`
827
2080
  );
828
2081
  try {
829
2082
  if (args.config.copyScreenshots !== false) {
830
- await import_promises2.default.mkdir(import_node_path2.default.dirname(targetPath), { recursive: true });
831
- await import_promises2.default.copyFile(sourcePath, targetPath);
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
- const checkpoints = orderedCheckpoints(args.run.checkpoints);
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
- storySlug: args.storySlug,
864
- stepOrder: order,
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
- ...args.config.frontmatter && typeof args.config.frontmatter === "object" ? args.config.frontmatter : {}
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 title = stripTags(storyTitle);
930
- const storySlug = slugify2(title);
931
- const markdownFile = import_node_path2.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.md`);
932
- const steps = await buildSteps({
933
- run: primaryRun,
934
- storySlug,
935
- outputDir: context.outputDir,
936
- markdownFile,
937
- config,
938
- writtenFiles
939
- });
940
- await import_promises2.default.mkdir(import_node_path2.default.dirname(markdownFile), { recursive: true });
941
- await import_promises2.default.writeFile(
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
- generatedAt
949
- }),
950
- "utf8"
951
- );
952
- writtenFiles.add(markdownFile);
953
- articleCount += 1;
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 import_promises3 = __toESM(require("fs/promises"), 1);
964
- var import_node_path3 = __toESM(require("path"), 1);
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 import_node_path3.default.isAbsolute(artifactPath) ? artifactPath : import_node_path3.default.resolve(import_node_path3.default.dirname(run.sourceManifestPath), artifactPath);
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 location = urlLabel2(checkpoint.url);
2397
+ const location2 = urlLabel2(checkpoint.url);
1065
2398
  if (pageTitle) {
1066
- return `This step captures **${pageTitle}** at \`${location}\`.`;
2399
+ return `This step captures **${pageTitle}** at \`${location2}\`.`;
1067
2400
  }
1068
- return `This step captures **${checkpoint.name}** at \`${location}\`.`;
2401
+ return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
1069
2402
  }
1070
2403
  function markdownRelativePath2(fromFile, toFile) {
1071
- const relativePath = import_node_path3.default.relative(import_node_path3.default.dirname(fromFile), toFile).split(import_node_path3.default.sep).join("/");
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 = import_node_path3.default.relative(outputDir, imageFile).split(import_node_path3.default.sep).join("/");
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 = import_node_path3.default.extname(sourcePath) || ".png";
1154
- const targetPath = import_node_path3.default.join(
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 import_promises3.default.mkdir(import_node_path3.default.dirname(targetPath), { recursive: true });
1163
- await import_promises3.default.copyFile(sourcePath, targetPath);
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 = import_node_path3.default.join(context.outputDir, config.storiesDir ?? ".", `${storySlug}.mdx`);
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 import_promises3.default.mkdir(import_node_path3.default.dirname(mdxFile), { recursive: true });
1304
- await import_promises3.default.writeFile(
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 import_promises4.default.readdir(directory, { withFileTypes: true });
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 = import_node_path4.default.join(directory, dirent.name);
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 = import_node_path4.default.basename(filePath);
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 import_promises4.default.readFile(manifestPath, "utf8"));
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 import_node_path6.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_RESULTS_DIR);
2809
+ return import_node_path19.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_RESULTS_DIR);
1473
2810
  }
1474
- const defaultDir = import_node_path6.default.resolve(process.cwd(), DEFAULT_RESULTS_DIR);
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 import_node_path6.default.resolve(firstProjectOutputDir ?? defaultDir);
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 import_node_path6.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_REPORT_DIR);
2820
+ return import_node_path19.default.resolve(process.env.PLAYWRIGHT_CHECKPOINT_REPORT_DIR);
1484
2821
  }
1485
- return import_node_path6.default.resolve(process.cwd(), DEFAULT_REPORT_OUTPUT_DIR);
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);