@ai-ecoverse/slicc-cdp 1.5.0 → 1.5.1
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/bin/curlwright.wasm +0 -0
- package/bin/playwright-cli.wasm +0 -0
- package/package.json +3 -3
- package/src/page.js +251 -0
package/bin/curlwright.wasm
CHANGED
|
Binary file
|
package/bin/playwright-cli.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-cdp",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "SLICC's CDP layer: client, browser API, transports, HAR, and ARIA snapshot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"build:wasm": "cargo build --release --target wasm32-wasip1 -p playwright-cli -p curlwright && mkdir -p bin && cp target/wasm32-wasip1/release/playwright-cli.wasm bin/playwright-cli.wasm && cp target/wasm32-wasip1/release/curlwright.wasm bin/curlwright.wasm",
|
|
49
49
|
"prepublishOnly": "npm run build:wasm",
|
|
50
50
|
"lint": "slicc-lint",
|
|
51
|
-
"typecheck": "node --check src/index.js && node --check src/cdp-client.js && node --check src/connect.js",
|
|
51
|
+
"typecheck": "node --check src/index.js && node --check src/cdp-client.js && node --check src/connect.js && node --check src/page.js",
|
|
52
52
|
"format": "biome check --write .",
|
|
53
53
|
"test": "true",
|
|
54
54
|
"test:integration": "PATH=\"$HOME/.cargo/bin:$PATH\" cargo build -p playwright-cli -p curlwright && PATH=\"$HOME/.cargo/bin:$PATH\" cargo build --target wasm32-wasip1 -p playwright-cli -p curlwright && npx playwright-core install chromium && node --test --test-timeout=180000 test/integration/chromium.test.mjs",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"release": "semantic-release"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@ai-ecoverse/slicc-kernel": "1.
|
|
61
|
+
"@ai-ecoverse/slicc-kernel": "1.54.0",
|
|
62
62
|
"@ai-ecoverse/slicc-shared-web": "1.12.1",
|
|
63
63
|
"@biomejs/biome": "2.5.15",
|
|
64
64
|
"lefthook": "2.1.16",
|
package/src/page.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { CDPClient } from './cdp-client.js';
|
|
2
|
+
|
|
3
|
+
const LOAD_TIMEOUT_MS = 15000;
|
|
4
|
+
const PROBE_TIMEOUT_MS = 1000;
|
|
5
|
+
|
|
6
|
+
export class PageSession {
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client ?? new CDPClient();
|
|
9
|
+
this.sessionId = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async listTargets() {
|
|
13
|
+
const result = await this.client.send('Target.getTargets', {});
|
|
14
|
+
const infos = Array.isArray(result?.targetInfos) ? result.targetInfos : [];
|
|
15
|
+
const pages = [];
|
|
16
|
+
for (const info of infos) {
|
|
17
|
+
if (info?.type !== 'page') continue;
|
|
18
|
+
const targetId = typeof info.targetId === 'string' ? info.targetId : '';
|
|
19
|
+
const url = typeof info.url === 'string' ? info.url : '';
|
|
20
|
+
const title = typeof info.title === 'string' ? info.title : '';
|
|
21
|
+
if (targetId === '' || isInternalTarget(url, title)) continue;
|
|
22
|
+
pages.push({ targetId, url, title });
|
|
23
|
+
}
|
|
24
|
+
return pages;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async attach(targetId) {
|
|
28
|
+
const result = await this.client.send('Target.attachToTarget', {
|
|
29
|
+
targetId,
|
|
30
|
+
flatten: true,
|
|
31
|
+
});
|
|
32
|
+
const sessionId = result?.sessionId;
|
|
33
|
+
if (typeof sessionId !== 'string' || sessionId === '') {
|
|
34
|
+
throw new Error('Target.attachToTarget returned no sessionId');
|
|
35
|
+
}
|
|
36
|
+
this.sessionId = sessionId;
|
|
37
|
+
await enablePage(this.client, sessionId);
|
|
38
|
+
return sessionId;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async navigate(url) {
|
|
42
|
+
const sessionId = this.requireSession();
|
|
43
|
+
await enablePage(this.client, sessionId);
|
|
44
|
+
const result = await this.client.send('Page.navigate', { url }, sessionId);
|
|
45
|
+
if (typeof result?.errorText === 'string' && result.errorText !== '') {
|
|
46
|
+
throw new Error(`navigate: ${result.errorText}`);
|
|
47
|
+
}
|
|
48
|
+
if (await pageIsSettled(this.client, sessionId)) return;
|
|
49
|
+
try {
|
|
50
|
+
await waitForLoad(this.client, sessionId, LOAD_TIMEOUT_MS);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (!isLoadTimeout(err)) throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async evaluate(expression, options = {}) {
|
|
57
|
+
const sessionId = this.requireSession();
|
|
58
|
+
const result = await this.client.send(
|
|
59
|
+
'Runtime.evaluate',
|
|
60
|
+
{
|
|
61
|
+
expression,
|
|
62
|
+
returnByValue: options.returnByValue ?? true,
|
|
63
|
+
awaitPromise: options.awaitPromise ?? true,
|
|
64
|
+
},
|
|
65
|
+
sessionId
|
|
66
|
+
);
|
|
67
|
+
if (result?.exceptionDetails) {
|
|
68
|
+
throw new Error(exceptionMessage(result.exceptionDetails));
|
|
69
|
+
}
|
|
70
|
+
return result?.result?.value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async screenshot(options = {}) {
|
|
74
|
+
const sessionId = this.requireSession();
|
|
75
|
+
const params = { format: 'png' };
|
|
76
|
+
if (options.fullPage) params.captureBeyondViewport = true;
|
|
77
|
+
if (options.clip) params.clip = options.clip;
|
|
78
|
+
const result = await this.client.send('Page.captureScreenshot', params, sessionId);
|
|
79
|
+
if (typeof result?.data !== 'string') {
|
|
80
|
+
throw new Error('screenshot returned no data');
|
|
81
|
+
}
|
|
82
|
+
return result.data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async accessibilitySnapshot(frameId) {
|
|
86
|
+
const sessionId = this.requireSession();
|
|
87
|
+
try {
|
|
88
|
+
await this.client.send('Accessibility.enable', {}, sessionId);
|
|
89
|
+
} catch {}
|
|
90
|
+
const params = frameId ? { frameId } : {};
|
|
91
|
+
const result = await this.client.send('Accessibility.getFullAXTree', params, sessionId);
|
|
92
|
+
const nodes = Array.isArray(result?.nodes) ? result.nodes : [];
|
|
93
|
+
return axFromCdp(nodes);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
requireSession() {
|
|
97
|
+
if (!this.sessionId) {
|
|
98
|
+
throw new Error('Not attached. Call attach(targetId) first.');
|
|
99
|
+
}
|
|
100
|
+
return this.sessionId;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isInternalTarget(url, title) {
|
|
105
|
+
const trimmedUrl = url.trim();
|
|
106
|
+
const trimmedTitle = title.trim();
|
|
107
|
+
if (trimmedTitle === 'Omnibox Popup') return true;
|
|
108
|
+
if (trimmedUrl.startsWith('chrome://')) return true;
|
|
109
|
+
if (trimmedUrl.startsWith('chrome-search://')) return true;
|
|
110
|
+
if (trimmedUrl.startsWith('chrome-untrusted://')) return true;
|
|
111
|
+
if (trimmedUrl.startsWith('devtools://')) return true;
|
|
112
|
+
return trimmedUrl === '' && trimmedTitle.toLowerCase().endsWith('popup');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function enablePage(client, sessionId) {
|
|
116
|
+
return client.send('Page.enable', {}, sessionId).catch((err) => {
|
|
117
|
+
if (isCdpError(err)) return undefined;
|
|
118
|
+
throw err;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isCdpError(err) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
+
return message.startsWith('Page.enable:');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function waitForLoad(client, sessionId, timeout) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
let timer;
|
|
130
|
+
let settled = false;
|
|
131
|
+
const finish = (error) => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
client.off('Page.loadEventFired', listener);
|
|
136
|
+
if (error) reject(error);
|
|
137
|
+
else resolve();
|
|
138
|
+
};
|
|
139
|
+
const listener = (message) => {
|
|
140
|
+
const eventSession = message?.sessionId;
|
|
141
|
+
if (typeof eventSession === 'string' && eventSession !== sessionId) return;
|
|
142
|
+
finish();
|
|
143
|
+
};
|
|
144
|
+
timer = setTimeout(() => {
|
|
145
|
+
finish(new Error('Timed out waiting for event: Page.loadEventFired'));
|
|
146
|
+
}, timeout);
|
|
147
|
+
try {
|
|
148
|
+
client.on('Page.loadEventFired', listener);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
finish(err instanceof Error ? err : new Error(String(err)));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function pageIsSettled(client, sessionId) {
|
|
156
|
+
return client
|
|
157
|
+
.send(
|
|
158
|
+
'Runtime.evaluate',
|
|
159
|
+
{ expression: 'document.readyState', returnByValue: true },
|
|
160
|
+
sessionId,
|
|
161
|
+
PROBE_TIMEOUT_MS
|
|
162
|
+
)
|
|
163
|
+
.then((result) => {
|
|
164
|
+
const state = result?.result?.value;
|
|
165
|
+
return state !== 'loading' && state !== 'interactive';
|
|
166
|
+
})
|
|
167
|
+
.catch((err) => {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
if (
|
|
170
|
+
message.startsWith('Runtime.evaluate timed out') ||
|
|
171
|
+
message.startsWith('Runtime.evaluate:')
|
|
172
|
+
) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isLoadTimeout(err) {
|
|
180
|
+
return (
|
|
181
|
+
err instanceof Error &&
|
|
182
|
+
err.message.startsWith('Timed out waiting for event: Page.loadEventFired')
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function exceptionMessage(details) {
|
|
187
|
+
const description = details.exception?.description;
|
|
188
|
+
if (typeof description === 'string') return description;
|
|
189
|
+
if (typeof details.text === 'string') return details.text;
|
|
190
|
+
return 'evaluation failed';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function axFromCdp(nodes) {
|
|
194
|
+
const byId = new Map();
|
|
195
|
+
for (const node of nodes) {
|
|
196
|
+
if (node && typeof node.nodeId === 'string') byId.set(node.nodeId, node);
|
|
197
|
+
}
|
|
198
|
+
const roots = [];
|
|
199
|
+
for (const node of nodes) {
|
|
200
|
+
if (!node || typeof node.nodeId !== 'string') continue;
|
|
201
|
+
if (node.parentId != null) continue;
|
|
202
|
+
roots.push(...buildAxNode(node.nodeId, byId, new Set()));
|
|
203
|
+
}
|
|
204
|
+
return roots;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildAxNode(id, byId, stack) {
|
|
208
|
+
if (stack.has(id)) return [];
|
|
209
|
+
const node = byId.get(id);
|
|
210
|
+
if (!node) return [];
|
|
211
|
+
stack.add(id);
|
|
212
|
+
const children = [];
|
|
213
|
+
for (const childId of childIds(node)) {
|
|
214
|
+
children.push(...buildAxNode(childId, byId, stack));
|
|
215
|
+
}
|
|
216
|
+
stack.delete(id);
|
|
217
|
+
if (node.ignored === true) return children;
|
|
218
|
+
const built = {
|
|
219
|
+
role: textOf(fieldValue(node.role), 'unknown'),
|
|
220
|
+
name: textOf(fieldValue(node.name), ''),
|
|
221
|
+
value: textOf(fieldValue(node.value), ''),
|
|
222
|
+
children,
|
|
223
|
+
};
|
|
224
|
+
if (typeof node.backendDOMNodeId === 'number') built.backendNodeId = node.backendDOMNodeId;
|
|
225
|
+
return [built];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function fieldValue(field) {
|
|
229
|
+
if (field == null) return undefined;
|
|
230
|
+
if (typeof field === 'object' && 'value' in field) return field.value;
|
|
231
|
+
return field;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function childIds(node) {
|
|
235
|
+
if (!Array.isArray(node.childIds)) return [];
|
|
236
|
+
return node.childIds.filter((id) => typeof id === 'string');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function textOf(value, fallback) {
|
|
240
|
+
if (value == null) return fallback;
|
|
241
|
+
if (typeof value === 'string') return value;
|
|
242
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
243
|
+
return String(value);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const json = JSON.stringify(value);
|
|
247
|
+
return json ?? fallback;
|
|
248
|
+
} catch {
|
|
249
|
+
return fallback;
|
|
250
|
+
}
|
|
251
|
+
}
|