@empiricalrun/playwright-utils 0.39.5 → 0.40.0

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,20 @@
1
1
  # @empiricalrun/playwright-utils
2
2
 
3
+ ## 0.40.0
4
+
5
+ ### Minor Changes
6
+
7
+ - df795ce: feat: use external service dependencies via proxy
8
+
9
+ ## 0.39.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 4947d9a: feat: move failing line collection to reporter
14
+ - @empiricalrun/cua@0.2.0
15
+ - @empiricalrun/dashboard-client@0.2.0
16
+ - @empiricalrun/llm@0.25.2
17
+
3
18
  ## 0.39.5
4
19
 
5
20
  ### 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 = 5000;
10
+ const MAX_POLLING_TIME = 180_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 === 1 && 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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAmBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;;IAM1C,OAAO,CAAC,6BAA6B,CAkCnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IAoFtC,KAAK,CAAC,MAAM,EAAE,UAAU;IA8G9B,OAAO,CAAC,gBAAgB;YAoBV,gBAAgB;YAOhB,iBAAiB;CAmChC;AAED,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAoBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;;IAM1C,OAAO,CAAC,6BAA6B,CAkCnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU;IA6FtC,KAAK,CAAC,MAAM,EAAE,UAAU;IA8G9B,OAAO,CAAC,gBAAgB;YAoBV,gBAAgB;YAOhB,iBAAiB;CAmChC;AAED,eAAe,iBAAiB,CAAC"}
@@ -8,6 +8,7 @@ const path_1 = __importDefault(require("path"));
8
8
  const logger_1 = require("../logger");
9
9
  const telemetry_1 = require("../telemetry");
10
10
  const blob_utils_1 = require("./blob-utils");
11
+ const failing_line_1 = require("./failing-line");
11
12
  const uploader_1 = require("./uploader");
12
13
  const util_1 = require("./util");
13
14
  class EmpiricalReporter {
@@ -88,6 +89,10 @@ class EmpiricalReporter {
88
89
  try {
89
90
  logger_1.logger.debug(`[Empirical Reporter] Attachments for test ${test.title} are uploaded:`, successfulAttachments);
90
91
  const { suites, projectName } = (0, util_1.suitesAndProjectForTest)(test);
92
+ const errorLocation = result.error?.location;
93
+ const failingLine = errorLocation
94
+ ? (0, failing_line_1.getFailingLineFromSource)(errorLocation.file, errorLocation.line, errorLocation.column)
95
+ : undefined;
91
96
  const params = {
92
97
  test,
93
98
  suites,
@@ -95,6 +100,7 @@ class EmpiricalReporter {
95
100
  projectName,
96
101
  attachments: successfulAttachments,
97
102
  runId: process.env.TEST_RUN_GITHUB_ACTION_ID,
103
+ failingLine,
98
104
  };
99
105
  return (0, util_1.sendTestCaseUpdateToDashboard)(params);
100
106
  }
@@ -0,0 +1,2 @@
1
+ export declare function getFailingLineFromSource(filePath: string, line: number, column: number): string | undefined;
2
+ //# sourceMappingURL=failing-line.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failing-line.d.ts","sourceRoot":"","sources":["../../src/reporter/failing-line.ts"],"names":[],"mappings":"AAmDA,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,SAAS,CAQpB"}
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getFailingLineFromSource = getFailingLineFromSource;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const ts_morph_1 = require("ts-morph");
9
+ const supportedSyntaxes = [
10
+ ts_morph_1.SyntaxKind.ExpressionStatement,
11
+ ts_morph_1.SyntaxKind.VariableStatement,
12
+ ts_morph_1.SyntaxKind.ReturnStatement,
13
+ ts_morph_1.SyntaxKind.IfStatement,
14
+ ts_morph_1.SyntaxKind.ForStatement,
15
+ ts_morph_1.SyntaxKind.WhileStatement,
16
+ ts_morph_1.SyntaxKind.DoStatement,
17
+ ts_morph_1.SyntaxKind.SwitchStatement,
18
+ ts_morph_1.SyntaxKind.TryStatement,
19
+ ts_morph_1.SyntaxKind.FunctionDeclaration,
20
+ ts_morph_1.SyntaxKind.ArrowFunction,
21
+ ts_morph_1.SyntaxKind.ClassDeclaration,
22
+ ts_morph_1.SyntaxKind.Block,
23
+ ts_morph_1.SyntaxKind.ThrowStatement,
24
+ ];
25
+ function getTestStatementFromCode(code, line, column) {
26
+ const project = new ts_morph_1.Project();
27
+ const sourceFile = project.createSourceFile("temp-file.ts", `${code}`);
28
+ const position = sourceFile
29
+ .getFullText()
30
+ .split("\n")
31
+ .slice(0, line - 1)
32
+ .reduce((sum, l) => sum + l.length + 1, 0) +
33
+ (column - 1);
34
+ const nodeAtPos = sourceFile.getDescendantAtPos(position);
35
+ if (nodeAtPos) {
36
+ let statement = nodeAtPos.getFirstAncestor((ancestor) => supportedSyntaxes.includes(ancestor.getKind()));
37
+ if (!statement) {
38
+ if (supportedSyntaxes.includes(nodeAtPos.getKind())) {
39
+ statement = nodeAtPos;
40
+ }
41
+ }
42
+ if (statement) {
43
+ return statement.getText();
44
+ }
45
+ }
46
+ return "";
47
+ }
48
+ function getFailingLineFromSource(filePath, line, column) {
49
+ try {
50
+ const code = fs_1.default.readFileSync(filePath, "utf-8");
51
+ const statement = getTestStatementFromCode(code, line, column);
52
+ return statement || undefined;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.39.5",
3
+ "version": "0.40.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -32,7 +32,7 @@
32
32
  "@types/pg": "^8.11.6",
33
33
  "playwright-core": "1.57.0",
34
34
  "serve-handler": "^6.1.6",
35
- "@empiricalrun/shared-types": "0.12.0"
35
+ "@empiricalrun/shared-types": "0.12.1"
36
36
  },
37
37
  "dependencies": {
38
38
  "@babel/code-frame": "^7.24.7",
@@ -42,8 +42,9 @@
42
42
  "console-log-level": "^1.4.1",
43
43
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
44
44
  "rimraf": "^6.0.1",
45
- "@empiricalrun/dashboard-client": "^0.2.0",
45
+ "ts-morph": "^23.0.0",
46
46
  "@empiricalrun/cua": "^0.2.0",
47
+ "@empiricalrun/dashboard-client": "^0.2.0",
47
48
  "@empiricalrun/llm": "^0.25.2",
48
49
  "@empiricalrun/r2-uploader": "^0.9.1"
49
50
  },
@@ -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/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
- }