donobu 5.41.3 → 5.42.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/dist/esm/lib/ai/PageAi.js +3 -0
- package/dist/esm/lib/page/DonobuExtendedPage.d.ts +30 -0
- package/dist/esm/lib/page/extendPage.js +9 -0
- package/dist/esm/reporter/buildReport.js +4 -0
- package/dist/esm/reporter/html.d.ts +11 -0
- package/dist/esm/reporter/html.js +55 -0
- package/dist/esm/reporter/render.d.ts +15 -0
- package/dist/esm/reporter/render.js +351 -40
- package/dist/esm/tools/AssertTool.js +18 -1
- package/dist/lib/ai/PageAi.js +3 -0
- package/dist/lib/page/DonobuExtendedPage.d.ts +30 -0
- package/dist/lib/page/extendPage.js +9 -0
- package/dist/reporter/buildReport.js +4 -0
- package/dist/reporter/html.d.ts +11 -0
- package/dist/reporter/html.js +55 -0
- package/dist/reporter/render.d.ts +15 -0
- package/dist/reporter/render.js +351 -40
- package/dist/tools/AssertTool.js +18 -1
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.loadTriageData = loadTriageData;
|
|
12
12
|
exports.renderHtml = renderHtml;
|
|
13
|
+
exports.renderPerTestStub = renderPerTestStub;
|
|
13
14
|
const fs_1 = require("fs");
|
|
14
15
|
const path_1 = require("path");
|
|
15
16
|
const ansi_1 = require("../utils/ansi");
|
|
@@ -366,6 +367,7 @@ function extractTests(jsonData) {
|
|
|
366
367
|
tests.push({
|
|
367
368
|
file: suite.file,
|
|
368
369
|
specTitle: spec.title,
|
|
370
|
+
testId: typeof test.testId === 'string' ? test.testId : '',
|
|
369
371
|
status,
|
|
370
372
|
isSelfHealed,
|
|
371
373
|
objective: objectiveAnnotation?.description ?? null,
|
|
@@ -577,18 +579,42 @@ function renderErrors(errors) {
|
|
|
577
579
|
}
|
|
578
580
|
return html;
|
|
579
581
|
}
|
|
580
|
-
function renderNativeStep(ns, childrenHtml) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
582
|
+
function renderNativeStep(ns, childrenHtml, verifyContext = false) {
|
|
583
|
+
// Expects inside an assert tool's cache-worthiness verification window are
|
|
584
|
+
// not real assertion checks — they're AssertTool re-running its own
|
|
585
|
+
// AI-emitted structured `expect()` calls to decide whether to cache them.
|
|
586
|
+
// When one fails, the AI's screenshot-based verdict still stands; only the
|
|
587
|
+
// structured locator faithfulness is in question. Render those with a
|
|
588
|
+
// distinct status (passed → "verified", failed → "diverged") so they
|
|
589
|
+
// don't look like assertion failures sitting under a passing assertion.
|
|
590
|
+
const statusIcon = verifyContext
|
|
591
|
+
? ns.passed
|
|
592
|
+
? '<span class="step-status-verified" title="Cache-verify check passed">✓</span>'
|
|
593
|
+
: '<span class="step-status-diverged" title="Cache-verify locator did not match the AI's visual verdict">❙</span>'
|
|
594
|
+
: ns.passed
|
|
595
|
+
? '<span class="step-status-ok">✓</span>'
|
|
596
|
+
: '<span class="step-status-fail">✗</span>';
|
|
597
|
+
const categoryLabel = verifyContext
|
|
598
|
+
? ns.passed
|
|
599
|
+
? 'verify-cache'
|
|
600
|
+
: 'verify-cache diverged'
|
|
601
|
+
: ns.category;
|
|
602
|
+
const categoryClass = verifyContext
|
|
603
|
+
? ns.passed
|
|
604
|
+
? 'native-step-badge--verify'
|
|
605
|
+
: 'native-step-badge--verify-diverged'
|
|
606
|
+
: `native-step-badge--${ns.category}`;
|
|
607
|
+
const categoryBadge = `<span class="native-step-badge ${categoryClass}">${esc(categoryLabel)}</span>`;
|
|
585
608
|
const locationStr = ns.location?.file
|
|
586
609
|
? esc(`${ns.location.file.replace(/.*[/\\]/, '')}:${ns.location.line}`)
|
|
587
610
|
: '';
|
|
588
611
|
const snippet = ns.location?.file
|
|
589
612
|
? readSourceSnippet(ns.location.file, ns.location.line)
|
|
590
613
|
: null;
|
|
591
|
-
|
|
614
|
+
// Cache-verify failures aren't surfaced as red errors; the message lives
|
|
615
|
+
// alongside the parent invocation's `cache · miss` pill instead. We still
|
|
616
|
+
// want the body open so the locator's call log is visible at a glance.
|
|
617
|
+
const hasError = !ns.passed && !!ns.error?.message && !verifyContext;
|
|
592
618
|
const hasBody = !!snippet || hasError || !!childrenHtml;
|
|
593
619
|
const renderHeader = (tag) => {
|
|
594
620
|
let header = `<${tag} class="filmstrip-header">`;
|
|
@@ -609,9 +635,17 @@ function renderNativeStep(ns, childrenHtml) {
|
|
|
609
635
|
// Failures always render expanded so the error is immediately visible.
|
|
610
636
|
// test.step blocks with nested content also default open so users see
|
|
611
637
|
// what's inside; bare passing expects with just a snippet collapse to
|
|
612
|
-
// keep tests with many assertions scannable.
|
|
613
|
-
|
|
614
|
-
const
|
|
638
|
+
// keep tests with many assertions scannable. Cache-verify divergences
|
|
639
|
+
// are routine signal — start collapsed so they don't dominate the view.
|
|
640
|
+
const defaultOpen = !verifyContext &&
|
|
641
|
+
(!ns.passed || (ns.category === 'test.step' && !!childrenHtml));
|
|
642
|
+
const passClass = verifyContext
|
|
643
|
+
? ns.passed
|
|
644
|
+
? 'native-step--verify'
|
|
645
|
+
: 'native-step--verify-diverged'
|
|
646
|
+
: ns.passed
|
|
647
|
+
? 'native-step--passed'
|
|
648
|
+
: 'native-step--failed';
|
|
615
649
|
let html = `<details class="filmstrip-step native-step expandable ${passClass}"${defaultOpen ? ' open' : ''}>`;
|
|
616
650
|
html += renderHeader('summary');
|
|
617
651
|
if (hasError) {
|
|
@@ -679,12 +713,31 @@ function renderAiInvocation(inv, childrenHtml) {
|
|
|
679
713
|
? '<span class="step-status-ok">✓</span>'
|
|
680
714
|
: '<span class="step-status-fail">✗</span>';
|
|
681
715
|
const kindBadge = `<span class="ai-invocation-badge ai-invocation-badge--${inv.kind}">${esc(AI_KIND_LABELS[inv.kind])}</span>`;
|
|
682
|
-
const
|
|
683
|
-
? '
|
|
684
|
-
:
|
|
716
|
+
const cacheState = inv.cacheHit
|
|
717
|
+
? 'hit'
|
|
718
|
+
: inv.cacheStored
|
|
719
|
+
? 'stored'
|
|
720
|
+
: 'miss';
|
|
721
|
+
const cacheLabel = {
|
|
722
|
+
hit: 'cache · hit',
|
|
723
|
+
stored: 'cache · stored',
|
|
724
|
+
miss: 'cache · miss',
|
|
725
|
+
};
|
|
726
|
+
const cacheTitle = {
|
|
727
|
+
hit: 'Replayed from the page-AI cache; no AI used for this step.',
|
|
728
|
+
stored: 'Live AI run; the resulting locators/steps were recorded to the page-AI cache. The next run can replay them without calling the AI.',
|
|
729
|
+
miss: "Live AI run; nothing was recorded to the page-AI cache. The next run will hit the AI again. For asserts, this typically means the AI's structured Playwright locators didn't reproduce its screenshot verdict.",
|
|
730
|
+
};
|
|
731
|
+
const cacheBadge = `<span class="ai-cache-badge ai-cache-badge--${cacheState}" title="${esc(cacheTitle[cacheState])}">${cacheLabel[cacheState]}</span>`;
|
|
732
|
+
// For a passing assert whose structured-step verifier failed, surface
|
|
733
|
+
// *why* the cache outcome was `miss`. The header pill carries the
|
|
734
|
+
// at-a-glance signal; this body content is the technical detail.
|
|
735
|
+
// (When the assert itself failed, the regular failure path already
|
|
736
|
+
// covers it.)
|
|
737
|
+
const showVerifierDetail = inv.passed && inv.verification?.failed === true;
|
|
685
738
|
const hasError = !inv.passed && !!inv.error?.message;
|
|
686
739
|
const hasAssertSteps = !!inv.assertSteps && inv.assertSteps.length > 0;
|
|
687
|
-
const hasBody = hasError || !!childrenHtml || hasAssertSteps;
|
|
740
|
+
const hasBody = hasError || !!childrenHtml || hasAssertSteps || showVerifierDetail;
|
|
688
741
|
const renderHeader = (tag) => {
|
|
689
742
|
let header = `<${tag} class="filmstrip-header">`;
|
|
690
743
|
header +=
|
|
@@ -692,7 +745,7 @@ function renderAiInvocation(inv, childrenHtml) {
|
|
|
692
745
|
header += statusIcon;
|
|
693
746
|
header += `<span class="ai-invocation-title">${esc(inv.description)}</span>`;
|
|
694
747
|
header += kindBadge;
|
|
695
|
-
header +=
|
|
748
|
+
header += cacheBadge;
|
|
696
749
|
header += `</${tag}>`;
|
|
697
750
|
return header;
|
|
698
751
|
};
|
|
@@ -706,13 +759,20 @@ function renderAiInvocation(inv, childrenHtml) {
|
|
|
706
759
|
// by default so the contents are visible without an extra click.
|
|
707
760
|
const defaultOpen = !inv.passed || !!childrenHtml || hasAssertSteps;
|
|
708
761
|
const passClass = inv.passed
|
|
709
|
-
?
|
|
762
|
+
? showVerifierDetail
|
|
763
|
+
? 'ai-invocation--passed ai-invocation--cache-miss'
|
|
764
|
+
: 'ai-invocation--passed'
|
|
710
765
|
: 'ai-invocation--failed';
|
|
711
766
|
let html = `<details class="filmstrip-step ai-invocation expandable ${passClass}"${defaultOpen ? ' open' : ''}>`;
|
|
712
767
|
html += renderHeader('summary');
|
|
713
768
|
if (hasError) {
|
|
714
769
|
html += `<pre class="native-step-error">${ansiToHtml(inv.error.message)}</pre>`;
|
|
715
770
|
}
|
|
771
|
+
if (showVerifierDetail && inv.verification?.errorMessage) {
|
|
772
|
+
html +=
|
|
773
|
+
`<div class="ai-cache-miss-explainer">The AI’s screenshot verdict (passed) is what counts. Its structured Playwright steps did not reproduce that verdict against the live page — most often an over-broad locator — so they were not cached. The diverging check is highlighted below.</div>` +
|
|
774
|
+
`<pre class="ai-cache-miss-detail">${ansiToHtml(inv.verification.errorMessage)}</pre>`;
|
|
775
|
+
}
|
|
716
776
|
if (hasAssertSteps) {
|
|
717
777
|
const lines = inv
|
|
718
778
|
.assertSteps.map((s) => esc(formatAssertionStep(s)))
|
|
@@ -1070,6 +1130,28 @@ function renderSteps(steps, stepScreenshots, nativeSteps, aiInvocations, outputD
|
|
|
1070
1130
|
}
|
|
1071
1131
|
return c;
|
|
1072
1132
|
};
|
|
1133
|
+
// A native step is part of an AssertTool cache-worthiness verification
|
|
1134
|
+
// (rather than a user-authored assertion) iff its time window falls
|
|
1135
|
+
// inside the `verification` window of some enclosing AI invocation.
|
|
1136
|
+
// `verifyWindows` is the ordered list of those windows; `inVerify`
|
|
1137
|
+
// checks membership without scanning the tree.
|
|
1138
|
+
const verifyWindows = [];
|
|
1139
|
+
for (const inv of aiInvocations) {
|
|
1140
|
+
if (inv.verification) {
|
|
1141
|
+
verifyWindows.push({
|
|
1142
|
+
start: inv.verification.startedAt,
|
|
1143
|
+
end: inv.verification.endedAt,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const inVerify = (t, tEnd) => {
|
|
1148
|
+
for (const w of verifyWindows) {
|
|
1149
|
+
if (t >= w.start && tEnd <= w.end) {
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return false;
|
|
1154
|
+
};
|
|
1073
1155
|
const renderNode = (node) => {
|
|
1074
1156
|
if (node.kind === 'donobu') {
|
|
1075
1157
|
return renderFilmstripStep(node.ss, outputDir);
|
|
@@ -1083,7 +1165,7 @@ function renderSteps(steps, stepScreenshots, nativeSteps, aiInvocations, outputD
|
|
|
1083
1165
|
const childrenHtml = node.children.length > 0
|
|
1084
1166
|
? `<div class="native-step-children">${node.children.map(renderNode).join('')}</div>`
|
|
1085
1167
|
: '';
|
|
1086
|
-
return renderNativeStep(node.ns, childrenHtml);
|
|
1168
|
+
return renderNativeStep(node.ns, childrenHtml, inVerify(node.t, node.tEnd));
|
|
1087
1169
|
};
|
|
1088
1170
|
const stepCount = countNodes(roots);
|
|
1089
1171
|
let html = '<details class="steps-section"><summary>Steps (' +
|
|
@@ -1450,8 +1532,10 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1450
1532
|
// Group by file
|
|
1451
1533
|
const uniqueFiles = new Set(tests.map((t) => t.file));
|
|
1452
1534
|
// --- Build HTML sections ---
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1535
|
+
// Per-card IDs used by the test-detail div, bar-block `data-target`, and the
|
|
1536
|
+
// outer `.test-card` anchor that `#?testId=<id>` deep links target. Sourced
|
|
1537
|
+
// from Playwright's stable `TestCase.id`.
|
|
1538
|
+
const testIds = tests.map((t) => `test-${t.testId}`);
|
|
1455
1539
|
// Build test bar blocks (one square per test, ordered by status, clickable)
|
|
1456
1540
|
const statusOrder = [
|
|
1457
1541
|
'passed',
|
|
@@ -1594,7 +1678,7 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1594
1678
|
? `<div class="flow-id-detail"><span class="detail-label">Flow ID</span><span class="flow-id-value">${esc(test.flowId)}<button class="copy-flow-id" data-flow-id="${esc(test.flowId)}" title="Copy flow ID"><svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg></button></span></div>`
|
|
1595
1679
|
: '';
|
|
1596
1680
|
testSectionsHtml += `
|
|
1597
|
-
<div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" data-status="${test.status}" ${hasDetails ? `data-detail="${testId}"` : ''}>
|
|
1681
|
+
<div class="test-card ${sc.label.toLowerCase().replace(/ /g, '')} ${expandableClass}" id="${testId}" data-status="${test.status}" data-tags="${esc(JSON.stringify(test.tags))}" ${hasDetails ? `data-detail="${testId}"` : ''}>
|
|
1598
1682
|
<div class="test-summary">
|
|
1599
1683
|
${chevron}
|
|
1600
1684
|
<span class="status-dot" style="background:${sc.color}" title="${sc.label}"></span>
|
|
@@ -1606,7 +1690,7 @@ function renderHtml(report, triage, outputDir) {
|
|
|
1606
1690
|
${totalStepCount > 0 ? `<span class="test-step-count" title="${totalStepCount} steps">${totalStepCount} steps</span>` : ''}
|
|
1607
1691
|
<span class="test-duration">${fmtDuration(totalTestDuration)}</span>
|
|
1608
1692
|
</div>
|
|
1609
|
-
${hasDetails ? `<div class="test-detail" id="
|
|
1693
|
+
${hasDetails ? `<div class="test-detail" id="detail-${testId}">${flowIdDetailHtml}${detailsHtml}</div>` : ''}
|
|
1610
1694
|
</div>`;
|
|
1611
1695
|
}
|
|
1612
1696
|
const mergedBanner = isMergedReport
|
|
@@ -1688,6 +1772,32 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
|
|
|
1688
1772
|
.clear-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
|
|
1689
1773
|
.clear-filter.visible{display:flex}
|
|
1690
1774
|
|
|
1775
|
+
/* Tag filter controls: + button opens a menu of available tags; selecting one
|
|
1776
|
+
* adds a removable chip. Multiple chips combine with logical AND. */
|
|
1777
|
+
.tag-filter-controls{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1778
|
+
.tag-filter-trigger-wrap{position:relative;display:inline-flex}
|
|
1779
|
+
.add-tag-filter{background:var(--surface);border:1px solid var(--border);color:var(--text-muted);height:28px;padding:0 12px;border-radius:var(--radius);cursor:pointer;font-size:12px;font-weight:600;font-family:inherit;display:inline-flex;align-items:center;gap:4px;flex-shrink:0;transition:all .2s;line-height:1}
|
|
1780
|
+
.add-tag-filter .add-tag-plus{font-size:15px;line-height:1}
|
|
1781
|
+
.add-tag-filter:hover{background:var(--surface-raised);border-color:var(--text-dim);color:var(--text)}
|
|
1782
|
+
.add-tag-filter.active{background:var(--accent);border-color:var(--accent);color:#fff}
|
|
1783
|
+
.tag-menu{position:absolute;top:calc(100% + 6px);left:0;min-width:200px;max-width:320px;max-height:280px;overflow-y:auto;background:var(--surface-raised);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:20;padding:4px;display:none}
|
|
1784
|
+
.tag-menu:not([hidden]){display:block}
|
|
1785
|
+
.tag-menu-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;font-size:12px;font-family:var(--mono);color:var(--text);background:transparent;border:none;border-radius:4px;cursor:pointer;text-align:left;width:100%;transition:background .15s}
|
|
1786
|
+
.tag-menu-item:hover{background:var(--surface)}
|
|
1787
|
+
.tag-menu-item .tag-menu-count{color:var(--text-muted);font-size:11px;font-family:var(--mono)}
|
|
1788
|
+
.tag-menu-empty{padding:8px 10px;font-size:12px;color:var(--text-muted);font-style:italic}
|
|
1789
|
+
.active-tag-filters{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1790
|
+
.tag-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(255,127,58,.12);border:1px solid rgba(255,127,58,.3);color:var(--accent);font-size:11px;font-family:var(--mono);padding:3px 4px 3px 8px;border-radius:4px}
|
|
1791
|
+
.tag-chip-remove{background:transparent;border:none;color:inherit;cursor:pointer;font-size:14px;line-height:1;padding:0 4px;font-family:inherit;opacity:.7;transition:opacity .15s}
|
|
1792
|
+
.tag-chip-remove:hover{opacity:1}
|
|
1793
|
+
|
|
1794
|
+
/* Total of cards visible under the currently composed filters (status + tags).
|
|
1795
|
+
* The stat-pill counts always reflect totals; this disambiguates when filters
|
|
1796
|
+
* intersect. Hidden until any filter is active. */
|
|
1797
|
+
.match-count{display:none;align-items:center;font-size:12px;color:var(--text-muted);font-family:var(--mono);padding:0 8px;height:28px;flex-shrink:0}
|
|
1798
|
+
.match-count.visible{display:inline-flex}
|
|
1799
|
+
.match-count-value{color:var(--text);font-weight:600;margin-left:6px}
|
|
1800
|
+
|
|
1691
1801
|
/* Test cards */
|
|
1692
1802
|
.test-card{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);margin-bottom:10px;overflow:hidden}
|
|
1693
1803
|
.test-card.failed,.test-card.timedout,.test-card.interrupted{border-color:rgba(239,68,68,.2)}
|
|
@@ -1781,6 +1891,8 @@ body::before{content:'';position:fixed;top:-750px;left:50%;transform:translateX(
|
|
|
1781
1891
|
.filmstrip-summary{font-size:11px;color:var(--text-dim);margin-top:2px;padding-left:44px}
|
|
1782
1892
|
.step-status-ok{color:var(--green);font-size:12px;font-weight:bold}
|
|
1783
1893
|
.step-status-fail{color:var(--red);font-size:12px;font-weight:bold}
|
|
1894
|
+
.step-status-verified{color:#94a3b8;font-size:12px;font-weight:bold}
|
|
1895
|
+
.step-status-diverged{color:#fbbf24;font-size:14px;font-weight:bold;line-height:1}
|
|
1784
1896
|
.filmstrip-detail{display:none;padding:8px 0 4px 44px;flex-direction:row;gap:12px;align-items:flex-start}
|
|
1785
1897
|
.filmstrip-step.open .filmstrip-detail{display:flex}
|
|
1786
1898
|
.filmstrip-detail>a{flex-shrink:0;max-width:50%}
|
|
@@ -1840,6 +1952,8 @@ details.native-step>summary::-webkit-details-marker{display:none}
|
|
|
1840
1952
|
.native-step-badge{font-size:10px;font-weight:600;padding:1px 5px;border-radius:3px;white-space:nowrap;flex-shrink:0}
|
|
1841
1953
|
.native-step-badge--expect{background:rgba(99,102,241,.12);color:#818cf8}
|
|
1842
1954
|
.native-step-badge--test\.step{background:rgba(16,185,129,.10);color:#34d399}
|
|
1955
|
+
.native-step-badge--verify{background:rgba(148,163,184,.12);color:#94a3b8}
|
|
1956
|
+
.native-step-badge--verify-diverged{background:rgba(245,158,11,.12);color:#fbbf24}
|
|
1843
1957
|
.native-step-location{font-size:10px;color:var(--text-dim);font-family:var(--mono);margin-left:auto;flex-shrink:0;white-space:nowrap}
|
|
1844
1958
|
details.native-step[open]>summary .native-step-chevron{transform:rotate(90deg)}
|
|
1845
1959
|
.native-step-error{font-size:11px;font-family:var(--mono);padding:4px 0 2px 44px;margin:0;white-space:pre-wrap;word-break:break-word;color:var(--text-muted)}
|
|
@@ -1860,7 +1974,17 @@ details.ai-invocation>summary::-webkit-details-marker{display:none}
|
|
|
1860
1974
|
.ai-invocation-badge--act{background:rgba(168,85,247,.12);color:#c084fc}
|
|
1861
1975
|
.ai-invocation-badge--assert{background:rgba(236,72,153,.12);color:#f472b6}
|
|
1862
1976
|
.ai-invocation-badge--locate{background:rgba(59,130,246,.12);color:#60a5fa}
|
|
1863
|
-
.ai-
|
|
1977
|
+
.ai-cache-badge{font-size:10px;font-weight:600;padding:1px 5px;border-radius:3px;white-space:nowrap;flex-shrink:0;font-family:var(--mono);cursor:help}
|
|
1978
|
+
.ai-cache-badge--hit{background:rgba(59,130,246,.12);color:#60a5fa}
|
|
1979
|
+
.ai-cache-badge--stored{background:rgba(52,211,153,.12);color:#34d399}
|
|
1980
|
+
.ai-cache-badge--miss{background:rgba(245,158,11,.12);color:#fbbf24}
|
|
1981
|
+
.ai-cache-miss-explainer{font-size:11px;color:var(--text-muted);padding:4px 0 2px 44px;line-height:1.45}
|
|
1982
|
+
.ai-cache-miss-detail{font-size:11px;font-family:var(--mono);padding:4px 0 2px 44px;margin:0;white-space:pre-wrap;word-break:break-word;color:var(--text-dim)}
|
|
1983
|
+
.ai-invocation--cache-miss>summary{box-shadow:inset 3px 0 0 0 rgba(245,158,11,.6)}
|
|
1984
|
+
.native-step--verify .snippet-line--target{background:rgba(148,163,184,.10)}
|
|
1985
|
+
.native-step--verify .snippet-line--target .snippet-linenum{color:#94a3b8}
|
|
1986
|
+
.native-step--verify-diverged .snippet-line--target{background:rgba(245,158,11,.10)}
|
|
1987
|
+
.native-step--verify-diverged .snippet-line--target .snippet-linenum{color:#fbbf24}
|
|
1864
1988
|
details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)}
|
|
1865
1989
|
.ai-assert-steps{font-size:11px;font-family:var(--mono);background:var(--bg);border:1px solid var(--border-subtle);border-radius:var(--radius);padding:8px 12px;margin:6px 0 2px 44px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word;overflow-x:auto;max-height:240px;overflow-y:auto}
|
|
1866
1990
|
.snippet-line{display:flex;padding:1px 8px;white-space:pre}
|
|
@@ -2010,7 +2134,15 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2010
2134
|
<div class="test-bar">${testBarHtml}</div>
|
|
2011
2135
|
<div class="summary-stats">
|
|
2012
2136
|
<div class="stat-pills">${statPillsHtml}</div>
|
|
2013
|
-
<
|
|
2137
|
+
<div class="tag-filter-controls" data-tag-filter-controls hidden>
|
|
2138
|
+
<div class="tag-filter-trigger-wrap">
|
|
2139
|
+
<button class="add-tag-filter" data-add-tag-filter title="Filter by tag"><span class="add-tag-plus">+</span> Tag</button>
|
|
2140
|
+
<div class="tag-menu" data-tag-menu hidden></div>
|
|
2141
|
+
</div>
|
|
2142
|
+
<div class="active-tag-filters" data-active-tag-filters></div>
|
|
2143
|
+
</div>
|
|
2144
|
+
<span class="match-count" data-match-count>Matches:<span class="match-count-value" data-match-count-value>0</span></span>
|
|
2145
|
+
<button class="clear-filter" data-clear-filter>✖ Clear Filters</button>
|
|
2014
2146
|
</div>
|
|
2015
2147
|
</div>
|
|
2016
2148
|
|
|
@@ -2028,20 +2160,113 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2028
2160
|
|
|
2029
2161
|
<script>
|
|
2030
2162
|
(function(){
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2163
|
+
// Filters compose: a card is visible iff its status matches the active
|
|
2164
|
+
// status filter (if any) AND it carries every tag in activeTags. The two
|
|
2165
|
+
// dimensions are independent; "Clear Filter" wipes both.
|
|
2166
|
+
var activeStatus=null;
|
|
2167
|
+
var activeTags=new Set();
|
|
2168
|
+
var allTags=[];
|
|
2169
|
+
|
|
2170
|
+
function cardTags(card){var raw=card.getAttribute('data-tags');if(!raw)return [];try{var v=JSON.parse(raw);return Array.isArray(v)?v:[]}catch(_){return []}}
|
|
2171
|
+
function tagCount(t){var n=0;document.querySelectorAll('.test-card').forEach(function(c){if(cardTags(c).indexOf(t)!==-1)n++});return n}
|
|
2172
|
+
|
|
2173
|
+
function applyFilters(){
|
|
2174
|
+
var anyActive=activeStatus!==null||activeTags.size>0;
|
|
2175
|
+
document.querySelector('.clear-filter').classList.toggle('visible',anyActive);
|
|
2176
|
+
var visible=0;
|
|
2177
|
+
document.querySelectorAll('.test-card').forEach(function(card){
|
|
2178
|
+
var statusOk=activeStatus===null||card.getAttribute('data-status')===activeStatus;
|
|
2179
|
+
var tagsOk=true;
|
|
2180
|
+
if(activeTags.size>0){
|
|
2181
|
+
var t=cardTags(card);
|
|
2182
|
+
activeTags.forEach(function(want){if(t.indexOf(want)===-1)tagsOk=false});
|
|
2183
|
+
}
|
|
2184
|
+
var hide=!(statusOk&&tagsOk);
|
|
2185
|
+
card.classList.toggle('hidden-by-filter',hide);
|
|
2186
|
+
if(!hide)visible++;
|
|
2187
|
+
});
|
|
2188
|
+
var mc=document.querySelector('[data-match-count]');
|
|
2189
|
+
if(mc){
|
|
2190
|
+
mc.classList.toggle('visible',anyActive);
|
|
2191
|
+
var mv=mc.querySelector('[data-match-count-value]');
|
|
2192
|
+
if(mv)mv.textContent=visible;
|
|
2193
|
+
}
|
|
2194
|
+
syncFiltersToUrl();
|
|
2038
2195
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2196
|
+
|
|
2197
|
+
// Reflect the active filter state in the URL query string so the current
|
|
2198
|
+
// view is shareable. replaceState (not pushState) keeps the back button
|
|
2199
|
+
// useful; the existing #?testId=<id> hash is preserved.
|
|
2200
|
+
function syncFiltersToUrl(){
|
|
2201
|
+
var p=new URLSearchParams();
|
|
2202
|
+
if(activeStatus)p.set('status',activeStatus);
|
|
2203
|
+
activeTags.forEach(function(t){p.append('tag',t)});
|
|
2204
|
+
var qs=p.toString();
|
|
2205
|
+
var next=location.pathname+(qs?'?'+qs:'')+(location.hash||'');
|
|
2206
|
+
if(next!==location.pathname+location.search+location.hash){
|
|
2207
|
+
history.replaceState(null,'',next);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function toggleStatus(s){
|
|
2212
|
+
activeStatus=(activeStatus===s)?null:s;
|
|
2213
|
+
document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.toggle('active',p.getAttribute('data-filter')===activeStatus)});
|
|
2214
|
+
applyFilters();
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
function renderActiveChips(){
|
|
2218
|
+
var c=document.querySelector('[data-active-tag-filters]');
|
|
2219
|
+
if(!c)return;
|
|
2220
|
+
c.innerHTML='';
|
|
2221
|
+
activeTags.forEach(function(t){
|
|
2222
|
+
var chip=document.createElement('span');chip.className='tag-chip';
|
|
2223
|
+
var label=document.createElement('span');label.textContent=t;
|
|
2224
|
+
var btn=document.createElement('button');btn.className='tag-chip-remove';btn.setAttribute('data-remove-tag',t);btn.setAttribute('title','Remove filter');btn.textContent='×';
|
|
2225
|
+
chip.appendChild(label);chip.appendChild(btn);
|
|
2226
|
+
c.appendChild(chip);
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
function addTag(t){if(!t||activeTags.has(t))return;activeTags.add(t);renderActiveChips();applyFilters()}
|
|
2230
|
+
function removeTag(t){if(!activeTags.delete(t))return;renderActiveChips();applyFilters()}
|
|
2231
|
+
|
|
2232
|
+
function openTagMenu(){
|
|
2233
|
+
var menu=document.querySelector('[data-tag-menu]');
|
|
2234
|
+
if(!menu)return;
|
|
2235
|
+
var trigger=document.querySelector('[data-add-tag-filter]');
|
|
2236
|
+
menu.innerHTML='';
|
|
2237
|
+
var available=allTags.filter(function(t){return !activeTags.has(t)});
|
|
2238
|
+
if(available.length===0){
|
|
2239
|
+
var empty=document.createElement('div');empty.className='tag-menu-empty';
|
|
2240
|
+
empty.textContent=allTags.length?'All tags selected':'No tags';
|
|
2241
|
+
menu.appendChild(empty);
|
|
2242
|
+
}else{
|
|
2243
|
+
available.forEach(function(t){
|
|
2244
|
+
var item=document.createElement('button');item.className='tag-menu-item';item.setAttribute('data-tag-menu-item',t);
|
|
2245
|
+
var label=document.createElement('span');label.textContent=t;
|
|
2246
|
+
var count=document.createElement('span');count.className='tag-menu-count';count.textContent=tagCount(t);
|
|
2247
|
+
item.appendChild(label);item.appendChild(count);
|
|
2248
|
+
menu.appendChild(item);
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
menu.hidden=false;
|
|
2252
|
+
if(trigger)trigger.classList.add('active');
|
|
2253
|
+
}
|
|
2254
|
+
function closeTagMenu(){
|
|
2255
|
+
var menu=document.querySelector('[data-tag-menu]');var trigger=document.querySelector('[data-add-tag-filter]');
|
|
2256
|
+
if(menu)menu.hidden=true;
|
|
2257
|
+
if(trigger)trigger.classList.remove('active');
|
|
2258
|
+
}
|
|
2259
|
+
function tagMenuOpen(){var m=document.querySelector('[data-tag-menu]');return !!(m&&!m.hidden)}
|
|
2260
|
+
|
|
2261
|
+
function clearAllFilters(){
|
|
2262
|
+
activeStatus=null;
|
|
2263
|
+
activeTags.clear();
|
|
2041
2264
|
document.querySelectorAll('.stat-pill').forEach(function(p){p.classList.remove('active')});
|
|
2042
|
-
|
|
2043
|
-
|
|
2265
|
+
renderActiveChips();
|
|
2266
|
+
closeTagMenu();
|
|
2267
|
+
applyFilters();
|
|
2044
2268
|
}
|
|
2269
|
+
|
|
2045
2270
|
function closeLightbox(){document.getElementById('lightbox').classList.remove('open')}
|
|
2046
2271
|
|
|
2047
2272
|
document.addEventListener('click',function(e){
|
|
@@ -2071,14 +2296,28 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2071
2296
|
if(flowBtn){e.stopPropagation();navigator.clipboard.writeText(flowBtn.getAttribute('data-flow-id'));var copySvg=flowBtn.innerHTML;flowBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){flowBtn.innerHTML=copySvg},2000);return}
|
|
2072
2297
|
var jsonBtn=e.target.closest('.copy-json');
|
|
2073
2298
|
if(jsonBtn){e.stopPropagation();var pre=jsonBtn.closest('.step-json-wrap').querySelector('.step-json');navigator.clipboard.writeText(pre.textContent);var s=jsonBtn.innerHTML;jsonBtn.innerHTML='<svg class="check-icon" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>';setTimeout(function(){jsonBtn.innerHTML=s},2000);return}
|
|
2299
|
+
// Tag filter: add (+ button toggles the menu) / select (menu item) /
|
|
2300
|
+
// remove (chip × button). Outside-clicks close the menu via the catch-all
|
|
2301
|
+
// at the end of this handler.
|
|
2302
|
+
var addTagBtn=e.target.closest('[data-add-tag-filter]');
|
|
2303
|
+
if(addTagBtn){if(tagMenuOpen()){closeTagMenu()}else{openTagMenu()}return}
|
|
2304
|
+
var tagItem=e.target.closest('[data-tag-menu-item]');
|
|
2305
|
+
if(tagItem){addTag(tagItem.getAttribute('data-tag-menu-item'));closeTagMenu();return}
|
|
2306
|
+
var tagRemove=e.target.closest('[data-remove-tag]');
|
|
2307
|
+
if(tagRemove){removeTag(tagRemove.getAttribute('data-remove-tag'));return}
|
|
2074
2308
|
// Stat pill filter
|
|
2075
2309
|
var pill=e.target.closest('.stat-pill[data-filter]');
|
|
2076
|
-
if(pill){
|
|
2310
|
+
if(pill){toggleStatus(pill.getAttribute('data-filter'));return}
|
|
2077
2311
|
// Clear filter
|
|
2078
|
-
if(e.target.closest('[data-clear-filter]')){
|
|
2079
|
-
//
|
|
2312
|
+
if(e.target.closest('[data-clear-filter]')){clearAllFilters();return}
|
|
2313
|
+
// Any other click closes the tag menu (outside-click dismiss).
|
|
2314
|
+
if(tagMenuOpen()&&!e.target.closest('[data-tag-menu]')){closeTagMenu()}
|
|
2315
|
+
// Test bar block click — scroll to and highlight the target test card,
|
|
2316
|
+
// and reflect the selection in location.hash so the URL is shareable.
|
|
2317
|
+
// replaceState (not pushState) keeps the back button useful after the
|
|
2318
|
+
// user has clicked through several bar blocks.
|
|
2080
2319
|
var barBlock=e.target.closest('.test-bar-block[data-target]');
|
|
2081
|
-
if(barBlock){var tid=barBlock.getAttribute('data-target');var card=document.getElementById(tid);if(card){var tc=card.closest('.test-card');if(tc){tc.classList.add('expanded');tc.scrollIntoView({behavior:'smooth',block:'center'});tc.style.outline='2px solid var(--accent)';setTimeout(function(){tc.style.outline=''},1500)}}return}
|
|
2320
|
+
if(barBlock){var tid=barBlock.getAttribute('data-target');var card=document.getElementById(tid);if(card){var tc=card.closest('.test-card');if(tc){tc.classList.add('expanded');tc.scrollIntoView({behavior:'smooth',block:'center'});tc.style.outline='2px solid var(--accent)';setTimeout(function(){tc.style.outline=''},1500)}}if(tid&&tid.indexOf('test-')===0){history.replaceState(null,'','#?testId='+encodeURIComponent(tid.slice(5)))}return}
|
|
2082
2321
|
// Filmstrip step expand (skip if clicking a link inside).
|
|
2083
2322
|
// <div>-based steps: toggle the open class on any click in the wrapper.
|
|
2084
2323
|
// <details>-based steps: <summary> clicks are handled natively; padding
|
|
@@ -2098,18 +2337,90 @@ details.ai-invocation[open]>summary .native-step-chevron{transform:rotate(90deg)
|
|
|
2098
2337
|
// Audit check row expand
|
|
2099
2338
|
var auditCheck=e.target.closest('.audit-check.expandable');
|
|
2100
2339
|
if(auditCheck){auditCheck.classList.toggle('open');return}
|
|
2101
|
-
// Test card expand
|
|
2340
|
+
// Test card expand — also reflect the selection in location.hash on
|
|
2341
|
+
// open so the URL is shareable. Same replaceState rationale as the bar
|
|
2342
|
+
// block handler above. Collapses leave the URL alone.
|
|
2102
2343
|
var row=e.target.closest('.test-card[data-detail]');
|
|
2103
|
-
if(row&&!e.target.closest('.test-detail')){row.classList.toggle('expanded');return}
|
|
2344
|
+
if(row&&!e.target.closest('.test-detail')){var nowOpen=row.classList.toggle('expanded');if(nowOpen&&row.id&&row.id.indexOf('test-')===0){history.replaceState(null,'','#?testId='+encodeURIComponent(row.id.slice(5)))}return}
|
|
2104
2345
|
});
|
|
2105
2346
|
|
|
2106
|
-
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();
|
|
2347
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeLightbox();closeTagMenu();clearAllFilters()}});
|
|
2107
2348
|
|
|
2108
2349
|
// Auto-expand failed/timedout/interrupted/healed tests
|
|
2109
2350
|
document.querySelectorAll('.test-card.failed,.test-card.timedout,.test-card.interrupted,.test-card.healed').forEach(function(r){r.classList.add('expanded')});
|
|
2351
|
+
|
|
2352
|
+
// Collect the cumulative set of tags present across all test cards and
|
|
2353
|
+
// reveal the +tag-filter controls only when at least one tag exists.
|
|
2354
|
+
(function(){
|
|
2355
|
+
var seen=Object.create(null);
|
|
2356
|
+
document.querySelectorAll('.test-card').forEach(function(card){
|
|
2357
|
+
var raw=card.getAttribute('data-tags');if(!raw)return;
|
|
2358
|
+
try{var tags=JSON.parse(raw);if(Array.isArray(tags)){tags.forEach(function(t){if(typeof t==='string'&&t)seen[t]=true})}}catch(_){}
|
|
2359
|
+
});
|
|
2360
|
+
allTags=Object.keys(seen).sort();
|
|
2361
|
+
var controls=document.querySelector('[data-tag-filter-controls]');
|
|
2362
|
+
if(controls&&allTags.length>0)controls.hidden=false;
|
|
2363
|
+
})();
|
|
2364
|
+
|
|
2365
|
+
// Seed filter state from ?status=...&tag=... so shared URLs restore the
|
|
2366
|
+
// view. Status values not in the known stat-pill set are ignored; tag
|
|
2367
|
+
// values not present in this report are dropped so a stale URL can't
|
|
2368
|
+
// poison the state.
|
|
2369
|
+
(function(){
|
|
2370
|
+
var p=new URLSearchParams(location.search);
|
|
2371
|
+
var s=p.get('status');
|
|
2372
|
+
var validStatuses={};
|
|
2373
|
+
document.querySelectorAll('.stat-pill[data-filter]').forEach(function(el){validStatuses[el.getAttribute('data-filter')]=true});
|
|
2374
|
+
if(s&&validStatuses[s]){
|
|
2375
|
+
activeStatus=s;
|
|
2376
|
+
document.querySelectorAll('.stat-pill').forEach(function(el){el.classList.toggle('active',el.getAttribute('data-filter')===s)});
|
|
2377
|
+
}
|
|
2378
|
+
var tagSet={};allTags.forEach(function(t){tagSet[t]=true});
|
|
2379
|
+
p.getAll('tag').forEach(function(t){if(tagSet[t])activeTags.add(t)});
|
|
2380
|
+
if(activeTags.size>0)renderActiveChips();
|
|
2381
|
+
if(activeStatus!==null||activeTags.size>0)applyFilters();
|
|
2382
|
+
})();
|
|
2383
|
+
|
|
2384
|
+
// Open #?testId=<id> deep links to the matching test card. Used by the
|
|
2385
|
+
// per-test redirect stubs (one per Playwright test-results directory) and
|
|
2386
|
+
// by any external link that wants to permalink a specific test.
|
|
2387
|
+
function focusTestFromHash(){
|
|
2388
|
+
var h=location.hash||'';
|
|
2389
|
+
if(h.indexOf('#?')!==0)return;
|
|
2390
|
+
var params=new URLSearchParams(h.slice(2));
|
|
2391
|
+
var id=params.get('testId');
|
|
2392
|
+
if(!id)return;
|
|
2393
|
+
var card=document.getElementById('test-'+id);
|
|
2394
|
+
if(!card||!card.classList.contains('test-card'))return;
|
|
2395
|
+
card.classList.add('expanded');
|
|
2396
|
+
card.scrollIntoView({behavior:'smooth',block:'center'});
|
|
2397
|
+
}
|
|
2398
|
+
focusTestFromHash();
|
|
2399
|
+
window.addEventListener('hashchange',focusTestFromHash);
|
|
2110
2400
|
})();
|
|
2111
2401
|
</script>
|
|
2112
2402
|
</body>
|
|
2113
2403
|
</html>`;
|
|
2114
2404
|
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Render the tiny redirect HTML that Donobu drops into each Playwright-managed
|
|
2407
|
+
* per-test directory under `test-results/`. The stub bounces straight to the
|
|
2408
|
+
* combined report's `#?testId=<id>` deep link — meta-refresh + JS replace +
|
|
2409
|
+
* visible fallback link, so it works with or without JS, online or `file://`.
|
|
2410
|
+
*
|
|
2411
|
+
* Strictly additive: Donobu does not create or rename Playwright's per-test
|
|
2412
|
+
* directories — the caller in `html.ts` only writes this file into directories
|
|
2413
|
+
* Playwright already created for the test's attachments.
|
|
2414
|
+
*/
|
|
2415
|
+
function renderPerTestStub(params) {
|
|
2416
|
+
const { testId, title, relPathToReport } = params;
|
|
2417
|
+
const href = `${relPathToReport}#?testId=${encodeURIComponent(testId)}`;
|
|
2418
|
+
return `<!DOCTYPE html>
|
|
2419
|
+
<meta charset="UTF-8">
|
|
2420
|
+
<title>Donobu — ${esc(title)}</title>
|
|
2421
|
+
<meta http-equiv="refresh" content="0; url=${esc(href)}">
|
|
2422
|
+
<script>location.replace(${JSON.stringify(href)});</script>
|
|
2423
|
+
<p>Redirecting to <a href="${esc(href)}">test report</a>…</p>
|
|
2424
|
+
`;
|
|
2425
|
+
}
|
|
2115
2426
|
//# sourceMappingURL=render.js.map
|
|
@@ -207,18 +207,34 @@ careful positioning lost, etc. A screenshot of the webpage has also been provide
|
|
|
207
207
|
// When the AI assertion passes and structured steps were returned,
|
|
208
208
|
// verify the steps against the live page before considering them
|
|
209
209
|
// cacheable. If the steps fail, discard them but still return the
|
|
210
|
-
// passing AI result.
|
|
210
|
+
// passing AI result. The verification window is recorded so the HTML
|
|
211
|
+
// reporter can label its `expect()` calls as cache-worthiness checks
|
|
212
|
+
// rather than treating an internal locator mismatch as an assertion
|
|
213
|
+
// failure.
|
|
211
214
|
let verifiedSteps = assertionOutcome.output.playwrightAssertionSteps;
|
|
215
|
+
let verification;
|
|
212
216
|
if (assertPassed &&
|
|
213
217
|
Array.isArray(verifiedSteps) &&
|
|
214
218
|
verifiedSteps.length > 0) {
|
|
219
|
+
const verifyStartedAt = Date.now();
|
|
215
220
|
try {
|
|
216
221
|
const executor = (0, assertCache_1.buildAssertExecutor)(verifiedSteps);
|
|
217
222
|
await executor({ page: page, envData: context.envData });
|
|
223
|
+
verification = {
|
|
224
|
+
startedAt: verifyStartedAt,
|
|
225
|
+
endedAt: Date.now(),
|
|
226
|
+
failed: false,
|
|
227
|
+
};
|
|
218
228
|
}
|
|
219
229
|
catch (error) {
|
|
220
230
|
Logger_1.appLogger.debug(`Structured assertion steps failed verification for: "${parameters.assertionToTestFor}" — discarding steps. Error: ${error.message}`);
|
|
221
231
|
verifiedSteps = null;
|
|
232
|
+
verification = {
|
|
233
|
+
startedAt: verifyStartedAt,
|
|
234
|
+
endedAt: Date.now(),
|
|
235
|
+
failed: true,
|
|
236
|
+
errorMessage: error.message,
|
|
237
|
+
};
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
240
|
const result = {
|
|
@@ -227,6 +243,7 @@ careful positioning lost, etc. A screenshot of the webpage has also been provide
|
|
|
227
243
|
metadata: {
|
|
228
244
|
...assertionOutcome.output,
|
|
229
245
|
playwrightAssertionSteps: verifiedSteps,
|
|
246
|
+
verification,
|
|
230
247
|
attempt: attempt + 1,
|
|
231
248
|
},
|
|
232
249
|
};
|
package/dist/lib/ai/PageAi.js
CHANGED
|
@@ -150,6 +150,7 @@ class PageAi {
|
|
|
150
150
|
async ai(page, instruction, options) {
|
|
151
151
|
const startedAt = Date.now();
|
|
152
152
|
let cacheHit = false;
|
|
153
|
+
let cacheStored = false;
|
|
153
154
|
let thrownError = undefined;
|
|
154
155
|
try {
|
|
155
156
|
const descriptor = this.buildDescriptor(page, instruction, options);
|
|
@@ -197,6 +198,7 @@ class PageAi {
|
|
|
197
198
|
}, this.donobu.toolRegistry);
|
|
198
199
|
const cacheEntry = cacheEntryBuilder_1.PageAiCacheEntryBuilder.fromMetadata(descriptor.key.pageUrl, runResult.donobuFlow.metadata, preparedToolCalls);
|
|
199
200
|
await this.cache.put(cacheEntry);
|
|
201
|
+
cacheStored = true;
|
|
200
202
|
}
|
|
201
203
|
return runResult.parsedResult;
|
|
202
204
|
}
|
|
@@ -212,6 +214,7 @@ class PageAi {
|
|
|
212
214
|
startedAt,
|
|
213
215
|
endedAt: Date.now(),
|
|
214
216
|
cacheHit,
|
|
217
|
+
cacheStored,
|
|
215
218
|
passed: thrownError === undefined,
|
|
216
219
|
error: thrownError !== undefined
|
|
217
220
|
? { message: thrownError?.message }
|