playwright-checkpoint 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/{chunk-KG37WSYS.js → chunk-M3BRR3LT.js} +9 -3
- package/dist/{chunk-KG37WSYS.js.map → chunk-M3BRR3LT.js.map} +1 -1
- package/dist/{chunk-X5IPL32H.js → chunk-WXZOP7XI.js} +153 -35
- package/dist/chunk-WXZOP7XI.js.map +1 -0
- package/dist/{chunk-K5DX32TO.js → chunk-YUFXGGZM.js} +2 -2
- package/dist/cli/bin.cjs +2501 -2386
- package/dist/cli/bin.cjs.map +1 -1
- package/dist/cli/bin.js +3 -2
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/index.cjs +1405 -68
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +2 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +3 -2
- package/dist/{core-CD4jHGgI.d.cts → core-6gyzs35M.d.ts} +2 -1
- package/dist/{core-CZvnc0rE.d.ts → core-Dd3WLuTs.d.cts} +2 -1
- package/dist/core.cjs +8 -2
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +1 -1
- package/dist/{index-BjYQX_hK.d.ts → index-CvcgBzvl.d.ts} +1 -1
- package/dist/{index-Cabk31qi.d.cts → index-OQx9qcVO.d.cts} +1 -1
- package/dist/index.cjs +212 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +69 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +148 -34
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +4 -4
- package/dist/teardown.cjs +1409 -72
- package/dist/teardown.cjs.map +1 -1
- package/dist/teardown.js +3 -2
- package/dist/teardown.js.map +1 -1
- package/dist/{types-G7w4n8kR.d.cts → types-wX4eB9mb.d.cts} +16 -1
- package/dist/{types-G7w4n8kR.d.ts → types-wX4eB9mb.d.ts} +16 -1
- package/package.json +2 -1
- package/dist/chunk-X5IPL32H.js.map +0 -1
- /package/dist/{chunk-K5DX32TO.js.map → chunk-YUFXGGZM.js.map} +0 -0
package/dist/cli/index.cjs
CHANGED
|
@@ -37,11 +37,11 @@ __export(cli_exports, {
|
|
|
37
37
|
runCli: () => runCli
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(cli_exports);
|
|
40
|
-
var
|
|
40
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
41
41
|
|
|
42
42
|
// src/report/index.ts
|
|
43
|
-
var
|
|
44
|
-
var
|
|
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
|
|
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
|
|
2019
|
+
const location2 = urlLabel(checkpoint.url);
|
|
771
2020
|
if (pageTitle) {
|
|
772
|
-
return `This step captures **${pageTitle}** at \`${
|
|
2021
|
+
return `This step captures **${pageTitle}** at \`${location2}\`.`;
|
|
773
2022
|
}
|
|
774
|
-
return `This step captures **${checkpoint.name}** at \`${
|
|
2023
|
+
return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
|
|
775
2024
|
}
|
|
776
2025
|
function markdownRelativePath(fromFile, toFile) {
|
|
777
|
-
const relativePath =
|
|
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 =
|
|
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
|
|
821
|
-
|
|
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.
|
|
825
|
-
`${
|
|
2077
|
+
args.screenshotDirSlug,
|
|
2078
|
+
`${args.screenshotFileSlug}${extension}`
|
|
826
2079
|
);
|
|
827
2080
|
try {
|
|
828
2081
|
if (args.config.copyScreenshots !== false) {
|
|
829
|
-
await
|
|
830
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
863
|
-
|
|
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
|
-
|
|
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
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
|
963
|
-
var
|
|
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
|
|
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
|
|
2396
|
+
const location2 = urlLabel2(checkpoint.url);
|
|
1064
2397
|
if (pageTitle) {
|
|
1065
|
-
return `This step captures **${pageTitle}** at \`${
|
|
2398
|
+
return `This step captures **${pageTitle}** at \`${location2}\`.`;
|
|
1066
2399
|
}
|
|
1067
|
-
return `This step captures **${checkpoint.name}** at \`${
|
|
2400
|
+
return `This step captures **${checkpoint.name}** at \`${location2}\`.`;
|
|
1068
2401
|
}
|
|
1069
2402
|
function markdownRelativePath2(fromFile, toFile) {
|
|
1070
|
-
const relativePath =
|
|
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 =
|
|
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 =
|
|
1153
|
-
const targetPath =
|
|
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
|
|
1162
|
-
await
|
|
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 =
|
|
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
|
|
1303
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
1642
|
-
const 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);
|