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 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 = `(selector, value, label) => {
872
- const el = document.querySelector(selector);
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
- el.value = option.value;
879
- el.dispatchEvent(new Event("input", {bubbles: true}));
880
- 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}));
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
- ["Fetch.enable", { patterns: this._browser._fetchPatterns }]
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
- if (this._captures.length === 0) await this.send("Network.enable");
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 instanceof CDPError) || !err.message.includes("Cannot find context")) throw 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
- const details = result.exceptionDetails;
1296
- if (details) throw new CDPError(`evaluate failed: ${(details.exception ?? {}).description ?? details.text}`);
1297
- 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
+ });
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
- 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);
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
- const xpath = asXPath(selector);
1336
- if (xpath !== void 0) return (await this.#search(xpath)).length;
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 ?? []).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
+ };
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 picked = await this.evaluate(SELECT, { args: [
1346
- selector,
1347
- value ?? null,
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
- const xpath = asXPath(selector);
1380
- if (xpath !== void 0) return (await this.#search(xpath))[0];
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
- })).nodeId || void 0;
1441
+ });
1442
+ if (found.nodeId) return found.nodeId;
1443
+ return (await this.#search(selector))[0];
1385
1444
  }
1386
- async #search(query) {
1387
- 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
+ });
1388
1451
  try {
1389
1452
  if (!resultCount) return [];
1390
- return (await this.send("DOM.getSearchResults", {
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
- const status = event.responseStatusCode;
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 [...patterns, STATUS_PATTERN];
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.4";
2681
+ const VERSION = "0.0.6";
2622
2682
  //#endregion
2623
2683
  //#region src/client.ts
2624
2684
  function userAgent() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surfsky",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "TypeScript SDK for the Surfsky antidetect browser cloud.",
5
5
  "keywords": [
6
6
  "surfsky",