surfsky 0.0.4 → 0.0.6
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/README.md +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +115 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Cookies and browser state persist between leases. Handlers are not retried.
|
|
|
99
99
|
|
|
100
100
|
- [SDK API reference](https://github.com/surfskyio/surfsky-js/blob/main/docs/api.md)
|
|
101
101
|
- [REST API](https://docs.surfsky.io/api-reference)
|
|
102
|
-
- [Examples](https://github.com/surfskyio/surfsky-js/tree/main/examples): forms, tabs, retries, profiles and CDP connections.
|
|
102
|
+
- [Examples](https://github.com/surfskyio/surfsky-js/tree/main/examples): forms, tabs, retries, profiles, a closed shadow root and CDP connections.
|
|
103
103
|
|
|
104
104
|
`client.session()` exposes a WebSocket URL for external browser clients.
|
|
105
105
|
See the [Playwright](https://github.com/surfskyio/surfsky-js/blob/main/examples/playwright_connect.ts),
|
package/dist/index.d.ts
CHANGED
|
@@ -1097,8 +1097,6 @@ declare abstract class Actions {
|
|
|
1097
1097
|
click(selector: string, options?: ClickOptions): Promise<any>;
|
|
1098
1098
|
dblclick(selector: string, options?: DblclickOptions): Promise<any>;
|
|
1099
1099
|
hover(selector: string, options?: HoverOptions): Promise<any>;
|
|
1100
|
-
type(selector: string, text: string): Promise<any>;
|
|
1101
|
-
fill(selector: string, text: string): Promise<any>;
|
|
1102
1100
|
scroll(options?: ScrollOptions): Promise<any>;
|
|
1103
1101
|
scrollIntoView(selector: string, options?: {
|
|
1104
1102
|
behavior?: ScrollBehavior;
|
|
@@ -1368,8 +1366,16 @@ declare class Page extends Actions {
|
|
|
1368
1366
|
waitForFunction(expression: string | ((...args: any[]) => unknown), options?: EvaluateOptions & WaitOptions): Promise<any>;
|
|
1369
1367
|
innerText(selector: string): Promise<string | null>;
|
|
1370
1368
|
allInnerTexts(selector: string): Promise<string[]>;
|
|
1369
|
+
type(selector: string, text: string): Promise<any>;
|
|
1370
|
+
fill(selector: string, text: string): Promise<any>;
|
|
1371
1371
|
getAttribute(selector: string, name: string): Promise<string | null>;
|
|
1372
1372
|
count(selector: string): Promise<number>;
|
|
1373
|
+
boundingBox(selector: string): Promise<{
|
|
1374
|
+
x: number;
|
|
1375
|
+
y: number;
|
|
1376
|
+
width: number;
|
|
1377
|
+
height: number;
|
|
1378
|
+
} | null>;
|
|
1373
1379
|
selectOption(selector: string, option: string | {
|
|
1374
1380
|
value?: string;
|
|
1375
1381
|
label?: string;
|
package/dist/index.js
CHANGED
|
@@ -85,14 +85,6 @@ var Actions = class {
|
|
|
85
85
|
...options
|
|
86
86
|
}));
|
|
87
87
|
}
|
|
88
|
-
async type(selector, text) {
|
|
89
|
-
await this.click(selector);
|
|
90
|
-
return this.keyboard.type(text);
|
|
91
|
-
}
|
|
92
|
-
async fill(selector, text) {
|
|
93
|
-
await this.click(selector, { clickCount: 3 });
|
|
94
|
-
return this.keyboard.type(text);
|
|
95
|
-
}
|
|
96
88
|
scroll(options = {}) {
|
|
97
89
|
return this.send("Human.scroll", cdpParams({ ...options }));
|
|
98
90
|
}
|
|
@@ -868,18 +860,26 @@ const FUNCTION = /^\s*(async\s+)?(function\b|\([^()]*\)\s*=>|[\w$]+\s*=>)/;
|
|
|
868
860
|
function asXPath(s) {
|
|
869
861
|
return /^\s*(xpath=|\/\/|\.\.)/.test(s) ? s.trim().replace(/^xpath=/, "") : void 0;
|
|
870
862
|
}
|
|
871
|
-
const SELECT = `(
|
|
872
|
-
const
|
|
873
|
-
if (!el) return null;
|
|
874
|
-
const option = Array.from(el.options).find(
|
|
863
|
+
const SELECT = `function (value, label) {
|
|
864
|
+
const option = Array.from(this.options).find(
|
|
875
865
|
o => label === null ? o.value === value : o.label === label || o.text === label
|
|
876
866
|
);
|
|
877
867
|
if (!option) return false;
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
868
|
+
this.value = option.value;
|
|
869
|
+
this.dispatchEvent(new Event("input", {bubbles: true}));
|
|
870
|
+
this.dispatchEvent(new Event("change", {bubbles: true}));
|
|
881
871
|
return option.value;
|
|
882
872
|
}`;
|
|
873
|
+
const INNER_TEXT = "function () { return this.innerText }";
|
|
874
|
+
const IS_FOCUSED = "function () { return this.getRootNode().activeElement === this }";
|
|
875
|
+
const MATCHES = "function (s) { return this.nodeType === 1 && (s == null || this.matches(s)) }";
|
|
876
|
+
const WORLD_GONE = ["Cannot find context", "does not belong to the document"];
|
|
877
|
+
const worldIsGone = (err) => err instanceof CDPError && WORLD_GONE.some((text) => err.message.includes(text));
|
|
878
|
+
function remoteResult(result, what) {
|
|
879
|
+
const details = result.exceptionDetails;
|
|
880
|
+
if (details) throw new CDPError(`${what} failed: ${(details.exception ?? {}).description ?? details.text}`);
|
|
881
|
+
return result.result;
|
|
882
|
+
}
|
|
883
883
|
function cookieParam(cookie) {
|
|
884
884
|
const out = dropNulls(cookie);
|
|
885
885
|
const expires = out.expirationDate;
|
|
@@ -1070,11 +1070,13 @@ var Page = class extends Actions {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
/** @internal */
|
|
1072
1072
|
async _setup(waiting) {
|
|
1073
|
+
const patterns = this._browser._fetchPatterns;
|
|
1073
1074
|
const commands = [
|
|
1074
1075
|
["Page.enable", void 0],
|
|
1075
1076
|
["Page.setLifecycleEventsEnabled", { enabled: true }],
|
|
1076
|
-
["
|
|
1077
|
+
["Network.enable", void 0]
|
|
1077
1078
|
];
|
|
1079
|
+
if (patterns.length > 0) commands.push(["Fetch.enable", { patterns }]);
|
|
1078
1080
|
if (waiting) commands.push(["Runtime.runIfWaitingForDebugger", void 0]);
|
|
1079
1081
|
const timeout = this._browser.commandTimeout;
|
|
1080
1082
|
let posted = [];
|
|
@@ -1175,15 +1177,13 @@ var Page = class extends Actions {
|
|
|
1175
1177
|
/** Record responses whose URL contains a fragment. Call before navigating. */
|
|
1176
1178
|
async captureResponses(...fragments) {
|
|
1177
1179
|
if (fragments.length === 0) throw new TypeError("captureResponses needs at least one URL fragment");
|
|
1178
|
-
|
|
1180
|
+
this._requireOpen();
|
|
1179
1181
|
this._captures.push(...fragments);
|
|
1180
1182
|
}
|
|
1181
1183
|
async stopCapturing() {
|
|
1182
|
-
if (this._captures.length === 0) return;
|
|
1183
1184
|
this._captures = [];
|
|
1184
1185
|
this._responses = [];
|
|
1185
1186
|
this._inFlight.clear();
|
|
1186
|
-
await this.send("Network.disable");
|
|
1187
1187
|
}
|
|
1188
1188
|
async waitForResponse(fragment, options = {}) {
|
|
1189
1189
|
if (this._captures.length === 0) throw new Error("call captureResponses() before the navigation");
|
|
@@ -1199,6 +1199,7 @@ var Page = class extends Actions {
|
|
|
1199
1199
|
/** @internal */
|
|
1200
1200
|
_onResponse(event) {
|
|
1201
1201
|
const response = isRecord(event.response) ? event.response : {};
|
|
1202
|
+
if (event.type === "Document" && event.frameId === this._frameId) this._status = typeof response.status === "number" ? response.status : void 0;
|
|
1202
1203
|
const url = typeof response.url === "string" ? response.url : "";
|
|
1203
1204
|
if (this._captures.some((fragment) => url.includes(fragment))) this._inFlight.set(String(event.requestId), response);
|
|
1204
1205
|
}
|
|
@@ -1287,14 +1288,38 @@ var Page = class extends Actions {
|
|
|
1287
1288
|
try {
|
|
1288
1289
|
result = await this.send("Runtime.evaluate", params);
|
|
1289
1290
|
} catch (err) {
|
|
1290
|
-
if (!isolated || !(err
|
|
1291
|
+
if (!isolated || !worldIsGone(err)) throw err;
|
|
1291
1292
|
this._worldId = void 0;
|
|
1292
1293
|
params.contextId = await this.#world();
|
|
1293
1294
|
result = await this.send("Runtime.evaluate", params);
|
|
1294
1295
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1296
|
+
return remoteResult(result, "evaluate");
|
|
1297
|
+
}
|
|
1298
|
+
async #call(nodeId, fn, ...args) {
|
|
1299
|
+
const params = {
|
|
1300
|
+
functionDeclaration: fn,
|
|
1301
|
+
arguments: args.map((value) => ({ value })),
|
|
1302
|
+
returnByValue: true
|
|
1303
|
+
};
|
|
1304
|
+
let result;
|
|
1305
|
+
try {
|
|
1306
|
+
result = await this.#callInWorld(nodeId, params);
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
if (!worldIsGone(err)) throw err;
|
|
1309
|
+
this._worldId = void 0;
|
|
1310
|
+
result = await this.#callInWorld(nodeId, params);
|
|
1311
|
+
}
|
|
1312
|
+
return remoteResult(result, "call").value;
|
|
1313
|
+
}
|
|
1314
|
+
async #callInWorld(nodeId, params) {
|
|
1315
|
+
const resolved = await this.send("DOM.resolveNode", {
|
|
1316
|
+
nodeId,
|
|
1317
|
+
executionContextId: await this.#world()
|
|
1318
|
+
});
|
|
1319
|
+
return await this.send("Runtime.callFunctionOn", {
|
|
1320
|
+
...params,
|
|
1321
|
+
objectId: resolved.object.objectId
|
|
1322
|
+
});
|
|
1298
1323
|
}
|
|
1299
1324
|
async #world() {
|
|
1300
1325
|
if (this._worldId === void 0) {
|
|
@@ -1317,12 +1342,29 @@ var Page = class extends Actions {
|
|
|
1317
1342
|
await sleep(100);
|
|
1318
1343
|
}
|
|
1319
1344
|
}
|
|
1320
|
-
innerText(selector) {
|
|
1321
|
-
|
|
1345
|
+
async innerText(selector) {
|
|
1346
|
+
const nodeId = await this.#nodeId(selector);
|
|
1347
|
+
if (nodeId === void 0) return null;
|
|
1348
|
+
return await this.#call(nodeId, INNER_TEXT);
|
|
1322
1349
|
}
|
|
1323
1350
|
allInnerTexts(selector) {
|
|
1324
1351
|
return this.evaluate("s => Array.from(document.querySelectorAll(s), e => e.innerText)", { args: [selector] });
|
|
1325
1352
|
}
|
|
1353
|
+
async type(selector, text) {
|
|
1354
|
+
await this.click(selector);
|
|
1355
|
+
await this.#validateFocus(selector);
|
|
1356
|
+
return this.keyboard.type(text);
|
|
1357
|
+
}
|
|
1358
|
+
async fill(selector, text) {
|
|
1359
|
+
await this.click(selector, { clickCount: 3 });
|
|
1360
|
+
await this.#validateFocus(selector);
|
|
1361
|
+
if (text === "") return this.keyboard.press("Backspace");
|
|
1362
|
+
return this.keyboard.type(text);
|
|
1363
|
+
}
|
|
1364
|
+
async #validateFocus(selector) {
|
|
1365
|
+
const nodeId = await this.#nodeId(selector);
|
|
1366
|
+
if (nodeId === void 0 || !await this.#call(nodeId, IS_FOCUSED)) throw new Error(`'${selector}' did not take focus`);
|
|
1367
|
+
}
|
|
1326
1368
|
async getAttribute(selector, name) {
|
|
1327
1369
|
const nodeId = await this.#nodeId(selector);
|
|
1328
1370
|
if (nodeId === void 0) return null;
|
|
@@ -1332,22 +1374,38 @@ var Page = class extends Actions {
|
|
|
1332
1374
|
}
|
|
1333
1375
|
async count(selector) {
|
|
1334
1376
|
const document = await this.send("DOM.getDocument", { depth: 0 });
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
return ((await this.send("DOM.querySelectorAll", {
|
|
1377
|
+
if (asXPath(selector) !== void 0) return (await this.#search(selector)).length;
|
|
1378
|
+
const nodeIds = (await this.send("DOM.querySelectorAll", {
|
|
1338
1379
|
nodeId: document.root.nodeId,
|
|
1339
1380
|
selector
|
|
1340
|
-
})).nodeIds ?? []
|
|
1381
|
+
})).nodeIds ?? [];
|
|
1382
|
+
if (nodeIds.length > 0) return nodeIds.length;
|
|
1383
|
+
return (await this.#search(selector)).length;
|
|
1384
|
+
}
|
|
1385
|
+
async boundingBox(selector) {
|
|
1386
|
+
const nodeId = await this.#nodeId(selector);
|
|
1387
|
+
if (nodeId === void 0) return null;
|
|
1388
|
+
let box;
|
|
1389
|
+
try {
|
|
1390
|
+
box = await this.send("DOM.getBoxModel", { nodeId });
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
if (err instanceof CDPError) return null;
|
|
1393
|
+
throw err;
|
|
1394
|
+
}
|
|
1395
|
+
const quad = box.model.content;
|
|
1396
|
+
return {
|
|
1397
|
+
x: quad[0],
|
|
1398
|
+
y: quad[1],
|
|
1399
|
+
width: quad[2] - quad[0],
|
|
1400
|
+
height: quad[5] - quad[1]
|
|
1401
|
+
};
|
|
1341
1402
|
}
|
|
1342
1403
|
async selectOption(selector, option) {
|
|
1343
1404
|
const { value, label } = typeof option === "string" ? { value: option } : option;
|
|
1344
1405
|
if (value === void 0 === (label === void 0)) throw new TypeError("selectOption takes either a value or a label");
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
label ?? null
|
|
1349
|
-
] });
|
|
1350
|
-
if (picked === null) throw new Error(`nothing matches '${selector}'`);
|
|
1406
|
+
const nodeId = await this.#nodeId(selector);
|
|
1407
|
+
if (nodeId === void 0) throw new Error(`nothing matches '${selector}'`);
|
|
1408
|
+
const picked = await this.#call(nodeId, SELECT, value ?? null, label ?? null);
|
|
1351
1409
|
if (picked === false) throw new Error(`'${selector}' has no option '${label ?? value}'`);
|
|
1352
1410
|
return picked;
|
|
1353
1411
|
}
|
|
@@ -1376,22 +1434,34 @@ var Page = class extends Actions {
|
|
|
1376
1434
|
}
|
|
1377
1435
|
async #nodeId(selector) {
|
|
1378
1436
|
const document = await this.send("DOM.getDocument", { depth: 0 });
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
return (await this.send("DOM.querySelector", {
|
|
1437
|
+
if (asXPath(selector) !== void 0) return (await this.#search(selector))[0];
|
|
1438
|
+
const found = await this.send("DOM.querySelector", {
|
|
1382
1439
|
nodeId: document.root.nodeId,
|
|
1383
1440
|
selector
|
|
1384
|
-
})
|
|
1441
|
+
});
|
|
1442
|
+
if (found.nodeId) return found.nodeId;
|
|
1443
|
+
return (await this.#search(selector))[0];
|
|
1385
1444
|
}
|
|
1386
|
-
async #search(
|
|
1387
|
-
const
|
|
1445
|
+
async #search(selector) {
|
|
1446
|
+
const xpath = asXPath(selector);
|
|
1447
|
+
const { searchId, resultCount = 0 } = await this.send("DOM.performSearch", {
|
|
1448
|
+
query: xpath ?? selector,
|
|
1449
|
+
includeUserAgentShadowDOM: false
|
|
1450
|
+
});
|
|
1388
1451
|
try {
|
|
1389
1452
|
if (!resultCount) return [];
|
|
1390
|
-
|
|
1453
|
+
const nodeIds = (await this.send("DOM.getSearchResults", {
|
|
1391
1454
|
searchId,
|
|
1392
1455
|
fromIndex: 0,
|
|
1393
1456
|
toIndex: resultCount
|
|
1394
1457
|
})).nodeIds ?? [];
|
|
1458
|
+
const css = xpath === void 0 ? selector : null;
|
|
1459
|
+
await this.#world();
|
|
1460
|
+
const kept = await Promise.all(nodeIds.map((nodeId) => this.#call(nodeId, MATCHES, css).catch((err) => {
|
|
1461
|
+
if (err instanceof CDPError) return false;
|
|
1462
|
+
throw err;
|
|
1463
|
+
})));
|
|
1464
|
+
return nodeIds.filter((_, i) => kept[i]);
|
|
1395
1465
|
} finally {
|
|
1396
1466
|
await this.send("DOM.discardSearchResults", { searchId }).catch(() => void 0);
|
|
1397
1467
|
}
|
|
@@ -1514,12 +1584,7 @@ var Page = class extends Actions {
|
|
|
1514
1584
|
this._browser.logger.warn("Fetch.requestPaused without a requestId");
|
|
1515
1585
|
return;
|
|
1516
1586
|
}
|
|
1517
|
-
|
|
1518
|
-
if (status !== void 0 && status !== null) {
|
|
1519
|
-
if (event.frameId === this._frameId) this._status = status;
|
|
1520
|
-
this._spawn(this._send("Fetch.continueResponse", { requestId }));
|
|
1521
|
-
} else if (event.responseErrorReason) this._spawn(this._send("Fetch.continueRequest", { requestId }));
|
|
1522
|
-
else this._spawn(this._send("Fetch.failRequest", {
|
|
1587
|
+
this._spawn(this._send("Fetch.failRequest", {
|
|
1523
1588
|
requestId,
|
|
1524
1589
|
errorReason: "BlockedByClient"
|
|
1525
1590
|
}));
|
|
@@ -1531,11 +1596,6 @@ var Page = class extends Actions {
|
|
|
1531
1596
|
};
|
|
1532
1597
|
//#endregion
|
|
1533
1598
|
//#region src/browser/browser.ts
|
|
1534
|
-
const STATUS_PATTERN = {
|
|
1535
|
-
urlPattern: "*",
|
|
1536
|
-
resourceType: "Document",
|
|
1537
|
-
requestStage: "Response"
|
|
1538
|
-
};
|
|
1539
1599
|
const AUTO_ATTACH = {
|
|
1540
1600
|
autoAttach: true,
|
|
1541
1601
|
waitForDebuggerOnStart: true,
|
|
@@ -1653,7 +1713,7 @@ var Browser = class extends Page {
|
|
|
1653
1713
|
resourceType: RESOURCE_TYPES[name]
|
|
1654
1714
|
}));
|
|
1655
1715
|
for (const pattern of this.blockedUrls) patterns.push({ urlPattern: pattern });
|
|
1656
|
-
return
|
|
1716
|
+
return patterns;
|
|
1657
1717
|
}
|
|
1658
1718
|
/** Replace this browser with a fresh one after the lease. */
|
|
1659
1719
|
retire() {
|
|
@@ -2618,7 +2678,7 @@ var Proxies = class {
|
|
|
2618
2678
|
};
|
|
2619
2679
|
//#endregion
|
|
2620
2680
|
//#region src/version.ts
|
|
2621
|
-
const VERSION = "0.0.
|
|
2681
|
+
const VERSION = "0.0.6";
|
|
2622
2682
|
//#endregion
|
|
2623
2683
|
//#region src/client.ts
|
|
2624
2684
|
function userAgent() {
|