donobu 5.60.4 → 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/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/utils/BrowserUtils.js +1 -16
- 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/utils/BrowserUtils.js +1 -16
- package/package.json +7 -7
|
@@ -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
|
|
@@ -6,11 +6,22 @@ const PlaywrightUtils_1 = require("../utils/PlaywrightUtils");
|
|
|
6
6
|
class InteractionVisualizer {
|
|
7
7
|
constructor(defaultMessageDurationMillis) {
|
|
8
8
|
this.defaultMessageDurationMillis = defaultMessageDurationMillis;
|
|
9
|
-
|
|
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, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"');
|
|
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
|
-
* -
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
state.pos = target;
|
|
44
56
|
await Promise.all([
|
|
45
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
await
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
131
|
+
await previous.dispose();
|
|
159
132
|
}
|
|
160
|
-
catch
|
|
161
|
-
//
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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);
|
|
@@ -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
|
|
@@ -6,11 +6,22 @@ const PlaywrightUtils_1 = require("../utils/PlaywrightUtils");
|
|
|
6
6
|
class InteractionVisualizer {
|
|
7
7
|
constructor(defaultMessageDurationMillis) {
|
|
8
8
|
this.defaultMessageDurationMillis = defaultMessageDurationMillis;
|
|
9
|
-
|
|
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, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"');
|
|
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
|
-
* -
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
state.pos = target;
|
|
44
56
|
await Promise.all([
|
|
45
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
await
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
131
|
+
await previous.dispose();
|
|
159
132
|
}
|
|
160
|
-
catch
|
|
161
|
-
//
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "donobu",
|
|
3
|
-
"version": "5.60.
|
|
3
|
+
"version": "5.60.5",
|
|
4
4
|
"description": "Create browser automations with an LLM agent and replay them as Playwright scripts.",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"module": "dist/esm/main.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"license": "UNLICENSED",
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@eslint/js": "^10.0.1",
|
|
49
|
-
"@playwright/test": "^1.
|
|
49
|
+
"@playwright/test": "^1.61.0",
|
|
50
50
|
"@types/better-sqlite3": "^7.6.13",
|
|
51
51
|
"@types/express": "^5.0.6",
|
|
52
52
|
"@types/node": "^22.10.5",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"eslint-plugin-perfectionist": "^5.9.0",
|
|
60
60
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
61
61
|
"globals": "^16.0.0",
|
|
62
|
-
"playwright": "^1.
|
|
63
|
-
"playwright-core": "^1.
|
|
62
|
+
"playwright": "^1.61.0",
|
|
63
|
+
"playwright-core": "^1.61.0",
|
|
64
64
|
"typescript-eslint": "^8.47.0",
|
|
65
65
|
"vitest": "^4.0.17",
|
|
66
66
|
"winston-transport": "^4.9.0"
|
|
@@ -86,9 +86,9 @@
|
|
|
86
86
|
"zod": "^4.3.5"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@playwright/test": ">=1.
|
|
90
|
-
"playwright": ">=1.
|
|
91
|
-
"playwright-core": ">=1.
|
|
89
|
+
"@playwright/test": ">=1.61.0",
|
|
90
|
+
"playwright": ">=1.61.0",
|
|
91
|
+
"playwright-core": ">=1.61.0"
|
|
92
92
|
},
|
|
93
93
|
"peerDependenciesMeta": {
|
|
94
94
|
"playwright": {
|