@empiricalrun/playwright-utils 0.39.6 → 0.40.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @empiricalrun/playwright-utils
2
2
 
3
+ ## 0.40.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 644366a: fix: incorrect types
8
+
9
+ ## 0.40.0
10
+
11
+ ### Minor Changes
12
+
13
+ - df795ce: feat: use external service dependencies via proxy
14
+
3
15
  ## 0.39.6
4
16
 
5
17
  ### Patch Changes
@@ -1,7 +1,3 @@
1
1
  import type { Page } from "@playwright/test";
2
- type CaptchaSolveOptions = {
3
- provider: "2captcha" | "llm-vision";
4
- };
5
- export declare function solveRecaptcha(page: Page, options?: CaptchaSolveOptions): Promise<void>;
6
- export {};
2
+ export declare function solveRecaptcha(page: Page): Promise<void>;
7
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/captcha/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAK7C,KAAK,mBAAmB,GAAG;IACzB,QAAQ,EAAE,UAAU,GAAG,YAAY,CAAC;CACrC,CAAC;AAMF,wBAAsB,cAAc,CAClC,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,mBAAiC,iBAmB3C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/captcha/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAmI7C,wBAAsB,cAAc,CAAC,IAAI,EAAE,IAAI,iBAU9C"}
@@ -4,28 +4,96 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.solveRecaptcha = solveRecaptcha;
7
+ const dashboard_client_1 = require("@empiricalrun/dashboard-client");
7
8
  const puppeteer_extra_plugin_recaptcha_1 = __importDefault(require("puppeteer-extra-plugin-recaptcha"));
8
- const vision_1 = require("./vision");
9
- const defaultOpts = {
10
- provider: "2captcha",
11
- };
12
- async function solveRecaptcha(page, options = defaultOpts) {
13
- const { provider } = options;
14
- if (provider === "2captcha") {
15
- const token = process.env.TWOCAPTCHA_API_KEY;
16
- if (!token) {
17
- throw new Error("Missing environment variable: TWOCAPTCHA_API_KEY");
9
+ const POLLING_INTERVAL = 10_000;
10
+ const MAX_POLLING_TIME = 300_000;
11
+ function buildTask(captcha) {
12
+ const taskType = captcha._vendor === "hcaptcha"
13
+ ? "HCaptchaTaskProxyless"
14
+ : "RecaptchaV2TaskProxyless";
15
+ const task = {
16
+ type: taskType,
17
+ websiteURL: captcha.url,
18
+ websiteKey: captcha.sitekey,
19
+ };
20
+ if (captcha.isEnterprise) {
21
+ task.isEnterprise = true;
22
+ }
23
+ if (captcha.s) {
24
+ task.recaptchaDataSValue = captcha.s;
25
+ }
26
+ return task;
27
+ }
28
+ function assertNoError(result) {
29
+ if (result.errorId && result.errorId > 0) {
30
+ throw new Error(`2captcha error: ${result.errorCode || "unknown"}`);
31
+ }
32
+ }
33
+ async function solveViaProxy(captchas) {
34
+ const apiClient = new dashboard_client_1.DashboardAPIClient({
35
+ authType: "project-api-key",
36
+ });
37
+ const solutions = await Promise.all(captchas.map((captcha) => solveSingleViaProxy(apiClient, captcha)));
38
+ return { solutions, error: solutions.find((s) => !!s.error) };
39
+ }
40
+ async function solveSingleViaProxy(apiClient, captcha) {
41
+ const solution = {
42
+ _vendor: captcha._vendor,
43
+ provider: "2captcha",
44
+ };
45
+ try {
46
+ if (!captcha.sitekey || !captcha.url || !captcha.id) {
47
+ throw new Error("Missing data in captcha");
18
48
  }
19
- const rcPlugin = (0, puppeteer_extra_plugin_recaptcha_1.default)({
20
- provider: {
21
- id: "2captcha",
22
- token,
23
- },
24
- visualFeedback: true,
49
+ solution.id = captcha.id;
50
+ solution.requestAt = new Date();
51
+ const createResult = await apiClient.request(`/api/twocaptcha/proxy`, {
52
+ method: "POST",
53
+ body: { endpoint: "/createTask", body: { task: buildTask(captcha) } },
25
54
  });
26
- await rcPlugin.solveRecaptchas(page);
55
+ assertNoError(createResult);
56
+ if (!createResult.taskId) {
57
+ throw new Error("2captcha error: no taskId returned");
58
+ }
59
+ const taskId = createResult.taskId;
60
+ const startTime = Date.now();
61
+ while (Date.now() - startTime < MAX_POLLING_TIME) {
62
+ await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL));
63
+ const result = await apiClient.request(`/api/twocaptcha/proxy`, {
64
+ method: "POST",
65
+ body: { endpoint: "/getTaskResult", body: { taskId } },
66
+ });
67
+ assertNoError(result);
68
+ if (result.status === "ready" && result.solution) {
69
+ const token = result.solution.gRecaptchaResponse || result.solution.token;
70
+ if (token) {
71
+ solution.text = token;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ if (!solution.text) {
77
+ throw new Error("2captcha error: polling timed out");
78
+ }
79
+ solution.responseAt = new Date();
80
+ solution.hasSolution = true;
81
+ solution.duration =
82
+ (solution.responseAt.getTime() - solution.requestAt.getTime()) / 1000;
27
83
  }
28
- else if (provider === "llm-vision") {
29
- return (0, vision_1.solveRecaptcha)(page);
84
+ catch (error) {
85
+ solution.error = String(error);
30
86
  }
87
+ return solution;
88
+ }
89
+ async function solveRecaptcha(page) {
90
+ const rcPlugin = (0, puppeteer_extra_plugin_recaptcha_1.default)({
91
+ provider: {
92
+ id: "2captcha",
93
+ token: "proxy",
94
+ fn: solveViaProxy,
95
+ },
96
+ visualFeedback: true,
97
+ });
98
+ await rcPlugin.solveRecaptchas(page);
31
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.39.6",
3
+ "version": "0.40.1",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -44,9 +44,9 @@
44
44
  "rimraf": "^6.0.1",
45
45
  "ts-morph": "^23.0.0",
46
46
  "@empiricalrun/cua": "^0.2.0",
47
- "@empiricalrun/r2-uploader": "^0.9.1",
47
+ "@empiricalrun/dashboard-client": "^0.2.0",
48
48
  "@empiricalrun/llm": "^0.25.2",
49
- "@empiricalrun/dashboard-client": "^0.2.0"
49
+ "@empiricalrun/r2-uploader": "^0.9.1"
50
50
  },
51
51
  "scripts": {
52
52
  "dev": "tsc --build --watch",
@@ -1 +1 @@
1
- {"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/postgres.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/captcha/vision.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}
1
+ {"root":["./src/email.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/postgres.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"],"version":"5.8.3"}
@@ -1,3 +0,0 @@
1
- import type { Page } from "playwright-core";
2
- export declare function solveRecaptcha(page: Page): Promise<void>;
3
- //# sourceMappingURL=vision.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"vision.d.ts","sourceRoot":"","sources":["../../src/captcha/vision.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAkB,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAyE5D,wBAAsB,cAAc,CAAC,IAAI,EAAE,IAAI,iBAiB9C"}
@@ -1,82 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.solveRecaptcha = solveRecaptcha;
4
- const vision_1 = require("@empiricalrun/llm/vision");
5
- const RECAPTCHA_FRAME_URL = "https://www.google.com/recaptcha/api2";
6
- const MAX_ATTEMPTS = 5;
7
- async function isVisible(locator) {
8
- const isVisibleAccordingToPlaywright = await locator.isVisible();
9
- if (isVisibleAccordingToPlaywright) {
10
- const bbox = await locator.boundingBox();
11
- const isValidBbox = !!bbox && bbox.width > 0 && bbox.height > 0 && bbox.x > 0 && bbox.y > 0;
12
- return isValidBbox;
13
- }
14
- return false;
15
- }
16
- async function isVerifyCaptchaVisible(page) {
17
- const frames = page.frames();
18
- for (const frame of frames) {
19
- if (frame.url().includes(RECAPTCHA_FRAME_URL)) {
20
- if (await isVisible(frame.getByText("Select all images"))) {
21
- return { frame, type: "images" };
22
- }
23
- else if (await isVisible(frame.getByText("Select all squares"))) {
24
- return { frame, type: "squares" };
25
- }
26
- }
27
- }
28
- }
29
- async function getObjectiveCaptcha(capFrame) {
30
- const { frame, type } = capFrame;
31
- const text = await frame.getByText(`Select all ${type}`).textContent();
32
- return text
33
- ?.replace(`Select all ${type} with`, "")
34
- .replace(`Click verify once there are none left`, "")
35
- .replace(`If there are none, click skip`, "")
36
- .trim();
37
- }
38
- async function clickImageTile(frame, nth) {
39
- const image = frame.locator(".rc-imageselect-tile").nth(nth);
40
- await image.dispatchEvent("click");
41
- }
42
- async function handleCaptcha(cf) {
43
- const screenshot = await cf.frame
44
- .locator(".rc-imageselect-challenge")
45
- .screenshot();
46
- const gridBoxesToSelect = await (0, vision_1.solveRecaptchaGrid)({
47
- captchaType: cf.type,
48
- captchaObject: (await getObjectiveCaptcha(cf)),
49
- screenshotBase64: screenshot.toString("base64"),
50
- });
51
- for (const n of gridBoxesToSelect) {
52
- await clickImageTile(cf.frame, n - 1);
53
- }
54
- const verifyButton = cf.frame.getByRole("button", { name: "Verify" });
55
- const nextButton = cf.frame.getByRole("button", { name: "Next" });
56
- if (await isVisible(verifyButton)) {
57
- await verifyButton.dispatchEvent("click");
58
- }
59
- else if (await isVisible(nextButton)) {
60
- await nextButton.dispatchEvent("click");
61
- }
62
- await cf.frame.waitForTimeout(3000);
63
- }
64
- async function solveRecaptcha(page) {
65
- const frame = page.frameLocator('iframe[title="reCAPTCHA"]');
66
- await frame.getByLabel("not a robot").click();
67
- await page.waitForTimeout(2000);
68
- for (let i = 0; i < MAX_ATTEMPTS; i++) {
69
- const cf = await isVerifyCaptchaVisible(page);
70
- if (cf) {
71
- await handleCaptcha(cf);
72
- }
73
- else {
74
- break;
75
- }
76
- if (i === MAX_ATTEMPTS - 1) {
77
- if (await isVerifyCaptchaVisible(page)) {
78
- throw new Error(`Failed to solve captcha in ${MAX_ATTEMPTS} attempts.`);
79
- }
80
- }
81
- }
82
- }