autokap 1.0.7 → 1.0.10
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/assets/cursors/macos.svg +4 -0
- package/assets/cursors/windows.svg +15 -0
- package/assets/skill/OPCODE-REFERENCE.md +635 -0
- package/assets/skill/README.md +39 -0
- package/assets/skill/SKILL.md +454 -468
- package/assets/skill/STUDIO-SKILL.md +476 -0
- package/assets/skill/references/examples.md +104 -0
- package/assets/skill/references/interactive-demo.md +225 -0
- package/assets/skill/references/mock-data.md +178 -0
- package/dist/action-verifier.d.ts +29 -0
- package/dist/action-verifier.js +133 -0
- package/dist/agent-action-recovery.d.ts +45 -0
- package/dist/agent-action-recovery.js +370 -0
- package/dist/agent-message-utils.d.ts +21 -0
- package/dist/agent-message-utils.js +77 -0
- package/dist/agent-url-utils.d.ts +30 -0
- package/dist/agent-url-utils.js +138 -0
- package/dist/agent.d.ts +92 -8
- package/dist/agent.js +2936 -781
- package/dist/ak-tree.d.ts +39 -0
- package/dist/ak-tree.js +368 -0
- package/dist/alt-text.d.ts +26 -0
- package/dist/alt-text.js +55 -0
- package/dist/auth-capture.d.ts +17 -0
- package/dist/auth-capture.js +197 -0
- package/dist/benchmark.d.ts +59 -0
- package/dist/benchmark.js +135 -0
- package/dist/billing-operation-logging.d.ts +3 -1
- package/dist/billing-operation-logging.js +4 -0
- package/dist/browser-bar.d.ts +14 -6
- package/dist/browser-bar.js +145 -8
- package/dist/browser-pool.d.ts +7 -0
- package/dist/browser-pool.js +15 -5
- package/dist/browser-utils.d.ts +31 -0
- package/dist/browser-utils.js +97 -0
- package/dist/browser.d.ts +61 -11
- package/dist/browser.js +1513 -59
- package/dist/capture-alt-text.js +2 -1
- package/dist/capture-encryption.d.ts +3 -1
- package/dist/capture-encryption.js +21 -6
- package/dist/capture-language-preflight.js +14 -0
- package/dist/capture-llm-page-identity.js +22 -10
- package/dist/capture-page-identity.d.ts +5 -7
- package/dist/capture-page-identity.js +211 -78
- package/dist/capture-preset-credentials.d.ts +50 -0
- package/dist/capture-preset-credentials.js +127 -0
- package/dist/capture-request-plan.d.ts +2 -2
- package/dist/capture-request-plan.js +64 -16
- package/dist/capture-run-optimizer.js +48 -33
- package/dist/capture-selector-memory.d.ts +5 -0
- package/dist/capture-selector-memory.js +18 -0
- package/dist/capture-strategy.d.ts +36 -0
- package/dist/capture-strategy.js +96 -0
- package/dist/capture-studio-sync.d.ts +1 -0
- package/dist/capture-studio-sync.js +9 -3
- package/dist/capture-surface-contract.d.ts +36 -0
- package/dist/capture-surface-contract.js +299 -0
- package/dist/capture-transition-engine.d.ts +28 -0
- package/dist/capture-transition-engine.js +292 -0
- package/dist/capture-variant-state.d.ts +2 -0
- package/dist/capture-variant-state.js +26 -0
- package/dist/capture-verification.d.ts +35 -0
- package/dist/capture-verification.js +95 -0
- package/dist/capture-viewport-lock.d.ts +48 -0
- package/dist/capture-viewport-lock.js +74 -0
- package/dist/circuit-breaker.d.ts +42 -0
- package/dist/circuit-breaker.js +119 -0
- package/dist/cli-config.d.ts +9 -1
- package/dist/cli-config.js +112 -7
- package/dist/cli-contract.d.ts +19 -0
- package/dist/cli-contract.js +173 -0
- package/dist/cli-runner-local.d.ts +12 -0
- package/dist/cli-runner-local.js +115 -0
- package/dist/cli-runner.d.ts +34 -0
- package/dist/cli-runner.js +580 -0
- package/dist/cli-utils.d.ts +0 -1
- package/dist/cli-utils.js +2 -5
- package/dist/cli.js +1011 -267
- package/dist/clip-begin-frame-recorder.d.ts +44 -0
- package/dist/clip-begin-frame-recorder.js +250 -0
- package/dist/clip-capture-backend.d.ts +25 -0
- package/dist/clip-capture-backend.js +189 -0
- package/dist/clip-capture-loop.d.ts +61 -0
- package/dist/clip-capture-loop.js +111 -0
- package/dist/clip-frame-recorder.d.ts +63 -0
- package/dist/clip-frame-recorder.js +305 -0
- package/dist/clip-orchestrator.js +9 -2
- package/dist/clip-postprocess.d.ts +31 -2
- package/dist/clip-postprocess.js +194 -68
- package/dist/clip-runtime.d.ts +18 -0
- package/dist/clip-runtime.js +67 -0
- package/dist/clip-scale.d.ts +10 -0
- package/dist/clip-scale.js +21 -0
- package/dist/clip-screencast-recorder.d.ts +42 -0
- package/dist/clip-screencast-recorder.js +242 -0
- package/dist/clip-sidecar.d.ts +54 -0
- package/dist/clip-sidecar.js +208 -0
- package/dist/cookie-dismiss.d.ts +2 -0
- package/dist/cookie-dismiss.js +48 -13
- package/dist/cost-logging.d.ts +8 -0
- package/dist/cost-logging.js +160 -46
- package/dist/cost-resolution-monitor.d.ts +16 -0
- package/dist/cost-resolution-monitor.js +34 -0
- package/dist/credential-templates.js +2 -2
- package/dist/cursor-overlay-script.d.ts +6 -0
- package/dist/cursor-overlay-script.js +169 -0
- package/dist/dom-css-purger.d.ts +65 -0
- package/dist/dom-css-purger.js +333 -0
- package/dist/dom-font-inliner.d.ts +45 -0
- package/dist/dom-font-inliner.js +148 -0
- package/dist/dom-patch-resolver.d.ts +52 -0
- package/dist/dom-patch-resolver.js +242 -0
- package/dist/dom-serializer.d.ts +82 -0
- package/dist/dom-serializer.js +378 -0
- package/dist/element-capture.d.ts +1 -41
- package/dist/element-capture.js +202 -446
- package/dist/env-validation.d.ts +5 -0
- package/dist/env-validation.js +63 -0
- package/dist/execution-schema.d.ts +4753 -0
- package/dist/execution-schema.js +563 -0
- package/dist/execution-types.d.ts +936 -0
- package/dist/execution-types.js +66 -0
- package/dist/fonts-loader.d.ts +14 -0
- package/dist/fonts-loader.js +55 -0
- package/dist/hybrid-navigator.js +12 -12
- package/dist/index.d.ts +11 -6
- package/dist/index.js +11 -4
- package/dist/legacy/agent-action-recovery.d.ts +45 -0
- package/dist/legacy/agent-action-recovery.js +370 -0
- package/dist/legacy/agent-message-utils.d.ts +21 -0
- package/dist/legacy/agent-message-utils.js +77 -0
- package/dist/legacy/agent-url-utils.d.ts +30 -0
- package/dist/legacy/agent-url-utils.js +138 -0
- package/dist/legacy/agent.d.ts +226 -0
- package/dist/legacy/agent.js +6666 -0
- package/dist/legacy/clip-orchestrator.d.ts +148 -0
- package/dist/legacy/clip-orchestrator.js +957 -0
- package/dist/legacy/credential-templates.d.ts +5 -0
- package/dist/legacy/credential-templates.js +60 -0
- package/dist/legacy/hybrid-navigator.d.ts +138 -0
- package/dist/legacy/hybrid-navigator.js +468 -0
- package/dist/legacy/llm-usage.d.ts +17 -0
- package/dist/legacy/llm-usage.js +45 -0
- package/dist/legacy/prompt-cache.d.ts +10 -0
- package/dist/legacy/prompt-cache.js +24 -0
- package/dist/legacy/prompts.d.ts +175 -0
- package/dist/legacy/prompts.js +1038 -0
- package/dist/legacy/tools.d.ts +4 -0
- package/dist/legacy/tools.js +216 -0
- package/dist/legacy/video-agent.d.ts +143 -0
- package/dist/legacy/video-agent.js +4788 -0
- package/dist/legacy/video-observation.d.ts +36 -0
- package/dist/legacy/video-observation.js +192 -0
- package/dist/legacy/video-planner.d.ts +12 -0
- package/dist/legacy/video-planner.js +501 -0
- package/dist/legacy/video-prompts.d.ts +37 -0
- package/dist/legacy/video-prompts.js +569 -0
- package/dist/legacy/video-tools.d.ts +3 -0
- package/dist/legacy/video-tools.js +59 -0
- package/dist/legacy/video-variant-state.d.ts +29 -0
- package/dist/legacy/video-variant-state.js +80 -0
- package/dist/legacy/vision-model.d.ts +17 -0
- package/dist/legacy/vision-model.js +74 -0
- package/dist/llm-healer.d.ts +55 -0
- package/dist/llm-healer.js +213 -0
- package/dist/llm-provider.d.ts +29 -0
- package/dist/llm-provider.js +83 -0
- package/dist/logger.d.ts +6 -2
- package/dist/logger.js +15 -1
- package/dist/mockup-html.js +35 -25
- package/dist/mockup.d.ts +95 -2
- package/dist/mockup.js +427 -166
- package/dist/mouse-animation.d.ts +2 -2
- package/dist/mouse-animation.js +34 -20
- package/dist/opcode-actions.d.ts +42 -0
- package/dist/opcode-actions.js +524 -0
- package/dist/opcode-runner.d.ts +51 -0
- package/dist/opcode-runner.js +779 -0
- package/dist/openrouter-client.d.ts +40 -0
- package/dist/openrouter-client.js +16 -0
- package/dist/overlay-engine.d.ts +24 -0
- package/dist/overlay-engine.js +176 -0
- package/dist/postcondition.d.ts +16 -0
- package/dist/postcondition.js +269 -0
- package/dist/program-patcher.d.ts +25 -0
- package/dist/program-patcher.js +44 -0
- package/dist/program-signing.d.ts +1094 -0
- package/dist/program-signing.js +140 -0
- package/dist/prompts.d.ts +13 -5
- package/dist/prompts.js +224 -351
- package/dist/provider-config.d.ts +17 -0
- package/dist/provider-config.js +42 -0
- package/dist/recovery-chain.d.ts +37 -0
- package/dist/recovery-chain.js +374 -0
- package/dist/remote-browser.d.ts +28 -4
- package/dist/remote-browser.js +60 -5
- package/dist/safari-browser-bar.d.ts +15 -0
- package/dist/safari-browser-bar.js +95 -0
- package/dist/safari-toolbar-asset.d.ts +15 -0
- package/dist/safari-toolbar-asset.js +12 -0
- package/dist/security.d.ts +2 -1
- package/dist/security.js +49 -10
- package/dist/selector-resolver.d.ts +34 -0
- package/dist/selector-resolver.js +181 -0
- package/dist/semantic-resolver.d.ts +35 -0
- package/dist/semantic-resolver.js +161 -0
- package/dist/server-capture-runtime.d.ts +5 -3
- package/dist/server-capture-runtime.js +42 -95
- package/dist/server-credit-usage.d.ts +2 -2
- package/dist/server-project-webhooks.d.ts +15 -1
- package/dist/server-project-webhooks.js +34 -8
- package/dist/server-screenshot-watermark.js +27 -5
- package/dist/session-profile.js +164 -1
- package/dist/sf-pro-symbols.d.ts +1 -0
- package/dist/sf-pro-symbols.js +55 -0
- package/dist/skill-packaging.d.ts +28 -0
- package/dist/skill-packaging.js +169 -0
- package/dist/smart-wait.d.ts +27 -0
- package/dist/smart-wait.js +81 -0
- package/dist/status-bar-render.d.ts +20 -0
- package/dist/status-bar-render.js +410 -0
- package/dist/status-bar.d.ts +9 -0
- package/dist/status-bar.js +298 -14
- package/dist/svg-browser-bar.d.ts +33 -0
- package/dist/svg-browser-bar.js +206 -0
- package/dist/svg-status-bar.d.ts +36 -0
- package/dist/svg-status-bar.js +597 -0
- package/dist/svg-text.d.ts +61 -0
- package/dist/svg-text.js +118 -0
- package/dist/tools.js +89 -451
- package/dist/types.d.ts +248 -7
- package/dist/types.js +23 -1
- package/dist/v2/action-verifier.d.ts +29 -0
- package/dist/v2/action-verifier.js +133 -0
- package/dist/v2/alt-text.d.ts +26 -0
- package/dist/v2/alt-text.js +55 -0
- package/dist/v2/benchmark.d.ts +59 -0
- package/dist/v2/benchmark.js +135 -0
- package/dist/v2/capture-strategy.d.ts +30 -0
- package/dist/v2/capture-strategy.js +67 -0
- package/dist/v2/capture-verification.d.ts +35 -0
- package/dist/v2/capture-verification.js +95 -0
- package/dist/v2/circuit-breaker.d.ts +42 -0
- package/dist/v2/circuit-breaker.js +119 -0
- package/dist/v2/cli-runner-local.d.ts +11 -0
- package/dist/v2/cli-runner-local.js +91 -0
- package/dist/v2/cli-runner.d.ts +34 -0
- package/dist/v2/cli-runner.js +300 -0
- package/dist/v2/compiler-prompts.d.ts +27 -0
- package/dist/v2/compiler-prompts.js +123 -0
- package/dist/v2/compiler.d.ts +37 -0
- package/dist/v2/compiler.js +147 -0
- package/dist/v2/explorer.d.ts +41 -0
- package/dist/v2/explorer.js +56 -0
- package/dist/v2/index.d.ts +37 -0
- package/dist/v2/index.js +31 -0
- package/dist/v2/llm-healer.d.ts +62 -0
- package/dist/v2/llm-healer.js +166 -0
- package/dist/v2/llm-provider.d.ts +29 -0
- package/dist/v2/llm-provider.js +80 -0
- package/dist/v2/opcode-runner.d.ts +47 -0
- package/dist/v2/opcode-runner.js +634 -0
- package/dist/v2/overlay-engine.d.ts +24 -0
- package/dist/v2/overlay-engine.js +150 -0
- package/dist/v2/postcondition.d.ts +16 -0
- package/dist/v2/postcondition.js +249 -0
- package/dist/v2/program-patcher.d.ts +25 -0
- package/dist/v2/program-patcher.js +44 -0
- package/dist/v2/recovery-chain.d.ts +30 -0
- package/dist/v2/recovery-chain.js +368 -0
- package/dist/v2/schema.d.ts +2580 -0
- package/dist/v2/schema.js +295 -0
- package/dist/v2/selector-resolver.d.ts +34 -0
- package/dist/v2/selector-resolver.js +181 -0
- package/dist/v2/semantic-resolver.d.ts +35 -0
- package/dist/v2/semantic-resolver.js +161 -0
- package/dist/v2/smart-wait.d.ts +27 -0
- package/dist/v2/smart-wait.js +81 -0
- package/dist/v2/types.d.ts +444 -0
- package/dist/v2/types.js +19 -0
- package/dist/v2/web-playwright-local.d.ts +69 -0
- package/dist/v2/web-playwright-local.js +392 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/video-agent.js +18 -13
- package/dist/video-planner.js +2 -1
- package/dist/video-prompts.js +3 -3
- package/dist/web-playwright-local.d.ts +156 -0
- package/dist/web-playwright-local.js +989 -0
- package/dist/ws-auth.js +4 -1
- package/dist/ws-broadcast.d.ts +34 -0
- package/dist/ws-broadcast.js +85 -0
- package/dist/ws-connection-limits.d.ts +12 -0
- package/dist/ws-connection-limits.js +44 -0
- package/dist/ws-handler-utils.d.ts +32 -0
- package/dist/ws-handler-utils.js +139 -0
- package/dist/ws-handler.js +294 -164
- package/dist/ws-metrics-server.d.ts +9 -0
- package/dist/ws-metrics-server.js +31 -0
- package/dist/ws-server.js +41 -1
- package/package.json +62 -35
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Agent — WebPlaywrightLocal RuntimeAdapter
|
|
3
|
+
*
|
|
4
|
+
* Thin adapter delegating to the existing Browser class from src/browser.ts.
|
|
5
|
+
* This is the first (and for now only) RuntimeAdapter implementation.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { humanType, moveMouse } from './mouse-animation.js';
|
|
11
|
+
import { resolveTarget } from './semantic-resolver.js';
|
|
12
|
+
import { logger } from './logger.js';
|
|
13
|
+
import { ClipCaptureLoop } from './clip-capture-loop.js';
|
|
14
|
+
import { assembleMp4FromFrames } from './clip-postprocess.js';
|
|
15
|
+
export class WebPlaywrightLocal {
|
|
16
|
+
browser;
|
|
17
|
+
recordingDir;
|
|
18
|
+
sessionStartedAt = Date.now();
|
|
19
|
+
recording = null;
|
|
20
|
+
clipCursor = null;
|
|
21
|
+
constructor(browser, recordingDir) {
|
|
22
|
+
this.browser = browser;
|
|
23
|
+
this.recordingDir = recordingDir;
|
|
24
|
+
}
|
|
25
|
+
async navigate(url) {
|
|
26
|
+
await this.browser.navigateTo(url);
|
|
27
|
+
}
|
|
28
|
+
async getCurrentUrl() {
|
|
29
|
+
const page = await this.browser.currentPage;
|
|
30
|
+
return page.url();
|
|
31
|
+
}
|
|
32
|
+
async getAKTree() {
|
|
33
|
+
return this.browser.getAKTree();
|
|
34
|
+
}
|
|
35
|
+
async getPageSignals() {
|
|
36
|
+
return this.browser.capturePageSignals();
|
|
37
|
+
}
|
|
38
|
+
async click(selector, options) {
|
|
39
|
+
const page = await this.browser.currentPage;
|
|
40
|
+
const t0 = Date.now();
|
|
41
|
+
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})` : ''}`);
|
|
42
|
+
try {
|
|
43
|
+
if (options?.coordinates) {
|
|
44
|
+
await this.moveClipCursorToPoint(options.coordinates);
|
|
45
|
+
await this.browser.clickByCoordinates(options.coordinates.x, options.coordinates.y);
|
|
46
|
+
logger.debug(`[click] done coords took ${Date.now() - t0}ms`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const locator = page.locator(selector).first();
|
|
50
|
+
const animatedTarget = await this.moveClipCursorToLocator(locator);
|
|
51
|
+
if (options?.useKeyboard) {
|
|
52
|
+
await locator.focus();
|
|
53
|
+
await page.keyboard.press('Enter');
|
|
54
|
+
logger.debug(`[click] done keyboard took ${Date.now() - t0}ms`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (options?.useJsDispatch) {
|
|
58
|
+
await locator.dispatchEvent('click');
|
|
59
|
+
logger.debug(`[click] done js_dispatch took ${Date.now() - t0}ms`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Click at the exact point we animated to (when the overlay is active)
|
|
63
|
+
// so Playwright doesn't recenter the mouse and produce a last-frame snap.
|
|
64
|
+
const clickPosition = animatedTarget
|
|
65
|
+
? await this.relativeClickPosition(locator, animatedTarget)
|
|
66
|
+
: null;
|
|
67
|
+
if (options?.button && options.button !== 'left') {
|
|
68
|
+
await locator.click({
|
|
69
|
+
button: options.button,
|
|
70
|
+
timeout: 5000,
|
|
71
|
+
force: options?.force,
|
|
72
|
+
...(clickPosition ? { position: clickPosition } : {}),
|
|
73
|
+
});
|
|
74
|
+
logger.debug(`[click] done button=${options.button} took ${Date.now() - t0}ms`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (clickPosition) {
|
|
78
|
+
await locator.click({
|
|
79
|
+
timeout: 5000,
|
|
80
|
+
force: options?.force,
|
|
81
|
+
position: clickPosition,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
await this.browser.clickBySelector(selector, { force: options?.force });
|
|
86
|
+
}
|
|
87
|
+
await this.emitClipClickPulse();
|
|
88
|
+
logger.debug(`[click] done normal took ${Date.now() - t0}ms`);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
logger.debug(`[click] FAILED after ${Date.now() - t0}ms — ${err instanceof Error ? err.message : String(err)}`);
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Click an element using semantic target resolution.
|
|
97
|
+
* Tries CSS selector first, falls back to Playwright semantic locators.
|
|
98
|
+
*/
|
|
99
|
+
async clickByTarget(opts) {
|
|
100
|
+
const page = await this.browser.currentPage;
|
|
101
|
+
const resolved = await resolveTarget(page, opts);
|
|
102
|
+
if (!resolved) {
|
|
103
|
+
throw new Error(`cannot find target: ${describeResolveOptions(opts)}`);
|
|
104
|
+
}
|
|
105
|
+
const target = await this.moveClipCursorToLocator(resolved.locator);
|
|
106
|
+
const position = target
|
|
107
|
+
? await this.relativeClickPosition(resolved.locator, target)
|
|
108
|
+
: null;
|
|
109
|
+
await resolved.locator.click({
|
|
110
|
+
timeout: 5000,
|
|
111
|
+
...(position ? { position } : {}),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Type into an element using semantic target resolution.
|
|
116
|
+
*/
|
|
117
|
+
async typeByTarget(opts, text, clearFirst = true) {
|
|
118
|
+
const page = await this.browser.currentPage;
|
|
119
|
+
const resolved = await resolveTarget(page, opts);
|
|
120
|
+
if (!resolved) {
|
|
121
|
+
throw new Error(`cannot find target for typing: ${describeResolveOptions(opts)}`);
|
|
122
|
+
}
|
|
123
|
+
if (this.clipCursor) {
|
|
124
|
+
await this.typeIntoLocator(resolved.locator, text, clearFirst);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (clearFirst) {
|
|
128
|
+
await resolved.locator.fill(text);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await resolved.locator.click();
|
|
132
|
+
await resolved.locator.pressSequentially(text);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Wait for an element using semantic target resolution.
|
|
136
|
+
*/
|
|
137
|
+
async waitForTarget(opts, timeoutMs = 10000) {
|
|
138
|
+
const page = await this.browser.currentPage;
|
|
139
|
+
const resolved = await resolveTarget(page, { ...opts, timeoutMs });
|
|
140
|
+
return resolved !== null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Scroll an element into view using semantic target resolution.
|
|
144
|
+
*/
|
|
145
|
+
async scrollIntoViewByTarget(opts) {
|
|
146
|
+
const page = await this.browser.currentPage;
|
|
147
|
+
const resolved = await resolveTarget(page, opts);
|
|
148
|
+
if (!resolved) {
|
|
149
|
+
throw new Error(`cannot find target to scroll into view: ${describeResolveOptions(opts)}`);
|
|
150
|
+
}
|
|
151
|
+
if (this.clipCursor) {
|
|
152
|
+
await this.moveClipCursorToViewportCenter();
|
|
153
|
+
await resolved.locator.evaluate((node) => {
|
|
154
|
+
node.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
155
|
+
});
|
|
156
|
+
await page.waitForTimeout(350);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await resolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 });
|
|
160
|
+
}
|
|
161
|
+
async type(selector, text, clearFirst = true) {
|
|
162
|
+
if (this.clipCursor) {
|
|
163
|
+
const page = await this.browser.currentPage;
|
|
164
|
+
await this.typeIntoLocator(page.locator(selector).first(), text, clearFirst);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
await this.browser.typeText(text, { selector, clearFirst });
|
|
168
|
+
}
|
|
169
|
+
async pressKey(key) {
|
|
170
|
+
const page = await this.browser.currentPage;
|
|
171
|
+
if (this.clipCursor) {
|
|
172
|
+
await page.waitForTimeout(90);
|
|
173
|
+
await page.keyboard.press(key);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
await this.browser.pressKey(key);
|
|
177
|
+
}
|
|
178
|
+
async scroll(direction, amount) {
|
|
179
|
+
if (this.clipCursor) {
|
|
180
|
+
const page = await this.browser.currentPage;
|
|
181
|
+
await this.moveClipCursorToViewportCenter();
|
|
182
|
+
const dx = direction === 'right' ? (amount ?? 500) : direction === 'left' ? -(amount ?? 500) : 0;
|
|
183
|
+
const dy = direction === 'down' ? (amount ?? 500) : direction === 'up' ? -(amount ?? 500) : 0;
|
|
184
|
+
await page.evaluate(({ deltaX, deltaY }) => {
|
|
185
|
+
window.scrollBy({ left: deltaX, top: deltaY, behavior: 'smooth' });
|
|
186
|
+
}, { deltaX: dx, deltaY: dy });
|
|
187
|
+
await page.waitForTimeout(420);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await this.browser.scroll(direction, amount);
|
|
191
|
+
}
|
|
192
|
+
async scrollIntoView(selector) {
|
|
193
|
+
const page = await this.browser.currentPage;
|
|
194
|
+
if (this.clipCursor) {
|
|
195
|
+
await this.moveClipCursorToViewportCenter();
|
|
196
|
+
await page.locator(selector).first().evaluate((node) => {
|
|
197
|
+
node.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
198
|
+
});
|
|
199
|
+
await page.waitForTimeout(350);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await page.locator(selector).first().scrollIntoViewIfNeeded({ timeout: 5000 });
|
|
203
|
+
}
|
|
204
|
+
async waitFor(condition) {
|
|
205
|
+
try {
|
|
206
|
+
const page = await this.browser.currentPage;
|
|
207
|
+
const stateMap = { visible: 'visible', attached: 'attached' };
|
|
208
|
+
await page.locator(condition.selector).waitFor({
|
|
209
|
+
state: stateMap[condition.state],
|
|
210
|
+
timeout: condition.timeoutMs,
|
|
211
|
+
});
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async dismissOverlays() {
|
|
219
|
+
// Pass 1: Built-in cookie/widget dismissal (cookie-dismiss.ts)
|
|
220
|
+
const result = await this.browser.dismissOverlays();
|
|
221
|
+
if (result.dismissed)
|
|
222
|
+
return result;
|
|
223
|
+
// Pass 2: Playwright-level sweep for common accept/close buttons
|
|
224
|
+
// in cookie-like containers that the built-in module might miss
|
|
225
|
+
const page = await this.browser.currentPage;
|
|
226
|
+
const acceptPatterns = [
|
|
227
|
+
'button:has-text("Accept")',
|
|
228
|
+
'button:has-text("Accept all")',
|
|
229
|
+
'button:has-text("Accepter")',
|
|
230
|
+
'button:has-text("Tout accepter")',
|
|
231
|
+
'button:has-text("Got it")',
|
|
232
|
+
'button:has-text("I agree")',
|
|
233
|
+
'button:has-text("OK")',
|
|
234
|
+
'[role="button"]:has-text("Accept")',
|
|
235
|
+
'[role="button"]:has-text("Accept all")',
|
|
236
|
+
];
|
|
237
|
+
for (const pattern of acceptPatterns) {
|
|
238
|
+
try {
|
|
239
|
+
const btn = page.locator(pattern).first();
|
|
240
|
+
if (await btn.isVisible({ timeout: 500 })) {
|
|
241
|
+
// Check if the button is near the bottom of the viewport (likely a banner)
|
|
242
|
+
const box = await btn.boundingBox();
|
|
243
|
+
const viewport = page.viewportSize();
|
|
244
|
+
if (box && viewport && box.y > viewport.height * 0.6) {
|
|
245
|
+
await btn.click({ timeout: 2000 });
|
|
246
|
+
await page.waitForTimeout(500);
|
|
247
|
+
return { dismissed: true, method: `overlay-sweep:${pattern}` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Try next pattern
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Pass 3: Try Escape key for modal overlays
|
|
256
|
+
try {
|
|
257
|
+
await page.keyboard.press('Escape');
|
|
258
|
+
await page.waitForTimeout(300);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Non-fatal
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
async takeScreenshot() {
|
|
266
|
+
return this.browser.takeScreenshot();
|
|
267
|
+
}
|
|
268
|
+
async takeElementScreenshot(selector) {
|
|
269
|
+
const { buffer } = await this.browser.screenshotBySelector(selector);
|
|
270
|
+
return buffer;
|
|
271
|
+
}
|
|
272
|
+
async takeCleanScreenshot() {
|
|
273
|
+
return this.browser.takeScreenshot();
|
|
274
|
+
}
|
|
275
|
+
async beginRecording(options) {
|
|
276
|
+
const page = await this.browser.currentPage;
|
|
277
|
+
const baseDir = this.recordingDir
|
|
278
|
+
?? await fs.mkdtemp(path.join(os.tmpdir(), 'autokap-recording-'));
|
|
279
|
+
const framesDir = path.join(baseDir, 'frames');
|
|
280
|
+
await fs.mkdir(framesDir, { recursive: true });
|
|
281
|
+
const loop = new ClipCaptureLoop({ page, framesDir });
|
|
282
|
+
await loop.start();
|
|
283
|
+
this.recording = {
|
|
284
|
+
mediaMode: options.mediaMode,
|
|
285
|
+
startedAt: Date.now(),
|
|
286
|
+
framesDir,
|
|
287
|
+
mp4Path: path.join(baseDir, `${options.mediaMode}.mp4`),
|
|
288
|
+
loop,
|
|
289
|
+
finalized: false,
|
|
290
|
+
};
|
|
291
|
+
this.clipCursor = { currentPosition: null };
|
|
292
|
+
await this.seedClipCursor();
|
|
293
|
+
}
|
|
294
|
+
async endRecording() {
|
|
295
|
+
if (!this.recording) {
|
|
296
|
+
throw new Error('recording was not started');
|
|
297
|
+
}
|
|
298
|
+
if (this.recording.finalized) {
|
|
299
|
+
const buffer = await fs.readFile(this.recording.mp4Path);
|
|
300
|
+
return {
|
|
301
|
+
buffer,
|
|
302
|
+
durationMs: Date.now() - this.recording.startedAt,
|
|
303
|
+
mimeType: 'video/mp4',
|
|
304
|
+
trimStartMs: this.recording.result?.trimStartMs ?? 0,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const result = await this.recording.loop.stop();
|
|
308
|
+
await this.browser.closeContext();
|
|
309
|
+
await assembleMp4FromFrames({
|
|
310
|
+
framesDir: this.recording.framesDir,
|
|
311
|
+
outputPath: this.recording.mp4Path,
|
|
312
|
+
fps: result.measuredFps,
|
|
313
|
+
frameOffsetsMs: result.frameOffsetsMs,
|
|
314
|
+
});
|
|
315
|
+
this.recording.finalized = true;
|
|
316
|
+
this.recording.result = result;
|
|
317
|
+
this.clipCursor = null;
|
|
318
|
+
const buffer = await fs.readFile(this.recording.mp4Path);
|
|
319
|
+
return {
|
|
320
|
+
buffer,
|
|
321
|
+
durationMs: Date.now() - this.recording.startedAt,
|
|
322
|
+
mimeType: 'video/mp4',
|
|
323
|
+
trimStartMs: result.trimStartMs,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async setLocale(locale) {
|
|
327
|
+
await this.browser.setLanguage(locale);
|
|
328
|
+
}
|
|
329
|
+
async setColorScheme(scheme) {
|
|
330
|
+
await this.browser.setColorScheme(scheme);
|
|
331
|
+
}
|
|
332
|
+
async reloadPage() {
|
|
333
|
+
await this.browser.reloadCurrentPage();
|
|
334
|
+
}
|
|
335
|
+
async writeStorageHint(params) {
|
|
336
|
+
if (params.storage === 'cookie') {
|
|
337
|
+
const page = await this.browser.currentPage;
|
|
338
|
+
const url = new URL(page.url());
|
|
339
|
+
await this.browser.addCookies([{
|
|
340
|
+
name: params.key,
|
|
341
|
+
value: params.value,
|
|
342
|
+
domain: url.hostname,
|
|
343
|
+
path: '/',
|
|
344
|
+
httpOnly: false,
|
|
345
|
+
secure: url.protocol === 'https:',
|
|
346
|
+
sameSite: 'Lax',
|
|
347
|
+
}]);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
return this.browser.writeStorageHintCandidate({
|
|
351
|
+
storageName: params.storage,
|
|
352
|
+
key: params.key,
|
|
353
|
+
candidate: params.value,
|
|
354
|
+
kind: params.kind,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
async hover(selector) {
|
|
358
|
+
const page = await this.browser.currentPage;
|
|
359
|
+
const locator = page.locator(selector).first();
|
|
360
|
+
const target = await this.moveClipCursorToLocator(locator);
|
|
361
|
+
const position = target
|
|
362
|
+
? await this.relativeClickPosition(locator, target)
|
|
363
|
+
: null;
|
|
364
|
+
if (position) {
|
|
365
|
+
await locator.hover({ timeout: 5000, position });
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
await this.browser.hoverBySelector(selector);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async hoverByTarget(opts) {
|
|
372
|
+
const page = await this.browser.currentPage;
|
|
373
|
+
const resolved = await resolveTarget(page, opts);
|
|
374
|
+
if (!resolved) {
|
|
375
|
+
throw new Error(`cannot find target for hover: ${describeResolveOptions(opts)}`);
|
|
376
|
+
}
|
|
377
|
+
const target = await this.moveClipCursorToLocator(resolved.locator);
|
|
378
|
+
const position = target
|
|
379
|
+
? await this.relativeClickPosition(resolved.locator, target)
|
|
380
|
+
: null;
|
|
381
|
+
await resolved.locator.hover({
|
|
382
|
+
timeout: 5000,
|
|
383
|
+
...(position ? { position } : {}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async selectOption(selector, option) {
|
|
387
|
+
await this.browser.selectOption({
|
|
388
|
+
selector,
|
|
389
|
+
optionLabel: option.label,
|
|
390
|
+
optionValue: option.value,
|
|
391
|
+
optionIndex: option.index,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async check(selector, checked) {
|
|
395
|
+
const page = await this.browser.currentPage;
|
|
396
|
+
const locator = page.locator(selector).first();
|
|
397
|
+
const target = await this.moveClipCursorToLocator(locator);
|
|
398
|
+
const position = target
|
|
399
|
+
? await this.relativeClickPosition(locator, target)
|
|
400
|
+
: null;
|
|
401
|
+
const opts = { timeout: 5000, ...(position ? { position } : {}) };
|
|
402
|
+
if (checked) {
|
|
403
|
+
await locator.check(opts);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
await locator.uncheck(opts);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async doubleClick(selector) {
|
|
410
|
+
const page = await this.browser.currentPage;
|
|
411
|
+
const locator = page.locator(selector).first();
|
|
412
|
+
const target = await this.moveClipCursorToLocator(locator);
|
|
413
|
+
const position = target
|
|
414
|
+
? await this.relativeClickPosition(locator, target)
|
|
415
|
+
: null;
|
|
416
|
+
await locator.dblclick({
|
|
417
|
+
timeout: 5000,
|
|
418
|
+
...(position ? { position } : {}),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async drag(opts) {
|
|
422
|
+
const page = await this.browser.currentPage;
|
|
423
|
+
const viewport = page.viewportSize();
|
|
424
|
+
if (!viewport)
|
|
425
|
+
throw new Error('cannot drag without a known viewport');
|
|
426
|
+
const sourceResolved = await resolveTarget(page, {
|
|
427
|
+
selector: opts.selector,
|
|
428
|
+
target: opts.target,
|
|
429
|
+
selectorAlternates: opts.selectorAlternates,
|
|
430
|
+
});
|
|
431
|
+
if (!sourceResolved) {
|
|
432
|
+
throw new Error(`cannot find drag source: ${describeResolveOptions({ selector: opts.selector, target: opts.target })}`);
|
|
433
|
+
}
|
|
434
|
+
await sourceResolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => undefined);
|
|
435
|
+
const sourceBox = await sourceResolved.locator.boundingBox();
|
|
436
|
+
if (!sourceBox) {
|
|
437
|
+
throw new Error('drag source has no bounding box');
|
|
438
|
+
}
|
|
439
|
+
const sourcePoint = getHumanPointInBox(sourceBox, viewport);
|
|
440
|
+
const sourceCenter = {
|
|
441
|
+
x: sourceBox.x + sourceBox.width / 2,
|
|
442
|
+
y: sourceBox.y + sourceBox.height / 2,
|
|
443
|
+
};
|
|
444
|
+
let destPoint;
|
|
445
|
+
if (opts.toSelector || opts.toTarget) {
|
|
446
|
+
const destResolved = await resolveTarget(page, {
|
|
447
|
+
selector: opts.toSelector,
|
|
448
|
+
target: opts.toTarget,
|
|
449
|
+
selectorAlternates: opts.toSelectorAlternates,
|
|
450
|
+
});
|
|
451
|
+
if (!destResolved) {
|
|
452
|
+
throw new Error(`cannot find drag destination: ${describeResolveOptions({ selector: opts.toSelector, target: opts.toTarget })}`);
|
|
453
|
+
}
|
|
454
|
+
await destResolved.locator.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => undefined);
|
|
455
|
+
const destBox = await destResolved.locator.boundingBox();
|
|
456
|
+
if (!destBox) {
|
|
457
|
+
throw new Error('drag destination has no bounding box');
|
|
458
|
+
}
|
|
459
|
+
destPoint = getHumanPointInBox(destBox, viewport);
|
|
460
|
+
}
|
|
461
|
+
else if (opts.offset) {
|
|
462
|
+
destPoint = {
|
|
463
|
+
x: clampPoint(sourceCenter.x + opts.offset.dx, 4, viewport.width - 4),
|
|
464
|
+
y: clampPoint(sourceCenter.y + opts.offset.dy, 4, viewport.height - 4),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
throw new Error('DRAG requires toSelector, toTarget, or offset');
|
|
469
|
+
}
|
|
470
|
+
const distance = Math.hypot(destPoint.x - sourcePoint.x, destPoint.y - sourcePoint.y);
|
|
471
|
+
const dragDurationMs = Math.min(1100, Math.max(400, 500 + distance));
|
|
472
|
+
if (this.clipCursor) {
|
|
473
|
+
await this.moveClipCursorToPoint(sourcePoint);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
await page.mouse.move(sourcePoint.x, sourcePoint.y);
|
|
477
|
+
}
|
|
478
|
+
await page.waitForTimeout(80 + Math.random() * 80);
|
|
479
|
+
await page.mouse.down();
|
|
480
|
+
if (this.clipCursor) {
|
|
481
|
+
await page.evaluate(() => {
|
|
482
|
+
if (typeof window.__akPressCursor === 'function') {
|
|
483
|
+
window.__akPressCursor();
|
|
484
|
+
}
|
|
485
|
+
}).catch(() => { });
|
|
486
|
+
}
|
|
487
|
+
await moveMouse(page, sourcePoint, destPoint, { durationMs: dragDurationMs });
|
|
488
|
+
await page.waitForTimeout(70 + Math.random() * 60);
|
|
489
|
+
await page.mouse.up();
|
|
490
|
+
if (this.clipCursor) {
|
|
491
|
+
await page.evaluate(() => {
|
|
492
|
+
if (typeof window.__akReleaseCursor === 'function') {
|
|
493
|
+
window.__akReleaseCursor();
|
|
494
|
+
}
|
|
495
|
+
}).catch(() => { });
|
|
496
|
+
this.clipCursor.currentPosition = destPoint;
|
|
497
|
+
await this.emitClipClickPulse();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// ── Mock data DOM mutations (soft / non-blocking) ──
|
|
501
|
+
async cloneElement(opts) {
|
|
502
|
+
const page = await this.browser.currentPage;
|
|
503
|
+
const clonedCount = await page.evaluate(({ sourceSelector, containerSelector, count, removeSource }) => {
|
|
504
|
+
const source = document.querySelector(sourceSelector);
|
|
505
|
+
const container = document.querySelector(containerSelector);
|
|
506
|
+
if (!source) {
|
|
507
|
+
throw new Error(`cloneElement: source "${sourceSelector}" not found`);
|
|
508
|
+
}
|
|
509
|
+
if (!container) {
|
|
510
|
+
throw new Error(`cloneElement: container "${containerSelector}" not found`);
|
|
511
|
+
}
|
|
512
|
+
let cloned = 0;
|
|
513
|
+
for (let i = 0; i < count; i++) {
|
|
514
|
+
const clone = source.cloneNode(true);
|
|
515
|
+
// Tag the clone so the mock-data row addressing can find it
|
|
516
|
+
// independently of nth-child math (which breaks when the
|
|
517
|
+
// container has non-template siblings like placeholders).
|
|
518
|
+
clone.setAttribute('data-ak-mock-clone', String(i));
|
|
519
|
+
container.appendChild(clone);
|
|
520
|
+
cloned++;
|
|
521
|
+
}
|
|
522
|
+
if (removeSource && source.parentElement === container) {
|
|
523
|
+
source.remove();
|
|
524
|
+
}
|
|
525
|
+
return cloned;
|
|
526
|
+
}, opts);
|
|
527
|
+
return { clonedCount };
|
|
528
|
+
}
|
|
529
|
+
async setAttribute(opts) {
|
|
530
|
+
const page = await this.browser.currentPage;
|
|
531
|
+
await page.evaluate(({ selector, attribute, value }) => {
|
|
532
|
+
const el = document.querySelector(selector);
|
|
533
|
+
if (!el) {
|
|
534
|
+
throw new Error(`setAttribute: element "${selector}" not found`);
|
|
535
|
+
}
|
|
536
|
+
el.setAttribute(attribute, value);
|
|
537
|
+
}, opts);
|
|
538
|
+
}
|
|
539
|
+
async setTextContent(opts) {
|
|
540
|
+
const page = await this.browser.currentPage;
|
|
541
|
+
await page.evaluate(({ selector, text }) => {
|
|
542
|
+
const el = document.querySelector(selector);
|
|
543
|
+
if (!el) {
|
|
544
|
+
throw new Error(`setTextContent: element "${selector}" not found`);
|
|
545
|
+
}
|
|
546
|
+
el.textContent = text;
|
|
547
|
+
}, opts);
|
|
548
|
+
}
|
|
549
|
+
async removeElement(opts) {
|
|
550
|
+
const page = await this.browser.currentPage;
|
|
551
|
+
const removedCount = await page.evaluate(({ selector }) => {
|
|
552
|
+
const nodes = document.querySelectorAll(selector);
|
|
553
|
+
if (nodes.length === 0) {
|
|
554
|
+
throw new Error(`removeElement: no elements match "${selector}"`);
|
|
555
|
+
}
|
|
556
|
+
nodes.forEach((n) => n.remove());
|
|
557
|
+
return nodes.length;
|
|
558
|
+
}, opts);
|
|
559
|
+
return { removedCount };
|
|
560
|
+
}
|
|
561
|
+
async setInputValue(opts) {
|
|
562
|
+
const page = await this.browser.currentPage;
|
|
563
|
+
await page.evaluate(({ selector, value }) => {
|
|
564
|
+
const el = document.querySelector(selector);
|
|
565
|
+
if (!el) {
|
|
566
|
+
throw new Error(`setInputValue: element "${selector}" not found`);
|
|
567
|
+
}
|
|
568
|
+
// Use the native value setter so React (and other controlled-component
|
|
569
|
+
// libraries) notice the change. Setting `el.value` directly bypasses
|
|
570
|
+
// React's internal value tracker and is silently ignored on next render.
|
|
571
|
+
const proto = el instanceof HTMLTextAreaElement
|
|
572
|
+
? window.HTMLTextAreaElement.prototype
|
|
573
|
+
: el instanceof HTMLSelectElement
|
|
574
|
+
? window.HTMLSelectElement.prototype
|
|
575
|
+
: window.HTMLInputElement.prototype;
|
|
576
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
|
|
577
|
+
if (desc?.set) {
|
|
578
|
+
desc.set.call(el, value);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
el.value = value;
|
|
582
|
+
}
|
|
583
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
584
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
585
|
+
}, opts);
|
|
586
|
+
}
|
|
587
|
+
async clickHidden(opts) {
|
|
588
|
+
const page = await this.browser.currentPage;
|
|
589
|
+
await page.evaluate(({ selector }) => {
|
|
590
|
+
const el = document.querySelector(selector);
|
|
591
|
+
if (!el) {
|
|
592
|
+
throw new Error(`clickHidden: element "${selector}" not found`);
|
|
593
|
+
}
|
|
594
|
+
// Use the native click() method so React onClick handlers fire.
|
|
595
|
+
el.click();
|
|
596
|
+
}, opts);
|
|
597
|
+
}
|
|
598
|
+
async serializeDom(selector) {
|
|
599
|
+
const page = await this.browser.currentPage;
|
|
600
|
+
const baseUrl = page.url();
|
|
601
|
+
const fallbackViewport = page.viewportSize() ?? { width: 1440, height: 900 };
|
|
602
|
+
const rawCapture = await page.evaluate((targetSelector) => {
|
|
603
|
+
function serializeDoctype() {
|
|
604
|
+
return document.doctype
|
|
605
|
+
? `<!DOCTYPE ${document.doctype.name}` +
|
|
606
|
+
(document.doctype.publicId ? ` PUBLIC "${document.doctype.publicId}"` : '') +
|
|
607
|
+
(document.doctype.systemId && !document.doctype.publicId ? ' SYSTEM' : '') +
|
|
608
|
+
(document.doctype.systemId ? ` "${document.doctype.systemId}"` : '') +
|
|
609
|
+
'>'
|
|
610
|
+
: '';
|
|
611
|
+
}
|
|
612
|
+
function copyAttributes(source, target) {
|
|
613
|
+
for (const attr of Array.from(source.attributes)) {
|
|
614
|
+
target.setAttribute(attr.name, attr.value);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function buildFocusedBranch(element) {
|
|
618
|
+
let branch = element.cloneNode(true);
|
|
619
|
+
let currentSource = element.parentElement;
|
|
620
|
+
while (currentSource && currentSource !== document.body) {
|
|
621
|
+
const shell = currentSource.cloneNode(false);
|
|
622
|
+
shell.appendChild(branch);
|
|
623
|
+
branch = shell;
|
|
624
|
+
currentSource = currentSource.parentElement;
|
|
625
|
+
}
|
|
626
|
+
return branch;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Inline same-origin <link rel="stylesheet"> tags as <style> blocks.
|
|
630
|
+
* This makes the captured HTML self-contained — CSS chunk names are
|
|
631
|
+
* build-specific and will 404 when served from a different build.
|
|
632
|
+
* Cross-origin stylesheets (e.g. Google Fonts CSS) are left as <link>
|
|
633
|
+
* tags since their URLs are stable public CDN paths.
|
|
634
|
+
*/
|
|
635
|
+
function inlineStylesheets(doc) {
|
|
636
|
+
const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]'));
|
|
637
|
+
for (const link of links) {
|
|
638
|
+
const href = link.getAttribute('href');
|
|
639
|
+
if (!href)
|
|
640
|
+
continue;
|
|
641
|
+
// Skip cross-origin stylesheets — their cssRules aren't accessible
|
|
642
|
+
// and their URLs are stable (e.g. fonts.googleapis.com).
|
|
643
|
+
try {
|
|
644
|
+
const url = new URL(href, location.origin);
|
|
645
|
+
if (url.origin !== location.origin)
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
// Find the matching CSSStyleSheet and read its rules.
|
|
652
|
+
const sheet = Array.from(doc.styleSheets).find((s) => s.ownerNode === link);
|
|
653
|
+
if (!sheet)
|
|
654
|
+
continue;
|
|
655
|
+
let cssText = '';
|
|
656
|
+
try {
|
|
657
|
+
const rules = sheet.cssRules;
|
|
658
|
+
for (let i = 0; i < rules.length; i++) {
|
|
659
|
+
cssText += rules[i].cssText + '\n';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
continue;
|
|
664
|
+
} // SecurityError for cross-origin sheets
|
|
665
|
+
if (!cssText)
|
|
666
|
+
continue;
|
|
667
|
+
const style = doc.createElement('style');
|
|
668
|
+
style.textContent = cssText;
|
|
669
|
+
link.replaceWith(style);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const doctype = serializeDoctype();
|
|
673
|
+
const root = document.documentElement;
|
|
674
|
+
if (!targetSelector) {
|
|
675
|
+
inlineStylesheets(document);
|
|
676
|
+
return {
|
|
677
|
+
html: doctype + (root ? root.outerHTML : ''),
|
|
678
|
+
viewport: {
|
|
679
|
+
width: window.innerWidth || document.documentElement.clientWidth || 1440,
|
|
680
|
+
height: window.innerHeight || document.documentElement.clientHeight || 900,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const element = document.querySelector(targetSelector);
|
|
685
|
+
if (!element) {
|
|
686
|
+
throw new Error(`serializeDom: no element matched selector "${targetSelector}"`);
|
|
687
|
+
}
|
|
688
|
+
if (element === document.documentElement) {
|
|
689
|
+
inlineStylesheets(document);
|
|
690
|
+
return {
|
|
691
|
+
html: doctype + root.outerHTML,
|
|
692
|
+
viewport: {
|
|
693
|
+
width: window.innerWidth || document.documentElement.clientWidth || 1440,
|
|
694
|
+
height: window.innerHeight || document.documentElement.clientHeight || 900,
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const nextDoc = document.implementation.createHTMLDocument(document.title || '');
|
|
699
|
+
copyAttributes(document.documentElement, nextDoc.documentElement);
|
|
700
|
+
nextDoc.head.innerHTML = document.head ? document.head.innerHTML : '';
|
|
701
|
+
copyAttributes(document.body, nextDoc.body);
|
|
702
|
+
if (element === document.body) {
|
|
703
|
+
nextDoc.body.innerHTML = document.body.innerHTML;
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
const branch = buildFocusedBranch(element);
|
|
707
|
+
nextDoc.body.replaceChildren(nextDoc.importNode(branch, true));
|
|
708
|
+
}
|
|
709
|
+
// Inline stylesheets in the cloned document. The <link> elements were
|
|
710
|
+
// copied from the live document's <head>, so their ownerNode points to
|
|
711
|
+
// the cloned <link>. The CSSStyleSheet objects on the *live* document
|
|
712
|
+
// still hold the parsed rules — iterate those and match by href.
|
|
713
|
+
const liveSheets = Array.from(document.styleSheets);
|
|
714
|
+
const clonedLinks = Array.from(nextDoc.querySelectorAll('link[rel="stylesheet"]'));
|
|
715
|
+
for (const link of clonedLinks) {
|
|
716
|
+
const href = link.getAttribute('href');
|
|
717
|
+
if (!href)
|
|
718
|
+
continue;
|
|
719
|
+
try {
|
|
720
|
+
const url = new URL(href, location.origin);
|
|
721
|
+
if (url.origin !== location.origin)
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
// Match by href against the live document's stylesheets.
|
|
728
|
+
const liveSheet = liveSheets.find((s) => {
|
|
729
|
+
try {
|
|
730
|
+
return s.href === new URL(href, location.origin).href;
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
if (!liveSheet)
|
|
737
|
+
continue;
|
|
738
|
+
let cssText = '';
|
|
739
|
+
try {
|
|
740
|
+
const rules = liveSheet.cssRules;
|
|
741
|
+
for (let i = 0; i < rules.length; i++) {
|
|
742
|
+
cssText += rules[i].cssText + '\n';
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (!cssText)
|
|
749
|
+
continue;
|
|
750
|
+
const style = nextDoc.createElement('style');
|
|
751
|
+
style.textContent = cssText;
|
|
752
|
+
link.replaceWith(style);
|
|
753
|
+
}
|
|
754
|
+
const rect = element.getBoundingClientRect();
|
|
755
|
+
return {
|
|
756
|
+
html: doctype + nextDoc.documentElement.outerHTML,
|
|
757
|
+
viewport: {
|
|
758
|
+
width: Math.max(1, Math.ceil(rect.width || window.innerWidth || document.documentElement.clientWidth || 1440)),
|
|
759
|
+
height: Math.max(1, Math.ceil(rect.height || window.innerHeight || document.documentElement.clientHeight || 900)),
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
}, selector ?? null);
|
|
763
|
+
// Lazy import to avoid loading parse5 unless DOM capture is actually used.
|
|
764
|
+
const { sanitizeDom } = await import('./dom-serializer.js');
|
|
765
|
+
const viewport = rawCapture?.viewport ?? fallbackViewport;
|
|
766
|
+
const result = sanitizeDom({ html: rawCapture.html, baseUrl, viewport });
|
|
767
|
+
return {
|
|
768
|
+
html: result.html,
|
|
769
|
+
assetUrls: result.assetUrls,
|
|
770
|
+
viewport,
|
|
771
|
+
capturedAt: new Date().toISOString(),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
async serializeFragment(selector) {
|
|
775
|
+
const page = await this.browser.currentPage;
|
|
776
|
+
const baseUrl = page.url();
|
|
777
|
+
const rawHtml = await page.evaluate((sel) => {
|
|
778
|
+
const el = document.querySelector(sel);
|
|
779
|
+
return el ? el.outerHTML : null;
|
|
780
|
+
}, selector);
|
|
781
|
+
if (!rawHtml) {
|
|
782
|
+
throw new Error(`serializeFragment: no element matched selector "${selector}"`);
|
|
783
|
+
}
|
|
784
|
+
const { sanitizeFragment } = await import('./dom-serializer.js');
|
|
785
|
+
const result = sanitizeFragment({ html: rawHtml, baseUrl });
|
|
786
|
+
return {
|
|
787
|
+
html: result.html,
|
|
788
|
+
assetUrls: result.assetUrls,
|
|
789
|
+
capturedAt: new Date().toISOString(),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async extractFavicon() {
|
|
793
|
+
try {
|
|
794
|
+
const page = await this.browser.currentPage;
|
|
795
|
+
const faviconUrl = await page.evaluate(() => {
|
|
796
|
+
const icons = Array.from(document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]'));
|
|
797
|
+
if (icons.length > 0) {
|
|
798
|
+
// Prefer apple-touch-icon > icon > shortcut icon, largest size
|
|
799
|
+
icons.sort((a, b) => {
|
|
800
|
+
const priority = (el) => el.rel === 'apple-touch-icon' ? 3 : el.rel === 'icon' ? 2 : 1;
|
|
801
|
+
const size = (el) => {
|
|
802
|
+
const s = el.getAttribute('sizes')?.split('x')[0];
|
|
803
|
+
return s ? parseInt(s, 10) : 0;
|
|
804
|
+
};
|
|
805
|
+
return priority(b) - priority(a) || size(b) - size(a);
|
|
806
|
+
});
|
|
807
|
+
return icons[0].href;
|
|
808
|
+
}
|
|
809
|
+
return new URL('/favicon.ico', window.location.origin).href;
|
|
810
|
+
});
|
|
811
|
+
if (!faviconUrl)
|
|
812
|
+
return null;
|
|
813
|
+
const response = await page.context().request.get(faviconUrl, { timeout: 5000 });
|
|
814
|
+
if (!response.ok())
|
|
815
|
+
return null;
|
|
816
|
+
const buffer = Buffer.from(await response.body());
|
|
817
|
+
if (buffer.length === 0 || buffer.length > 512 * 1024)
|
|
818
|
+
return null;
|
|
819
|
+
const contentType = response.headers()['content-type'] ?? 'image/png';
|
|
820
|
+
const mimeType = contentType.split(';')[0].trim();
|
|
821
|
+
return { buffer, mimeType };
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async close() {
|
|
828
|
+
await this.browser.close();
|
|
829
|
+
}
|
|
830
|
+
async typeIntoLocator(locator, text, clearFirst) {
|
|
831
|
+
const page = await this.browser.currentPage;
|
|
832
|
+
await locator.waitFor({ state: 'visible', timeout: 5000 });
|
|
833
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => undefined);
|
|
834
|
+
const target = await this.moveClipCursorToLocator(locator);
|
|
835
|
+
const position = target
|
|
836
|
+
? await this.relativeClickPosition(locator, target)
|
|
837
|
+
: null;
|
|
838
|
+
await locator.click({
|
|
839
|
+
timeout: 5000,
|
|
840
|
+
...(position ? { position } : {}),
|
|
841
|
+
});
|
|
842
|
+
if (clearFirst) {
|
|
843
|
+
await page.keyboard.press('Control+A');
|
|
844
|
+
}
|
|
845
|
+
await page.waitForTimeout(70);
|
|
846
|
+
await humanType(page, text);
|
|
847
|
+
}
|
|
848
|
+
async seedClipCursor() {
|
|
849
|
+
if (!this.clipCursor)
|
|
850
|
+
return;
|
|
851
|
+
const page = await this.browser.currentPage;
|
|
852
|
+
const viewport = page.viewportSize();
|
|
853
|
+
if (!viewport)
|
|
854
|
+
return;
|
|
855
|
+
const startX = Math.round(viewport.width * (0.3 + Math.random() * 0.4));
|
|
856
|
+
const startY = Math.round(viewport.height * (0.3 + Math.random() * 0.4));
|
|
857
|
+
await page.mouse.move(startX, startY);
|
|
858
|
+
await page.evaluate(({ x, y }) => {
|
|
859
|
+
if (typeof window.__akMoveCursor === 'function')
|
|
860
|
+
window.__akMoveCursor(x, y);
|
|
861
|
+
}, { x: startX, y: startY }).catch(() => { });
|
|
862
|
+
this.clipCursor.currentPosition = { x: startX, y: startY };
|
|
863
|
+
await page.waitForTimeout(60);
|
|
864
|
+
}
|
|
865
|
+
async moveClipCursorToViewportCenter() {
|
|
866
|
+
if (!this.clipCursor)
|
|
867
|
+
return;
|
|
868
|
+
const page = await this.browser.currentPage;
|
|
869
|
+
const viewport = page.viewportSize();
|
|
870
|
+
if (!viewport)
|
|
871
|
+
return;
|
|
872
|
+
await this.moveClipCursorToPoint({
|
|
873
|
+
x: Math.round(viewport.width / 2),
|
|
874
|
+
y: Math.round(viewport.height / 2),
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Animate the cursor to a point inside `locator` and return that point so
|
|
879
|
+
* the caller can click at the SAME coordinates (via
|
|
880
|
+
* `locator.click({ position })`). Returning the target avoids the "snap"
|
|
881
|
+
* bug where Playwright's default click recentered the mouse after the
|
|
882
|
+
* human-like Bezier landed somewhere off-center.
|
|
883
|
+
*
|
|
884
|
+
* Returns null when the cursor overlay is disabled, when the element has
|
|
885
|
+
* no bounding box, or when the cursor is already inside the element (no
|
|
886
|
+
* move needed, caller should let Playwright click at its default center).
|
|
887
|
+
*/
|
|
888
|
+
async moveClipCursorToLocator(locator) {
|
|
889
|
+
if (!this.clipCursor)
|
|
890
|
+
return null;
|
|
891
|
+
await locator.waitFor({ state: 'visible', timeout: 5000 });
|
|
892
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => undefined);
|
|
893
|
+
const page = await this.browser.currentPage;
|
|
894
|
+
const viewport = page.viewportSize();
|
|
895
|
+
const box = await locator.boundingBox();
|
|
896
|
+
if (!box || !viewport)
|
|
897
|
+
return null;
|
|
898
|
+
// Skip the move if the cursor is already inside the element — avoids
|
|
899
|
+
// erratic micro-movements when HOVER is immediately followed by CLICK
|
|
900
|
+
// on the same target.
|
|
901
|
+
const cur = this.clipCursor.currentPosition;
|
|
902
|
+
if (cur
|
|
903
|
+
&& cur.x >= box.x && cur.x <= box.x + box.width
|
|
904
|
+
&& cur.y >= box.y && cur.y <= box.y + box.height) {
|
|
905
|
+
return cur;
|
|
906
|
+
}
|
|
907
|
+
const target = getHumanPointInBox(box, viewport);
|
|
908
|
+
await this.moveClipCursorToPoint(target);
|
|
909
|
+
return target;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Convert an absolute viewport point to a position relative to `locator`'s
|
|
913
|
+
* bounding box, suitable for `locator.click({ position })`. Returns null
|
|
914
|
+
* if the point is outside the box (defensive fallback — callers should
|
|
915
|
+
* then let Playwright click at the default center).
|
|
916
|
+
*/
|
|
917
|
+
async relativeClickPosition(locator, absolute) {
|
|
918
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
919
|
+
if (!box)
|
|
920
|
+
return null;
|
|
921
|
+
const rx = absolute.x - box.x;
|
|
922
|
+
const ry = absolute.y - box.y;
|
|
923
|
+
if (rx < 0 || ry < 0 || rx > box.width || ry > box.height)
|
|
924
|
+
return null;
|
|
925
|
+
return { x: rx, y: ry };
|
|
926
|
+
}
|
|
927
|
+
async moveClipCursorToPoint(point, options) {
|
|
928
|
+
if (!this.clipCursor)
|
|
929
|
+
return;
|
|
930
|
+
const page = await this.browser.currentPage;
|
|
931
|
+
const from = this.clipCursor.currentPosition;
|
|
932
|
+
if (from) {
|
|
933
|
+
await moveMouse(page, from, point, options);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
await page.mouse.move(point.x, point.y);
|
|
937
|
+
await page.evaluate(({ x, y }) => {
|
|
938
|
+
if (typeof window.__akMoveCursor === 'function')
|
|
939
|
+
window.__akMoveCursor(x, y);
|
|
940
|
+
}, { x: Math.round(point.x), y: Math.round(point.y) }).catch(() => { });
|
|
941
|
+
}
|
|
942
|
+
this.clipCursor.currentPosition = point;
|
|
943
|
+
}
|
|
944
|
+
async emitClipClickPulse() {
|
|
945
|
+
if (!this.clipCursor?.currentPosition)
|
|
946
|
+
return;
|
|
947
|
+
const page = await this.browser.currentPage;
|
|
948
|
+
const { x, y } = this.clipCursor.currentPosition;
|
|
949
|
+
await page.evaluate(({ px, py }) => {
|
|
950
|
+
if (typeof window.__akClickPulse === 'function')
|
|
951
|
+
window.__akClickPulse(px, py);
|
|
952
|
+
}, { px: Math.round(x), py: Math.round(y) }).catch(() => { });
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function describeResolveOptions(opts) {
|
|
956
|
+
const parts = [];
|
|
957
|
+
if (opts.selector)
|
|
958
|
+
parts.push(`selector="${opts.selector}"`);
|
|
959
|
+
if (opts.target?.text)
|
|
960
|
+
parts.push(`text="${opts.target.text}"`);
|
|
961
|
+
if (opts.target?.role)
|
|
962
|
+
parts.push(`role="${opts.target.role}"`);
|
|
963
|
+
if (opts.target?.label)
|
|
964
|
+
parts.push(`label="${opts.target.label}"`);
|
|
965
|
+
if (opts.target?.placeholder)
|
|
966
|
+
parts.push(`placeholder="${opts.target.placeholder}"`);
|
|
967
|
+
return parts.join(', ') || 'no target specified';
|
|
968
|
+
}
|
|
969
|
+
function getHumanPointInBox(box, viewport) {
|
|
970
|
+
const insetX = Math.min(Math.max(box.width * 0.2, 6), Math.max(6, box.width / 2));
|
|
971
|
+
const insetY = Math.min(Math.max(box.height * 0.2, 6), Math.max(6, box.height / 2));
|
|
972
|
+
const minX = box.x + insetX;
|
|
973
|
+
const maxX = box.x + Math.max(insetX, box.width - insetX);
|
|
974
|
+
const minY = box.y + insetY;
|
|
975
|
+
const maxY = box.y + Math.max(insetY, box.height - insetY);
|
|
976
|
+
const targetX = maxX <= minX ? box.x + box.width / 2 : randomBetween(minX, maxX);
|
|
977
|
+
const targetY = maxY <= minY ? box.y + box.height / 2 : randomBetween(minY, maxY);
|
|
978
|
+
return {
|
|
979
|
+
x: clampPoint(targetX, 4, viewport.width - 4),
|
|
980
|
+
y: clampPoint(targetY, 4, viewport.height - 4),
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function randomBetween(min, max) {
|
|
984
|
+
return min + Math.random() * Math.max(0, max - min);
|
|
985
|
+
}
|
|
986
|
+
function clampPoint(value, min, max) {
|
|
987
|
+
return Math.round(Math.max(min, Math.min(max, value)));
|
|
988
|
+
}
|
|
989
|
+
//# sourceMappingURL=web-playwright-local.js.map
|