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
|
@@ -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 }
|
|
@@ -488,10 +488,40 @@ export interface AiInvocationRecord {
|
|
|
488
488
|
startedAt: number;
|
|
489
489
|
endedAt: number;
|
|
490
490
|
cacheHit: boolean;
|
|
491
|
+
/**
|
|
492
|
+
* For live (non-replay) invocations: `true` once this run successfully
|
|
493
|
+
* wrote an entry into the relevant page-AI cache, `false` if a write was
|
|
494
|
+
* attempted (or would have been) but didn't land. Combined with
|
|
495
|
+
* `cacheHit`, this gives the reporter a tri-state cache outcome — hit
|
|
496
|
+
* (replayed), stored (live + recorded), or miss (live + nothing cached).
|
|
497
|
+
* Always `false` when `cacheHit` is `true`.
|
|
498
|
+
*/
|
|
499
|
+
cacheStored: boolean;
|
|
491
500
|
passed: boolean;
|
|
492
501
|
error?: {
|
|
493
502
|
message?: string;
|
|
494
503
|
};
|
|
504
|
+
/**
|
|
505
|
+
* For live `page.ai.assert` runs: metadata about the post-pass structured
|
|
506
|
+
* step verification. After the AI judges the assertion passed against a
|
|
507
|
+
* screenshot, AssertTool re-executes the AI-emitted Playwright `expect()`
|
|
508
|
+
* calls against the page to decide whether those structured steps are
|
|
509
|
+
* cache-worthy. When `failed: true`, the AI's visual verdict still stands
|
|
510
|
+
* — the tool returns success — but one of the structured `expect()` calls
|
|
511
|
+
* underneath threw. The reporter uses this to surface the divergence as a
|
|
512
|
+
* labelled signal rather than render the inner expect failure as a regular
|
|
513
|
+
* assertion failure.
|
|
514
|
+
*
|
|
515
|
+
* Undefined when verification didn't run (no structured steps emitted, AI
|
|
516
|
+
* verdict was failed, cached replay path, or AssertTool invoked outside
|
|
517
|
+
* the page.ai.assert wrapper).
|
|
518
|
+
*/
|
|
519
|
+
verification?: {
|
|
520
|
+
startedAt: number;
|
|
521
|
+
endedAt: number;
|
|
522
|
+
failed: boolean;
|
|
523
|
+
errorMessage?: string;
|
|
524
|
+
};
|
|
495
525
|
/**
|
|
496
526
|
* For cached `page.ai.assert` invocations: the structured Playwright
|
|
497
527
|
* assertion steps that were replayed. The reporter formats these back
|
|
@@ -220,8 +220,10 @@ Valid options:
|
|
|
220
220
|
assert: async (assertion, options) => {
|
|
221
221
|
const aiInvocationStartedAt = Date.now();
|
|
222
222
|
let aiInvocationCacheHit = false;
|
|
223
|
+
let aiInvocationCacheStored = false;
|
|
223
224
|
let aiInvocationError = undefined;
|
|
224
225
|
let aiInvocationAssertSteps;
|
|
226
|
+
let aiInvocationVerification;
|
|
225
227
|
try {
|
|
226
228
|
const useCache = options?.cache !== false;
|
|
227
229
|
const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
|
|
@@ -322,6 +324,7 @@ Valid options:
|
|
|
322
324
|
finally {
|
|
323
325
|
sharedState.envVals = previousEnvVals;
|
|
324
326
|
}
|
|
327
|
+
aiInvocationVerification = result.outcome.metadata?.verification;
|
|
325
328
|
if (!result.outcome.isSuccessful) {
|
|
326
329
|
throw new ToolCallFailedException_1.ToolCallFailedException(AssertTool_1.AssertTool.NAME, result.outcome);
|
|
327
330
|
}
|
|
@@ -333,6 +336,7 @@ Valid options:
|
|
|
333
336
|
const cache = getOrInitPageAiCache();
|
|
334
337
|
const pageUrl = (0, cacheLocator_1.extractCacheKeyHostname)(page.url());
|
|
335
338
|
await cache.putAssert({ pageUrl, assertion, steps });
|
|
339
|
+
aiInvocationCacheStored = true;
|
|
336
340
|
Logger_1.appLogger.debug(`Assert cache STORED for: "${assertion}"`);
|
|
337
341
|
}
|
|
338
342
|
catch (error) {
|
|
@@ -352,11 +356,13 @@ Valid options:
|
|
|
352
356
|
startedAt: aiInvocationStartedAt,
|
|
353
357
|
endedAt: Date.now(),
|
|
354
358
|
cacheHit: aiInvocationCacheHit,
|
|
359
|
+
cacheStored: aiInvocationCacheStored,
|
|
355
360
|
passed: aiInvocationError === undefined,
|
|
356
361
|
error: aiInvocationError !== undefined
|
|
357
362
|
? { message: aiInvocationError?.message }
|
|
358
363
|
: undefined,
|
|
359
364
|
assertSteps: aiInvocationAssertSteps,
|
|
365
|
+
verification: aiInvocationVerification,
|
|
360
366
|
});
|
|
361
367
|
}
|
|
362
368
|
},
|
|
@@ -434,6 +440,7 @@ Use this information to return an appropriate JSON object.`,
|
|
|
434
440
|
locate: async (description, options) => {
|
|
435
441
|
const aiInvocationStartedAt = Date.now();
|
|
436
442
|
let aiInvocationCacheHit = false;
|
|
443
|
+
let aiInvocationCacheStored = false;
|
|
437
444
|
let aiInvocationError = undefined;
|
|
438
445
|
const useCache = options?.cache !== false;
|
|
439
446
|
const clearCache = sharedState.runtimeDirectives?.clearPageAiCache ?? false;
|
|
@@ -525,6 +532,7 @@ Use this information to return an appropriate JSON object.`,
|
|
|
525
532
|
try {
|
|
526
533
|
const cache = getOrInitPageAiCache();
|
|
527
534
|
await cache.putLocate({ pageUrl, description, result });
|
|
535
|
+
aiInvocationCacheStored = true;
|
|
528
536
|
Logger_1.appLogger.debug(`Locate cache STORED for: "${description}"`);
|
|
529
537
|
}
|
|
530
538
|
catch (error) {
|
|
@@ -545,6 +553,7 @@ Use this information to return an appropriate JSON object.`,
|
|
|
545
553
|
startedAt: aiInvocationStartedAt,
|
|
546
554
|
endedAt: Date.now(),
|
|
547
555
|
cacheHit: aiInvocationCacheHit,
|
|
556
|
+
cacheStored: aiInvocationCacheStored,
|
|
548
557
|
passed: aiInvocationError === undefined,
|
|
549
558
|
error: aiInvocationError !== undefined
|
|
550
559
|
? { message: aiInvocationError?.message }
|
|
@@ -48,6 +48,10 @@ function buildDonobuReport(resultsByTest, rootDir) {
|
|
|
48
48
|
const testEntries = tests.map((test) => {
|
|
49
49
|
const results = resultsByTest.get(test) ?? [];
|
|
50
50
|
return {
|
|
51
|
+
// Playwright's stable per-(file, project, title) ID — used by the
|
|
52
|
+
// merge step's `byId` index and by the HTML renderer for permalink
|
|
53
|
+
// anchors (`index.html#?testId=<id>`) and the per-test redirect stubs.
|
|
54
|
+
testId: test.id,
|
|
51
55
|
annotations: test.annotations,
|
|
52
56
|
tags: test.tags,
|
|
53
57
|
projectName: getProjectName(test),
|
|
@@ -51,5 +51,16 @@ export default class DonobuHtmlReporter implements Reporter {
|
|
|
51
51
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
52
52
|
onEnd(_result: FullResult): Promise<void>;
|
|
53
53
|
printsToStdio(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Drop a tiny redirect `index.html` into each Playwright-managed per-test
|
|
56
|
+
* directory under `outputDir`. The stub points back at the combined report's
|
|
57
|
+
* `#?testId=<id>` deep link, giving every test a stable URL inside its own
|
|
58
|
+
* directory without duplicating any of the rendered HTML.
|
|
59
|
+
*
|
|
60
|
+
* Directories are discovered from `dirname(attachment.path)` — Donobu does
|
|
61
|
+
* not invent any directory naming or layout. Tests with no attachments (and
|
|
62
|
+
* therefore no Playwright-created directory) get no stub.
|
|
63
|
+
*/
|
|
64
|
+
private writePerTestStubs;
|
|
54
65
|
}
|
|
55
66
|
//# sourceMappingURL=html.d.ts.map
|
|
@@ -82,10 +82,65 @@ class DonobuHtmlReporter {
|
|
|
82
82
|
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
83
83
|
(0, fs_1.writeFileSync)(outputFile, html, 'utf8');
|
|
84
84
|
Logger_1.appLogger.info(`Donobu report written to ${outputFile}`);
|
|
85
|
+
this.writePerTestStubs(outputFile, outputDir);
|
|
85
86
|
}
|
|
86
87
|
printsToStdio() {
|
|
87
88
|
return false;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Drop a tiny redirect `index.html` into each Playwright-managed per-test
|
|
92
|
+
* directory under `outputDir`. The stub points back at the combined report's
|
|
93
|
+
* `#?testId=<id>` deep link, giving every test a stable URL inside its own
|
|
94
|
+
* directory without duplicating any of the rendered HTML.
|
|
95
|
+
*
|
|
96
|
+
* Directories are discovered from `dirname(attachment.path)` — Donobu does
|
|
97
|
+
* not invent any directory naming or layout. Tests with no attachments (and
|
|
98
|
+
* therefore no Playwright-created directory) get no stub.
|
|
99
|
+
*/
|
|
100
|
+
writePerTestStubs(outputFile, outputDir) {
|
|
101
|
+
for (const [test, results] of this.resultsByTest) {
|
|
102
|
+
if (!test.id) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const testDirs = new Set();
|
|
106
|
+
for (const result of results) {
|
|
107
|
+
for (const att of result.attachments) {
|
|
108
|
+
if (!att.path) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const attDir = (0, path_1.dirname)((0, path_1.resolve)(att.path));
|
|
112
|
+
const rel = (0, path_1.relative)(outputDir, attDir);
|
|
113
|
+
if (!rel || rel.startsWith('..')) {
|
|
114
|
+
// Attachment lives outside the report's output directory — skip;
|
|
115
|
+
// we shouldn't be writing into arbitrary paths.
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
testDirs.add(attDir);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (testDirs.size === 0) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const fileBase = test.location.file.split('/').pop() ?? test.location.file;
|
|
125
|
+
const title = `${fileBase} › ${test.title}`;
|
|
126
|
+
for (const testDir of testDirs) {
|
|
127
|
+
const stubPath = (0, path_1.resolve)(testDir, 'index.html');
|
|
128
|
+
const relPathToReport = (0, path_1.relative)(testDir, outputFile);
|
|
129
|
+
const stub = (0, render_1.renderPerTestStub)({
|
|
130
|
+
testId: test.id,
|
|
131
|
+
title,
|
|
132
|
+
relPathToReport,
|
|
133
|
+
});
|
|
134
|
+
try {
|
|
135
|
+
(0, fs_1.mkdirSync)(testDir, { recursive: true });
|
|
136
|
+
(0, fs_1.writeFileSync)(stubPath, stub, 'utf8');
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
Logger_1.appLogger.warn(`Failed to write per-test redirect stub at ${stubPath}: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
89
144
|
}
|
|
90
145
|
exports.default = DonobuHtmlReporter;
|
|
91
146
|
//# sourceMappingURL=html.js.map
|
|
@@ -140,5 +140,20 @@ export interface TriageData {
|
|
|
140
140
|
}
|
|
141
141
|
export declare function loadTriageData(triageDir: string): TriageData;
|
|
142
142
|
export declare function renderHtml(report: DonobuReport, triage: TriageData, outputDir: string | null): string;
|
|
143
|
+
/**
|
|
144
|
+
* Render the tiny redirect HTML that Donobu drops into each Playwright-managed
|
|
145
|
+
* per-test directory under `test-results/`. The stub bounces straight to the
|
|
146
|
+
* combined report's `#?testId=<id>` deep link — meta-refresh + JS replace +
|
|
147
|
+
* visible fallback link, so it works with or without JS, online or `file://`.
|
|
148
|
+
*
|
|
149
|
+
* Strictly additive: Donobu does not create or rename Playwright's per-test
|
|
150
|
+
* directories — the caller in `html.ts` only writes this file into directories
|
|
151
|
+
* Playwright already created for the test's attachments.
|
|
152
|
+
*/
|
|
153
|
+
export declare function renderPerTestStub(params: {
|
|
154
|
+
testId: string;
|
|
155
|
+
title: string;
|
|
156
|
+
relPathToReport: string;
|
|
157
|
+
}): string;
|
|
143
158
|
export {};
|
|
144
159
|
//# sourceMappingURL=render.d.ts.map
|