autokap 1.5.3 → 1.5.4
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.
|
@@ -113,6 +113,14 @@ export function buildCursorOverlayScript(theme = 'minimal') {
|
|
|
113
113
|
triggerPulse();
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
+
// AUT-80 — Browser-side mousedown timestamp buffer. Real CDP-dispatched
|
|
117
|
+
// \`mousedown\` events fire here at the exact moment the click happens in
|
|
118
|
+
// the recorded video (the cursor pulse is purely decorative and fires
|
|
119
|
+
// slightly later). The runner reads this buffer after each click action
|
|
120
|
+
// and uses the timestamps for the mouse SFX track so audio = visual
|
|
121
|
+
// click, even across cursor animation latency and frame quantisation.
|
|
122
|
+
window.__akClickAt = [];
|
|
123
|
+
|
|
116
124
|
// Keep DOM event listeners as fallback for real mouse events (headed mode)
|
|
117
125
|
document.addEventListener('mousemove', function(e) {
|
|
118
126
|
setCursorPosition(e.clientX, e.clientY);
|
|
@@ -122,6 +130,7 @@ export function buildCursorOverlayScript(theme = 'minimal') {
|
|
|
122
130
|
setCursorPosition(e.clientX, e.clientY);
|
|
123
131
|
cursor.classList.add('__ak_pressed');
|
|
124
132
|
triggerPulse();
|
|
133
|
+
window.__akClickAt.push(Date.now());
|
|
125
134
|
}, true);
|
|
126
135
|
window.addEventListener('mouseup', function(e) {
|
|
127
136
|
setCursorPosition(e.clientX, e.clientY);
|
|
@@ -130,6 +139,14 @@ export function buildCursorOverlayScript(theme = 'minimal') {
|
|
|
130
139
|
window.addEventListener('click', function(e) {
|
|
131
140
|
setCursorPosition(e.clientX, e.clientY);
|
|
132
141
|
triggerPulse();
|
|
142
|
+
// Capture synthetic click() dispatches that bypass mousedown (e.g.
|
|
143
|
+
// dispatchEvent('click') from JS-dispatch opcode paths). Skip if a
|
|
144
|
+
// mousedown landed in the last 80 ms so we don't double-count a
|
|
145
|
+
// regular mouse-driven click.
|
|
146
|
+
var last = window.__akClickAt[window.__akClickAt.length - 1];
|
|
147
|
+
if (last == null || (Date.now() - last) > 80) {
|
|
148
|
+
window.__akClickAt.push(Date.now());
|
|
149
|
+
}
|
|
133
150
|
}, true);
|
|
134
151
|
}
|
|
135
152
|
|
|
@@ -165,4 +165,18 @@ export declare class WebPlaywrightLocal implements RuntimeAdapter {
|
|
|
165
165
|
private relativeClickPosition;
|
|
166
166
|
private moveClipCursorToPoint;
|
|
167
167
|
private emitClipClickPulse;
|
|
168
|
+
/**
|
|
169
|
+
* Drain the browser-side `__akClickAt` buffer for timestamps newer than
|
|
170
|
+
* `sinceMs`, replay them through `onClick`, and reset the buffer so the
|
|
171
|
+
* next click action starts fresh. This is what makes mouse SFX line up
|
|
172
|
+
* exactly with the visual click in the recorded video — the mousedown
|
|
173
|
+
* listener inside the cursor overlay timestamps each click at the same
|
|
174
|
+
* instant the browser dispatches it, which is also the frame the CDP
|
|
175
|
+
* screencast captures.
|
|
176
|
+
*
|
|
177
|
+
* Falls back to `sinceMs` (Node wall-clock at action dispatch) when the
|
|
178
|
+
* buffer is empty (e.g. `useKeyboard` Enter-press path, or transient
|
|
179
|
+
* page.evaluate failure).
|
|
180
|
+
*/
|
|
181
|
+
private reportClickSfxTimestamps;
|
|
168
182
|
}
|
|
@@ -77,12 +77,12 @@ export class WebPlaywrightLocal {
|
|
|
77
77
|
const page = await this.browser.currentPage;
|
|
78
78
|
const t0 = Date.now();
|
|
79
79
|
logger.debug(`[click] start selector="${selector}"${options?.useKeyboard ? ' mode=keyboard' : ''}${options?.useJsDispatch ? ' mode=js_dispatch' : ''}${options?.coordinates ? ` mode=coords(${options.coordinates.x},${options.coordinates.y})` : ''}`);
|
|
80
|
-
const fireClickSfx = () => options?.onClick?.(Date.now());
|
|
81
80
|
try {
|
|
82
81
|
if (options?.coordinates) {
|
|
83
82
|
await this.moveClipCursorToPoint(options.coordinates);
|
|
84
|
-
|
|
83
|
+
const dispatchedAt = Date.now();
|
|
85
84
|
await this.browser.clickByCoordinates(options.coordinates.x, options.coordinates.y);
|
|
85
|
+
await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
|
|
86
86
|
logger.debug(`[click] done coords took ${Date.now() - t0}ms`);
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
@@ -90,14 +90,17 @@ export class WebPlaywrightLocal {
|
|
|
90
90
|
const animatedTarget = await this.moveClipCursorToLocator(locator);
|
|
91
91
|
if (options?.useKeyboard) {
|
|
92
92
|
await locator.focus();
|
|
93
|
-
|
|
93
|
+
const dispatchedAt = Date.now();
|
|
94
94
|
await page.keyboard.press('Enter');
|
|
95
|
+
// No real mousedown fires on Enter — fall back to Node timing.
|
|
96
|
+
options?.onClick?.(dispatchedAt);
|
|
95
97
|
logger.debug(`[click] done keyboard took ${Date.now() - t0}ms`);
|
|
96
98
|
return;
|
|
97
99
|
}
|
|
98
100
|
if (options?.useJsDispatch) {
|
|
99
|
-
|
|
101
|
+
const dispatchedAt = Date.now();
|
|
100
102
|
await locator.dispatchEvent('click');
|
|
103
|
+
await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
|
|
101
104
|
logger.debug(`[click] done js_dispatch took ${Date.now() - t0}ms`);
|
|
102
105
|
return;
|
|
103
106
|
}
|
|
@@ -107,18 +110,19 @@ export class WebPlaywrightLocal {
|
|
|
107
110
|
? await this.relativeClickPosition(locator, animatedTarget)
|
|
108
111
|
: null;
|
|
109
112
|
if (options?.button && options.button !== 'left') {
|
|
110
|
-
|
|
113
|
+
const dispatchedAt = Date.now();
|
|
111
114
|
await locator.click({
|
|
112
115
|
button: options.button,
|
|
113
116
|
timeout: 5000,
|
|
114
117
|
force: options?.force,
|
|
115
118
|
...(clickPosition ? { position: clickPosition } : {}),
|
|
116
119
|
});
|
|
120
|
+
await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
|
|
117
121
|
logger.debug(`[click] done button=${options.button} took ${Date.now() - t0}ms`);
|
|
118
122
|
return;
|
|
119
123
|
}
|
|
124
|
+
const dispatchedAt = Date.now();
|
|
120
125
|
if (clickPosition) {
|
|
121
|
-
fireClickSfx();
|
|
122
126
|
await locator.click({
|
|
123
127
|
timeout: 5000,
|
|
124
128
|
force: options?.force,
|
|
@@ -126,9 +130,9 @@ export class WebPlaywrightLocal {
|
|
|
126
130
|
});
|
|
127
131
|
}
|
|
128
132
|
else {
|
|
129
|
-
fireClickSfx();
|
|
130
133
|
await this.browser.clickBySelector(selector, { force: options?.force });
|
|
131
134
|
}
|
|
135
|
+
await this.reportClickSfxTimestamps(page, dispatchedAt, options?.onClick);
|
|
132
136
|
await this.emitClipClickPulse();
|
|
133
137
|
logger.debug(`[click] done normal took ${Date.now() - t0}ms`);
|
|
134
138
|
}
|
|
@@ -151,11 +155,12 @@ export class WebPlaywrightLocal {
|
|
|
151
155
|
const position = target
|
|
152
156
|
? await this.relativeClickPosition(resolved.locator, target)
|
|
153
157
|
: null;
|
|
154
|
-
|
|
158
|
+
const dispatchedAt = Date.now();
|
|
155
159
|
await resolved.locator.click({
|
|
156
160
|
timeout: 5000,
|
|
157
161
|
...(position ? { position } : {}),
|
|
158
162
|
});
|
|
163
|
+
await this.reportClickSfxTimestamps(page, dispatchedAt, opts.onClick);
|
|
159
164
|
}
|
|
160
165
|
/**
|
|
161
166
|
* Type into an element using semantic target resolution.
|
|
@@ -649,13 +654,14 @@ export class WebPlaywrightLocal {
|
|
|
649
654
|
? await this.relativeClickPosition(locator, target)
|
|
650
655
|
: null;
|
|
651
656
|
const opts = { timeout: 5000, ...(position ? { position } : {}) };
|
|
652
|
-
|
|
657
|
+
const dispatchedAt = Date.now();
|
|
653
658
|
if (checked) {
|
|
654
659
|
await locator.check(opts);
|
|
655
660
|
}
|
|
656
661
|
else {
|
|
657
662
|
await locator.uncheck(opts);
|
|
658
663
|
}
|
|
664
|
+
await this.reportClickSfxTimestamps(page, dispatchedAt, actionOpts?.onClick);
|
|
659
665
|
}
|
|
660
666
|
async doubleClick(selector, actionOpts) {
|
|
661
667
|
const page = await this.browser.currentPage;
|
|
@@ -664,11 +670,12 @@ export class WebPlaywrightLocal {
|
|
|
664
670
|
const position = target
|
|
665
671
|
? await this.relativeClickPosition(locator, target)
|
|
666
672
|
: null;
|
|
667
|
-
|
|
673
|
+
const dispatchedAt = Date.now();
|
|
668
674
|
await locator.dblclick({
|
|
669
675
|
timeout: 5000,
|
|
670
676
|
...(position ? { position } : {}),
|
|
671
677
|
});
|
|
678
|
+
await this.reportClickSfxTimestamps(page, dispatchedAt, actionOpts?.onClick);
|
|
672
679
|
}
|
|
673
680
|
async drag(opts) {
|
|
674
681
|
const page = await this.browser.currentPage;
|
|
@@ -1021,6 +1028,47 @@ export class WebPlaywrightLocal {
|
|
|
1021
1028
|
window.__akClickPulse(px, py);
|
|
1022
1029
|
}, { px: Math.round(x), py: Math.round(y) }).catch(() => { });
|
|
1023
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Drain the browser-side `__akClickAt` buffer for timestamps newer than
|
|
1033
|
+
* `sinceMs`, replay them through `onClick`, and reset the buffer so the
|
|
1034
|
+
* next click action starts fresh. This is what makes mouse SFX line up
|
|
1035
|
+
* exactly with the visual click in the recorded video — the mousedown
|
|
1036
|
+
* listener inside the cursor overlay timestamps each click at the same
|
|
1037
|
+
* instant the browser dispatches it, which is also the frame the CDP
|
|
1038
|
+
* screencast captures.
|
|
1039
|
+
*
|
|
1040
|
+
* Falls back to `sinceMs` (Node wall-clock at action dispatch) when the
|
|
1041
|
+
* buffer is empty (e.g. `useKeyboard` Enter-press path, or transient
|
|
1042
|
+
* page.evaluate failure).
|
|
1043
|
+
*/
|
|
1044
|
+
async reportClickSfxTimestamps(page, sinceMs, onClick) {
|
|
1045
|
+
if (!onClick)
|
|
1046
|
+
return;
|
|
1047
|
+
let captured = [];
|
|
1048
|
+
try {
|
|
1049
|
+
captured = (await page.evaluate((cutoff) => {
|
|
1050
|
+
const buf = window.__akClickAt;
|
|
1051
|
+
if (!Array.isArray(buf))
|
|
1052
|
+
return [];
|
|
1053
|
+
const fresh = buf.filter((t) => typeof t === 'number' && t >= cutoff);
|
|
1054
|
+
// Reset the buffer so successive click actions don't see each
|
|
1055
|
+
// others' timestamps. We mutate the array in place to keep the
|
|
1056
|
+
// reference stable for any other consumers (none today).
|
|
1057
|
+
buf.length = 0;
|
|
1058
|
+
return fresh;
|
|
1059
|
+
}, sinceMs));
|
|
1060
|
+
}
|
|
1061
|
+
catch {
|
|
1062
|
+
captured = [];
|
|
1063
|
+
}
|
|
1064
|
+
if (captured.length > 0) {
|
|
1065
|
+
for (const t of captured)
|
|
1066
|
+
onClick(t);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
onClick(sinceMs);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1024
1072
|
}
|
|
1025
1073
|
function describeResolveOptions(opts) {
|
|
1026
1074
|
const parts = [];
|