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.
@@ -6,11 +6,22 @@ const PlaywrightUtils_1 = require("../utils/PlaywrightUtils");
6
6
  class InteractionVisualizer {
7
7
  constructor(defaultMessageDurationMillis) {
8
8
  this.defaultMessageDurationMillis = defaultMessageDurationMillis;
9
- this.cursorPos = { x: 150, y: 150 };
9
+ /**
10
+ * Per-page cursor state. Keyed weakly so closed/GC'd pages drop out without
11
+ * us tracking page lifecycles.
12
+ */
13
+ this.states = new WeakMap();
10
14
  }
11
15
  /* ------------------------------------------------------------------ */
12
16
  /* Public API */
13
17
  /* ------------------------------------------------------------------ */
18
+ static escapeHtml(text) {
19
+ return text
20
+ .replace(/&/g, '&')
21
+ .replace(/</g, '&lt;')
22
+ .replace(/>/g, '&gt;')
23
+ .replace(/"/g, '&quot;');
24
+ }
14
25
  /**
15
26
  * Moves the virtual cursor to the center of the specified element and optionally displays a message.
16
27
  *
@@ -23,26 +34,29 @@ class InteractionVisualizer {
23
34
  * @returns Promise that resolves when the cursor movement and message display are complete
24
35
  *
25
36
  * @remarks
26
- * - The target element will be scrolled into view if necessary
27
37
  * - The cursor animates smoothly to the element's center point
28
38
  * - Messages are positioned automatically to avoid viewport edges
29
- * - The virtual cursor does not interfere with actual page interactions
39
+ * - Overlays are `pointer-events: none`, so they never intercept real interactions
30
40
  */
31
41
  async pointAt(page, locator, message, duration = this.defaultMessageDurationMillis) {
32
42
  if (!duration || duration <= 0) {
33
43
  return;
34
44
  }
35
45
  try {
36
- let target = { x: this.cursorPos.x, y: this.cursorPos.y };
46
+ const state = this.getState(page);
47
+ const from = { ...state.pos };
48
+ let target = from;
37
49
  if (locator) {
38
50
  const box = await locator.boundingBox();
39
51
  if (box) {
40
52
  target = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
41
53
  }
42
54
  }
43
- await this.ensureContainer(page);
55
+ state.pos = target;
44
56
  await Promise.all([
45
- this.moveCursor(page, target, duration / 2),
57
+ state.visible
58
+ ? this.renderCursor(page, state, from, target, duration / 2)
59
+ : Promise.resolve(),
46
60
  message?.trim()
47
61
  ? this.showMessage(page, target, message.trim(), duration)
48
62
  : Promise.resolve(),
@@ -55,28 +69,16 @@ class InteractionVisualizer {
55
69
  }
56
70
  }
57
71
  /**
58
- * Shows the virtual mouse cursor on the page.
72
+ * Shows the virtual mouse cursor on the page at its current position.
59
73
  *
60
74
  * @param page - The Playwright page instance where the cursor will be displayed.
61
- *
62
75
  * @returns Promise that resolves when the cursor is shown.
63
- *
64
- * @remarks
65
- * - If the cursor doesn't exist yet, it will be created.
66
- * - The cursor will be made visible with a smooth opacity transition.
67
76
  */
68
77
  async showMouse(page) {
69
78
  try {
70
- await this.ensureContainer(page);
71
- await this.ensureCursor(page);
72
- await page.evaluate(([containerId]) => {
73
- const root = document.getElementById(containerId)
74
- .shadowRoot;
75
- const cursor = root.querySelector('.donobu-virtual-mouse');
76
- if (cursor) {
77
- cursor.classList.remove('hidden');
78
- }
79
- }, [InteractionVisualizer.CONTAINER_ID]);
79
+ const state = this.getState(page);
80
+ state.visible = true;
81
+ await this.renderCursor(page, state, state.pos, state.pos, 0);
80
82
  }
81
83
  catch (error) {
82
84
  if (!PlaywrightUtils_1.PlaywrightUtils.isPageClosedError(error)) {
@@ -84,30 +86,22 @@ class InteractionVisualizer {
84
86
  }
85
87
  }
86
88
  }
89
+ /* ------------------------------------------------------------------ */
90
+ /* Private helpers */
91
+ /* ------------------------------------------------------------------ */
87
92
  /**
88
93
  * Hides the virtual mouse cursor on the page.
89
94
  *
90
95
  * @param page - The Playwright page instance where the cursor is displayed.
91
- *
92
- * @returns Promise that resolves when the cursor is hidden
93
- *
94
- * @remarks
95
- * - The cursor will be hidden with a smooth opacity transition
96
- * - The cursor element remains in the DOM but becomes invisible
96
+ * @returns Promise that resolves when the cursor is hidden.
97
97
  */
98
98
  async hideMouse(page) {
99
99
  try {
100
- const id = InteractionVisualizer.CONTAINER_ID;
101
- await page.evaluate(([containerId]) => {
102
- const container = document.getElementById(containerId);
103
- if (!container?.shadowRoot) {
104
- return;
105
- }
106
- const cursor = container.shadowRoot.querySelector('.donobu-virtual-mouse');
107
- if (cursor) {
108
- cursor.classList.add('hidden');
109
- }
110
- }, [id]);
100
+ const state = this.getState(page);
101
+ state.visible = false;
102
+ const cursor = state.cursor;
103
+ state.cursor = undefined;
104
+ await cursor?.dispose();
111
105
  }
112
106
  catch (error) {
113
107
  if (!PlaywrightUtils_1.PlaywrightUtils.isPageClosedError(error)) {
@@ -115,138 +109,110 @@ class InteractionVisualizer {
115
109
  }
116
110
  }
117
111
  }
118
- /* ------------------------------------------------------------------ */
119
- /* Private helpers */
120
- /* ------------------------------------------------------------------ */
121
- async ensureContainer(page) {
122
- const id = InteractionVisualizer.CONTAINER_ID;
123
- await page.evaluate(([containerId, css]) => {
124
- if (document.getElementById(containerId)) {
125
- return;
126
- }
127
- const el = document.createElement('div');
128
- el.id = containerId;
129
- Object.assign(el.style, {
130
- position: 'fixed',
131
- top: '0',
132
- left: '0',
133
- width: '100vw',
134
- height: '100vh',
135
- pointerEvents: 'none',
136
- zIndex: '2147483646', // just below the message itself
137
- });
138
- const shadow = el.attachShadow({ mode: 'open' });
139
- const style = document.createElement('style');
140
- style.textContent = css;
141
- shadow.appendChild(style);
142
- // Append to <html> so it is not clipped by overflow/transform on <body>
143
- (document.documentElement || document.body).appendChild(el);
144
- }, [id, InteractionVisualizer.CSS]);
112
+ getState(page) {
113
+ let state = this.states.get(page);
114
+ if (!state) {
115
+ state = { pos: { x: 150, y: 150 }, visible: false };
116
+ this.states.set(page, state);
117
+ }
118
+ return state;
145
119
  }
146
- async ensureCursor(page) {
147
- const id = InteractionVisualizer.CONTAINER_ID;
148
- await page.evaluate(([containerId, position, svg, svgAttrs, tipX, tipY]) => {
149
- const root = document.getElementById(containerId).shadowRoot;
150
- let cursor = root.querySelector('.donobu-virtual-mouse');
151
- if (cursor) {
152
- return;
153
- }
154
- cursor = document.createElement('div');
155
- cursor.className = 'donobu-virtual-mouse hidden';
156
- // Hybrid approach: try insertAdjacentHTML first, fallback to createElementNS
120
+ /**
121
+ * Replaces the sticky cursor overlay with one positioned at `to`, gliding
122
+ * from `from` when `animMs > 0`. We show the new overlay before disposing
123
+ * the old one to avoid a visible flicker, and tolerate a stale disposable
124
+ * (e.g. cleared by a navigation) by swallowing its dispose error.
125
+ */
126
+ async renderCursor(page, state, from, to, animMs) {
127
+ const previous = state.cursor;
128
+ state.cursor = await page.screencast.showOverlay(this.buildCursorHtml(from, to, animMs));
129
+ if (previous) {
157
130
  try {
158
- cursor.insertAdjacentHTML('beforeend', svg);
131
+ await previous.dispose();
159
132
  }
160
- catch (_error) {
161
- // Fallback: create SVG programmatically using attributes object
162
- const attrs = JSON.parse(svgAttrs);
163
- const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
164
- // Set SVG attributes
165
- Object.entries(attrs.svg).forEach(([key, value]) => {
166
- svgElement.setAttribute(key, value);
167
- });
168
- // Create and add path element
169
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
170
- Object.entries(attrs.path).forEach(([key, value]) => {
171
- path.setAttribute(key, value);
172
- });
173
- svgElement.appendChild(path);
174
- cursor.appendChild(svgElement);
133
+ catch {
134
+ // Stale overlay (page navigated/closed) the fresh one already replaced it.
175
135
  }
176
- const pos = position;
177
- cursor.style.transitionDuration = '0s';
178
- cursor.style.transform = `translate(${pos.x - tipX}px, ${pos.y - tipY}px)`;
179
- root.appendChild(cursor);
180
- }, [
181
- id,
182
- this.cursorPos,
183
- InteractionVisualizer.SVG_MOUSE,
184
- JSON.stringify(InteractionVisualizer.SVG_MOUSE_JSON),
185
- InteractionVisualizer.TIP_X,
186
- InteractionVisualizer.TIP_Y,
187
- ]);
188
- }
189
- async moveCursor(page, target, duration) {
190
- await this.ensureCursor(page);
191
- const id = InteractionVisualizer.CONTAINER_ID;
192
- await page.evaluate(([containerId, end, duration, tipX, tipY]) => {
193
- const root = document.getElementById(containerId).shadowRoot;
194
- const cursor = root.querySelector('.donobu-virtual-mouse');
195
- cursor.style.transitionDuration = `${duration}ms`;
196
- const endPos = end;
197
- cursor.style.transform = `translate(${endPos.x - tipX}px, ${endPos.y - tipY}px)`;
198
- cursor.classList.add('rippling');
199
- const done = new Promise((resolve) => {
200
- const finish = () => {
201
- cursor.removeEventListener('transitionend', finish);
202
- cursor.classList.remove('rippling');
203
- resolve();
204
- };
205
- cursor.addEventListener('transitionend', finish);
206
- setTimeout(finish, duration + 50);
136
+ }
137
+ // showOverlay resolves the moment the overlay is added, before the CSS
138
+ // glide plays. Callers (e.g. ClickTool) await pointAt expecting the cursor
139
+ // to reach the element *before* they act, so block for the animation here.
140
+ if (animMs > 0) {
141
+ await new Promise((resolve) => {
142
+ setTimeout(resolve, animMs);
207
143
  });
208
- return done;
209
- }, [
210
- id,
211
- target,
212
- duration,
213
- InteractionVisualizer.TIP_X,
214
- InteractionVisualizer.TIP_Y,
215
- ]);
216
- this.cursorPos = target;
144
+ }
217
145
  }
218
146
  async showMessage(page, target, text, duration) {
219
- const id = InteractionVisualizer.CONTAINER_ID;
220
- await page.evaluate(([containerId, target, text, duration]) => {
221
- containerId = containerId;
222
- target = target;
223
- text = text;
224
- duration = duration;
225
- const root = document.getElementById(containerId)
226
- .shadowRoot;
227
- const msg = document.createElement('div');
228
- msg.className = 'donobu-message';
229
- msg.textContent = text;
230
- root.appendChild(msg);
231
- /* ── compute final coordinates ────────────────────────────────── */
232
- const { width, height } = msg.getBoundingClientRect();
233
- let x = target.x - width / 2;
234
- let y = target.y + 24; // default - below cursor
235
- /* clamp horizontally inside viewport */
236
- x = Math.max(8, Math.min(window.innerWidth - width - 8, x));
237
- /* if it overflows bottom, flip above the element */
238
- if (y + height + 8 > window.innerHeight) {
239
- y = target.y - height - 24;
240
- }
241
- /* if still off-screen at the top, clamp to 8 px margin */
242
- if (y < 8) {
243
- y = 8;
244
- }
245
- msg.style.left = `${x}px`;
246
- msg.style.top = `${y}px`;
247
- /* auto-remove after the requested duration */
248
- setTimeout(() => msg.remove(), duration);
249
- }, [id, target, text, duration]);
147
+ // Sticky overlay + self-managed removal. The `duration` option on
148
+ // showOverlay only composites into the recorded screencast stream, not the
149
+ // live page render layer (so it would be invisible in headed/screenshots);
150
+ // a no-duration overlay renders everywhere, matching the cursor.
151
+ const handle = await page.screencast.showOverlay(this.buildMessageHtml(page, target, text));
152
+ const timer = setTimeout(() => {
153
+ handle.dispose().catch(() => {
154
+ // Page navigated/closed before the message expired — nothing to clean up.
155
+ });
156
+ }, duration);
157
+ // Don't let a pending message removal keep the process alive on shutdown.
158
+ timer.unref?.();
159
+ }
160
+ /**
161
+ * Builds the cursor overlay markup: the arrow SVG plus a `@keyframes` glide
162
+ * from the previous position and a one-shot ripple at the arrow tip.
163
+ */
164
+ buildCursorHtml(from, to, animMs) {
165
+ const tipX = InteractionVisualizer.TIP_X;
166
+ const tipY = InteractionVisualizer.TIP_Y;
167
+ const fx = from.x - tipX;
168
+ const fy = from.y - tipY;
169
+ const tx = to.x - tipX;
170
+ const ty = to.y - tipY;
171
+ const animate = animMs > 0 && (from.x !== to.x || from.y !== to.y);
172
+ return `
173
+ <style>
174
+ @keyframes donobu-glide { from { transform: translate(${fx}px, ${fy}px); } to { transform: translate(${tx}px, ${ty}px); } }
175
+ @keyframes donobu-ripple { 0% { transform: translate(-50%,-50%) scale(.5); opacity:0; } 40% { transform: translate(-50%,-50%) scale(1); opacity:.5; } 100% { transform: translate(-50%,-50%) scale(2); opacity:0; } }
176
+ .donobu-cursor { position:fixed; left:0; top:0; width:32px; height:32px; z-index:2147483646; pointer-events:none; filter:drop-shadow(1px 1px 1px rgba(0,0,0,.5)); transform: translate(${tx}px, ${ty}px); ${animate ? `animation: donobu-glide ${animMs}ms ease-in-out;` : ''} }
177
+ .donobu-cursor svg { width:100%; height:100%; display:block; }
178
+ ${animate ? `.donobu-cursor::after { content:""; position:absolute; left:${tipX}px; top:${tipY}px; width:24px; height:24px; border-radius:50%; background:#FF781B; opacity:0; animation: donobu-ripple .6s ease-out ${animMs}ms both; }` : ''}
179
+ </style>
180
+ <div class="donobu-cursor">${InteractionVisualizer.SVG_MOUSE}</div>`;
181
+ }
182
+ /**
183
+ * Builds the message tooltip markup, positioned near `target` and clamped
184
+ * inside the viewport. Without being able to measure the rendered box, we
185
+ * keep it on-screen with a conservative max-width clamp and anchor it above
186
+ * the cursor (via `translateY(-100%)`) when the cursor sits low in the page.
187
+ */
188
+ buildMessageHtml(page, target, text) {
189
+ const maxW = InteractionVisualizer.MESSAGE_MAX_WIDTH;
190
+ const margin = InteractionVisualizer.MESSAGE_MARGIN;
191
+ const vp = page.viewportSize() ?? { width: 1280, height: 720 };
192
+ // Clamp the (centered) anchor so a max-width box can't overflow either edge.
193
+ const centerX = Math.max(maxW / 2 + margin, Math.min(vp.width - maxW / 2 - margin, target.x));
194
+ const placeAbove = target.y > vp.height * 0.6;
195
+ const top = placeAbove ? Math.max(margin, target.y - 24) : target.y + 24;
196
+ const translate = placeAbove
197
+ ? 'translate(-50%, -100%)'
198
+ : 'translate(-50%, 0)';
199
+ const style = [
200
+ 'position:fixed',
201
+ `left:${centerX}px`,
202
+ `top:${top}px`,
203
+ `transform:${translate}`,
204
+ `max-width:${maxW}px`,
205
+ 'z-index:2147483647',
206
+ 'pointer-events:none',
207
+ 'background:#000',
208
+ 'color:#fff',
209
+ 'padding:8px 10px',
210
+ 'border-radius:5px',
211
+ "font:12px/1 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif",
212
+ 'white-space:pre-wrap',
213
+ 'box-shadow:2px 2px 10px rgba(0,0,0,.2)',
214
+ ].join(';');
215
+ return `<div style="${style}">${InteractionVisualizer.escapeHtml(text)}</div>`;
250
216
  }
251
217
  }
252
218
  exports.InteractionVisualizer = InteractionVisualizer;
@@ -269,29 +235,7 @@ InteractionVisualizer.SVG_MOUSE = `
269
235
  stroke-linejoin="round">
270
236
  <path d="${InteractionVisualizer.RAW_MOUSE_D}" />
271
237
  </svg>`;
272
- InteractionVisualizer.SVG_MOUSE_JSON = {
273
- svg: {
274
- xmlns: 'http://www.w3.org/2000/svg',
275
- width: '32',
276
- height: '32',
277
- viewBox: '0 0 24 24',
278
- fill: 'oklch(13.09% 0.005 165.18)',
279
- stroke: '#FF781B',
280
- 'stroke-width': '2',
281
- 'stroke-linecap': 'round',
282
- 'stroke-linejoin': 'round',
283
- },
284
- path: {
285
- d: InteractionVisualizer.RAW_MOUSE_D,
286
- },
287
- };
288
- InteractionVisualizer.CSS = `
289
- .donobu-message{position:absolute;z-index:2147483647;background:#000;color:#fff;padding:8px 10px;border-radius:5px;font:12px/1 'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;max-width:300px;white-space:pre-wrap;box-shadow:2px 2px 10px rgba(0,0,0,.2);pointer-events:none}
290
- .donobu-virtual-mouse{width:24px;height:24px;position:absolute;z-index:2147483646;filter:drop-shadow(1px 1px 1px rgba(0,0,0,.5));transition:transform .15s ease-in-out;pointer-events:none;opacity:1;visibility:visible}
291
- .donobu-virtual-mouse.hidden{opacity:0;visibility:hidden}
292
- .donobu-virtual-mouse svg{width:100%;height:100%;display:block}
293
- .donobu-virtual-mouse.rippling::after{content:"";position:absolute;left:${InteractionVisualizer.TIP_X}px;top:${InteractionVisualizer.TIP_Y}px;width:24px;height:24px;border-radius:50%;background:#FF781B;transform:translate(-50%,-50%) scale(.5);opacity:.4;animation:ripple-pop .6s ease-out forwards}
294
- @keyframes ripple-pop{40%{transform:translate(-50%,-50%) scale(1);opacity:.5}100%{transform:translate(-50%,-50%) scale(2);opacity:0}}
295
- `;
296
- InteractionVisualizer.CONTAINER_ID = 'donobu-iv-overlay';
238
+ /** Message tooltip box styling — mirrors the legacy `.donobu-message` rule. */
239
+ InteractionVisualizer.MESSAGE_MAX_WIDTH = 300;
240
+ InteractionVisualizer.MESSAGE_MARGIN = 8;
297
241
  //# sourceMappingURL=InteractionVisualizer.js.map
@@ -41,27 +41,6 @@ function buildDonobuReport(resultsByTest, rootDir) {
41
41
  }
42
42
  byTitle.get(test.title).push(test);
43
43
  }
44
- // Declared project dependency graph (`FullProject.dependencies`), keyed by
45
- // project name. The auto-heal orchestrator uses it to keep the rerun gate
46
- // away from projects that are declared dependencies of heal targets — those
47
- // must run in full, exactly as Playwright schedules them.
48
- const projectDependencies = {};
49
- for (const test of resultsByTest.keys()) {
50
- try {
51
- let suite = test.parent;
52
- while (suite && suite.type !== 'project') {
53
- suite = suite.parent;
54
- }
55
- const project = suite?.project();
56
- if (project?.name && !(project.name in projectDependencies)) {
57
- projectDependencies[project.name] = [...(project.dependencies ?? [])];
58
- }
59
- }
60
- catch {
61
- // Reporter shape drift — omit the entry; the orchestrator degrades to
62
- // gating only the heal targets' own projects.
63
- }
64
- }
65
44
  const suites = [];
66
45
  for (const [file, titleMap] of byFile) {
67
46
  const specs = [];
@@ -137,7 +116,7 @@ function buildDonobuReport(resultsByTest, rootDir) {
137
116
  }
138
117
  suites.push({ file, specs });
139
118
  }
140
- return { suites, metadata: { projectDependencies } };
119
+ return { suites, metadata: {} };
141
120
  }
142
121
  /**
143
122
  * Whether any enclosing suite is in serial mode (`test.describe.serial` or
@@ -57,13 +57,6 @@ export interface DonobuReportMetadata {
57
57
  /** True on reports that are the result of merging an initial + heal run. */
58
58
  donobuMergedReport?: boolean;
59
59
  mergedAtIso?: string;
60
- /**
61
- * Declared Playwright project dependency graph (`FullProject.dependencies`)
62
- * keyed by project name, recorded by the reporter. The auto-heal
63
- * orchestrator uses it to exclude declared dependencies of heal targets
64
- * from the rerun gate — they must run in full.
65
- */
66
- projectDependencies?: Record<string, string[]>;
67
60
  sources?: {
68
61
  initial?: string | null;
69
62
  autoHeal?: string | null;
@@ -199,22 +199,7 @@ class BrowserUtils {
199
199
  // If the entry exists but doesn't have sessionStorage yet, add the property
200
200
  originEntry.sessionStorage = [];
201
201
  }
202
- // Extract sessionStorage from the page
203
- const sessionStorageItems = await page.evaluate(() => {
204
- const items = [];
205
- for (let i = 0; i < sessionStorage.length; i++) {
206
- const name = sessionStorage.key(i);
207
- if (name) {
208
- items.push({
209
- name,
210
- value: sessionStorage.getItem(name) || '',
211
- });
212
- }
213
- }
214
- return items;
215
- });
216
- // Add sessionStorage items to the origin entry
217
- originEntry.sessionStorage = sessionStorageItems;
202
+ originEntry.sessionStorage = await page.sessionStorage.items();
218
203
  }
219
204
  catch (error) {
220
205
  Logger_1.appLogger.warn(`Failed to extract sessionStorage for page: ${pageUrl}`, error);
@@ -22,7 +22,15 @@
22
22
  * `expandTargetsWithSerialCompanions`) using the `serialScoped` flags the
23
23
  * Donobu reporter recorded during the initial run — the runner process
24
24
  * sees the suite tree; the worker (where this gate runs) does not.
25
- * - Declared dependency projects, which Playwright always runs in full.
25
+ * - Declared dependency (setup) projects, in full. Playwright schedules them
26
+ * because a target project lists them in `dependencies`, and the rerun
27
+ * command only ever passes `--project=<target>`, so any project that runs
28
+ * and is NOT a heal-target project is, by construction, such a dependency
29
+ * project. The gate exempts every test whose project is not a target
30
+ * project, so the auth/storage-state and fixture seeding those projects
31
+ * perform actually runs. (A prior version skipped them too — every test
32
+ * not literally in the plan — which broke targets that depend on the
33
+ * `.auth` storage state a setup project produces.)
26
34
  *
27
35
  * Implicit ordering (checkpoint files between plain tests, cross-file state
28
36
  * with `workers: 1`) is deliberately NOT honored: tests relying on it will
@@ -44,34 +52,43 @@ export interface HealRerunPlan {
44
52
  title: string;
45
53
  projectName?: string;
46
54
  }>;
47
- /**
48
- * Projects the gate applies to. Tests in any other project — declared
49
- * dependency projects (auth/setup), teardown projects — run untouched,
50
- * exactly as Playwright schedules them. Computed by the orchestrator via
51
- * `computeGatedProjects`; when absent, the gate falls back to the targets'
52
- * own project names.
53
- */
54
- gatedProjects?: string[];
55
55
  }
56
- /** Targets indexed by absolute spec path for O(1) per-test decisions. */
57
- export type HealRerunPlanIndex = Map<string, Set<string>>;
58
- export declare function buildPlanIndex(plan: HealRerunPlan): HealRerunPlanIndex;
59
56
  /**
60
- * The projects the rerun gate should apply to: the heal targets' own
61
- * projects, minus any project that is a declared (transitive) dependency of
62
- * another target project the dependency declaration wins, and that project
63
- * runs in full.
57
+ * Heal-rerun targets indexed for O(1) per-test decisions.
58
+ *
59
+ * `byFile` maps an absolute spec path to the heal-target titles in that file.
60
+ * `targetProjects` is the set of projects that own those targets; the rerun is
61
+ * launched with `--project=<target>` only, so any project Playwright also runs
62
+ * is there purely as a declared dependency (setup) project — see
63
+ * `shouldRunDuringHealRerun`. Empty when no target carries a project name, in
64
+ * which case the gate degrades to pure file+title matching.
64
65
  */
65
- export declare function computeGatedProjects(targetProjects: Array<string | undefined>, projectDependencies: Record<string, string[]> | undefined): string[];
66
+ export interface HealRerunPlanIndex {
67
+ byFile: Map<string, Set<string>>;
68
+ targetProjects: Set<string>;
69
+ }
70
+ export declare function buildPlanIndex(plan: HealRerunPlan): HealRerunPlanIndex;
66
71
  /**
67
- * Pure decision: should the test in `file` with `title` actually execute
68
- * during the heal rerun? The plan is fully explicit — serial companions were
69
- * already expanded into it by the orchestrator.
72
+ * Pure decision: should the test in `file` with `title` (owned by project
73
+ * `projectName`) actually execute during the heal rerun? The plan is fully
74
+ * explicit — serial companions were already expanded into it by the
75
+ * orchestrator.
76
+ *
77
+ * Dependency/setup projects (auth login, fixture seeding, …) are pulled in by
78
+ * Playwright because a target project declares them in `dependencies`. They
79
+ * are never themselves heal targets, but their tests MUST run so the state the
80
+ * targets depend on (storage-state auth files, seeded documents) is in place
81
+ * before the rerun. The rerun command only ever passes `--project=<target>`,
82
+ * so any project Playwright runs that is not a target project is, by
83
+ * construction, such a dependency project — run it in full. Guarded on a
84
+ * non-empty target-project set so the gate degrades to pure file+title
85
+ * matching when project names are unavailable.
70
86
  */
71
87
  export declare function shouldRunDuringHealRerun(params: {
72
88
  index: HealRerunPlanIndex;
73
89
  file: string;
74
90
  title: string;
91
+ projectName?: string;
75
92
  }): boolean;
76
93
  /**
77
94
  * Expand heal targets with their `describe.serial` siblings, using the
@@ -91,10 +108,8 @@ export declare function expandTargetsWithSerialCompanions(targets: HealRerunPlan
91
108
  export declare function resetHealRerunPlanCacheForTesting(): void;
92
109
  /**
93
110
  * Called from the Donobu auto fixture before any browser fixture initializes.
94
- * Outside heal reruns this is a no-op. During a rerun, tests in gated
95
- * projects that are outside the plan are annotated and skipped on the spot —
96
- * no context, no page, no cost. Tests in ungated projects (declared
97
- * dependencies of the targets, teardown projects) always run.
111
+ * Outside heal reruns this is a no-op. During a rerun, tests outside the plan
112
+ * are annotated and skipped on the spot — no context, no page, no cost.
98
113
  */
99
114
  export declare function maybeSkipForHealRerun(testInfo: TestInfo, options?: {
100
115
  planPath?: string;