surfsky 0.0.5 → 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 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
@@ -1370,6 +1370,12 @@ declare class Page extends Actions {
1370
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
@@ -860,18 +860,26 @@ const FUNCTION = /^\s*(async\s+)?(function\b|\([^()]*\)\s*=>|[\w$]+\s*=>)/;
860
860
  function asXPath(s) {
861
861
  return /^\s*(xpath=|\/\/|\.\.)/.test(s) ? s.trim().replace(/^xpath=/, "") : void 0;
862
862
  }
863
- const SELECT = `(selector, value, label) => {
864
- const el = document.querySelector(selector);
865
- if (!el) return null;
866
- const option = Array.from(el.options).find(
863
+ const SELECT = `function (value, label) {
864
+ const option = Array.from(this.options).find(
867
865
  o => label === null ? o.value === value : o.label === label || o.text === label
868
866
  );
869
867
  if (!option) return false;
870
- el.value = option.value;
871
- el.dispatchEvent(new Event("input", {bubbles: true}));
872
- el.dispatchEvent(new Event("change", {bubbles: true}));
868
+ this.value = option.value;
869
+ this.dispatchEvent(new Event("input", {bubbles: true}));
870
+ this.dispatchEvent(new Event("change", {bubbles: true}));
873
871
  return option.value;
874
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
+ }
875
883
  function cookieParam(cookie) {
876
884
  const out = dropNulls(cookie);
877
885
  const expires = out.expirationDate;
@@ -1062,11 +1070,13 @@ var Page = class extends Actions {
1062
1070
  }
1063
1071
  /** @internal */
1064
1072
  async _setup(waiting) {
1073
+ const patterns = this._browser._fetchPatterns;
1065
1074
  const commands = [
1066
1075
  ["Page.enable", void 0],
1067
1076
  ["Page.setLifecycleEventsEnabled", { enabled: true }],
1068
- ["Fetch.enable", { patterns: this._browser._fetchPatterns }]
1077
+ ["Network.enable", void 0]
1069
1078
  ];
1079
+ if (patterns.length > 0) commands.push(["Fetch.enable", { patterns }]);
1070
1080
  if (waiting) commands.push(["Runtime.runIfWaitingForDebugger", void 0]);
1071
1081
  const timeout = this._browser.commandTimeout;
1072
1082
  let posted = [];
@@ -1167,15 +1177,13 @@ var Page = class extends Actions {
1167
1177
  /** Record responses whose URL contains a fragment. Call before navigating. */
1168
1178
  async captureResponses(...fragments) {
1169
1179
  if (fragments.length === 0) throw new TypeError("captureResponses needs at least one URL fragment");
1170
- if (this._captures.length === 0) await this.send("Network.enable");
1180
+ this._requireOpen();
1171
1181
  this._captures.push(...fragments);
1172
1182
  }
1173
1183
  async stopCapturing() {
1174
- if (this._captures.length === 0) return;
1175
1184
  this._captures = [];
1176
1185
  this._responses = [];
1177
1186
  this._inFlight.clear();
1178
- await this.send("Network.disable");
1179
1187
  }
1180
1188
  async waitForResponse(fragment, options = {}) {
1181
1189
  if (this._captures.length === 0) throw new Error("call captureResponses() before the navigation");
@@ -1191,6 +1199,7 @@ var Page = class extends Actions {
1191
1199
  /** @internal */
1192
1200
  _onResponse(event) {
1193
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;
1194
1203
  const url = typeof response.url === "string" ? response.url : "";
1195
1204
  if (this._captures.some((fragment) => url.includes(fragment))) this._inFlight.set(String(event.requestId), response);
1196
1205
  }
@@ -1279,14 +1288,38 @@ var Page = class extends Actions {
1279
1288
  try {
1280
1289
  result = await this.send("Runtime.evaluate", params);
1281
1290
  } catch (err) {
1282
- if (!isolated || !(err instanceof CDPError) || !err.message.includes("Cannot find context")) throw err;
1291
+ if (!isolated || !worldIsGone(err)) throw err;
1283
1292
  this._worldId = void 0;
1284
1293
  params.contextId = await this.#world();
1285
1294
  result = await this.send("Runtime.evaluate", params);
1286
1295
  }
1287
- const details = result.exceptionDetails;
1288
- if (details) throw new CDPError(`evaluate failed: ${(details.exception ?? {}).description ?? details.text}`);
1289
- return result.result;
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
+ });
1290
1323
  }
1291
1324
  async #world() {
1292
1325
  if (this._worldId === void 0) {
@@ -1309,8 +1342,10 @@ var Page = class extends Actions {
1309
1342
  await sleep(100);
1310
1343
  }
1311
1344
  }
1312
- innerText(selector) {
1313
- return this.evaluate("s => document.querySelector(s)?.innerText ?? null", { args: [selector] });
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);
1314
1349
  }
1315
1350
  allInnerTexts(selector) {
1316
1351
  return this.evaluate("s => Array.from(document.querySelectorAll(s), e => e.innerText)", { args: [selector] });
@@ -1327,7 +1362,8 @@ var Page = class extends Actions {
1327
1362
  return this.keyboard.type(text);
1328
1363
  }
1329
1364
  async #validateFocus(selector) {
1330
- if (!await this.evaluate("s => document.activeElement === document.querySelector(s)", { args: [selector] })) throw new Error(`'${selector}' did not take focus`);
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`);
1331
1367
  }
1332
1368
  async getAttribute(selector, name) {
1333
1369
  const nodeId = await this.#nodeId(selector);
@@ -1338,22 +1374,38 @@ var Page = class extends Actions {
1338
1374
  }
1339
1375
  async count(selector) {
1340
1376
  const document = await this.send("DOM.getDocument", { depth: 0 });
1341
- const xpath = asXPath(selector);
1342
- if (xpath !== void 0) return (await this.#search(xpath)).length;
1343
- 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", {
1344
1379
  nodeId: document.root.nodeId,
1345
1380
  selector
1346
- })).nodeIds ?? []).length;
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
+ };
1347
1402
  }
1348
1403
  async selectOption(selector, option) {
1349
1404
  const { value, label } = typeof option === "string" ? { value: option } : option;
1350
1405
  if (value === void 0 === (label === void 0)) throw new TypeError("selectOption takes either a value or a label");
1351
- const picked = await this.evaluate(SELECT, { args: [
1352
- selector,
1353
- value ?? null,
1354
- label ?? null
1355
- ] });
1356
- 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);
1357
1409
  if (picked === false) throw new Error(`'${selector}' has no option '${label ?? value}'`);
1358
1410
  return picked;
1359
1411
  }
@@ -1382,22 +1434,34 @@ var Page = class extends Actions {
1382
1434
  }
1383
1435
  async #nodeId(selector) {
1384
1436
  const document = await this.send("DOM.getDocument", { depth: 0 });
1385
- const xpath = asXPath(selector);
1386
- if (xpath !== void 0) return (await this.#search(xpath))[0];
1387
- 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", {
1388
1439
  nodeId: document.root.nodeId,
1389
1440
  selector
1390
- })).nodeId || void 0;
1441
+ });
1442
+ if (found.nodeId) return found.nodeId;
1443
+ return (await this.#search(selector))[0];
1391
1444
  }
1392
- async #search(query) {
1393
- const { searchId, resultCount = 0 } = await this.send("DOM.performSearch", { query });
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
+ });
1394
1451
  try {
1395
1452
  if (!resultCount) return [];
1396
- return (await this.send("DOM.getSearchResults", {
1453
+ const nodeIds = (await this.send("DOM.getSearchResults", {
1397
1454
  searchId,
1398
1455
  fromIndex: 0,
1399
1456
  toIndex: resultCount
1400
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]);
1401
1465
  } finally {
1402
1466
  await this.send("DOM.discardSearchResults", { searchId }).catch(() => void 0);
1403
1467
  }
@@ -1520,12 +1584,7 @@ var Page = class extends Actions {
1520
1584
  this._browser.logger.warn("Fetch.requestPaused without a requestId");
1521
1585
  return;
1522
1586
  }
1523
- const status = event.responseStatusCode;
1524
- if (status !== void 0 && status !== null) {
1525
- if (event.frameId === this._frameId) this._status = status;
1526
- this._spawn(this._send("Fetch.continueResponse", { requestId }));
1527
- } else if (event.responseErrorReason) this._spawn(this._send("Fetch.continueRequest", { requestId }));
1528
- else this._spawn(this._send("Fetch.failRequest", {
1587
+ this._spawn(this._send("Fetch.failRequest", {
1529
1588
  requestId,
1530
1589
  errorReason: "BlockedByClient"
1531
1590
  }));
@@ -1537,11 +1596,6 @@ var Page = class extends Actions {
1537
1596
  };
1538
1597
  //#endregion
1539
1598
  //#region src/browser/browser.ts
1540
- const STATUS_PATTERN = {
1541
- urlPattern: "*",
1542
- resourceType: "Document",
1543
- requestStage: "Response"
1544
- };
1545
1599
  const AUTO_ATTACH = {
1546
1600
  autoAttach: true,
1547
1601
  waitForDebuggerOnStart: true,
@@ -1659,7 +1713,7 @@ var Browser = class extends Page {
1659
1713
  resourceType: RESOURCE_TYPES[name]
1660
1714
  }));
1661
1715
  for (const pattern of this.blockedUrls) patterns.push({ urlPattern: pattern });
1662
- return [...patterns, STATUS_PATTERN];
1716
+ return patterns;
1663
1717
  }
1664
1718
  /** Replace this browser with a fresh one after the lease. */
1665
1719
  retire() {
@@ -2624,7 +2678,7 @@ var Proxies = class {
2624
2678
  };
2625
2679
  //#endregion
2626
2680
  //#region src/version.ts
2627
- const VERSION = "0.0.5";
2681
+ const VERSION = "0.0.6";
2628
2682
  //#endregion
2629
2683
  //#region src/client.ts
2630
2684
  function userAgent() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surfsky",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "TypeScript SDK for the Surfsky antidetect browser cloud.",
5
5
  "keywords": [
6
6
  "surfsky",