donobu 5.60.3 → 5.60.5
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 +11 -14
- package/dist/esm/cli/donobu-cli.js +11 -14
- package/dist/esm/lib/test/healRerunGate.d.ts +39 -24
- package/dist/esm/lib/test/healRerunGate.js +55 -67
- package/dist/esm/lib/test/testExtension.js +20 -4
- package/dist/esm/managers/InteractionVisualizer.d.ts +32 -21
- package/dist/esm/managers/InteractionVisualizer.js +133 -189
- package/dist/esm/reporter/buildReport.js +1 -22
- package/dist/esm/reporter/model.d.ts +0 -7
- package/dist/esm/utils/BrowserUtils.js +1 -16
- package/dist/lib/test/healRerunGate.d.ts +39 -24
- package/dist/lib/test/healRerunGate.js +55 -67
- package/dist/lib/test/testExtension.js +20 -4
- package/dist/managers/InteractionVisualizer.d.ts +32 -21
- package/dist/managers/InteractionVisualizer.js +133 -189
- package/dist/reporter/buildReport.js +1 -22
- package/dist/reporter/model.d.ts +0 -7
- package/dist/utils/BrowserUtils.js +1 -16
- package/package.json +7 -7
|
@@ -23,7 +23,15 @@
|
|
|
23
23
|
* `expandTargetsWithSerialCompanions`) using the `serialScoped` flags the
|
|
24
24
|
* Donobu reporter recorded during the initial run — the runner process
|
|
25
25
|
* sees the suite tree; the worker (where this gate runs) does not.
|
|
26
|
-
* - Declared dependency projects,
|
|
26
|
+
* - Declared dependency (setup) projects, in full. Playwright schedules them
|
|
27
|
+
* because a target project lists them in `dependencies`, and the rerun
|
|
28
|
+
* command only ever passes `--project=<target>`, so any project that runs
|
|
29
|
+
* and is NOT a heal-target project is, by construction, such a dependency
|
|
30
|
+
* project. The gate exempts every test whose project is not a target
|
|
31
|
+
* project, so the auth/storage-state and fixture seeding those projects
|
|
32
|
+
* perform actually runs. (A prior version skipped them too — every test
|
|
33
|
+
* not literally in the plan — which broke targets that depend on the
|
|
34
|
+
* `.auth` storage state a setup project produces.)
|
|
27
35
|
*
|
|
28
36
|
* Implicit ordering (checkpoint files between plain tests, cross-file state
|
|
29
37
|
* with `workers: 1`) is deliberately NOT honored: tests relying on it will
|
|
@@ -41,7 +49,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
41
49
|
};
|
|
42
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
51
|
exports.buildPlanIndex = buildPlanIndex;
|
|
44
|
-
exports.computeGatedProjects = computeGatedProjects;
|
|
45
52
|
exports.shouldRunDuringHealRerun = shouldRunDuringHealRerun;
|
|
46
53
|
exports.expandTargetsWithSerialCompanions = expandTargetsWithSerialCompanions;
|
|
47
54
|
exports.resetHealRerunPlanCacheForTesting = resetHealRerunPlanCacheForTesting;
|
|
@@ -52,52 +59,47 @@ const envVars_1 = require("../../envVars");
|
|
|
52
59
|
const model_1 = require("../../reporter/model");
|
|
53
60
|
const Logger_1 = require("../../utils/Logger");
|
|
54
61
|
function buildPlanIndex(plan) {
|
|
55
|
-
const
|
|
62
|
+
const byFile = new Map();
|
|
63
|
+
const targetProjects = new Set();
|
|
56
64
|
for (const target of plan.targets ?? []) {
|
|
57
65
|
if (!target?.file || !target?.title) {
|
|
58
66
|
continue;
|
|
59
67
|
}
|
|
60
68
|
const file = path_1.default.resolve(target.file);
|
|
61
|
-
if (!
|
|
62
|
-
|
|
69
|
+
if (!byFile.has(file)) {
|
|
70
|
+
byFile.set(file, new Set());
|
|
63
71
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* The projects the rerun gate should apply to: the heal targets' own
|
|
70
|
-
* projects, minus any project that is a declared (transitive) dependency of
|
|
71
|
-
* another target project — the dependency declaration wins, and that project
|
|
72
|
-
* runs in full.
|
|
73
|
-
*/
|
|
74
|
-
function computeGatedProjects(targetProjects, projectDependencies) {
|
|
75
|
-
const targets = [
|
|
76
|
-
...new Set(targetProjects.filter((name) => Boolean(name))),
|
|
77
|
-
];
|
|
78
|
-
if (!projectDependencies) {
|
|
79
|
-
return targets;
|
|
80
|
-
}
|
|
81
|
-
// Every project reachable through any target's declared dependency chain.
|
|
82
|
-
const reachable = new Set();
|
|
83
|
-
const visit = (name) => {
|
|
84
|
-
for (const dependency of projectDependencies[name] ?? []) {
|
|
85
|
-
if (!reachable.has(dependency)) {
|
|
86
|
-
reachable.add(dependency);
|
|
87
|
-
visit(dependency);
|
|
88
|
-
}
|
|
72
|
+
byFile.get(file).add(target.title);
|
|
73
|
+
if (target.projectName) {
|
|
74
|
+
targetProjects.add(target.projectName);
|
|
89
75
|
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return targets.filter((name) => !reachable.has(name));
|
|
76
|
+
}
|
|
77
|
+
return { byFile, targetProjects };
|
|
93
78
|
}
|
|
94
79
|
/**
|
|
95
|
-
* Pure decision: should the test in `file` with `title`
|
|
96
|
-
* during the heal rerun? The plan is fully
|
|
97
|
-
* already expanded into it by the
|
|
80
|
+
* Pure decision: should the test in `file` with `title` (owned by project
|
|
81
|
+
* `projectName`) actually execute during the heal rerun? The plan is fully
|
|
82
|
+
* explicit — serial companions were already expanded into it by the
|
|
83
|
+
* orchestrator.
|
|
84
|
+
*
|
|
85
|
+
* Dependency/setup projects (auth login, fixture seeding, …) are pulled in by
|
|
86
|
+
* Playwright because a target project declares them in `dependencies`. They
|
|
87
|
+
* are never themselves heal targets, but their tests MUST run so the state the
|
|
88
|
+
* targets depend on (storage-state auth files, seeded documents) is in place
|
|
89
|
+
* before the rerun. The rerun command only ever passes `--project=<target>`,
|
|
90
|
+
* so any project Playwright runs that is not a target project is, by
|
|
91
|
+
* construction, such a dependency project — run it in full. Guarded on a
|
|
92
|
+
* non-empty target-project set so the gate degrades to pure file+title
|
|
93
|
+
* matching when project names are unavailable.
|
|
98
94
|
*/
|
|
99
95
|
function shouldRunDuringHealRerun(params) {
|
|
100
|
-
const
|
|
96
|
+
const { index, projectName } = params;
|
|
97
|
+
if (projectName !== undefined &&
|
|
98
|
+
index.targetProjects.size > 0 &&
|
|
99
|
+
!index.targetProjects.has(projectName)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const titles = index.byFile.get(path_1.default.resolve(params.file));
|
|
101
103
|
return titles?.has(params.title) ?? false;
|
|
102
104
|
}
|
|
103
105
|
/**
|
|
@@ -160,61 +162,47 @@ function expandTargetsWithSerialCompanions(targets, initialReport) {
|
|
|
160
162
|
}
|
|
161
163
|
return expanded;
|
|
162
164
|
}
|
|
163
|
-
let
|
|
164
|
-
function
|
|
165
|
-
if (planPathOverride === undefined &&
|
|
166
|
-
return
|
|
165
|
+
let cachedPlanIndex;
|
|
166
|
+
function getPlanIndex(planPathOverride) {
|
|
167
|
+
if (planPathOverride === undefined && cachedPlanIndex !== undefined) {
|
|
168
|
+
return cachedPlanIndex;
|
|
167
169
|
}
|
|
168
170
|
const planPath = planPathOverride ?? envVars_1.env.data.DONOBU_AUTO_HEAL_PLAN_PATH;
|
|
169
|
-
let
|
|
171
|
+
let index = null;
|
|
170
172
|
if (planPath) {
|
|
171
173
|
try {
|
|
172
174
|
const raw = fs_1.default.readFileSync(planPath, 'utf8');
|
|
173
|
-
|
|
174
|
-
loaded = {
|
|
175
|
-
index: buildPlanIndex(plan),
|
|
176
|
-
gatedProjects: new Set(plan.gatedProjects ??
|
|
177
|
-
// Older plans carry no project list — gate the targets' own
|
|
178
|
-
// projects, leaving everything else (dependencies) untouched.
|
|
179
|
-
plan.targets
|
|
180
|
-
?.map((target) => target.projectName)
|
|
181
|
-
.filter((name) => Boolean(name)) ??
|
|
182
|
-
[]),
|
|
183
|
-
};
|
|
175
|
+
index = buildPlanIndex(JSON.parse(raw));
|
|
184
176
|
}
|
|
185
177
|
catch (error) {
|
|
186
178
|
Logger_1.appLogger.warn(`Auto-heal rerun plan at ${planPath} could not be read; running all collected tests.`, error);
|
|
187
|
-
|
|
179
|
+
index = null;
|
|
188
180
|
}
|
|
189
181
|
}
|
|
190
182
|
if (planPathOverride === undefined) {
|
|
191
|
-
|
|
183
|
+
cachedPlanIndex = index;
|
|
192
184
|
}
|
|
193
|
-
return
|
|
185
|
+
return index;
|
|
194
186
|
}
|
|
195
187
|
/** Test-only: reset the memoized plan so each test can load its own. */
|
|
196
188
|
function resetHealRerunPlanCacheForTesting() {
|
|
197
|
-
|
|
189
|
+
cachedPlanIndex = undefined;
|
|
198
190
|
}
|
|
199
191
|
/**
|
|
200
192
|
* Called from the Donobu auto fixture before any browser fixture initializes.
|
|
201
|
-
* Outside heal reruns this is a no-op. During a rerun, tests
|
|
202
|
-
*
|
|
203
|
-
* no context, no page, no cost. Tests in ungated projects (declared
|
|
204
|
-
* dependencies of the targets, teardown projects) always run.
|
|
193
|
+
* Outside heal reruns this is a no-op. During a rerun, tests outside the plan
|
|
194
|
+
* are annotated and skipped on the spot — no context, no page, no cost.
|
|
205
195
|
*/
|
|
206
196
|
function maybeSkipForHealRerun(testInfo, options) {
|
|
207
|
-
const
|
|
208
|
-
if (!
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (!plan.gatedProjects.has(testInfo.project.name)) {
|
|
197
|
+
const index = getPlanIndex(options?.planPath);
|
|
198
|
+
if (!index) {
|
|
212
199
|
return;
|
|
213
200
|
}
|
|
214
201
|
const shouldRun = shouldRunDuringHealRerun({
|
|
215
|
-
index
|
|
202
|
+
index,
|
|
216
203
|
file: testInfo.file,
|
|
217
204
|
title: testInfo.title,
|
|
205
|
+
projectName: testInfo.project?.name,
|
|
218
206
|
});
|
|
219
207
|
if (shouldRun) {
|
|
220
208
|
return;
|
|
@@ -122,28 +122,44 @@ async function waitForPendingVideoPersists(timeoutMs) {
|
|
|
122
122
|
* - `'on'` → always retain → always persist.
|
|
123
123
|
* - `'retain-on-failure'` → retain only on non-passing → persist
|
|
124
124
|
* only when status !== 'passed'.
|
|
125
|
+
* - `'retain-on-first-failure'` → recorded only on the first run; a video
|
|
126
|
+
* only exists when that run failed → same
|
|
127
|
+
* persist condition as 'retain-on-failure'.
|
|
128
|
+
* - `'retain-on-failure-and-retries'` → retain on non-passing OR on any
|
|
129
|
+
* retry → persist when status !== 'passed'
|
|
130
|
+
* or retry > 0.
|
|
125
131
|
* - `'on-first-retry'` → recorded only on retries; if a video
|
|
126
132
|
* exists, we're on a retry → persist.
|
|
133
|
+
* - `'on-all-retries'` → recorded on every retry; same as
|
|
134
|
+
* 'on-first-retry' once a video exists.
|
|
127
135
|
* - any unknown future mode → conservatively SKIP, with a warn log.
|
|
128
136
|
* Better to under-persist than violate
|
|
129
137
|
* user intent for a mode we don't yet
|
|
130
138
|
* understand.
|
|
131
139
|
*/
|
|
132
|
-
function shouldPersistVideo(videoOption, status) {
|
|
140
|
+
function shouldPersistVideo(videoOption, status, retry) {
|
|
133
141
|
const mode = typeof videoOption === 'string' ? videoOption : videoOption?.mode;
|
|
134
142
|
switch (mode) {
|
|
135
143
|
case 'on':
|
|
136
144
|
return true;
|
|
137
145
|
case 'on-first-retry':
|
|
138
|
-
|
|
139
|
-
//
|
|
146
|
+
case 'on-all-retries':
|
|
147
|
+
// Playwright only records on retries under these modes; if a video
|
|
148
|
+
// exists it implies we're on a retry — always retain.
|
|
140
149
|
return true;
|
|
141
150
|
case 'retry-with-video':
|
|
142
151
|
// Deprecated alias for 'on-first-retry' that Playwright still
|
|
143
152
|
// accepts on the type. Same retain semantics.
|
|
144
153
|
return true;
|
|
145
154
|
case 'retain-on-failure':
|
|
155
|
+
case 'retain-on-first-failure':
|
|
156
|
+
// Both record up front and discard on a passing run. ('first-failure'
|
|
157
|
+
// only records the first run, but a video existing already implies
|
|
158
|
+
// that's the run we're persisting.)
|
|
146
159
|
return status !== 'passed';
|
|
160
|
+
case 'retain-on-failure-and-retries':
|
|
161
|
+
// Records every run; kept on failure OR on any retry attempt.
|
|
162
|
+
return status !== 'passed' || retry > 0;
|
|
147
163
|
case 'off':
|
|
148
164
|
case undefined:
|
|
149
165
|
return false;
|
|
@@ -182,7 +198,7 @@ function persistVideoIfApplicable(page, testInfo, videoOption) {
|
|
|
182
198
|
// and this isn't a retry, etc. Nothing to do.
|
|
183
199
|
return;
|
|
184
200
|
}
|
|
185
|
-
if (!shouldPersistVideo(videoOption, testInfo.status)) {
|
|
201
|
+
if (!shouldPersistVideo(videoOption, testInfo.status, testInfo.retry)) {
|
|
186
202
|
Logger_1.appLogger.info(`Skipping video persist for flow ${flowId}: video mode ` +
|
|
187
203
|
`"${describeVideoMode(videoOption)}" + status "${testInfo.status}" ` +
|
|
188
204
|
`means Playwright will discard the file and we'd be violating user ` +
|
|
@@ -5,11 +5,16 @@ export declare class InteractionVisualizer {
|
|
|
5
5
|
private static readonly TIP_X;
|
|
6
6
|
private static readonly TIP_Y;
|
|
7
7
|
private static readonly SVG_MOUSE;
|
|
8
|
-
|
|
9
|
-
private static readonly
|
|
10
|
-
private static readonly
|
|
11
|
-
|
|
8
|
+
/** Message tooltip box styling — mirrors the legacy `.donobu-message` rule. */
|
|
9
|
+
private static readonly MESSAGE_MAX_WIDTH;
|
|
10
|
+
private static readonly MESSAGE_MARGIN;
|
|
11
|
+
/**
|
|
12
|
+
* Per-page cursor state. Keyed weakly so closed/GC'd pages drop out without
|
|
13
|
+
* us tracking page lifecycles.
|
|
14
|
+
*/
|
|
15
|
+
private readonly states;
|
|
12
16
|
constructor(defaultMessageDurationMillis: number);
|
|
17
|
+
private static escapeHtml;
|
|
13
18
|
/**
|
|
14
19
|
* Moves the virtual cursor to the center of the specified element and optionally displays a message.
|
|
15
20
|
*
|
|
@@ -22,39 +27,45 @@ export declare class InteractionVisualizer {
|
|
|
22
27
|
* @returns Promise that resolves when the cursor movement and message display are complete
|
|
23
28
|
*
|
|
24
29
|
* @remarks
|
|
25
|
-
* - The target element will be scrolled into view if necessary
|
|
26
30
|
* - The cursor animates smoothly to the element's center point
|
|
27
31
|
* - Messages are positioned automatically to avoid viewport edges
|
|
28
|
-
* -
|
|
32
|
+
* - Overlays are `pointer-events: none`, so they never intercept real interactions
|
|
29
33
|
*/
|
|
30
34
|
pointAt(page: Page, locator?: Pick<Locator, 'boundingBox'>, message?: string, duration?: number): Promise<void>;
|
|
31
35
|
/**
|
|
32
|
-
* Shows the virtual mouse cursor on the page.
|
|
36
|
+
* Shows the virtual mouse cursor on the page at its current position.
|
|
33
37
|
*
|
|
34
38
|
* @param page - The Playwright page instance where the cursor will be displayed.
|
|
35
|
-
*
|
|
36
39
|
* @returns Promise that resolves when the cursor is shown.
|
|
37
|
-
*
|
|
38
|
-
* @remarks
|
|
39
|
-
* - If the cursor doesn't exist yet, it will be created.
|
|
40
|
-
* - The cursor will be made visible with a smooth opacity transition.
|
|
41
40
|
*/
|
|
42
41
|
showMouse(page: Page): Promise<void>;
|
|
43
42
|
/**
|
|
44
43
|
* Hides the virtual mouse cursor on the page.
|
|
45
44
|
*
|
|
46
45
|
* @param page - The Playwright page instance where the cursor is displayed.
|
|
47
|
-
*
|
|
48
|
-
* @returns Promise that resolves when the cursor is hidden
|
|
49
|
-
*
|
|
50
|
-
* @remarks
|
|
51
|
-
* - The cursor will be hidden with a smooth opacity transition
|
|
52
|
-
* - The cursor element remains in the DOM but becomes invisible
|
|
46
|
+
* @returns Promise that resolves when the cursor is hidden.
|
|
53
47
|
*/
|
|
54
48
|
hideMouse(page: Page): Promise<void>;
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
private getState;
|
|
50
|
+
/**
|
|
51
|
+
* Replaces the sticky cursor overlay with one positioned at `to`, gliding
|
|
52
|
+
* from `from` when `animMs > 0`. We show the new overlay before disposing
|
|
53
|
+
* the old one to avoid a visible flicker, and tolerate a stale disposable
|
|
54
|
+
* (e.g. cleared by a navigation) by swallowing its dispose error.
|
|
55
|
+
*/
|
|
56
|
+
private renderCursor;
|
|
58
57
|
private showMessage;
|
|
58
|
+
/**
|
|
59
|
+
* Builds the cursor overlay markup: the arrow SVG plus a `@keyframes` glide
|
|
60
|
+
* from the previous position and a one-shot ripple at the arrow tip.
|
|
61
|
+
*/
|
|
62
|
+
private buildCursorHtml;
|
|
63
|
+
/**
|
|
64
|
+
* Builds the message tooltip markup, positioned near `target` and clamped
|
|
65
|
+
* inside the viewport. Without being able to measure the rendered box, we
|
|
66
|
+
* keep it on-screen with a conservative max-width clamp and anchor it above
|
|
67
|
+
* the cursor (via `translateY(-100%)`) when the cursor sits low in the page.
|
|
68
|
+
*/
|
|
69
|
+
private buildMessageHtml;
|
|
59
70
|
}
|
|
60
71
|
//# sourceMappingURL=InteractionVisualizer.d.ts.map
|