@sentinelqa/playwright-reporter 0.1.53 → 0.1.54
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 +111 -146
- package/dist/localReport.js +360 -32
- package/dist/quickDiagnosis.d.ts +42 -2
- package/dist/quickDiagnosis.js +1326 -112
- package/dist/reporter.js +60 -25
- package/dist/runHistory.d.ts +3 -0
- package/dist/runHistory.js +11 -1
- package/dist/terminalSummary.js +44 -12
- package/package.json +2 -2
package/dist/localReport.js
CHANGED
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const crypto_1 = __importDefault(require("crypto"));
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const quickDiagnosis_1 = require("./quickDiagnosis");
|
|
12
|
+
const runHistory_1 = require("./runHistory");
|
|
12
13
|
const DEFAULT_REPORT_DIR = "sentinel-report";
|
|
13
14
|
const DEFAULT_REPORT_FILE = "index.html";
|
|
14
15
|
const DEFAULT_REDIRECT_FILE = "sentinel-debug.html";
|
|
@@ -100,6 +101,10 @@ const cleanTitleParts = (parts) => {
|
|
|
100
101
|
const withoutUnnamed = normalized.filter((part) => part !== "Unnamed test");
|
|
101
102
|
return withoutUnnamed.length ? withoutUnnamed : normalized;
|
|
102
103
|
};
|
|
104
|
+
const shortenTitle = (title) => {
|
|
105
|
+
const parts = cleanTitleParts(title.split(" > "));
|
|
106
|
+
return parts[parts.length - 1] || title;
|
|
107
|
+
};
|
|
103
108
|
const buildTitlePath = (baseTitles, test) => {
|
|
104
109
|
const title = typeof test?.title === "string" ? test.title : null;
|
|
105
110
|
const next = !title || baseTitles[baseTitles.length - 1] === title
|
|
@@ -555,7 +560,7 @@ const renderArtifactGroups = (artifacts) => {
|
|
|
555
560
|
`)
|
|
556
561
|
.join("\n");
|
|
557
562
|
};
|
|
558
|
-
const renderTestCard = (test) => {
|
|
563
|
+
const renderTestCard = (test, groupedIssue) => {
|
|
559
564
|
const statusClass = test.status === "passed" ? "status-passed" : "status-failed";
|
|
560
565
|
const fileLine = test.file ? `<div class="meta-item">${escapeHtml(test.file)}</div>` : "";
|
|
561
566
|
const projectLine = test.projectName
|
|
@@ -582,8 +587,47 @@ const renderTestCard = (test) => {
|
|
|
582
587
|
: `<pre>No error message was attached to this result.</pre>`;
|
|
583
588
|
const artifactMarkup = renderArtifactGroups(test.artifacts);
|
|
584
589
|
const diagnosis = test.diagnosis;
|
|
585
|
-
const diagnosisMarkup =
|
|
590
|
+
const diagnosisMarkup = groupedIssue
|
|
586
591
|
? `
|
|
592
|
+
<div class="diagnosis-shell">
|
|
593
|
+
<div>
|
|
594
|
+
<span class="artifact-kind">Grouped root cause</span>
|
|
595
|
+
<p class="diagnosis-copy"><strong>${escapeHtml(groupedIssue.title)}</strong></p>
|
|
596
|
+
<p class="diagnosis-copy">${escapeHtml(groupedIssue.cause)}</p>
|
|
597
|
+
</div>
|
|
598
|
+
<button
|
|
599
|
+
type="button"
|
|
600
|
+
class="copy-button"
|
|
601
|
+
data-copy-summary="${escapeHtml([
|
|
602
|
+
`Test: ${test.title}`,
|
|
603
|
+
`Issue: ${groupedIssue.title}`,
|
|
604
|
+
`Cause: ${groupedIssue.cause}`,
|
|
605
|
+
groupedIssue.where ? `Where: ${groupedIssue.where}` : null,
|
|
606
|
+
groupedIssue.failingStep ? `Failing step: ${groupedIssue.failingStep}` : null,
|
|
607
|
+
groupedIssue.expected ? `Expected: ${groupedIssue.expected}` : null,
|
|
608
|
+
groupedIssue.received ? `Received: ${groupedIssue.received}` : null,
|
|
609
|
+
`Likely fix: ${groupedIssue.next}`
|
|
610
|
+
]
|
|
611
|
+
.filter(Boolean)
|
|
612
|
+
.join("\n"))}"
|
|
613
|
+
aria-label="Copy grouped diagnosis"
|
|
614
|
+
>
|
|
615
|
+
Copy diagnosis
|
|
616
|
+
</button>
|
|
617
|
+
</div>
|
|
618
|
+
<div class="fact-row">
|
|
619
|
+
${groupedIssue.where ? `<span class="fact-chip">Where: ${escapeHtml(groupedIssue.where)}</span>` : ""}
|
|
620
|
+
${groupedIssue.failingStep ? `<span class="fact-chip">Failing step: ${escapeHtml(groupedIssue.failingStep)}</span>` : ""}
|
|
621
|
+
${groupedIssue.selector ? `<span class="fact-chip">Selector: ${escapeHtml(groupedIssue.selector)}</span>` : ""}
|
|
622
|
+
${groupedIssue.blocker ? `<span class="fact-chip">Blocker: ${escapeHtml(groupedIssue.blocker)}</span>` : ""}
|
|
623
|
+
${groupedIssue.targetState ? `<span class="fact-chip">Target state: ${escapeHtml(groupedIssue.targetState)}</span>` : ""}
|
|
624
|
+
${groupedIssue.expected ? `<span class="fact-chip">Expected: ${escapeHtml(groupedIssue.expected)}</span>` : ""}
|
|
625
|
+
${groupedIssue.received ? `<span class="fact-chip">Received: ${escapeHtml(groupedIssue.received)}</span>` : ""}
|
|
626
|
+
</div>
|
|
627
|
+
<p class="diagnosis-copy"><strong>Likely fix:</strong> ${escapeHtml(groupedIssue.next)}</p>
|
|
628
|
+
`
|
|
629
|
+
: diagnosis
|
|
630
|
+
? `
|
|
587
631
|
<div class="diagnosis-shell">
|
|
588
632
|
<div>
|
|
589
633
|
<span class="artifact-kind">Quick diagnosis</span>
|
|
@@ -605,9 +649,9 @@ const renderTestCard = (test) => {
|
|
|
605
649
|
${diagnosis.timeoutMs ? `<span class="fact-chip">Timeout: ${diagnosis.timeoutMs}ms</span>` : ""}
|
|
606
650
|
</div>
|
|
607
651
|
`
|
|
608
|
-
|
|
652
|
+
: "";
|
|
609
653
|
return `
|
|
610
|
-
<details class="test-card">
|
|
654
|
+
<details class="test-card" id="test-${safeSlug(test.id)}">
|
|
611
655
|
<summary class="test-summary">
|
|
612
656
|
<div>
|
|
613
657
|
<div class="status-pill ${statusClass}">${escapeHtml(test.status)}</div>
|
|
@@ -693,6 +737,237 @@ const renderFailureDigest = (tests) => {
|
|
|
693
737
|
</div>
|
|
694
738
|
`;
|
|
695
739
|
};
|
|
740
|
+
const renderDiagnosisOverview = (summary) => {
|
|
741
|
+
if (!summary) {
|
|
742
|
+
return `<div class="empty-state">No structured diagnosis was available for this run.</div>`;
|
|
743
|
+
}
|
|
744
|
+
const copyText = escapeHtml([
|
|
745
|
+
summary.headline || "",
|
|
746
|
+
summary.failureCountLine,
|
|
747
|
+
summary.collapseLine || "",
|
|
748
|
+
...summary.issues.flatMap((issue, index) => {
|
|
749
|
+
const lines = [
|
|
750
|
+
`Issue ${index + 1}: ${issue.title}`,
|
|
751
|
+
`Cause: ${issue.cause}`
|
|
752
|
+
];
|
|
753
|
+
if (issue.where)
|
|
754
|
+
lines.push(`Where: ${issue.where}`);
|
|
755
|
+
if (issue.failingStep)
|
|
756
|
+
lines.push(`Failing step: ${issue.failingStep}`);
|
|
757
|
+
if (issue.blocker)
|
|
758
|
+
lines.push(`Blocker: ${issue.blocker}`);
|
|
759
|
+
if (issue.targetState)
|
|
760
|
+
lines.push(`Target state: ${issue.targetState}`);
|
|
761
|
+
if (issue.expected)
|
|
762
|
+
lines.push(`Expected: ${issue.expected}`);
|
|
763
|
+
if (issue.received)
|
|
764
|
+
lines.push(`Received: ${issue.received}`);
|
|
765
|
+
if (issue.whatChanged)
|
|
766
|
+
lines.push(`What changed: ${issue.whatChanged}`);
|
|
767
|
+
if (issue.reason)
|
|
768
|
+
lines.push(`Reason: ${issue.reason}`);
|
|
769
|
+
lines.push(`Next: ${issue.next}`);
|
|
770
|
+
lines.push(`Impact: ${issue.impact}`);
|
|
771
|
+
return lines;
|
|
772
|
+
}),
|
|
773
|
+
...summary.footer
|
|
774
|
+
].filter(Boolean).join("\n"));
|
|
775
|
+
return `
|
|
776
|
+
<div class="diagnosis-overview">
|
|
777
|
+
<div class="diagnosis-shell">
|
|
778
|
+
<div>
|
|
779
|
+
<span class="artifact-kind">Sentinel diagnosis</span>
|
|
780
|
+
${summary.headline ? `<h2 class="diagnosis-headline">${escapeHtml(summary.headline)}</h2>` : ""}
|
|
781
|
+
<p class="diagnosis-kicker">${escapeHtml(summary.failureCountLine)}</p>
|
|
782
|
+
${summary.collapseLine ? `<p class="diagnosis-kicker diagnosis-kicker-secondary">${escapeHtml(summary.collapseLine)}</p>` : ""}
|
|
783
|
+
</div>
|
|
784
|
+
<button
|
|
785
|
+
type="button"
|
|
786
|
+
class="copy-button"
|
|
787
|
+
data-copy-summary="${copyText}"
|
|
788
|
+
aria-label="Copy diagnosis"
|
|
789
|
+
>
|
|
790
|
+
Copy diagnosis
|
|
791
|
+
</button>
|
|
792
|
+
</div>
|
|
793
|
+
${summary.footer.length ? `<p class="diagnosis-copy">${escapeHtml(summary.footer.join(" · "))}</p>` : ""}
|
|
794
|
+
</div>
|
|
795
|
+
`;
|
|
796
|
+
};
|
|
797
|
+
const renderRecurringInsight = (historySummary) => {
|
|
798
|
+
if (!historySummary?.isDominantRecurringIssue || !historySummary.dominantRecurringIssueTitle)
|
|
799
|
+
return "";
|
|
800
|
+
return `
|
|
801
|
+
<p class="diagnosis-copy">
|
|
802
|
+
<strong>Recurring signal:</strong>
|
|
803
|
+
Seen in ${escapeHtml(String(historySummary.dominantRecurringIssueCount))} recorded failed runs.
|
|
804
|
+
This is the most common recent failure in local history.
|
|
805
|
+
</p>
|
|
806
|
+
`;
|
|
807
|
+
};
|
|
808
|
+
const renderDiagnosisIssueCards = (summary) => {
|
|
809
|
+
if (!summary || !summary.issues.length) {
|
|
810
|
+
return `<div class="empty-state">No grouped diagnosis issues were available for this run.</div>`;
|
|
811
|
+
}
|
|
812
|
+
return `
|
|
813
|
+
<div class="digest-grid">
|
|
814
|
+
${summary.issues.map((issue, index) => `
|
|
815
|
+
<article class="digest-card">
|
|
816
|
+
<div class="digest-head">
|
|
817
|
+
<div>
|
|
818
|
+
<span class="artifact-kind">Issue ${index + 1}</span>
|
|
819
|
+
<h3>${escapeHtml(issue.title)}</h3>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
<p class="diagnosis-copy">${escapeHtml(issue.cause)}</p>
|
|
823
|
+
<div class="fact-row">
|
|
824
|
+
${issue.where ? `<span class="fact-chip">Where: ${escapeHtml(issue.where)}</span>` : ""}
|
|
825
|
+
${issue.selector ? `<span class="fact-chip">Selector: ${escapeHtml(issue.selector)}</span>` : ""}
|
|
826
|
+
${issue.targetState ? `<span class="fact-chip">Target state: ${escapeHtml(issue.targetState)}</span>` : ""}
|
|
827
|
+
${issue.blocker ? `<span class="fact-chip">Blocker: ${escapeHtml(issue.blocker)}</span>` : ""}
|
|
828
|
+
</div>
|
|
829
|
+
<div class="fact-row">
|
|
830
|
+
${issue.expected ? `<span class="fact-chip">Expected: ${escapeHtml(issue.expected)}</span>` : ""}
|
|
831
|
+
${issue.received ? `<span class="fact-chip">Received: ${escapeHtml(issue.received)}</span>` : ""}
|
|
832
|
+
</div>
|
|
833
|
+
${issue.failingCode ? `<pre>${escapeHtml(issue.failingCode)}</pre>` : ""}
|
|
834
|
+
${issue.failingStep ? `<p class="diagnosis-copy"><strong>Failing step:</strong> ${escapeHtml(issue.failingStep)}</p>` : ""}
|
|
835
|
+
${issue.whatChanged ? `<p class="diagnosis-copy"><strong>What changed:</strong> ${escapeHtml(issue.whatChanged)}</p>` : ""}
|
|
836
|
+
${issue.reason ? `<p class="diagnosis-copy"><strong>Reason:</strong> ${escapeHtml(issue.reason)}</p>` : ""}
|
|
837
|
+
<p class="diagnosis-copy"><strong>Next:</strong> ${escapeHtml(issue.next)}</p>
|
|
838
|
+
<p class="diagnosis-copy"><strong>Clears:</strong> ${escapeHtml(issue.clears)}</p>
|
|
839
|
+
<p class="diagnosis-copy"><strong>Impact:</strong> ${escapeHtml(issue.impact)}</p>
|
|
840
|
+
</article>
|
|
841
|
+
`).join("\n")}
|
|
842
|
+
</div>
|
|
843
|
+
`;
|
|
844
|
+
};
|
|
845
|
+
const renderDiagnosisIssueCardsForTests = (summary, tests) => {
|
|
846
|
+
if (!summary || !summary.issues.length) {
|
|
847
|
+
return `<div class="empty-state">No grouped diagnosis issues were available for this run.</div>`;
|
|
848
|
+
}
|
|
849
|
+
return `
|
|
850
|
+
<div class="digest-grid">
|
|
851
|
+
${summary.issues.map((issue, index) => {
|
|
852
|
+
const representative = pickRepresentativeTestForIssue(tests, issue);
|
|
853
|
+
return `
|
|
854
|
+
<article class="digest-card ${index === 0 ? "digest-card-primary" : ""}">
|
|
855
|
+
<div class="digest-head">
|
|
856
|
+
<div>
|
|
857
|
+
<span class="artifact-kind">${index === 0 ? "Top issue" : `Issue ${index + 1}`}</span>
|
|
858
|
+
<h3>${escapeHtml(issue.title)}</h3>
|
|
859
|
+
</div>
|
|
860
|
+
</div>
|
|
861
|
+
<p class="diagnosis-copy">${escapeHtml(issue.cause)}</p>
|
|
862
|
+
<div class="fact-row">
|
|
863
|
+
${issue.where ? `<span class="fact-chip">Where: ${escapeHtml(issue.where)}</span>` : ""}
|
|
864
|
+
${issue.failingStep ? `<span class="fact-chip">Failing step: ${escapeHtml(issue.failingStep)}</span>` : ""}
|
|
865
|
+
${issue.targetState ? `<span class="fact-chip">Target state: ${escapeHtml(issue.targetState)}</span>` : ""}
|
|
866
|
+
${issue.blocker ? `<span class="fact-chip">Blocker: ${escapeHtml(issue.blocker)}</span>` : ""}
|
|
867
|
+
</div>
|
|
868
|
+
<div class="fact-row">
|
|
869
|
+
${issue.selector ? `<span class="fact-chip">Selector: ${escapeHtml(issue.selector)}</span>` : ""}
|
|
870
|
+
${issue.expected ? `<span class="fact-chip">Expected: ${escapeHtml(issue.expected)}</span>` : ""}
|
|
871
|
+
${issue.received ? `<span class="fact-chip">Received: ${escapeHtml(issue.received)}</span>` : ""}
|
|
872
|
+
</div>
|
|
873
|
+
${issue.whatChanged ? `<p class="diagnosis-copy"><strong>What changed:</strong> ${escapeHtml(issue.whatChanged)}</p>` : ""}
|
|
874
|
+
${issue.reason ? `<p class="diagnosis-copy"><strong>Reason:</strong> ${escapeHtml(issue.reason)}</p>` : ""}
|
|
875
|
+
<p class="diagnosis-copy"><strong>Likely fix:</strong> ${escapeHtml(issue.next)}</p>
|
|
876
|
+
<p class="diagnosis-copy"><strong>Impact:</strong> ${escapeHtml(issue.impact)}</p>
|
|
877
|
+
${issue.affectedTitles.length > 1
|
|
878
|
+
? `<ul class="group-list">${issue.affectedTitles
|
|
879
|
+
.slice(0, 6)
|
|
880
|
+
.map((title) => `<li>${escapeHtml(shortenTitle(title))}</li>`)
|
|
881
|
+
.join("\n")}</ul>`
|
|
882
|
+
: ""}
|
|
883
|
+
${representative ? `<p class="diagnosis-copy"><strong>Representative test:</strong> ${escapeHtml(representative.title)}</p>` : ""}
|
|
884
|
+
${renderIssueArtifactActions(representative)}
|
|
885
|
+
</article>
|
|
886
|
+
`;
|
|
887
|
+
}).join("\n")}
|
|
888
|
+
</div>
|
|
889
|
+
`;
|
|
890
|
+
};
|
|
891
|
+
const reportTestDisplayTitle = (test) => cleanTitleParts(test.titlePath).join(" > ") || test.title;
|
|
892
|
+
const findIssueForTest = (summary, test) => {
|
|
893
|
+
if (!summary?.issues.length)
|
|
894
|
+
return null;
|
|
895
|
+
const title = reportTestDisplayTitle(test);
|
|
896
|
+
return (summary.issues.find((issue) => issue.affectedTitles.some((affectedTitle) => cleanTitleParts(affectedTitle.split(" > ")).join(" > ") === title)) || null);
|
|
897
|
+
};
|
|
898
|
+
const extractLineHints = (value) => Array.from(new Set(Array.from((value || "").matchAll(/:([0-9]+)(?::[0-9]+)?/g))
|
|
899
|
+
.map((match) => Number.parseInt(match[1] || "", 10))
|
|
900
|
+
.filter((line) => Number.isFinite(line))));
|
|
901
|
+
const pickRepresentativeTestForIssue = (tests, issue) => {
|
|
902
|
+
const failedTests = getFailureTests(tests);
|
|
903
|
+
const affected = new Set(issue.affectedTitles.map((title) => cleanTitleParts(title.split(" > ")).join(" > ")));
|
|
904
|
+
const matches = failedTests.filter((test) => affected.has(reportTestDisplayTitle(test)));
|
|
905
|
+
if (!matches.length)
|
|
906
|
+
return null;
|
|
907
|
+
const issueFile = (issue.where || "").split(":")[0]?.trim().toLowerCase();
|
|
908
|
+
const issueLines = extractLineHints(issue.where);
|
|
909
|
+
const issueSelector = (issue.selector || "").trim().toLowerCase();
|
|
910
|
+
const issueBlocker = (issue.blocker || "").trim().toLowerCase();
|
|
911
|
+
const issueFailingCode = (issue.failingCode || "").trim();
|
|
912
|
+
const issueTargetState = (issue.targetState || "").trim().toLowerCase();
|
|
913
|
+
const score = (test) => {
|
|
914
|
+
const diagnosis = test.diagnosis;
|
|
915
|
+
const diagnosisFile = (diagnosis?.likelyFile || diagnosis?.file || "").trim().toLowerCase();
|
|
916
|
+
const diagnosisSelector = (diagnosis?.locator || "").trim().toLowerCase();
|
|
917
|
+
const diagnosisLine = diagnosis?.codeContext?.line || null;
|
|
918
|
+
const diagnosisFocusLine = (diagnosis?.codeContext?.focusLine || "").trim();
|
|
919
|
+
const diagnosisMessage = (diagnosis?.message || "").toLowerCase();
|
|
920
|
+
const traceScore = test.artifacts.some((artifact) => artifact.kind === "trace") ? 50 : 0;
|
|
921
|
+
const screenshotScore = test.artifacts.some((artifact) => artifact.kind === "screenshot") ? 30 : 0;
|
|
922
|
+
const videoScore = test.artifacts.some((artifact) => artifact.kind === "video") ? 20 : 0;
|
|
923
|
+
const selectorScore = issueSelector && diagnosisSelector && issueSelector === diagnosisSelector ? 40 : 0;
|
|
924
|
+
const fileScore = issueFile && diagnosisFile && diagnosisFile.endsWith(issueFile) ? 20 : 0;
|
|
925
|
+
const lineScore = diagnosisLine && issueLines.includes(diagnosisLine) ? 35 : 0;
|
|
926
|
+
const focusScore = issueFailingCode && diagnosisFocusLine && issueFailingCode === diagnosisFocusLine ? 35 : 0;
|
|
927
|
+
const blockerScore = issueBlocker && diagnosisMessage.includes(issueBlocker) ? 25 : 0;
|
|
928
|
+
const targetStateScore = issueTargetState && diagnosis?.domCapture
|
|
929
|
+
? (() => {
|
|
930
|
+
const dom = diagnosis.domCapture;
|
|
931
|
+
const targetState = dom.targetFound === false || dom.matchedCount === 0
|
|
932
|
+
? "missing"
|
|
933
|
+
: dom.visible === false
|
|
934
|
+
? "hidden"
|
|
935
|
+
: dom.enabled === false
|
|
936
|
+
? "disabled"
|
|
937
|
+
: dom.targetFound === true && dom.visible === true
|
|
938
|
+
? "visible_blocked"
|
|
939
|
+
: "";
|
|
940
|
+
return targetState === issueTargetState ? 15 : 0;
|
|
941
|
+
})()
|
|
942
|
+
: 0;
|
|
943
|
+
const lineAwareScore = diagnosisLine ? 10 : 0;
|
|
944
|
+
return traceScore + screenshotScore + videoScore + selectorScore + fileScore + lineScore + focusScore + blockerScore + targetStateScore + lineAwareScore;
|
|
945
|
+
};
|
|
946
|
+
return matches.sort((a, b) => score(b) - score(a))[0] || matches[0];
|
|
947
|
+
};
|
|
948
|
+
const renderIssueArtifactActions = (test) => {
|
|
949
|
+
if (!test)
|
|
950
|
+
return "";
|
|
951
|
+
const trace = test.artifacts.find((artifact) => artifact.kind === "trace");
|
|
952
|
+
const screenshot = test.artifacts.find((artifact) => artifact.kind === "screenshot");
|
|
953
|
+
const video = test.artifacts.find((artifact) => artifact.kind === "video");
|
|
954
|
+
const jumpTarget = `#test-${safeSlug(test.id)}`;
|
|
955
|
+
const actions = [
|
|
956
|
+
trace
|
|
957
|
+
? `<a class="trace-button" href="${escapeHtml(trace.relativePath)}" data-trace-path="${escapeHtml(trace.relativePath)}" target="_blank" rel="noreferrer">Best trace</a>`
|
|
958
|
+
: "",
|
|
959
|
+
screenshot
|
|
960
|
+
? `<a class="trace-button" href="${escapeHtml(screenshot.relativePath)}" target="_blank" rel="noreferrer">Open screenshot</a>`
|
|
961
|
+
: "",
|
|
962
|
+
video
|
|
963
|
+
? `<a class="trace-button" href="${escapeHtml(video.relativePath)}" target="_blank" rel="noreferrer">Open video</a>`
|
|
964
|
+
: "",
|
|
965
|
+
`<a class="trace-button" href="${escapeHtml(jumpTarget)}">Jump to test</a>`
|
|
966
|
+
].filter(Boolean);
|
|
967
|
+
if (!actions.length)
|
|
968
|
+
return "";
|
|
969
|
+
return `<div class="issue-actions">${actions.join("\n")}</div>`;
|
|
970
|
+
};
|
|
696
971
|
const renderSimilarFailureGroups = (groups) => {
|
|
697
972
|
if (!groups.length) {
|
|
698
973
|
return `<div class="empty-state">No repeated failure fingerprint was detected in this run.</div>`;
|
|
@@ -762,6 +1037,33 @@ const renderRunDiff = (runDiff) => {
|
|
|
762
1037
|
</div>
|
|
763
1038
|
`;
|
|
764
1039
|
};
|
|
1040
|
+
const renderHistoryOverview = (historySummary, runDiff) => {
|
|
1041
|
+
if (!historySummary && !runDiff) {
|
|
1042
|
+
return `<div class="empty-state">No prior run history was available for this failure yet.</div>`;
|
|
1043
|
+
}
|
|
1044
|
+
return `
|
|
1045
|
+
${renderRecurringInsight(historySummary)}
|
|
1046
|
+
${runDiff
|
|
1047
|
+
? `
|
|
1048
|
+
<div class="summary-grid">
|
|
1049
|
+
<div class="summary-card">
|
|
1050
|
+
<span class="summary-label">New failures</span>
|
|
1051
|
+
<span class="summary-value">${runDiff.newFailures.length}</span>
|
|
1052
|
+
</div>
|
|
1053
|
+
<div class="summary-card">
|
|
1054
|
+
<span class="summary-label">Still failing</span>
|
|
1055
|
+
<span class="summary-value">${runDiff.stillFailing.length}</span>
|
|
1056
|
+
</div>
|
|
1057
|
+
<div class="summary-card">
|
|
1058
|
+
<span class="summary-label">Fixed since last run</span>
|
|
1059
|
+
<span class="summary-value">${runDiff.fixedTests.length}</span>
|
|
1060
|
+
</div>
|
|
1061
|
+
</div>
|
|
1062
|
+
<p class="diagnosis-copy">Compared with the latest saved run on the same branch: ${escapeHtml(runDiff.label)}</p>
|
|
1063
|
+
`
|
|
1064
|
+
: ""}
|
|
1065
|
+
`;
|
|
1066
|
+
};
|
|
765
1067
|
const renderAdditionalArtifacts = (artifacts) => {
|
|
766
1068
|
if (artifacts.length === 0) {
|
|
767
1069
|
return "";
|
|
@@ -789,7 +1091,7 @@ const tryMapRemainingArtifactsToTests = (tests, artifactPaths, reportDir, usedRe
|
|
|
789
1091
|
}
|
|
790
1092
|
}
|
|
791
1093
|
};
|
|
792
|
-
const buildHtml = (tests, summary, extraArtifacts, runDiff) => {
|
|
1094
|
+
const buildHtml = (tests, summary, extraArtifacts, runDiff, diagnosisSummary, failedRunHistory) => {
|
|
793
1095
|
const failedTests = tests.filter((test) => ["failed", "timedOut", "interrupted"].includes(test.status));
|
|
794
1096
|
const similarGroups = groupSimilarFailures(tests);
|
|
795
1097
|
const generatedAt = new Date().toLocaleString();
|
|
@@ -1116,6 +1418,11 @@ const buildHtml = (tests, summary, extraArtifacts, runDiff) => {
|
|
|
1116
1418
|
background: rgba(9, 13, 20, 0.72);
|
|
1117
1419
|
padding: 16px;
|
|
1118
1420
|
}
|
|
1421
|
+
.digest-card-primary {
|
|
1422
|
+
border-color: rgba(125, 211, 252, 0.38);
|
|
1423
|
+
box-shadow: 0 12px 36px rgba(4, 14, 26, 0.26);
|
|
1424
|
+
background: linear-gradient(180deg, rgba(14, 22, 34, 0.92), rgba(9, 13, 20, 0.78));
|
|
1425
|
+
}
|
|
1119
1426
|
.digest-head, .diagnosis-shell {
|
|
1120
1427
|
display: flex;
|
|
1121
1428
|
justify-content: space-between;
|
|
@@ -1130,12 +1437,42 @@ const buildHtml = (tests, summary, extraArtifacts, runDiff) => {
|
|
|
1130
1437
|
color: var(--muted);
|
|
1131
1438
|
line-height: 1.6;
|
|
1132
1439
|
}
|
|
1440
|
+
.diagnosis-overview {
|
|
1441
|
+
display: grid;
|
|
1442
|
+
gap: 12px;
|
|
1443
|
+
margin-top: 18px;
|
|
1444
|
+
}
|
|
1445
|
+
.diagnosis-headline {
|
|
1446
|
+
margin-top: 10px;
|
|
1447
|
+
font-size: 28px;
|
|
1448
|
+
line-height: 1.1;
|
|
1449
|
+
}
|
|
1450
|
+
.diagnosis-kicker {
|
|
1451
|
+
margin: 12px 0 0;
|
|
1452
|
+
color: var(--text);
|
|
1453
|
+
font-size: 20px;
|
|
1454
|
+
font-weight: 700;
|
|
1455
|
+
}
|
|
1456
|
+
.diagnosis-kicker-secondary {
|
|
1457
|
+
color: var(--accent);
|
|
1458
|
+
font-size: 16px;
|
|
1459
|
+
font-weight: 600;
|
|
1460
|
+
}
|
|
1133
1461
|
.fact-row {
|
|
1134
1462
|
display: flex;
|
|
1135
1463
|
flex-wrap: wrap;
|
|
1136
1464
|
gap: 8px;
|
|
1137
1465
|
margin-top: 14px;
|
|
1138
1466
|
}
|
|
1467
|
+
.issue-actions {
|
|
1468
|
+
display: flex;
|
|
1469
|
+
flex-wrap: wrap;
|
|
1470
|
+
gap: 10px;
|
|
1471
|
+
margin-top: 14px;
|
|
1472
|
+
}
|
|
1473
|
+
.section-shell-secondary {
|
|
1474
|
+
opacity: 0.9;
|
|
1475
|
+
}
|
|
1139
1476
|
.fact-chip {
|
|
1140
1477
|
display: inline-flex;
|
|
1141
1478
|
align-items: center;
|
|
@@ -1264,37 +1601,39 @@ const buildHtml = (tests, summary, extraArtifacts, runDiff) => {
|
|
|
1264
1601
|
|
|
1265
1602
|
<section class="section-shell">
|
|
1266
1603
|
<div class="failed-list-head">
|
|
1267
|
-
<h2>
|
|
1604
|
+
<h2>Why It Failed</h2>
|
|
1268
1605
|
</div>
|
|
1269
|
-
<p>
|
|
1270
|
-
${
|
|
1606
|
+
<p>The same grouped explanation shown in the CLI, with the most useful evidence first.</p>
|
|
1607
|
+
${renderDiagnosisOverview(diagnosisSummary)}
|
|
1271
1608
|
</section>
|
|
1272
1609
|
|
|
1273
1610
|
<section class="section-shell">
|
|
1274
1611
|
<div class="failed-list-head">
|
|
1275
|
-
<h2>
|
|
1612
|
+
<h2>Explanation</h2>
|
|
1276
1613
|
<div class="failed-count">${failedTests.length} failed</div>
|
|
1277
1614
|
</div>
|
|
1278
|
-
<p>
|
|
1279
|
-
${
|
|
1615
|
+
<p>Grouped root causes, likely fixes, and direct links to the best artifacts for each issue.</p>
|
|
1616
|
+
${renderDiagnosisIssueCardsForTests(diagnosisSummary, tests)}
|
|
1280
1617
|
</section>
|
|
1281
1618
|
|
|
1282
|
-
<section class="section-shell">
|
|
1619
|
+
<section class="section-shell section-shell-secondary">
|
|
1283
1620
|
<div class="failed-list-head">
|
|
1284
|
-
<h2>
|
|
1285
|
-
<div class="failed-count">${similarGroups.length} groups</div>
|
|
1621
|
+
<h2>History</h2>
|
|
1286
1622
|
</div>
|
|
1287
|
-
<p>
|
|
1288
|
-
${
|
|
1623
|
+
<p>Recent failure history and how this run changed from the previous saved run.</p>
|
|
1624
|
+
${renderHistoryOverview(failedRunHistory, runDiff)}
|
|
1289
1625
|
</section>
|
|
1290
1626
|
|
|
1291
1627
|
<section class="section-shell">
|
|
1292
1628
|
<div class="failed-list-head">
|
|
1293
|
-
<h2>
|
|
1629
|
+
<h2>Artifacts</h2>
|
|
1294
1630
|
<div class="failed-count">${failedTests.length} failed</div>
|
|
1295
1631
|
</div>
|
|
1632
|
+
<p>Failed tests with trace, screenshot, video, logs, and raw error details.</p>
|
|
1296
1633
|
${failedTests.length > 0
|
|
1297
|
-
? failedTests
|
|
1634
|
+
? failedTests
|
|
1635
|
+
.map((test) => renderTestCard(test, findIssueForTest(diagnosisSummary, test)))
|
|
1636
|
+
.join("\n")
|
|
1298
1637
|
: `<div class="empty-state">No failed tests were found in this run. The local report still includes collected artifacts below.</div>`}
|
|
1299
1638
|
</section>
|
|
1300
1639
|
|
|
@@ -1306,19 +1645,6 @@ const buildHtml = (tests, summary, extraArtifacts, runDiff) => {
|
|
|
1306
1645
|
: `<div class="empty-state">All detected artifacts were mapped onto failed tests.</div>`}
|
|
1307
1646
|
</section>
|
|
1308
1647
|
|
|
1309
|
-
<section class="section-shell">
|
|
1310
|
-
<h2>Optional: Sentinel Cloud</h2>
|
|
1311
|
-
<p>Upload runs to Sentinel Cloud for:</p>
|
|
1312
|
-
<ul>
|
|
1313
|
-
<li>CI history</li>
|
|
1314
|
-
<li>shareable run links</li>
|
|
1315
|
-
<li>AI failure summaries</li>
|
|
1316
|
-
</ul>
|
|
1317
|
-
<p>
|
|
1318
|
-
<a href="${SENTINEL_URL}" target="_blank" rel="noreferrer">More on sentinelqa.com</a>
|
|
1319
|
-
</p>
|
|
1320
|
-
</section>
|
|
1321
|
-
|
|
1322
1648
|
<footer>
|
|
1323
1649
|
Generated by <a href="${SENTINEL_URL}" target="_blank" rel="noreferrer">Sentinel Playwright Reporter</a>.
|
|
1324
1650
|
</footer>
|
|
@@ -1508,8 +1834,10 @@ function generateLocalDebugReport(options) {
|
|
|
1508
1834
|
const summary = summarizeTests(tests);
|
|
1509
1835
|
const snapshot = buildRunSnapshot(tests, summary);
|
|
1510
1836
|
const runDiff = buildRunDiff(tests, snapshot);
|
|
1837
|
+
const diagnosisSummary = (0, quickDiagnosis_1.buildQuickDiagnosisStructured)(options.playwrightJsonPath);
|
|
1838
|
+
const failedRunHistory = summary.failed > 0 ? (0, runHistory_1.buildFailedRunHistorySummary)(options.playwrightJsonPath) : null;
|
|
1511
1839
|
writeRunHistory(snapshot);
|
|
1512
|
-
const html = buildHtml(tests, summary, extraArtifacts, runDiff);
|
|
1840
|
+
const html = buildHtml(tests, summary, extraArtifacts, runDiff, diagnosisSummary, failedRunHistory);
|
|
1513
1841
|
fs_1.default.writeFileSync(reportHtmlPath, html, "utf8");
|
|
1514
1842
|
fs_1.default.writeFileSync(redirectPath, `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=${relativeFromCwd(reportHtmlPath)}" /><title>Sentinel Playwright Reporter</title></head><body><p>Open <a href="${relativeFromCwd(reportHtmlPath)}">${relativeFromCwd(reportHtmlPath)}</a>.</p></body></html>`, "utf8");
|
|
1515
1843
|
return {
|
package/dist/quickDiagnosis.d.ts
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
-
type DiagnosisSignal = "timeout" | "assertion_mismatch" | "locator_not_found" | "actionability" | "network" | "runtime" | "infra" | "unknown";
|
|
1
|
+
type DiagnosisSignal = "setup" | "timeout" | "assertion_mismatch" | "locator_not_found" | "actionability" | "network" | "runtime" | "infra" | "unknown";
|
|
2
2
|
export type QuickDiagnosis = {
|
|
3
3
|
lines: string[];
|
|
4
4
|
footer?: string[];
|
|
5
5
|
};
|
|
6
|
+
export type DiagnosisIssue = {
|
|
7
|
+
title: string;
|
|
8
|
+
cause: string;
|
|
9
|
+
affectedTitles: string[];
|
|
10
|
+
where: string | null;
|
|
11
|
+
failingCode: string | null;
|
|
12
|
+
failingStep: string | null;
|
|
13
|
+
selector: string | null;
|
|
14
|
+
blocker: string | null;
|
|
15
|
+
targetState: string | null;
|
|
16
|
+
expected: string | null;
|
|
17
|
+
received: string | null;
|
|
18
|
+
whatChanged: string | null;
|
|
19
|
+
reason: string | null;
|
|
20
|
+
next: string;
|
|
21
|
+
impact: string;
|
|
22
|
+
clears: string;
|
|
23
|
+
};
|
|
24
|
+
export type DiagnosisSummary = {
|
|
25
|
+
mode: "new_failure" | "recurring_failure" | "failure";
|
|
26
|
+
headline: string | null;
|
|
27
|
+
failureCountLine: string;
|
|
28
|
+
collapseLine: string | null;
|
|
29
|
+
issues: DiagnosisIssue[];
|
|
30
|
+
footer: string[];
|
|
31
|
+
};
|
|
6
32
|
type CodeContextCapture = {
|
|
7
33
|
file?: string | null;
|
|
8
34
|
line?: number | null;
|
|
@@ -20,6 +46,7 @@ type CodeContextCapture = {
|
|
|
20
46
|
};
|
|
21
47
|
type DomCapture = {
|
|
22
48
|
locator?: string | null;
|
|
49
|
+
normalizedLocator?: string | null;
|
|
23
50
|
expectedText?: string | null;
|
|
24
51
|
observedText?: string | null;
|
|
25
52
|
captureSource?: "live_page" | "error_fallback";
|
|
@@ -37,6 +64,7 @@ type DomCapture = {
|
|
|
37
64
|
placeholder?: string | null;
|
|
38
65
|
ariaLabel?: string | null;
|
|
39
66
|
textAlternatives?: string[] | null;
|
|
67
|
+
ariaSnapshot?: string | null;
|
|
40
68
|
matchedElements?: Array<{
|
|
41
69
|
index: number;
|
|
42
70
|
role: string | null;
|
|
@@ -45,6 +73,17 @@ type DomCapture = {
|
|
|
45
73
|
enabled: boolean | null;
|
|
46
74
|
text: string | null;
|
|
47
75
|
}> | null;
|
|
76
|
+
recentConsoleErrors?: Array<{
|
|
77
|
+
type: string;
|
|
78
|
+
text: string;
|
|
79
|
+
}> | null;
|
|
80
|
+
recentPageErrors?: string[] | null;
|
|
81
|
+
recentRequests?: Array<{
|
|
82
|
+
method: string | null;
|
|
83
|
+
url: string | null;
|
|
84
|
+
status: number | null;
|
|
85
|
+
failure: string | null;
|
|
86
|
+
}> | null;
|
|
48
87
|
};
|
|
49
88
|
export type FailureFacts = {
|
|
50
89
|
title: string;
|
|
@@ -83,6 +122,7 @@ export declare const parseFailureFacts: (title: string, titlePath: string[], mes
|
|
|
83
122
|
export declare const collectFailureFacts: (playwrightJsonPath: string) => FailureFacts[];
|
|
84
123
|
export declare const buildDebugSummary: (failure: FailureFacts) => string;
|
|
85
124
|
export declare const buildSimilarityKey: (failure: FailureFacts) => string;
|
|
86
|
-
export declare const summarizeSignal: (signal: DiagnosisSignal) => "timeout while waiting for UI or network conditions" | "assertion mismatch between expected and rendered UI state" | "missing or changed locator" | "target element was not actionable" | "network or API failure" | "runtime error thrown before the flow completed" | "browser or CI infrastructure failure" | "failure signal could not be classified cleanly";
|
|
125
|
+
export declare const summarizeSignal: (signal: DiagnosisSignal) => "Playwright setup or bootstrap error" | "timeout while waiting for UI or network conditions" | "assertion mismatch between expected and rendered UI state" | "missing or changed locator" | "target element was not actionable" | "network or API failure" | "runtime error thrown before the flow completed" | "browser or CI infrastructure failure" | "failure signal could not be classified cleanly";
|
|
126
|
+
export declare const buildQuickDiagnosisStructured: (playwrightJsonPath: string) => DiagnosisSummary | null;
|
|
87
127
|
export declare const buildQuickDiagnosis: (playwrightJsonPath: string) => QuickDiagnosis | null;
|
|
88
128
|
export {};
|