@weavetab/mcp 2.5.0-beta.0
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/AGENTS.md +48 -0
- package/ARCHITECTURE.md +590 -0
- package/LICENSE +631 -0
- package/README.md +909 -0
- package/SECURITY.md +196 -0
- package/TROUBLESHOOTING.md +365 -0
- package/dist/anticaptcha/detector.d.ts +49 -0
- package/dist/anticaptcha/detector.js +465 -0
- package/dist/anticaptcha/integration.d.ts +36 -0
- package/dist/anticaptcha/integration.js +123 -0
- package/dist/anticaptcha/solver.d.ts +27 -0
- package/dist/anticaptcha/solver.js +552 -0
- package/dist/audit/logger.d.ts +13 -0
- package/dist/audit/logger.js +177 -0
- package/dist/audit/telemetry.d.ts +36 -0
- package/dist/audit/telemetry.js +162 -0
- package/dist/cdp/bridge.d.ts +16 -0
- package/dist/cdp/bridge.js +826 -0
- package/dist/cdp/confirm.d.ts +26 -0
- package/dist/cdp/confirm.js +79 -0
- package/dist/cdp/connector.d.ts +37 -0
- package/dist/cdp/connector.js +661 -0
- package/dist/cdp/extension.d.ts +36 -0
- package/dist/cdp/extension.js +118 -0
- package/dist/cdp/finder.d.ts +1 -0
- package/dist/cdp/finder.js +2 -0
- package/dist/cdp/helpers.d.ts +11 -0
- package/dist/cdp/helpers.js +195 -0
- package/dist/cdp/label_cache.d.ts +12 -0
- package/dist/cdp/label_cache.js +51 -0
- package/dist/cdp/lock.d.ts +33 -0
- package/dist/cdp/lock.js +206 -0
- package/dist/cdp/omni_broker.d.ts +17 -0
- package/dist/cdp/omni_broker.js +119 -0
- package/dist/cdp/profiles.d.ts +1 -0
- package/dist/cdp/profiles.js +87 -0
- package/dist/cdp/pruner.d.ts +41 -0
- package/dist/cdp/pruner.js +263 -0
- package/dist/cdp/ref_cache.d.ts +101 -0
- package/dist/cdp/ref_cache.js +234 -0
- package/dist/cdp/retry.d.ts +5 -0
- package/dist/cdp/retry.js +39 -0
- package/dist/cdp/retry_enhanced.d.ts +44 -0
- package/dist/cdp/retry_enhanced.js +102 -0
- package/dist/cdp/security.d.ts +50 -0
- package/dist/cdp/security.js +237 -0
- package/dist/cdp/verify.d.ts +39 -0
- package/dist/cdp/verify.js +143 -0
- package/dist/cdp/walker.d.ts +91 -0
- package/dist/cdp/walker.js +896 -0
- package/dist/cdp/walker.test.d.ts +2 -0
- package/dist/cdp/walker.test.js +172 -0
- package/dist/cdp/websocket_transport.d.ts +15 -0
- package/dist/cdp/websocket_transport.js +118 -0
- package/dist/cli/plugin.d.ts +9 -0
- package/dist/cli/plugin.js +1134 -0
- package/dist/cli/updater.d.ts +1 -0
- package/dist/cli/updater.js +72 -0
- package/dist/config/loader.d.ts +120 -0
- package/dist/config/loader.js +704 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1248 -0
- package/dist/intelligence/hints.d.ts +11 -0
- package/dist/intelligence/hints.js +39 -0
- package/dist/intelligence/intent.d.ts +33 -0
- package/dist/intelligence/intent.js +171 -0
- package/dist/intelligence/sessionMemory.d.ts +66 -0
- package/dist/intelligence/sessionMemory.js +284 -0
- package/dist/intelligence/sessionMemory.test.d.ts +2 -0
- package/dist/intelligence/sessionMemory.test.js +62 -0
- package/dist/intelligence/trail.d.ts +64 -0
- package/dist/intelligence/trail.js +233 -0
- package/dist/intelligence/trail.test.d.ts +2 -0
- package/dist/intelligence/trail.test.js +110 -0
- package/dist/intelligence/vision.d.ts +24 -0
- package/dist/intelligence/vision.js +151 -0
- package/dist/lifecycle/mission_manager.d.ts +16 -0
- package/dist/lifecycle/mission_manager.js +44 -0
- package/dist/overlay/bubble.d.ts +16 -0
- package/dist/overlay/bubble.js +37 -0
- package/dist/overlay/ghost.d.ts +35 -0
- package/dist/overlay/ghost.js +508 -0
- package/dist/overlay/injector.d.ts +22 -0
- package/dist/overlay/injector.js +198 -0
- package/dist/plugin/interface.d.ts +134 -0
- package/dist/plugin/interface.js +1 -0
- package/dist/plugin/loader.d.ts +18 -0
- package/dist/plugin/loader.js +155 -0
- package/dist/plugin/registry.d.ts +50 -0
- package/dist/plugin/registry.js +234 -0
- package/dist/security/blacklist.d.ts +24 -0
- package/dist/security/blacklist.js +80 -0
- package/dist/security/filter.d.ts +2 -0
- package/dist/security/filter.js +20 -0
- package/dist/security/guard.d.ts +8 -0
- package/dist/security/guard.js +35 -0
- package/dist/security/ratelimit.d.ts +11 -0
- package/dist/security/ratelimit.js +57 -0
- package/dist/security/secrets.d.ts +75 -0
- package/dist/security/secrets.js +365 -0
- package/dist/sensors/actionability.d.ts +81 -0
- package/dist/sensors/actionability.js +303 -0
- package/dist/sensors/guard.d.ts +25 -0
- package/dist/sensors/guard.js +168 -0
- package/dist/sensors/mutations.d.ts +23 -0
- package/dist/sensors/mutations.js +93 -0
- package/dist/sensors/network.d.ts +67 -0
- package/dist/sensors/network.js +193 -0
- package/dist/sensors/operation_monitor.d.ts +80 -0
- package/dist/sensors/operation_monitor.js +203 -0
- package/dist/sensors/pacing.d.ts +44 -0
- package/dist/sensors/pacing.js +111 -0
- package/dist/sensors/thoughts.d.ts +2 -0
- package/dist/sensors/thoughts.js +59 -0
- package/dist/sensors/wait.d.ts +12 -0
- package/dist/sensors/wait.js +44 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1687 -0
- package/dist/state/agent.d.ts +36 -0
- package/dist/state/agent.js +101 -0
- package/dist/state/cache.d.ts +16 -0
- package/dist/state/cache.js +92 -0
- package/dist/state/cache.test.d.ts +2 -0
- package/dist/state/cache.test.js +76 -0
- package/dist/state/delta.d.ts +71 -0
- package/dist/state/delta.js +234 -0
- package/dist/state/delta.test.d.ts +2 -0
- package/dist/state/delta.test.js +279 -0
- package/dist/state/groups.d.ts +15 -0
- package/dist/state/groups.js +80 -0
- package/dist/state/session.d.ts +30 -0
- package/dist/state/session.js +155 -0
- package/dist/tools/accessibility.d.ts +14 -0
- package/dist/tools/accessibility.js +32 -0
- package/dist/tools/ask.d.ts +14 -0
- package/dist/tools/ask.js +49 -0
- package/dist/tools/automation.d.ts +36 -0
- package/dist/tools/automation.js +218 -0
- package/dist/tools/browser_detect.d.ts +13 -0
- package/dist/tools/browser_detect.js +73 -0
- package/dist/tools/burst.d.ts +35 -0
- package/dist/tools/burst.js +344 -0
- package/dist/tools/canvas.d.ts +25 -0
- package/dist/tools/canvas.js +132 -0
- package/dist/tools/captcha.d.ts +38 -0
- package/dist/tools/captcha.js +209 -0
- package/dist/tools/click.d.ts +36 -0
- package/dist/tools/click.js +561 -0
- package/dist/tools/click_and_wait.d.ts +33 -0
- package/dist/tools/click_and_wait.js +149 -0
- package/dist/tools/clipboard.d.ts +29 -0
- package/dist/tools/clipboard.js +279 -0
- package/dist/tools/console.d.ts +21 -0
- package/dist/tools/console.js +107 -0
- package/dist/tools/cookies.d.ts +67 -0
- package/dist/tools/cookies.js +121 -0
- package/dist/tools/design_extract.d.ts +109 -0
- package/dist/tools/design_extract.js +641 -0
- package/dist/tools/dialog.d.ts +24 -0
- package/dist/tools/dialog.js +109 -0
- package/dist/tools/drag.d.ts +15 -0
- package/dist/tools/drag.js +29 -0
- package/dist/tools/emulation.d.ts +33 -0
- package/dist/tools/emulation.js +69 -0
- package/dist/tools/eval.d.ts +20 -0
- package/dist/tools/eval.js +92 -0
- package/dist/tools/fill.d.ts +41 -0
- package/dist/tools/fill.js +667 -0
- package/dist/tools/find.d.ts +30 -0
- package/dist/tools/find.js +60 -0
- package/dist/tools/geo.d.ts +19 -0
- package/dist/tools/geo.js +29 -0
- package/dist/tools/github/analyze.d.ts +4 -0
- package/dist/tools/github/analyze.js +113 -0
- package/dist/tools/github/cache.d.ts +21 -0
- package/dist/tools/github/cache.js +28 -0
- package/dist/tools/github/index.d.ts +4 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/issues.d.ts +9 -0
- package/dist/tools/github/issues.js +88 -0
- package/dist/tools/github/pr.d.ts +5 -0
- package/dist/tools/github/pr.js +88 -0
- package/dist/tools/github/rateLimiter.d.ts +4 -0
- package/dist/tools/github/rateLimiter.js +46 -0
- package/dist/tools/github/read.d.ts +34 -0
- package/dist/tools/github/read.js +74 -0
- package/dist/tools/github/repoContext.d.ts +20 -0
- package/dist/tools/github/repoContext.js +129 -0
- package/dist/tools/highlight.d.ts +20 -0
- package/dist/tools/highlight.js +140 -0
- package/dist/tools/hover.d.ts +19 -0
- package/dist/tools/hover.js +57 -0
- package/dist/tools/inspect.d.ts +17 -0
- package/dist/tools/inspect.js +125 -0
- package/dist/tools/integration.test.d.ts +2 -0
- package/dist/tools/integration.test.js +96 -0
- package/dist/tools/key.d.ts +25 -0
- package/dist/tools/key.js +173 -0
- package/dist/tools/macro_compiler.d.ts +7 -0
- package/dist/tools/macro_compiler.js +82 -0
- package/dist/tools/mouse.d.ts +21 -0
- package/dist/tools/mouse.js +62 -0
- package/dist/tools/navigate.d.ts +16 -0
- package/dist/tools/navigate.js +188 -0
- package/dist/tools/network_intercept.d.ts +17 -0
- package/dist/tools/network_intercept.js +151 -0
- package/dist/tools/office/index.d.ts +9 -0
- package/dist/tools/office/index.js +7 -0
- package/dist/tools/office/list.d.ts +15 -0
- package/dist/tools/office/list.js +41 -0
- package/dist/tools/office/parse.d.ts +20 -0
- package/dist/tools/office/parse.js +172 -0
- package/dist/tools/pdf.d.ts +22 -0
- package/dist/tools/pdf.js +42 -0
- package/dist/tools/performance.d.ts +12 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/performance_test.d.ts +70 -0
- package/dist/tools/performance_test.js +128 -0
- package/dist/tools/plan.d.ts +39 -0
- package/dist/tools/plan.js +161 -0
- package/dist/tools/pointer.d.ts +35 -0
- package/dist/tools/pointer.js +522 -0
- package/dist/tools/read.d.ts +30 -0
- package/dist/tools/read.js +488 -0
- package/dist/tools/recording.d.ts +45 -0
- package/dist/tools/recording.js +124 -0
- package/dist/tools/response.d.ts +129 -0
- package/dist/tools/response.js +5 -0
- package/dist/tools/scrape.d.ts +24 -0
- package/dist/tools/scrape.js +140 -0
- package/dist/tools/screenshot.d.ts +40 -0
- package/dist/tools/screenshot.js +79 -0
- package/dist/tools/scroll.d.ts +23 -0
- package/dist/tools/scroll.js +196 -0
- package/dist/tools/select.d.ts +13 -0
- package/dist/tools/select.js +217 -0
- package/dist/tools/snapshot.d.ts +27 -0
- package/dist/tools/snapshot.js +99 -0
- package/dist/tools/storage.d.ts +18 -0
- package/dist/tools/storage.js +29 -0
- package/dist/tools/tabs.d.ts +77 -0
- package/dist/tools/tabs.js +342 -0
- package/dist/tools/thought.d.ts +16 -0
- package/dist/tools/thought.js +79 -0
- package/dist/tools/type.d.ts +65 -0
- package/dist/tools/type.js +1061 -0
- package/dist/tools/type_secret.d.ts +23 -0
- package/dist/tools/type_secret.js +229 -0
- package/dist/tools/upload.d.ts +19 -0
- package/dist/tools/upload.js +103 -0
- package/dist/tools/video_compiler.d.ts +24 -0
- package/dist/tools/video_compiler.js +199 -0
- package/dist/tools/viewport.d.ts +44 -0
- package/dist/tools/viewport.js +230 -0
- package/dist/tools/wait.d.ts +19 -0
- package/dist/tools/wait.js +202 -0
- package/dist/tools/wait_for.d.ts +23 -0
- package/dist/tools/wait_for.js +30 -0
- package/dist/tools/windows.d.ts +22 -0
- package/dist/tools/windows.js +57 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +12 -0
- package/docs/DEV_MODE.md +68 -0
- package/docs/DOCUMENTATION.md +532 -0
- package/docs/PLUGINS.md +538 -0
- package/docs/TOOLS.md +104 -0
- package/docs/dev_plugin.md +310 -0
- package/ext-dist/background.js +467 -0
- package/ext-dist/content.js +3089 -0
- package/ext-dist/dashboard.html +583 -0
- package/ext-dist/hud/hud.js +73 -0
- package/ext-dist/hud/index.html +20 -0
- package/ext-dist/manifest.json +77 -0
- package/ext-dist/sidepanel/sidepanel.js +25 -0
- package/ext-dist/sidepanel.html +547 -0
- package/ext-dist/styles.css +218 -0
- package/icon.png +0 -0
- package/mcp.json +796 -0
- package/package.json +113 -0
- package/server.json +23 -0
- package/smithery.yaml +11 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/sensors/actionability.ts
|
|
3
|
+
*
|
|
4
|
+
* Playwright-Strategy Actionability Engine for WeaveTab
|
|
5
|
+
* ─────────────────────────────────────────────────────
|
|
6
|
+
* Implements every correctness principle from playwright-strategy.md:
|
|
7
|
+
*
|
|
8
|
+
* 1. RAF-stability gate — element must not be animating before we touch it
|
|
9
|
+
* 2. Strict hit-target — elementsFromPoint at the computed click point must
|
|
10
|
+
* confirm the target is on top (or the target's child).
|
|
11
|
+
* 3. WeaveTab transparency — WeaveTab's own UI (data-wt-ignore nodes) is excluded
|
|
12
|
+
* from the hit stack so the tool never blocks itself.
|
|
13
|
+
* 4. 4-way scroll rotation — escape sticky headers / overlays by cycling four
|
|
14
|
+
* scrollIntoView alignments before giving up.
|
|
15
|
+
* 5. Per-action backoff — [0, 20, 100, 100, 500] ms retry on transient failures.
|
|
16
|
+
* 6. Rich blocker report — on permanent occlusion, return the blocking element's
|
|
17
|
+
* description so the model can reason about it.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* import { verifyActionability } from "../sensors/actionability.js";
|
|
21
|
+
* const result = await verifyActionability(session, selector);
|
|
22
|
+
* if (result.status !== "ACTIONABLE") { ... }
|
|
23
|
+
*/
|
|
24
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
25
|
+
/** Playwright's per-action retry backoff (ms). */
|
|
26
|
+
const BACKOFF = [0, 20, 100, 100, 500];
|
|
27
|
+
/**
|
|
28
|
+
* 4-way scroll alignment rotation — escape sticky headers.
|
|
29
|
+
* undefined = browser default (nearest), then progressively more aggressive.
|
|
30
|
+
*/
|
|
31
|
+
const SCROLL_ALIGNMENTS = [
|
|
32
|
+
undefined,
|
|
33
|
+
{ block: "end", inline: "end" },
|
|
34
|
+
{ block: "center", inline: "center" },
|
|
35
|
+
{ block: "start", inline: "start" },
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Injected expression that does the full Playwright-style actionability check
|
|
39
|
+
* in the page context. Returns a serialisable result object.
|
|
40
|
+
*
|
|
41
|
+
* Parameters injected at build-time via template literals:
|
|
42
|
+
* selector — CSS selector string
|
|
43
|
+
* scrollOptionsJSON — JSON string of ScrollIntoViewOptions or "null"
|
|
44
|
+
* fingerprintJSON — JSON string of {tag,role} or "null"
|
|
45
|
+
* stabilityMs — ms to wait for stable rect
|
|
46
|
+
*/
|
|
47
|
+
function buildPageExpression(selector, scrollOptions, fingerprint, stabilityMs) {
|
|
48
|
+
const selectorLiteral = JSON.stringify(selector);
|
|
49
|
+
const scrollLiteral = scrollOptions ? JSON.stringify(scrollOptions) : "null";
|
|
50
|
+
const fingerprintLiteral = fingerprint ? JSON.stringify(fingerprint) : "null";
|
|
51
|
+
return `(async function() {
|
|
52
|
+
// ── Helper: is this node owned by WeaveTab? ────────────────────────────
|
|
53
|
+
function isWtOwned(el) {
|
|
54
|
+
if (!el) return false;
|
|
55
|
+
// Attribute-based markers: data-wt-ignore and data-wt-internal both signal
|
|
56
|
+
// WeaveTab-owned UI that must not block page interactions.
|
|
57
|
+
if (el.getAttribute && (
|
|
58
|
+
el.getAttribute('data-wt-ignore') !== null ||
|
|
59
|
+
el.getAttribute('data-wt-internal') !== null ||
|
|
60
|
+
el.getAttribute('data-wt-overlay') !== null
|
|
61
|
+
)) return true;
|
|
62
|
+
const wtIds = ['Weavetab-hud', '__Weavetab_controlled_indicator', 'wt-main-container', 'wt-system-icon', 'wt-f3-host', 'Weavetab-root'];
|
|
63
|
+
if (el.id && wtIds.includes(el.id)) return true;
|
|
64
|
+
if (el.closest) {
|
|
65
|
+
if (el.closest('[data-wt-ignore],[data-wt-internal],[data-wt-overlay]')) return true;
|
|
66
|
+
if (wtIds.some(id => el.closest('#' + id))) return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Step 1: Resolve element ────────────────────────────────────────────
|
|
72
|
+
let el = document.querySelector(${selectorLiteral});
|
|
73
|
+
if (!el && window.__Weavetab_shadowQuery) {
|
|
74
|
+
el = window.__Weavetab_shadowQuery(${selectorLiteral});
|
|
75
|
+
}
|
|
76
|
+
if (!el) return { status: 'NOT_FOUND' };
|
|
77
|
+
|
|
78
|
+
// ── Step 2: Fingerprint check ──────────────────────────────────────────
|
|
79
|
+
const fp = ${fingerprintLiteral};
|
|
80
|
+
if (fp) {
|
|
81
|
+
if (fp.tag && el.tagName.toLowerCase() !== fp.tag.toLowerCase())
|
|
82
|
+
return { status: 'FINGERPRINT_MISMATCH' };
|
|
83
|
+
if (fp.role && el.getAttribute('role') !== fp.role)
|
|
84
|
+
return { status: 'FINGERPRINT_MISMATCH' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Step 3: Basic visibility ───────────────────────────────────────────
|
|
88
|
+
// Use checkVisibility() (Chrome 105+) as the most reliable single check.
|
|
89
|
+
// Falls back to manual computed-style + rect checks for older runtimes.
|
|
90
|
+
if (typeof el.checkVisibility === 'function') {
|
|
91
|
+
if (!el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }))
|
|
92
|
+
return { status: 'INVISIBLE' };
|
|
93
|
+
} else {
|
|
94
|
+
const cs = window.getComputedStyle(el);
|
|
95
|
+
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0')
|
|
96
|
+
return { status: 'INVISIBLE' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let rect = el.getBoundingClientRect();
|
|
100
|
+
if (rect.width === 0 || rect.height === 0) return { status: 'INVISIBLE' };
|
|
101
|
+
// Off-screen by more than 2× viewport dimension in any direction
|
|
102
|
+
if (rect.right < -window.innerWidth || rect.bottom < -window.innerHeight ||
|
|
103
|
+
rect.left > 2 * window.innerWidth || rect.top > 2 * window.innerHeight)
|
|
104
|
+
return { status: 'INVISIBLE' };
|
|
105
|
+
|
|
106
|
+
// ── Step 4: Scroll into view (using provided alignment) ────────────────
|
|
107
|
+
const scrollOpts = ${scrollLiteral};
|
|
108
|
+
if (scrollOpts) {
|
|
109
|
+
el.scrollIntoView(scrollOpts);
|
|
110
|
+
await new Promise(r => setTimeout(r, 120));
|
|
111
|
+
} else if (rect.top < 0 || rect.bottom > window.innerHeight ||
|
|
112
|
+
rect.left < 0 || rect.right > window.innerWidth) {
|
|
113
|
+
el.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
114
|
+
await new Promise(r => setTimeout(r, 120));
|
|
115
|
+
}
|
|
116
|
+
rect = el.getBoundingClientRect();
|
|
117
|
+
|
|
118
|
+
// ── Step 5: RAF-stability gate ─────────────────────────────────────────
|
|
119
|
+
const stabilityDeadline = Date.now() + ${stabilityMs};
|
|
120
|
+
let prevRect = el.getBoundingClientRect();
|
|
121
|
+
let stable = false;
|
|
122
|
+
while (Date.now() < stabilityDeadline) {
|
|
123
|
+
await new Promise(r => requestAnimationFrame(r));
|
|
124
|
+
await new Promise(r => requestAnimationFrame(r)); // two ticks like Playwright
|
|
125
|
+
const cur = el.getBoundingClientRect();
|
|
126
|
+
if (
|
|
127
|
+
Math.abs(cur.x - prevRect.x) < 0.5 &&
|
|
128
|
+
Math.abs(cur.y - prevRect.y) < 0.5 &&
|
|
129
|
+
Math.abs(cur.width - prevRect.width) < 0.5 &&
|
|
130
|
+
Math.abs(cur.height - prevRect.height) < 0.5
|
|
131
|
+
) {
|
|
132
|
+
stable = true;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
prevRect = cur;
|
|
136
|
+
}
|
|
137
|
+
if (!stable) return { status: 'UNSTABLE' };
|
|
138
|
+
rect = el.getBoundingClientRect();
|
|
139
|
+
|
|
140
|
+
// ── Step 6: Compute click centre (clamped to viewport) ─────────────────
|
|
141
|
+
const cx = Math.round(Math.min(Math.max(rect.left + rect.width / 2, 0), window.innerWidth - 1));
|
|
142
|
+
const cy = Math.round(Math.min(Math.max(rect.top + rect.height / 2, 0), window.innerHeight - 1));
|
|
143
|
+
|
|
144
|
+
// ── Step 7: Strict hit-target check ───────────────────────────────────
|
|
145
|
+
// elementsFromPoint returns the full composited stack, topmost first.
|
|
146
|
+
const stack = document.elementsFromPoint(cx, cy);
|
|
147
|
+
if (!stack || stack.length === 0) return { status: 'ACTIONABLE', x: cx, y: cy };
|
|
148
|
+
|
|
149
|
+
// Walk from the topmost element. Anything that is WeaveTab-owned is
|
|
150
|
+
// transparent — skip it. The first non-WT element must be el or its descendant.
|
|
151
|
+
let blocker = null;
|
|
152
|
+
for (const hit of stack) {
|
|
153
|
+
if (isWtOwned(hit)) continue; // WeaveTab transparency
|
|
154
|
+
blocker = hit;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!blocker || blocker === el || el.contains(blocker)) {
|
|
159
|
+
return { status: 'ACTIONABLE', x: cx, y: cy };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check composed tree (shadow DOM + slots)
|
|
163
|
+
let ancestor = blocker;
|
|
164
|
+
while (ancestor) {
|
|
165
|
+
if (ancestor === el) return { status: 'ACTIONABLE', x: cx, y: cy };
|
|
166
|
+
ancestor = ancestor.assignedSlot || ancestor.parentElement ||
|
|
167
|
+
(ancestor.getRootNode && ancestor.getRootNode({ composed: true }) !== ancestor
|
|
168
|
+
? ancestor.getRootNode({ composed: true }).host : null);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
status: 'OCCLUDED',
|
|
173
|
+
x: cx, y: cy,
|
|
174
|
+
occludedBy: {
|
|
175
|
+
tag: blocker.tagName.toLowerCase(),
|
|
176
|
+
id: blocker.id || '',
|
|
177
|
+
className: (blocker.className && typeof blocker.className === 'string')
|
|
178
|
+
? blocker.className.slice(0, 60) : '',
|
|
179
|
+
ariaLabel: blocker.getAttribute('aria-label') || blocker.getAttribute('title') || null,
|
|
180
|
+
isWeavetabOwned: isWtOwned(blocker)
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
})()`;
|
|
184
|
+
}
|
|
185
|
+
// ── Main export ───────────────────────────────────────────────────────────────
|
|
186
|
+
/**
|
|
187
|
+
* Full Playwright-strategy actionability verification.
|
|
188
|
+
*
|
|
189
|
+
* Runs up to `BACKOFF.length` attempts with 4-way scroll rotation,
|
|
190
|
+
* RAF stability gating, WeaveTab-transparent hit-target checking,
|
|
191
|
+
* and per-attempt retry backoff.
|
|
192
|
+
*
|
|
193
|
+
* Returns `status: "ACTIONABLE"` with computed (x, y) when safe to dispatch,
|
|
194
|
+
* or a failure status with an optional `occludedBy` description.
|
|
195
|
+
*/
|
|
196
|
+
export async function verifyActionability(session, selector, opts = {}) {
|
|
197
|
+
const stabilityMs = opts.stabilityTimeoutMs ?? 300;
|
|
198
|
+
const totalDeadline = Date.now() + (opts.totalTimeoutMs ?? 2000);
|
|
199
|
+
let lastResult = { status: "NOT_FOUND" };
|
|
200
|
+
let scrollIdx = 0;
|
|
201
|
+
for (let attempt = 0; attempt < BACKOFF.length; attempt++) {
|
|
202
|
+
// Honour total time budget
|
|
203
|
+
if (Date.now() >= totalDeadline)
|
|
204
|
+
break;
|
|
205
|
+
// Backoff delay (first attempt is instant)
|
|
206
|
+
if (BACKOFF[attempt] > 0) {
|
|
207
|
+
await new Promise((r) => setTimeout(r, BACKOFF[attempt]));
|
|
208
|
+
}
|
|
209
|
+
const scrollAlignment = SCROLL_ALIGNMENTS[scrollIdx % SCROLL_ALIGNMENTS.length];
|
|
210
|
+
const expr = buildPageExpression(selector, scrollAlignment, opts.fingerprint, stabilityMs);
|
|
211
|
+
let raw;
|
|
212
|
+
try {
|
|
213
|
+
const { result } = await session.Runtime.evaluate({
|
|
214
|
+
expression: expr,
|
|
215
|
+
awaitPromise: true,
|
|
216
|
+
returnByValue: true,
|
|
217
|
+
});
|
|
218
|
+
raw = result?.value;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// CDP error — element might have been detached; retry
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (!raw)
|
|
225
|
+
continue;
|
|
226
|
+
lastResult = raw;
|
|
227
|
+
switch (lastResult.status) {
|
|
228
|
+
case "ACTIONABLE":
|
|
229
|
+
return lastResult;
|
|
230
|
+
case "NOT_FOUND":
|
|
231
|
+
case "FINGERPRINT_MISMATCH":
|
|
232
|
+
case "INVISIBLE":
|
|
233
|
+
// These are likely permanent; still retry in case of SPA re-render
|
|
234
|
+
break;
|
|
235
|
+
case "UNSTABLE":
|
|
236
|
+
// Give the animation more time on next attempt
|
|
237
|
+
break;
|
|
238
|
+
case "OCCLUDED": {
|
|
239
|
+
const occ = lastResult.occludedBy;
|
|
240
|
+
// If the only blocker is WeaveTab's own UI, treat as transparent and retry
|
|
241
|
+
if (occ?.isWeavetabOwned) {
|
|
242
|
+
lastResult = { status: "ACTIONABLE", x: lastResult.x, y: lastResult.y };
|
|
243
|
+
return lastResult;
|
|
244
|
+
}
|
|
245
|
+
// Advance scroll alignment to try to escape the blocker
|
|
246
|
+
scrollIdx++;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return lastResult;
|
|
252
|
+
}
|
|
253
|
+
// ── Convenience wrapper ───────────────────────────────────────────────────────
|
|
254
|
+
/**
|
|
255
|
+
* Quick check: is an element at these coords actually blocked by a non-WT overlay?
|
|
256
|
+
* Used for lightweight pre-dispatch validation inside click.ts.
|
|
257
|
+
*/
|
|
258
|
+
export async function quickHitCheck(session, x, y, selectorOfTarget) {
|
|
259
|
+
const expr = `(function() {
|
|
260
|
+
function isWtOwned(el) {
|
|
261
|
+
if (!el) return false;
|
|
262
|
+
if (el.getAttribute && (
|
|
263
|
+
el.getAttribute('data-wt-ignore') !== null ||
|
|
264
|
+
el.getAttribute('data-wt-internal') !== null ||
|
|
265
|
+
el.getAttribute('data-wt-overlay') !== null
|
|
266
|
+
)) return true;
|
|
267
|
+
const ids = ['Weavetab-hud','__Weavetab_controlled_indicator','wt-main-container','wt-system-icon','wt-f3-host','Weavetab-root'];
|
|
268
|
+
if (el.id && ids.includes(el.id)) return true;
|
|
269
|
+
return !!(el.closest && (el.closest('[data-wt-ignore],[data-wt-internal],[data-wt-overlay]') || ids.some(id => el.closest('#' + id))));
|
|
270
|
+
}
|
|
271
|
+
const target = document.querySelector(${JSON.stringify(selectorOfTarget)})
|
|
272
|
+
|| (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(${JSON.stringify(selectorOfTarget)}) : null);
|
|
273
|
+
const stack = document.elementsFromPoint(${x}, ${y});
|
|
274
|
+
if (!stack || !stack.length) return { clear: true };
|
|
275
|
+
for (const hit of stack) {
|
|
276
|
+
if (isWtOwned(hit)) continue;
|
|
277
|
+
if (!target || hit === target || target.contains(hit)) return { clear: true };
|
|
278
|
+
// Walk composed tree for shadow DOM
|
|
279
|
+
let ancestor = hit;
|
|
280
|
+
while (ancestor) {
|
|
281
|
+
if (ancestor === target) return { clear: true };
|
|
282
|
+
ancestor = ancestor.assignedSlot || ancestor.parentElement ||
|
|
283
|
+
(ancestor.getRootNode && ancestor.getRootNode({composed:true}) !== ancestor
|
|
284
|
+
? ancestor.getRootNode({composed:true}).host : null);
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
clear: false,
|
|
288
|
+
blockerDescription: hit.tagName.toLowerCase() +
|
|
289
|
+
(hit.id ? '#' + hit.id : '') +
|
|
290
|
+
(hit.className && typeof hit.className === 'string' ? '.' + hit.className.split(' ')[0] : '') +
|
|
291
|
+
' intercepts pointer events'
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return { clear: true };
|
|
295
|
+
})()`;
|
|
296
|
+
try {
|
|
297
|
+
const { result } = await session.Runtime.evaluate({ expression: expr, returnByValue: true });
|
|
298
|
+
return result?.value ?? { clear: true };
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return { clear: true }; // non-fatal; proceed optimistically
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weavetab — sensors/guard.ts
|
|
3
|
+
* guardedAction pipeline: element readiness, DOM stability wait, and navigation await.
|
|
4
|
+
* Weavetab by fy2ne
|
|
5
|
+
*/
|
|
6
|
+
import CDP from "chrome-remote-interface";
|
|
7
|
+
export interface PollOptions {
|
|
8
|
+
timeout: number;
|
|
9
|
+
interval: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function pollUntil(predicate: () => Promise<boolean>, opts: PollOptions): Promise<void>;
|
|
12
|
+
export declare function waitForDOMStable(session: CDP.Client, quietPeriod?: number, // Reduced from 100ms
|
|
13
|
+
timeout?: number, // Reduced from 1500ms
|
|
14
|
+
fast?: boolean): Promise<void>;
|
|
15
|
+
export declare function guardedAction(session: CDP.Client, backendNodeId: number, action: () => Promise<void>, label?: string, postActionPipeline?: () => Promise<void>, fast?: boolean): Promise<void>;
|
|
16
|
+
export interface PageWeight {
|
|
17
|
+
level: "light" | "medium" | "heavy";
|
|
18
|
+
elementCount: number;
|
|
19
|
+
imageCount: number;
|
|
20
|
+
scriptCount: number;
|
|
21
|
+
iframeCount: number;
|
|
22
|
+
suggestedTimeout: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function detectPageWeight(session: CDP.Client): Promise<PageWeight>;
|
|
25
|
+
export declare function awaitNavigation(session: CDP.Client, timeout?: number): Promise<void>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export async function pollUntil(predicate, opts) {
|
|
2
|
+
const deadline = Date.now() + opts.timeout;
|
|
3
|
+
while (Date.now() < deadline) {
|
|
4
|
+
if (await predicate())
|
|
5
|
+
return;
|
|
6
|
+
await sleep(opts.interval);
|
|
7
|
+
}
|
|
8
|
+
throw new Error(`pollUntil: timed out after ${opts.timeout}ms`);
|
|
9
|
+
}
|
|
10
|
+
// sleep is ONLY permitted inside the auto-wait pipeline — never in tool handlers
|
|
11
|
+
function sleep(ms) {
|
|
12
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
13
|
+
}
|
|
14
|
+
// ─── DOM checks (all run inside CDP Runtime.callFunction for speed) ───────────
|
|
15
|
+
import { waitForNetworkIdle } from "./wait.js";
|
|
16
|
+
async function isElementReady(session, backendNodeId) {
|
|
17
|
+
try {
|
|
18
|
+
const { nodeIds } = await session.DOM.pushNodesByBackendIdsToFrontend({
|
|
19
|
+
backendNodeIds: [backendNodeId],
|
|
20
|
+
});
|
|
21
|
+
if (!nodeIds[0])
|
|
22
|
+
return false;
|
|
23
|
+
const { object } = await session.DOM.resolveNode({ nodeId: nodeIds[0] });
|
|
24
|
+
if (!object?.objectId)
|
|
25
|
+
return false;
|
|
26
|
+
const { result } = await session.Runtime.callFunctionOn({
|
|
27
|
+
objectId: object.objectId,
|
|
28
|
+
functionDeclaration: `function() {
|
|
29
|
+
if (this.disabled || this.getAttribute('aria-disabled') === 'true') return false;
|
|
30
|
+
const r = this.getBoundingClientRect();
|
|
31
|
+
const s = window.getComputedStyle(this);
|
|
32
|
+
if (r.width === 0 || r.height === 0 || s.visibility === 'hidden' || s.display === 'none' || s.opacity === '0') return false;
|
|
33
|
+
const topEl = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
|
|
34
|
+
return topEl !== null && (topEl === this || this.contains(topEl));
|
|
35
|
+
}`,
|
|
36
|
+
returnByValue: true,
|
|
37
|
+
});
|
|
38
|
+
return result?.value === true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ─── DOM stability (waits for no mutations for quietPeriod ms) ────────────────
|
|
45
|
+
export async function waitForDOMStable(session, quietPeriod = 50, // Reduced from 100ms
|
|
46
|
+
timeout = 800, // Reduced from 1500ms
|
|
47
|
+
fast = false) {
|
|
48
|
+
// Fast path: skip stability check entirely when fast=true
|
|
49
|
+
if (fast)
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
const result = await session.Runtime.evaluate({
|
|
52
|
+
expression: `(function(quiet, timeout) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
let timer;
|
|
55
|
+
const obs = new MutationObserver(() => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = setTimeout(() => { obs.disconnect(); resolve(true); }, quiet);
|
|
58
|
+
});
|
|
59
|
+
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
60
|
+
timer = setTimeout(() => { obs.disconnect(); resolve(true); }, quiet);
|
|
61
|
+
setTimeout(() => { obs.disconnect(); resolve(true); }, timeout);
|
|
62
|
+
});
|
|
63
|
+
})(${quietPeriod}, ${timeout})`,
|
|
64
|
+
awaitPromise: true,
|
|
65
|
+
returnByValue: true,
|
|
66
|
+
}).catch(() => ({ result: { value: true } }));
|
|
67
|
+
return Promise.resolve();
|
|
68
|
+
}
|
|
69
|
+
// ─── guardedAction — the mandatory wrapper for every CDP interaction ──────────
|
|
70
|
+
//
|
|
71
|
+
// Rule 9: Every CDP interaction goes through this pipeline:
|
|
72
|
+
// 1. DOM existence check
|
|
73
|
+
// 2. Visibility check
|
|
74
|
+
// 3. Interactability check
|
|
75
|
+
// 4. DOM stability wait (150ms quiet period for SPA re-renders)
|
|
76
|
+
// Then fires the action.
|
|
77
|
+
//
|
|
78
|
+
// Manual sleeps in tool handlers are BANNED. This is the only place they live.
|
|
79
|
+
export async function guardedAction(session, backendNodeId, action, label = "unknown", postActionPipeline, fast = false) {
|
|
80
|
+
// Fast path: minimal checks when fast=true
|
|
81
|
+
if (fast) {
|
|
82
|
+
// Quick single check (no polling)
|
|
83
|
+
const ready = await isElementReady(session, backendNodeId);
|
|
84
|
+
if (!ready) {
|
|
85
|
+
throw new Error(`guardedAction: element "${label}" not ready (fast mode)`);
|
|
86
|
+
}
|
|
87
|
+
await action();
|
|
88
|
+
if (postActionPipeline)
|
|
89
|
+
await postActionPipeline();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// 1. Single-pass check: existence, visibility, and interactability
|
|
93
|
+
await pollUntil(() => isElementReady(session, backendNodeId), {
|
|
94
|
+
timeout: 800, // Reduced from 1500ms
|
|
95
|
+
interval: 30, // Reduced from 50ms
|
|
96
|
+
}).catch(() => {
|
|
97
|
+
throw new Error(`guardedAction: element "${label}" not ready (missing, hidden, or blocked) after 0.8s`);
|
|
98
|
+
});
|
|
99
|
+
// 2. DOM stability pre-action
|
|
100
|
+
await waitForDOMStable(session, 50, 600); // Reduced from 100/1000
|
|
101
|
+
// 3. Fire
|
|
102
|
+
await action();
|
|
103
|
+
// 4. Post-action — DOM stability + network idle (parallel, shorter)
|
|
104
|
+
await Promise.all([
|
|
105
|
+
waitForDOMStable(session, 50, 800), // Reduced from 100/1500
|
|
106
|
+
waitForNetworkIdle(session, 200, 800).catch(() => { }), // Reduced from 300/1500
|
|
107
|
+
postActionPipeline ? postActionPipeline() : Promise.resolve(),
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
export async function detectPageWeight(session) {
|
|
111
|
+
try {
|
|
112
|
+
const { result } = await session.Runtime.evaluate({
|
|
113
|
+
expression: `(function() {
|
|
114
|
+
const all = document.querySelectorAll('*');
|
|
115
|
+
const images = document.querySelectorAll('img, picture, svg');
|
|
116
|
+
const scripts = document.querySelectorAll('script');
|
|
117
|
+
const iframes = document.querySelectorAll('iframe, frame');
|
|
118
|
+
return JSON.stringify({
|
|
119
|
+
elementCount: all.length,
|
|
120
|
+
imageCount: images.length,
|
|
121
|
+
scriptCount: scripts.length,
|
|
122
|
+
iframeCount: iframes.length
|
|
123
|
+
});
|
|
124
|
+
})()`,
|
|
125
|
+
returnByValue: true,
|
|
126
|
+
});
|
|
127
|
+
const data = JSON.parse(result.value);
|
|
128
|
+
const { elementCount, imageCount, scriptCount, iframeCount } = data;
|
|
129
|
+
// Weight scoring heuristic
|
|
130
|
+
let score = 0;
|
|
131
|
+
score += Math.min(elementCount / 500, 10); // 0-10: 0 to 5000+ elements
|
|
132
|
+
score += Math.min(imageCount / 10, 5); // 0-5: 0 to 50+ images
|
|
133
|
+
score += Math.min(scriptCount / 5, 3); // 0-3: 0 to 15+ scripts
|
|
134
|
+
score += Math.min(iframeCount, 2); // 0-2: 0+ iframes
|
|
135
|
+
const level = score < 5 ? "light" : score < 12 ? "medium" : "heavy";
|
|
136
|
+
const baseTimeout = 8000;
|
|
137
|
+
const suggestedTimeout = Math.min(Math.round(baseTimeout + score * 1500), 60000);
|
|
138
|
+
return { level, elementCount, imageCount, scriptCount, iframeCount, suggestedTimeout };
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return {
|
|
142
|
+
level: "light",
|
|
143
|
+
elementCount: 0,
|
|
144
|
+
imageCount: 0,
|
|
145
|
+
scriptCount: 0,
|
|
146
|
+
iframeCount: 0,
|
|
147
|
+
suggestedTimeout: 10000,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ─── Navigation wait — used after clicks that trigger navigation ──────────────
|
|
152
|
+
export async function awaitNavigation(session, timeout = 6000 // Reduced from 10000ms
|
|
153
|
+
) {
|
|
154
|
+
// Poll readyState instead of consuming one-shot event
|
|
155
|
+
await pollUntil(async () => {
|
|
156
|
+
const { result } = await session.Runtime.evaluate({
|
|
157
|
+
expression: `document.readyState`,
|
|
158
|
+
returnByValue: true,
|
|
159
|
+
}).catch(() => ({ result: { value: 'loading' } }));
|
|
160
|
+
return result?.value === 'complete' || result?.value === 'interactive';
|
|
161
|
+
}, { timeout, interval: 50 }).catch(() => {
|
|
162
|
+
throw new Error(`Navigation timeout after ${timeout}ms`);
|
|
163
|
+
});
|
|
164
|
+
// After DOM content fires, detect page weight for post-load stability wait
|
|
165
|
+
const weight = await detectPageWeight(session);
|
|
166
|
+
const stabilityTimeout = weight.level === "heavy" ? 3000 : weight.level === "medium" ? 2000 : 1000; // Reduced from 5000/3000/2000
|
|
167
|
+
await waitForDOMStable(session, 50, stabilityTimeout); // Reduced quiet period from 100ms
|
|
168
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import CDP from "chrome-remote-interface";
|
|
2
|
+
export type MutationSummary = {
|
|
3
|
+
newElements: {
|
|
4
|
+
backendNodeId: number;
|
|
5
|
+
tag: string;
|
|
6
|
+
text: string;
|
|
7
|
+
role?: string;
|
|
8
|
+
}[];
|
|
9
|
+
removedElements: {
|
|
10
|
+
backendNodeId: number;
|
|
11
|
+
tag: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}[];
|
|
14
|
+
attributeChanges: {
|
|
15
|
+
backendNodeId: number;
|
|
16
|
+
element: string;
|
|
17
|
+
attribute: string;
|
|
18
|
+
oldValue: string | null;
|
|
19
|
+
newValue: string;
|
|
20
|
+
}[];
|
|
21
|
+
significantChange: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare function watchMutations(session: CDP.Client, durationMs: number): Promise<MutationSummary>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Heuristics for "significant" changes
|
|
2
|
+
const SIGNIFICANT_TAGS = new Set(['button', 'a', 'input', 'textarea', 'select', 'h1', 'h2', 'h3', '[role="button"]', '[role="link"]']);
|
|
3
|
+
const MAX_TEXT_LEN = 150;
|
|
4
|
+
export async function watchMutations(session, durationMs) {
|
|
5
|
+
const { DOM } = session;
|
|
6
|
+
const summary = {
|
|
7
|
+
newElements: [],
|
|
8
|
+
removedElements: [],
|
|
9
|
+
attributeChanges: [],
|
|
10
|
+
significantChange: false,
|
|
11
|
+
};
|
|
12
|
+
let timeoutId = null;
|
|
13
|
+
let documentUpdated = false;
|
|
14
|
+
const onDocumentUpdated = () => {
|
|
15
|
+
documentUpdated = true;
|
|
16
|
+
summary.significantChange = true; // Full page load is always significant
|
|
17
|
+
};
|
|
18
|
+
const onAttributeModified = (params) => {
|
|
19
|
+
const { nodeId, name, value } = params;
|
|
20
|
+
// For simplicity, we can't get the old value here without more complex tracking.
|
|
21
|
+
// Let's just record the change.
|
|
22
|
+
summary.attributeChanges.push({
|
|
23
|
+
backendNodeId: nodeId,
|
|
24
|
+
element: `nodeId: ${nodeId}`,
|
|
25
|
+
attribute: name,
|
|
26
|
+
oldValue: "not_tracked", // The protocol doesn't provide the old value directly
|
|
27
|
+
newValue: value,
|
|
28
|
+
});
|
|
29
|
+
summary.significantChange = true; // Assume attribute changes can be significant
|
|
30
|
+
};
|
|
31
|
+
const onChildNodeInserted = async (params) => {
|
|
32
|
+
const { parentNodeId, previousNodeId, node } = params;
|
|
33
|
+
// We only care about element nodes
|
|
34
|
+
if (node.nodeType !== 1)
|
|
35
|
+
return; // 1 = Element Node
|
|
36
|
+
// nodeValue is always null for element nodes (nodeType === 1).
|
|
37
|
+
// For elements, approximate text from nodeValue of first text child.
|
|
38
|
+
// CDP Node type doesn't have textContent, so we fall back to nodeValue.
|
|
39
|
+
const textContent = node.nodeValue || '';
|
|
40
|
+
// CDP DOM attributes are a flattened string array: [name1, value1, name2, value2, ...]
|
|
41
|
+
// Find 'role' by iterating in pairs.
|
|
42
|
+
let role = '';
|
|
43
|
+
const attrs = node.attributes || [];
|
|
44
|
+
for (let ai = 0; ai < attrs.length - 1; ai += 2) {
|
|
45
|
+
if (attrs[ai] === 'role') {
|
|
46
|
+
role = attrs[ai + 1] || '';
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
summary.newElements.push({
|
|
51
|
+
backendNodeId: node.backendNodeId,
|
|
52
|
+
tag: node.localName,
|
|
53
|
+
text: textContent.substring(0, MAX_TEXT_LEN),
|
|
54
|
+
role,
|
|
55
|
+
});
|
|
56
|
+
if (SIGNIFICANT_TAGS.has(node.localName) || (role && SIGNIFICANT_TAGS.has(`[role="${role}"]`))) {
|
|
57
|
+
summary.significantChange = true;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const onChildNodeRemoved = (params) => {
|
|
61
|
+
const { parentNodeId, nodeId } = params;
|
|
62
|
+
// We don't have info about the removed node other than its ID.
|
|
63
|
+
// A more complex implementation would cache node info.
|
|
64
|
+
summary.removedElements.push({
|
|
65
|
+
backendNodeId: nodeId,
|
|
66
|
+
tag: 'unknown',
|
|
67
|
+
text: 'unknown'
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
// --- Main Logic ---
|
|
71
|
+
const promise = new Promise(resolve => {
|
|
72
|
+
timeoutId = setTimeout(resolve, durationMs);
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
// Enable DOM notifications
|
|
76
|
+
await DOM.enable();
|
|
77
|
+
const listeners = [
|
|
78
|
+
DOM.on("documentUpdated", onDocumentUpdated),
|
|
79
|
+
DOM.on("attributeModified", onAttributeModified),
|
|
80
|
+
DOM.on("childNodeInserted", onChildNodeInserted),
|
|
81
|
+
DOM.on("childNodeRemoved", onChildNodeRemoved)
|
|
82
|
+
];
|
|
83
|
+
await DOM.getDocument({ depth: -1, pierce: true });
|
|
84
|
+
await promise;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (timeoutId)
|
|
88
|
+
clearTimeout(timeoutId);
|
|
89
|
+
// Disabling the DOM agent automatically removes all listeners.
|
|
90
|
+
await DOM.disable();
|
|
91
|
+
}
|
|
92
|
+
return summary;
|
|
93
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import CDP from "chrome-remote-interface";
|
|
2
|
+
export type NetworkSecurityDetails = {
|
|
3
|
+
protocol?: string;
|
|
4
|
+
issuer?: string;
|
|
5
|
+
subjectName?: string;
|
|
6
|
+
validFrom?: number;
|
|
7
|
+
validTo?: number;
|
|
8
|
+
cipher?: string;
|
|
9
|
+
keyExchange?: string;
|
|
10
|
+
sanList?: string[];
|
|
11
|
+
certificateId?: number;
|
|
12
|
+
};
|
|
13
|
+
export type NetworkTimingBreakdown = {
|
|
14
|
+
dnsMs?: number;
|
|
15
|
+
connectMs?: number;
|
|
16
|
+
sslMs?: number;
|
|
17
|
+
sendMs?: number;
|
|
18
|
+
waitMs?: number;
|
|
19
|
+
downloadMs?: number;
|
|
20
|
+
totalMs?: number;
|
|
21
|
+
};
|
|
22
|
+
export type NetworkFailureDetails = {
|
|
23
|
+
errorText: string;
|
|
24
|
+
canceled?: boolean;
|
|
25
|
+
blockedReason?: string;
|
|
26
|
+
};
|
|
27
|
+
export type CapturedRequest = {
|
|
28
|
+
url: string;
|
|
29
|
+
method: string;
|
|
30
|
+
status?: number;
|
|
31
|
+
isApi: boolean;
|
|
32
|
+
timestamp?: number;
|
|
33
|
+
securityDetails?: NetworkSecurityDetails;
|
|
34
|
+
timing?: NetworkTimingBreakdown;
|
|
35
|
+
failure?: NetworkFailureDetails;
|
|
36
|
+
_startTimeMonotonic?: number;
|
|
37
|
+
_responseHeadersMonotonic?: number;
|
|
38
|
+
};
|
|
39
|
+
export type TelemetryReport = {
|
|
40
|
+
networkRequests: CapturedRequest[];
|
|
41
|
+
domMutations: string[];
|
|
42
|
+
navigationOccurred: boolean;
|
|
43
|
+
newUrl?: string;
|
|
44
|
+
toastMessages: string[];
|
|
45
|
+
pageTitle?: string;
|
|
46
|
+
errors: string[];
|
|
47
|
+
};
|
|
48
|
+
export declare class TelemetryCapture {
|
|
49
|
+
private session;
|
|
50
|
+
private requests;
|
|
51
|
+
private toastMessages;
|
|
52
|
+
private errors;
|
|
53
|
+
private navigationOccurred;
|
|
54
|
+
private newUrl?;
|
|
55
|
+
private active;
|
|
56
|
+
private _onRequestWillBeSent;
|
|
57
|
+
private _onResponseReceived;
|
|
58
|
+
private _onLoadingFinished;
|
|
59
|
+
private _onLoadingFailed;
|
|
60
|
+
private _onFrameNavigated;
|
|
61
|
+
private _onChildNodeInserted;
|
|
62
|
+
private _onConsoleAPICalled;
|
|
63
|
+
constructor(session: CDP.Client);
|
|
64
|
+
private setupListeners;
|
|
65
|
+
start(): Promise<void>;
|
|
66
|
+
stop(): Promise<TelemetryReport>;
|
|
67
|
+
}
|