donobu 5.60.0 → 5.60.2
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/cli/donobu-cli.js +151 -52
- package/dist/envVars.d.ts +4 -0
- package/dist/envVars.js +12 -0
- package/dist/esm/cli/donobu-cli.js +151 -52
- package/dist/esm/envVars.d.ts +4 -0
- package/dist/esm/envVars.js +12 -0
- package/dist/esm/lib/page/extendPage.d.ts +6 -0
- package/dist/esm/lib/page/extendPage.js +24 -1
- package/dist/esm/lib/test/healRerunGate.d.ts +85 -0
- package/dist/esm/lib/test/healRerunGate.js +186 -0
- package/dist/esm/lib/test/testExtension.d.ts +1 -0
- package/dist/esm/lib/test/testExtension.js +20 -10
- package/dist/esm/reporter/buildReport.js +32 -0
- package/dist/esm/reporter/merge.d.ts +1 -6
- package/dist/esm/reporter/merge.js +57 -35
- package/dist/esm/reporter/model.d.ts +9 -0
- package/dist/esm/reporter/model.js +10 -1
- package/dist/esm/reporter/render.js +34 -12
- package/dist/esm/reporter/renderMarkdown.js +148 -93
- package/dist/esm/reporter/renderSlack.js +39 -28
- package/dist/esm/reporter/reportWalk.d.ts +16 -6
- package/dist/esm/reporter/reportWalk.js +63 -13
- package/dist/esm/tools/RunInlineJavaScriptCodeTool.js +43 -2
- package/dist/esm/tools/RunSandboxedJavaScriptCodeTool.js +24 -2
- package/dist/esm/utils/PlaywrightUtils.d.ts +75 -0
- package/dist/esm/utils/PlaywrightUtils.js +122 -0
- package/dist/lib/page/extendPage.d.ts +6 -0
- package/dist/lib/page/extendPage.js +24 -1
- package/dist/lib/test/healRerunGate.d.ts +85 -0
- package/dist/lib/test/healRerunGate.js +186 -0
- package/dist/lib/test/testExtension.d.ts +1 -0
- package/dist/lib/test/testExtension.js +20 -10
- package/dist/reporter/buildReport.js +32 -0
- package/dist/reporter/merge.d.ts +1 -6
- package/dist/reporter/merge.js +57 -35
- package/dist/reporter/model.d.ts +9 -0
- package/dist/reporter/model.js +10 -1
- package/dist/reporter/render.js +34 -12
- package/dist/reporter/renderMarkdown.js +148 -93
- package/dist/reporter/renderSlack.js +39 -28
- package/dist/reporter/reportWalk.d.ts +16 -6
- package/dist/reporter/reportWalk.js +63 -13
- package/dist/tools/RunInlineJavaScriptCodeTool.js +43 -2
- package/dist/tools/RunSandboxedJavaScriptCodeTool.js +24 -2
- package/dist/utils/PlaywrightUtils.d.ts +75 -0
- package/dist/utils/PlaywrightUtils.js +122 -0
- package/package.json +1 -1
|
@@ -1,9 +1,47 @@
|
|
|
1
1
|
import type { BrowserContext, Locator, Page, PageScreenshotOptions } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* Envelope resolved by expressions built with
|
|
4
|
+
* {@link PlaywrightUtils.asSizeGuardedJavaScriptExpression}. Small results
|
|
5
|
+
* come back inline in `value`; oversized (or non-JSON-serializable) results
|
|
6
|
+
* come back as a structural summary plus preview, with `stashedAt` naming the
|
|
7
|
+
* `window.__dnb_result_<n>` variable holding the full value when stashing was
|
|
8
|
+
* requested.
|
|
9
|
+
*/
|
|
10
|
+
export type SizeGuardedEvaluationResult = {
|
|
11
|
+
__dnbGuard: true;
|
|
12
|
+
oversized: false;
|
|
13
|
+
value: unknown;
|
|
14
|
+
} | {
|
|
15
|
+
__dnbGuard: true;
|
|
16
|
+
oversized: true;
|
|
17
|
+
/** Serialized size in JSON characters, or null if not JSON-serializable. */
|
|
18
|
+
size: number | null;
|
|
19
|
+
summary: string;
|
|
20
|
+
preview: string;
|
|
21
|
+
stashedAt: string | null;
|
|
22
|
+
};
|
|
2
23
|
/**
|
|
3
24
|
* Miscellaneous utility functions for working with the Playwright SDK. If you are looking to
|
|
4
25
|
* instantiate a Playwright instance, see PlaywrightSetup instead.
|
|
5
26
|
*/
|
|
6
27
|
export declare class PlaywrightUtils {
|
|
28
|
+
/**
|
|
29
|
+
* Maximum serialized size (in JSON characters) of an evaluation result that
|
|
30
|
+
* {@link asSizeGuardedJavaScriptExpression} returns inline. Results above
|
|
31
|
+
* this go into the conversation history verbatim, so this bounds prompt
|
|
32
|
+
* growth per tool call.
|
|
33
|
+
*/
|
|
34
|
+
static readonly MAX_INLINE_RESULT_JSON_CHARS = 32000;
|
|
35
|
+
/**
|
|
36
|
+
* Number of characters of the serialized result included as a preview when
|
|
37
|
+
* a result is too large to return inline.
|
|
38
|
+
*/
|
|
39
|
+
static readonly OVERSIZED_RESULT_PREVIEW_CHARS = 2000;
|
|
40
|
+
/**
|
|
41
|
+
* Number of oversized results kept alive in the page via
|
|
42
|
+
* `window.__dnb_result_<n>` stash variables before the oldest is released.
|
|
43
|
+
*/
|
|
44
|
+
static readonly MAX_STASHED_RESULTS = 3;
|
|
7
45
|
private static _blankJpeg;
|
|
8
46
|
private static _blankPng;
|
|
9
47
|
private static readonly _browserInstallPromises;
|
|
@@ -49,6 +87,43 @@ export declare class PlaywrightUtils {
|
|
|
49
87
|
* scripts.
|
|
50
88
|
*/
|
|
51
89
|
static setupBasicBrowserContext(browserContext: BrowserContext): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Wraps arbitrary JavaScript code so that it can be passed as a string to
|
|
92
|
+
* Playwright's evaluate(). Playwright evaluates string arguments as bare
|
|
93
|
+
* expressions, so function-form code like `() => {...}` would evaluate to
|
|
94
|
+
* an (unserializable) function object that is never invoked, and the caller
|
|
95
|
+
* would get back undefined. The returned expression invokes function-form
|
|
96
|
+
* code and passes plain expressions through unchanged. Promise results
|
|
97
|
+
* (e.g. from async function-form code) are awaited by evaluate() itself.
|
|
98
|
+
*
|
|
99
|
+
* The wrapped code is placed on its own lines so that a trailing `//`
|
|
100
|
+
* comment cannot swallow the closing parenthesis.
|
|
101
|
+
*/
|
|
102
|
+
static asInvokedJavaScriptExpression(code: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Like {@link asInvokedJavaScriptExpression}, but the result is measured
|
|
105
|
+
* page-side before it crosses the Playwright wire, so an enormous result
|
|
106
|
+
* never gets serialized over CDP into this process. The returned expression
|
|
107
|
+
* always resolves to a {@link SizeGuardedEvaluationResult} envelope.
|
|
108
|
+
*
|
|
109
|
+
* When the serialized result exceeds `maxInlineChars`, the envelope carries
|
|
110
|
+
* a structural summary and a short preview instead of the value. With
|
|
111
|
+
* `stashOversizedResult`, the value itself (a reference, not a copy) is
|
|
112
|
+
* additionally kept in the page as `window.__dnb_result_<n>` so follow-up
|
|
113
|
+
* evaluations can slice or aggregate it. The stash counter lives on
|
|
114
|
+
* `window`, so it resets with the page — names stay deterministic across
|
|
115
|
+
* cached replays because navigations replay too. Only the most recent
|
|
116
|
+
* {@link MAX_STASHED_RESULTS} stashes are kept alive.
|
|
117
|
+
*
|
|
118
|
+
* Results that fail JSON serialization (circular structures, BigInt, etc.)
|
|
119
|
+
* are treated as oversized with a `null` size, since they could not be
|
|
120
|
+
* returned through the tool result either.
|
|
121
|
+
*/
|
|
122
|
+
static asSizeGuardedJavaScriptExpression(code: string, options: {
|
|
123
|
+
stashOversizedResult: boolean;
|
|
124
|
+
maxInlineChars?: number;
|
|
125
|
+
previewChars?: number;
|
|
126
|
+
}): string;
|
|
52
127
|
/**
|
|
53
128
|
* Returned true IFF the given error is a Playwright error regarding page closing,
|
|
54
129
|
* of if the given error is an instance of {@link PageClosedException}.
|
|
@@ -133,6 +133,111 @@ class PlaywrightUtils {
|
|
|
133
133
|
await browserContext.addInitScript(dialog_prompt_tracker_1.installDialogPromptTracker);
|
|
134
134
|
await browserContext.addInitScript(smart_selector_generator_1.installSmartSelectorGenerator);
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Wraps arbitrary JavaScript code so that it can be passed as a string to
|
|
138
|
+
* Playwright's evaluate(). Playwright evaluates string arguments as bare
|
|
139
|
+
* expressions, so function-form code like `() => {...}` would evaluate to
|
|
140
|
+
* an (unserializable) function object that is never invoked, and the caller
|
|
141
|
+
* would get back undefined. The returned expression invokes function-form
|
|
142
|
+
* code and passes plain expressions through unchanged. Promise results
|
|
143
|
+
* (e.g. from async function-form code) are awaited by evaluate() itself.
|
|
144
|
+
*
|
|
145
|
+
* The wrapped code is placed on its own lines so that a trailing `//`
|
|
146
|
+
* comment cannot swallow the closing parenthesis.
|
|
147
|
+
*/
|
|
148
|
+
static asInvokedJavaScriptExpression(code) {
|
|
149
|
+
return `(() => {
|
|
150
|
+
const __donobuEvalResult = (
|
|
151
|
+
${code}
|
|
152
|
+
);
|
|
153
|
+
return typeof __donobuEvalResult === 'function'
|
|
154
|
+
? __donobuEvalResult()
|
|
155
|
+
: __donobuEvalResult;
|
|
156
|
+
})()`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Like {@link asInvokedJavaScriptExpression}, but the result is measured
|
|
160
|
+
* page-side before it crosses the Playwright wire, so an enormous result
|
|
161
|
+
* never gets serialized over CDP into this process. The returned expression
|
|
162
|
+
* always resolves to a {@link SizeGuardedEvaluationResult} envelope.
|
|
163
|
+
*
|
|
164
|
+
* When the serialized result exceeds `maxInlineChars`, the envelope carries
|
|
165
|
+
* a structural summary and a short preview instead of the value. With
|
|
166
|
+
* `stashOversizedResult`, the value itself (a reference, not a copy) is
|
|
167
|
+
* additionally kept in the page as `window.__dnb_result_<n>` so follow-up
|
|
168
|
+
* evaluations can slice or aggregate it. The stash counter lives on
|
|
169
|
+
* `window`, so it resets with the page — names stay deterministic across
|
|
170
|
+
* cached replays because navigations replay too. Only the most recent
|
|
171
|
+
* {@link MAX_STASHED_RESULTS} stashes are kept alive.
|
|
172
|
+
*
|
|
173
|
+
* Results that fail JSON serialization (circular structures, BigInt, etc.)
|
|
174
|
+
* are treated as oversized with a `null` size, since they could not be
|
|
175
|
+
* returned through the tool result either.
|
|
176
|
+
*/
|
|
177
|
+
static asSizeGuardedJavaScriptExpression(code, options) {
|
|
178
|
+
const maxInlineChars = options.maxInlineChars ?? PlaywrightUtils.MAX_INLINE_RESULT_JSON_CHARS;
|
|
179
|
+
const previewChars = options.previewChars ?? PlaywrightUtils.OVERSIZED_RESULT_PREVIEW_CHARS;
|
|
180
|
+
const stashSnippet = options.stashOversizedResult
|
|
181
|
+
? `{
|
|
182
|
+
const __w = window;
|
|
183
|
+
__w.__dnb_stash_count = (__w.__dnb_stash_count || 0) + 1;
|
|
184
|
+
__stashedAt = '__dnb_result_' + __w.__dnb_stash_count;
|
|
185
|
+
__w[__stashedAt] = __value;
|
|
186
|
+
__w.__dnb_stash_names = __w.__dnb_stash_names || [];
|
|
187
|
+
__w.__dnb_stash_names.push(__stashedAt);
|
|
188
|
+
while (__w.__dnb_stash_names.length > ${PlaywrightUtils.MAX_STASHED_RESULTS}) {
|
|
189
|
+
delete __w[__w.__dnb_stash_names.shift()];
|
|
190
|
+
}
|
|
191
|
+
}`
|
|
192
|
+
: '';
|
|
193
|
+
return `(() => {
|
|
194
|
+
const __pending = Promise.resolve(
|
|
195
|
+
${PlaywrightUtils.asInvokedJavaScriptExpression(code)}
|
|
196
|
+
);
|
|
197
|
+
return __pending.then((__value) => {
|
|
198
|
+
if (__value === undefined) {
|
|
199
|
+
return { __dnbGuard: true, oversized: false, value: __value };
|
|
200
|
+
}
|
|
201
|
+
let __json;
|
|
202
|
+
try {
|
|
203
|
+
__json = JSON.stringify(__value);
|
|
204
|
+
} catch {
|
|
205
|
+
__json = undefined;
|
|
206
|
+
}
|
|
207
|
+
if (__json !== undefined && __json.length <= ${maxInlineChars}) {
|
|
208
|
+
return { __dnbGuard: true, oversized: false, value: __value };
|
|
209
|
+
}
|
|
210
|
+
const __summary = (() => {
|
|
211
|
+
if (Array.isArray(__value)) {
|
|
212
|
+
const __first = __value.find((el) => el && typeof el === 'object');
|
|
213
|
+
const __keys = __first ? Object.keys(__first).slice(0, 12).join(', ') : null;
|
|
214
|
+
return 'an array of ' + __value.length + ' elements' +
|
|
215
|
+
(__keys ? ' (element keys: ' + __keys + ')' : '');
|
|
216
|
+
}
|
|
217
|
+
if (typeof __value === 'object' && __value !== null) {
|
|
218
|
+
return 'an object with keys: ' + Object.keys(__value).slice(0, 24).join(', ');
|
|
219
|
+
}
|
|
220
|
+
return 'a ' + typeof __value;
|
|
221
|
+
})();
|
|
222
|
+
let __preview = '';
|
|
223
|
+
try {
|
|
224
|
+
__preview = (__json !== undefined ? __json : String(__value)).slice(0, ${previewChars});
|
|
225
|
+
} catch {
|
|
226
|
+
// Leave the preview empty if the value cannot be stringified at all.
|
|
227
|
+
}
|
|
228
|
+
let __stashedAt = null;
|
|
229
|
+
${stashSnippet}
|
|
230
|
+
return {
|
|
231
|
+
__dnbGuard: true,
|
|
232
|
+
oversized: true,
|
|
233
|
+
size: __json === undefined ? null : __json.length,
|
|
234
|
+
summary: __summary,
|
|
235
|
+
preview: __preview,
|
|
236
|
+
stashedAt: __stashedAt,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
})()`;
|
|
240
|
+
}
|
|
136
241
|
/**
|
|
137
242
|
* Returned true IFF the given error is a Playwright error regarding page closing,
|
|
138
243
|
* of if the given error is an instance of {@link PageClosedException}.
|
|
@@ -297,6 +402,23 @@ class PlaywrightUtils {
|
|
|
297
402
|
}
|
|
298
403
|
}
|
|
299
404
|
exports.PlaywrightUtils = PlaywrightUtils;
|
|
405
|
+
/**
|
|
406
|
+
* Maximum serialized size (in JSON characters) of an evaluation result that
|
|
407
|
+
* {@link asSizeGuardedJavaScriptExpression} returns inline. Results above
|
|
408
|
+
* this go into the conversation history verbatim, so this bounds prompt
|
|
409
|
+
* growth per tool call.
|
|
410
|
+
*/
|
|
411
|
+
PlaywrightUtils.MAX_INLINE_RESULT_JSON_CHARS = 32_000;
|
|
412
|
+
/**
|
|
413
|
+
* Number of characters of the serialized result included as a preview when
|
|
414
|
+
* a result is too large to return inline.
|
|
415
|
+
*/
|
|
416
|
+
PlaywrightUtils.OVERSIZED_RESULT_PREVIEW_CHARS = 2_000;
|
|
417
|
+
/**
|
|
418
|
+
* Number of oversized results kept alive in the page via
|
|
419
|
+
* `window.__dnb_result_<n>` stash variables before the oldest is released.
|
|
420
|
+
*/
|
|
421
|
+
PlaywrightUtils.MAX_STASHED_RESULTS = 3;
|
|
300
422
|
// Per-browser in-flight install promises — deduplicate concurrent requests.
|
|
301
423
|
PlaywrightUtils._browserInstallPromises = new Map();
|
|
302
424
|
//# sourceMappingURL=PlaywrightUtils.js.map
|
|
@@ -20,6 +20,12 @@ export declare function extendPage(page: Page, options?: {
|
|
|
20
20
|
flowId?: string;
|
|
21
21
|
visualCueDurationMs?: number;
|
|
22
22
|
cacheFilepath?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Spec file this page is serving. Used to decide whether the file-scoped
|
|
25
|
+
* Page.AI cache invalidation (`DONOBU_PAGE_AI_CLEAR_CACHE_FILES`) applies
|
|
26
|
+
* to this context.
|
|
27
|
+
*/
|
|
28
|
+
specFilePath?: string;
|
|
23
29
|
envVars?: string[];
|
|
24
30
|
gptClient?: GptClient;
|
|
25
31
|
headless?: boolean;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.extendPage = extendPage;
|
|
4
7
|
const crypto_1 = require("crypto");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
5
9
|
const v4_1 = require("zod/v4");
|
|
6
10
|
const GptClient_1 = require("../../clients/GptClient");
|
|
7
11
|
const VercelAiGptClient_1 = require("../../clients/VercelAiGptClient");
|
|
@@ -60,6 +64,25 @@ function resolveBaseUrl(page, url) {
|
|
|
60
64
|
// Donobu page extension helpers: decorate Playwright pages with Donobu behaviors and keep one
|
|
61
65
|
// coherent flow (and persistence record) per browser context so new tabs share state safely.
|
|
62
66
|
const PLACEHOLDER_FLOW_URL = 'https://example.com';
|
|
67
|
+
/**
|
|
68
|
+
* Whether Page.AI cache entries should be bypassed and invalidated for this
|
|
69
|
+
* context. Two knobs:
|
|
70
|
+
* - `DONOBU_PAGE_AI_CLEAR_CACHE` — run-wide, set by `--clear-ai-cache`.
|
|
71
|
+
* - `DONOBU_PAGE_AI_CLEAR_CACHE_FILES` — JSON array of spec paths, set by the
|
|
72
|
+
* auto-heal rerun so only heal-target spec files regenerate selectors while
|
|
73
|
+
* other re-running tests (serial prerequisites) keep their cache replay.
|
|
74
|
+
*/
|
|
75
|
+
function shouldClearPageAiCache(specFilePath) {
|
|
76
|
+
if (MiscUtils_1.MiscUtils.yn(envVars_1.env.data.DONOBU_PAGE_AI_CLEAR_CACHE)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const files = envVars_1.env.data.DONOBU_PAGE_AI_CLEAR_CACHE_FILES;
|
|
80
|
+
if (!files?.length || !specFilePath) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const resolved = path_1.default.resolve(specFilePath);
|
|
84
|
+
return files.some((file) => path_1.default.resolve(file) === resolved);
|
|
85
|
+
}
|
|
63
86
|
// Cache the shared Donobu state per browser context so every tab in that context reuses the same
|
|
64
87
|
// flow metadata, persistence, GPT client, and visualizer. WeakMap ensures cleanup when contexts die.
|
|
65
88
|
const contextSharedState = new WeakMap();
|
|
@@ -137,7 +160,7 @@ async function extendPage(page, options) {
|
|
|
137
160
|
gptClient: resolvedGptClient,
|
|
138
161
|
controlPanelFactory: options?.controlPanelFactory,
|
|
139
162
|
runtimeDirectives: {
|
|
140
|
-
clearPageAiCache:
|
|
163
|
+
clearPageAiCache: shouldClearPageAiCache(options?.specFilePath),
|
|
141
164
|
},
|
|
142
165
|
tbdSessions: [],
|
|
143
166
|
aiInvocations: [],
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Runtime gate for auto-heal reruns.
|
|
3
|
+
*
|
|
4
|
+
* The auto-heal rerun is launched with the same project-level arguments as the
|
|
5
|
+
* initial run so Playwright's scheduling (declared project `dependencies`,
|
|
6
|
+
* workers, ordering) behaves with full fidelity. Within the targeted projects,
|
|
7
|
+
* however, only the tests the heal actually needs should execute. This module
|
|
8
|
+
* enforces that from an auto fixture that runs before any browser fixture:
|
|
9
|
+
* when `DONOBU_AUTO_HEAL_PLAN_PATH` is set, every test not in the rerun plan
|
|
10
|
+
* skips immediately — before a context or page is created — annotated with
|
|
11
|
+
* `donobu-heal-skip-replay` so the merge step drops the entry and leaves the
|
|
12
|
+
* initial run's result untouched.
|
|
13
|
+
*
|
|
14
|
+
* What runs during a heal rerun (the "declared signals only" policy):
|
|
15
|
+
* - Heal targets (the failed tests with actionable treatment plans).
|
|
16
|
+
* - For a target inside a `test.describe.serial` scope (or a file marked
|
|
17
|
+
* serial via `test.describe.configure({ mode: 'serial' })`): the other
|
|
18
|
+
* serial-scoped tests in that file. Serial mode is Playwright's declared
|
|
19
|
+
* intra-file ordering contract — Playwright itself re-runs whole serial
|
|
20
|
+
* groups on retry, and we mirror that. The orchestrator expands the plan
|
|
21
|
+
* with these companions before the rerun (see
|
|
22
|
+
* `expandTargetsWithSerialCompanions`) using the `serialScoped` flags the
|
|
23
|
+
* Donobu reporter recorded during the initial run — the runner process
|
|
24
|
+
* sees the suite tree; the worker (where this gate runs) does not.
|
|
25
|
+
* - Declared dependency projects, which Playwright always runs in full.
|
|
26
|
+
*
|
|
27
|
+
* Implicit ordering (checkpoint files between plain tests, cross-file state
|
|
28
|
+
* with `workers: 1`) is deliberately NOT honored: tests relying on it will
|
|
29
|
+
* skip themselves during the rerun and surface as honest failures with
|
|
30
|
+
* guidance to declare the dependency.
|
|
31
|
+
*
|
|
32
|
+
* The gate runs at test runtime rather than collection time so Playwright's
|
|
33
|
+
* test-location attribution stays untouched (a collection-time wrapper would
|
|
34
|
+
* become every test's reported call site, breaking the merge's file-based
|
|
35
|
+
* matching) and so the plan can be matched against `testInfo.file`/`title`
|
|
36
|
+
* exactly instead of via stack inspection.
|
|
37
|
+
*/
|
|
38
|
+
import type { TestInfo } from '@playwright/test';
|
|
39
|
+
/** Shape of the plan file the auto-heal orchestrator writes before the rerun. */
|
|
40
|
+
export interface HealRerunPlan {
|
|
41
|
+
targets: Array<{
|
|
42
|
+
/** Spec file path; absolute, or relative to the rerun's CWD. */
|
|
43
|
+
file: string;
|
|
44
|
+
title: string;
|
|
45
|
+
projectName?: string;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
/** Targets indexed by absolute spec path for O(1) per-test decisions. */
|
|
49
|
+
export type HealRerunPlanIndex = Map<string, Set<string>>;
|
|
50
|
+
export declare function buildPlanIndex(plan: HealRerunPlan): HealRerunPlanIndex;
|
|
51
|
+
/**
|
|
52
|
+
* Pure decision: should the test in `file` with `title` actually execute
|
|
53
|
+
* during the heal rerun? The plan is fully explicit — serial companions were
|
|
54
|
+
* already expanded into it by the orchestrator.
|
|
55
|
+
*/
|
|
56
|
+
export declare function shouldRunDuringHealRerun(params: {
|
|
57
|
+
index: HealRerunPlanIndex;
|
|
58
|
+
file: string;
|
|
59
|
+
title: string;
|
|
60
|
+
}): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Expand heal targets with their `describe.serial` siblings, using the
|
|
63
|
+
* `serialScoped` flags the Donobu reporter recorded in the initial run's
|
|
64
|
+
* report. Companions live in the same file as their target by construction
|
|
65
|
+
* (serial scopes are intra-file), so they inherit the target's absolute file
|
|
66
|
+
* path; report file paths are rootDir-relative, hence the suffix match.
|
|
67
|
+
*
|
|
68
|
+
* Degrades to the unexpanded targets when the report (or the flags) are
|
|
69
|
+
* unavailable — serial chains then surface as honest not-reattempted
|
|
70
|
+
* failures instead of healing.
|
|
71
|
+
*/
|
|
72
|
+
export declare function expandTargetsWithSerialCompanions(targets: HealRerunPlan['targets'], initialReport: {
|
|
73
|
+
suites?: unknown;
|
|
74
|
+
} | null): HealRerunPlan['targets'];
|
|
75
|
+
/** Test-only: reset the memoized plan so each test can load its own. */
|
|
76
|
+
export declare function resetHealRerunPlanCacheForTesting(): void;
|
|
77
|
+
/**
|
|
78
|
+
* Called from the Donobu auto fixture before any browser fixture initializes.
|
|
79
|
+
* Outside heal reruns this is a no-op. During a rerun, tests outside the plan
|
|
80
|
+
* are annotated and skipped on the spot — no context, no page, no cost.
|
|
81
|
+
*/
|
|
82
|
+
export declare function maybeSkipForHealRerun(testInfo: TestInfo, options?: {
|
|
83
|
+
planPath?: string;
|
|
84
|
+
}): void;
|
|
85
|
+
//# sourceMappingURL=healRerunGate.d.ts.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Runtime gate for auto-heal reruns.
|
|
4
|
+
*
|
|
5
|
+
* The auto-heal rerun is launched with the same project-level arguments as the
|
|
6
|
+
* initial run so Playwright's scheduling (declared project `dependencies`,
|
|
7
|
+
* workers, ordering) behaves with full fidelity. Within the targeted projects,
|
|
8
|
+
* however, only the tests the heal actually needs should execute. This module
|
|
9
|
+
* enforces that from an auto fixture that runs before any browser fixture:
|
|
10
|
+
* when `DONOBU_AUTO_HEAL_PLAN_PATH` is set, every test not in the rerun plan
|
|
11
|
+
* skips immediately — before a context or page is created — annotated with
|
|
12
|
+
* `donobu-heal-skip-replay` so the merge step drops the entry and leaves the
|
|
13
|
+
* initial run's result untouched.
|
|
14
|
+
*
|
|
15
|
+
* What runs during a heal rerun (the "declared signals only" policy):
|
|
16
|
+
* - Heal targets (the failed tests with actionable treatment plans).
|
|
17
|
+
* - For a target inside a `test.describe.serial` scope (or a file marked
|
|
18
|
+
* serial via `test.describe.configure({ mode: 'serial' })`): the other
|
|
19
|
+
* serial-scoped tests in that file. Serial mode is Playwright's declared
|
|
20
|
+
* intra-file ordering contract — Playwright itself re-runs whole serial
|
|
21
|
+
* groups on retry, and we mirror that. The orchestrator expands the plan
|
|
22
|
+
* with these companions before the rerun (see
|
|
23
|
+
* `expandTargetsWithSerialCompanions`) using the `serialScoped` flags the
|
|
24
|
+
* Donobu reporter recorded during the initial run — the runner process
|
|
25
|
+
* sees the suite tree; the worker (where this gate runs) does not.
|
|
26
|
+
* - Declared dependency projects, which Playwright always runs in full.
|
|
27
|
+
*
|
|
28
|
+
* Implicit ordering (checkpoint files between plain tests, cross-file state
|
|
29
|
+
* with `workers: 1`) is deliberately NOT honored: tests relying on it will
|
|
30
|
+
* skip themselves during the rerun and surface as honest failures with
|
|
31
|
+
* guidance to declare the dependency.
|
|
32
|
+
*
|
|
33
|
+
* The gate runs at test runtime rather than collection time so Playwright's
|
|
34
|
+
* test-location attribution stays untouched (a collection-time wrapper would
|
|
35
|
+
* become every test's reported call site, breaking the merge's file-based
|
|
36
|
+
* matching) and so the plan can be matched against `testInfo.file`/`title`
|
|
37
|
+
* exactly instead of via stack inspection.
|
|
38
|
+
*/
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.buildPlanIndex = buildPlanIndex;
|
|
44
|
+
exports.shouldRunDuringHealRerun = shouldRunDuringHealRerun;
|
|
45
|
+
exports.expandTargetsWithSerialCompanions = expandTargetsWithSerialCompanions;
|
|
46
|
+
exports.resetHealRerunPlanCacheForTesting = resetHealRerunPlanCacheForTesting;
|
|
47
|
+
exports.maybeSkipForHealRerun = maybeSkipForHealRerun;
|
|
48
|
+
const fs_1 = __importDefault(require("fs"));
|
|
49
|
+
const path_1 = __importDefault(require("path"));
|
|
50
|
+
const envVars_1 = require("../../envVars");
|
|
51
|
+
const model_1 = require("../../reporter/model");
|
|
52
|
+
const Logger_1 = require("../../utils/Logger");
|
|
53
|
+
function buildPlanIndex(plan) {
|
|
54
|
+
const index = new Map();
|
|
55
|
+
for (const target of plan.targets ?? []) {
|
|
56
|
+
if (!target?.file || !target?.title) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const file = path_1.default.resolve(target.file);
|
|
60
|
+
if (!index.has(file)) {
|
|
61
|
+
index.set(file, new Set());
|
|
62
|
+
}
|
|
63
|
+
index.get(file).add(target.title);
|
|
64
|
+
}
|
|
65
|
+
return index;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Pure decision: should the test in `file` with `title` actually execute
|
|
69
|
+
* during the heal rerun? The plan is fully explicit — serial companions were
|
|
70
|
+
* already expanded into it by the orchestrator.
|
|
71
|
+
*/
|
|
72
|
+
function shouldRunDuringHealRerun(params) {
|
|
73
|
+
const titles = params.index.get(path_1.default.resolve(params.file));
|
|
74
|
+
return titles?.has(params.title) ?? false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Expand heal targets with their `describe.serial` siblings, using the
|
|
78
|
+
* `serialScoped` flags the Donobu reporter recorded in the initial run's
|
|
79
|
+
* report. Companions live in the same file as their target by construction
|
|
80
|
+
* (serial scopes are intra-file), so they inherit the target's absolute file
|
|
81
|
+
* path; report file paths are rootDir-relative, hence the suffix match.
|
|
82
|
+
*
|
|
83
|
+
* Degrades to the unexpanded targets when the report (or the flags) are
|
|
84
|
+
* unavailable — serial chains then surface as honest not-reattempted
|
|
85
|
+
* failures instead of healing.
|
|
86
|
+
*/
|
|
87
|
+
function expandTargetsWithSerialCompanions(targets, initialReport) {
|
|
88
|
+
const suites = (initialReport?.suites ?? []);
|
|
89
|
+
const expanded = [...targets];
|
|
90
|
+
const seen = new Set(targets.map((t) => `${path_1.default.resolve(t.file)}::${t.projectName}::${t.title}`));
|
|
91
|
+
for (const target of targets) {
|
|
92
|
+
if (!target.file || !target.title) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const targetFile = path_1.default.resolve(target.file);
|
|
96
|
+
for (const suite of suites) {
|
|
97
|
+
const suiteFile = String(suite.file ?? '');
|
|
98
|
+
if (!targetFile.endsWith(path_1.default.normalize(suiteFile)) ||
|
|
99
|
+
suiteFile.length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const specs = suite.specs ?? [];
|
|
103
|
+
const targetEntry = specs
|
|
104
|
+
.find((spec) => spec.title === target.title)
|
|
105
|
+
?.tests?.find((test) => !target.projectName || test.projectName === target.projectName);
|
|
106
|
+
if (!targetEntry?.serialScoped) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// The target is serial-scoped: every serial-scoped test in this file
|
|
110
|
+
// (same project) joins the plan. Bounded by the file, mirroring how
|
|
111
|
+
// Playwright re-runs whole serial groups on retry.
|
|
112
|
+
for (const spec of specs) {
|
|
113
|
+
for (const test of spec.tests ?? []) {
|
|
114
|
+
if (!test.serialScoped) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (target.projectName && test.projectName !== target.projectName) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const key = `${targetFile}::${test.projectName}::${spec.title}`;
|
|
121
|
+
if (seen.has(key)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
seen.add(key);
|
|
125
|
+
expanded.push({
|
|
126
|
+
file: target.file,
|
|
127
|
+
title: spec.title,
|
|
128
|
+
projectName: test.projectName,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return expanded;
|
|
135
|
+
}
|
|
136
|
+
let cachedPlanIndex;
|
|
137
|
+
function getPlanIndex(planPathOverride) {
|
|
138
|
+
if (planPathOverride === undefined && cachedPlanIndex !== undefined) {
|
|
139
|
+
return cachedPlanIndex;
|
|
140
|
+
}
|
|
141
|
+
const planPath = planPathOverride ?? envVars_1.env.data.DONOBU_AUTO_HEAL_PLAN_PATH;
|
|
142
|
+
let index = null;
|
|
143
|
+
if (planPath) {
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs_1.default.readFileSync(planPath, 'utf8');
|
|
146
|
+
index = buildPlanIndex(JSON.parse(raw));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
Logger_1.appLogger.warn(`Auto-heal rerun plan at ${planPath} could not be read; running all collected tests.`, error);
|
|
150
|
+
index = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (planPathOverride === undefined) {
|
|
154
|
+
cachedPlanIndex = index;
|
|
155
|
+
}
|
|
156
|
+
return index;
|
|
157
|
+
}
|
|
158
|
+
/** Test-only: reset the memoized plan so each test can load its own. */
|
|
159
|
+
function resetHealRerunPlanCacheForTesting() {
|
|
160
|
+
cachedPlanIndex = undefined;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Called from the Donobu auto fixture before any browser fixture initializes.
|
|
164
|
+
* Outside heal reruns this is a no-op. During a rerun, tests outside the plan
|
|
165
|
+
* are annotated and skipped on the spot — no context, no page, no cost.
|
|
166
|
+
*/
|
|
167
|
+
function maybeSkipForHealRerun(testInfo, options) {
|
|
168
|
+
const index = getPlanIndex(options?.planPath);
|
|
169
|
+
if (!index) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const shouldRun = shouldRunDuringHealRerun({
|
|
173
|
+
index,
|
|
174
|
+
file: testInfo.file,
|
|
175
|
+
title: testInfo.title,
|
|
176
|
+
});
|
|
177
|
+
if (shouldRun) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
testInfo.annotations.push({
|
|
181
|
+
type: model_1.HEAL_SKIP_REPLAY_ANNOTATION_TYPE,
|
|
182
|
+
description: 'Not part of the auto-heal rerun plan; the initial run result stands.',
|
|
183
|
+
});
|
|
184
|
+
testInfo.skip(true, 'Not part of the auto-heal rerun plan; the initial run result stands.');
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=healRerunGate.js.map
|
|
@@ -5,6 +5,7 @@ import { FlowLogBuffer } from '../../utils/FlowLogBuffer';
|
|
|
5
5
|
import type { DonobuExtendedPage } from '../page/DonobuExtendedPage';
|
|
6
6
|
export * from '@playwright/test';
|
|
7
7
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
8
|
+
healRerunGate: void;
|
|
8
9
|
flowLoggingContext: {
|
|
9
10
|
flowId: string;
|
|
10
11
|
logBuffer: FlowLogBuffer;
|
|
@@ -41,6 +41,7 @@ const PageLogListeners_1 = require("../../utils/PageLogListeners");
|
|
|
41
41
|
const cacheLocator_1 = require("../ai/cache/cacheLocator");
|
|
42
42
|
const extendPage_1 = require("../page/extendPage");
|
|
43
43
|
const tbd_1 = require("../page/tbd");
|
|
44
|
+
const healRerunGate_1 = require("./healRerunGate");
|
|
44
45
|
const selfHealing_1 = require("./utils/selfHealing");
|
|
45
46
|
const triageTestFailure_1 = require("./utils/triageTestFailure");
|
|
46
47
|
__exportStar(require("@playwright/test"), exports);
|
|
@@ -220,6 +221,20 @@ function persistVideoIfApplicable(page, testInfo, videoOption) {
|
|
|
220
221
|
*/
|
|
221
222
|
const UPLOAD_DRAIN_TIMEOUT_MS = 30_000;
|
|
222
223
|
exports.test = test_1.test.extend({
|
|
224
|
+
/**
|
|
225
|
+
* Auto-heal rerun gate. First in registration order so it runs before every
|
|
226
|
+
* other test-scoped fixture: during a heal rerun, tests outside the rerun
|
|
227
|
+
* plan skip here — before a browser context or page is ever created — with
|
|
228
|
+
* the `donobu-heal-skip-replay` annotation the merge step uses to drop
|
|
229
|
+
* their entries. A no-op outside heal reruns.
|
|
230
|
+
*/
|
|
231
|
+
healRerunGate: [
|
|
232
|
+
async ({}, use, testInfo) => {
|
|
233
|
+
(0, healRerunGate_1.maybeSkipForHealRerun)(testInfo);
|
|
234
|
+
await use();
|
|
235
|
+
},
|
|
236
|
+
{ scope: 'test', auto: true },
|
|
237
|
+
],
|
|
223
238
|
/**
|
|
224
239
|
* Establish a logging scope for the entire Playwright test *before* any other
|
|
225
240
|
* fixtures run. Playwright builds the fixture dependency graph eagerly, so
|
|
@@ -400,6 +415,7 @@ exports.test = test_1.test.extend({
|
|
|
400
415
|
flowId: flowId,
|
|
401
416
|
visualCueDurationMs: visualCueDurationMs,
|
|
402
417
|
cacheFilepath: (0, cacheLocator_1.buildPageAiCachePath)(testInfo.file),
|
|
418
|
+
specFilePath: testInfo.file,
|
|
403
419
|
envVars: (0, DonobuFlowsManager_1.distillAllowedEnvVariableNames)(overallObjective, testInfo.annotations
|
|
404
420
|
.filter((a) => a.type === 'ENV' && a.description)
|
|
405
421
|
.map((a) => a.description)),
|
|
@@ -1051,16 +1067,10 @@ async function finalizeTest(page, testInfo, logBuffer, videoOption) {
|
|
|
1051
1067
|
}
|
|
1052
1068
|
}
|
|
1053
1069
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
testInfo.annotations.push({
|
|
1059
|
-
type: 'self-healed',
|
|
1060
|
-
description: 'Automatically healed by Donobu auto-heal rerun.',
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1070
|
+
// Note: passing tests in an auto-heal rerun are NOT annotated here. Whether
|
|
1071
|
+
// a test was actually healed (failed initially, passed on the rerun) is
|
|
1072
|
+
// decided by the merge step, which sees both runs — annotating every rerun
|
|
1073
|
+
// pass would mislabel dependency tests that never failed.
|
|
1064
1074
|
// Flush any page.tbd() sessions: replace the tbd() call sites in the
|
|
1065
1075
|
// source files with the generated Playwright code for the recorded
|
|
1066
1076
|
// user interactions.
|
|
@@ -58,6 +58,14 @@ function buildDonobuReport(resultsByTest, rootDir) {
|
|
|
58
58
|
// Signal "skipped" tests to the renderers the same way the JSON
|
|
59
59
|
// reporter does.
|
|
60
60
|
status: test.expectedStatus === 'skipped' ? 'skipped' : undefined,
|
|
61
|
+
// Consumed by `reportWalk.statusOf` to classify `test.fail()`
|
|
62
|
+
// specs as expected failures rather than real ones; absent from
|
|
63
|
+
// the legacy state-file shape.
|
|
64
|
+
expectedStatus: test.expectedStatus,
|
|
65
|
+
// Whether the test sits inside a `describe.serial` scope — declared
|
|
66
|
+
// intra-file ordering. The auto-heal orchestrator uses this to
|
|
67
|
+
// include a heal target's serial siblings in the rerun plan.
|
|
68
|
+
serialScoped: isSerialScoped(test),
|
|
61
69
|
results: results.map((r) => ({
|
|
62
70
|
status: r.status,
|
|
63
71
|
duration: r.duration,
|
|
@@ -110,6 +118,30 @@ function buildDonobuReport(resultsByTest, rootDir) {
|
|
|
110
118
|
}
|
|
111
119
|
return { suites, metadata: {} };
|
|
112
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether any enclosing suite is in serial mode (`test.describe.serial` or
|
|
123
|
+
* `test.describe.configure({ mode: 'serial' })`).
|
|
124
|
+
*
|
|
125
|
+
* Reads the runner-side suite internals (`_parallelMode`) — reporters receive
|
|
126
|
+
* the runner's own Suite instances, which carry it. Verified against
|
|
127
|
+
* @playwright/test 1.58; guarded so shape drift degrades to `false` (serial
|
|
128
|
+
* chains then lose heal eligibility, but reporting stays honest).
|
|
129
|
+
*/
|
|
130
|
+
function isSerialScoped(test) {
|
|
131
|
+
try {
|
|
132
|
+
let suite = test.parent;
|
|
133
|
+
while (suite) {
|
|
134
|
+
if (suite._parallelMode === 'serial') {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
suite = suite.parent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Internal shape drifted — treat as non-serial.
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
113
145
|
/** Walk up the suite chain to find the enclosing project suite's title. */
|
|
114
146
|
function getProjectName(test) {
|
|
115
147
|
let suite = test.parent;
|