From d0558b292d10d983ef01645c9d8fcbb9bcf9bde5 Mon Sep 17 00:00:00 2001 From: Matteo Borgato Date: Tue, 15 Jul 2025 15:20:14 -0300 Subject: [PATCH 1/6] fix: get ownerDocument before calling element.evaluate(...) refactor: extract getOwnerDocument helper function to follow DRY principle --- .../features/broker-protection/utils/utils.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/injected/src/features/broker-protection/utils/utils.js b/injected/src/features/broker-protection/utils/utils.js index 8429232ead..b30d02266d 100644 --- a/injected/src/features/broker-protection/utils/utils.js +++ b/injected/src/features/broker-protection/utils/utils.js @@ -1,3 +1,13 @@ +/** + * Gets the owner document of an element or uses document as fallback + * + * @param {Node|Element} element + * @return {Document} The owner document + */ +function getOwnerDocument(element) { + return element.ownerDocument || document; +} + /** * Get a single element. * @@ -75,7 +85,8 @@ export function getElementMatches(element, selector) { * @return {boolean} */ function matchesXPath(element, selector) { - const xpathResult = document.evaluate(selector, element, null, XPathResult.BOOLEAN_TYPE, null); + const ownerDoc = getOwnerDocument(element); + const xpathResult = ownerDoc.evaluate(selector, element, null, XPathResult.BOOLEAN_TYPE, null); return xpathResult.booleanValue; } @@ -131,7 +142,8 @@ function safeQuerySelector(element, selector) { */ function safeQuerySelectorXPath(element, selector) { try { - const match = document.evaluate(selector, element, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); + const ownerDoc = getOwnerDocument(element); + const match = ownerDoc.evaluate(selector, element, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const single = match?.singleNodeValue; if (single) { return /** @type {HTMLElement} */ (single); @@ -150,8 +162,9 @@ function safeQuerySelectorXPath(element, selector) { */ function safeQuerySelectorAllXpath(element, selector) { try { + const ownerDoc = getOwnerDocument(element); // gets all elements matching the xpath query - const xpathResult = document.evaluate(selector, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + const xpathResult = ownerDoc.evaluate(selector, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); if (xpathResult) { /** @type {HTMLElement[]} */ const matchedNodes = []; From 6e39244a1f1a8754f81009ba7defd9832360212e Mon Sep 17 00:00:00 2001 From: Matteo Borgato Date: Mon, 21 Jul 2025 16:51:26 -0300 Subject: [PATCH 2/6] feat: implement CAPTCHA retry mechanism Add automatic retry support for solveCaptcha actions with configurable attempts and delays to improve opt-out success rates. --- .../broker-protection.spec.js | 28 ++++ .../pages/captcha-retry.html | 141 ++++++++++++++++++ injected/src/features/broker-protection.js | 1 + .../src/features/broker-protection/types.js | 1 + 4 files changed, 171 insertions(+) create mode 100644 injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html diff --git a/injected/integration-test/broker-protection-tests/broker-protection.spec.js b/injected/integration-test/broker-protection-tests/broker-protection.spec.js index 6e1415c34a..beb0187c63 100644 --- a/injected/integration-test/broker-protection-tests/broker-protection.spec.js +++ b/injected/integration-test/broker-protection-tests/broker-protection.spec.js @@ -640,6 +640,34 @@ test.describe('Broker Protection communications', () => { const response = await dbp.collector.waitForMessage('actionCompleted'); dbp.isSuccessMessage(response); }); + test('retrying a solveCaptcha', async ({ page }, workerInfo) => { + const dbp = BrokerProtectionPage.create(page, workerInfo.project.use); + await dbp.enabled(); + await dbp.navigatesTo('captcha-retry.html?failAttempts=2'); + + await dbp.simulateSubscriptionMessage('onActionReceived', { + state: { + action: { + actionType: 'solveCaptcha', + id: '2', + selector: '#svgCaptchaInputId', + retry: { + environment: 'web', + maxAttempts: 3, + interval: { ms: 100 }, + }, + }, + data: { + token: 'ABC123', + }, + }, + }); + + // Wait for the action to complete (should succeed after retries) + const response = await dbp.collector.waitForMessage('actionCompleted'); + dbp.isSuccessMessage(response); + }); + test('ensuring retry doesnt apply everywhere', async ({ page }, workerInfo) => { const dbp = BrokerProtectionPage.create(page, workerInfo.project.use); await dbp.enabled(); diff --git a/injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html b/injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html new file mode 100644 index 0000000000..c88e2fd817 --- /dev/null +++ b/injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html @@ -0,0 +1,141 @@ + + + + + + + Captcha Retry Test + + + +

Captcha Retry Test

+

This page simulates a captcha that fails the first few attempts before succeeding.

+ +
+
+ Test Captcha: ABC123 +
+ + + + + + + +
+ +
+

Configuration

+ + +
+ + + + diff --git a/injected/src/features/broker-protection.js b/injected/src/features/broker-protection.js index 58fe55aa74..ba5ad03726 100644 --- a/injected/src/features/broker-protection.js +++ b/injected/src/features/broker-protection.js @@ -110,6 +110,7 @@ export default class BrokerProtection extends ContentFeature { }; } } + return retryConfig; } } diff --git a/injected/src/features/broker-protection/types.js b/injected/src/features/broker-protection/types.js index 45a2793477..6b81a9c93a 100644 --- a/injected/src/features/broker-protection/types.js +++ b/injected/src/features/broker-protection/types.js @@ -12,6 +12,7 @@ * @property {string} [captchaType] * @property {string} [injectCaptchaHandler] * @property {string} [dataSource] + * @property {{environment: string, maxAttempts: number, interval: {ms: number}}} [retry] */ /** From 93ac38e86eef6caccdbe409f43e1253649f06d11 Mon Sep 17 00:00:00 2001 From: Matteo Borgato Date: Mon, 21 Jul 2025 17:18:50 -0300 Subject: [PATCH 3/6] WIP: improving captcha retry tests --- .../broker-protection-captcha.spec.js | 30 +++++++++++++++++++ .../broker-protection.spec.js | 27 ----------------- .../pages/captcha-retry.html | 13 ++------ 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js index 3a91d6dfca..52f79c1f94 100644 --- a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js +++ b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js @@ -138,6 +138,36 @@ test.describe('Broker Protection Captcha', () => { await dbp.isCaptchaTokenFilled(imageCaptchaResponseSelector); }); + + test('solves the captcha with retry configuration', async ({ createConfiguredDbp }) => { + const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default); + await dbp.navigatesTo(imageCaptchaTargetPage); + + // Create action manually to ensure retry field is properly included + const retryAction = { + state: { + action: { + id: 'retry-test', + actionType: 'solveCaptcha', + captchaType: 'image', + selector: imageCaptchaResponseSelector, + retry: { + environment: 'web', + maxAttempts: 3, + interval: { ms: 200 }, + }, + }, + data: { + token: 'test_token', + }, + }, + }; + + await dbp.receivesInlineAction(retryAction); + await dbp.getSuccessResponse(); + + await dbp.isCaptchaTokenFilled(imageCaptchaResponseSelector); + }); }); }); diff --git a/injected/integration-test/broker-protection-tests/broker-protection.spec.js b/injected/integration-test/broker-protection-tests/broker-protection.spec.js index beb0187c63..128bd507da 100644 --- a/injected/integration-test/broker-protection-tests/broker-protection.spec.js +++ b/injected/integration-test/broker-protection-tests/broker-protection.spec.js @@ -640,33 +640,6 @@ test.describe('Broker Protection communications', () => { const response = await dbp.collector.waitForMessage('actionCompleted'); dbp.isSuccessMessage(response); }); - test('retrying a solveCaptcha', async ({ page }, workerInfo) => { - const dbp = BrokerProtectionPage.create(page, workerInfo.project.use); - await dbp.enabled(); - await dbp.navigatesTo('captcha-retry.html?failAttempts=2'); - - await dbp.simulateSubscriptionMessage('onActionReceived', { - state: { - action: { - actionType: 'solveCaptcha', - id: '2', - selector: '#svgCaptchaInputId', - retry: { - environment: 'web', - maxAttempts: 3, - interval: { ms: 100 }, - }, - }, - data: { - token: 'ABC123', - }, - }, - }); - - // Wait for the action to complete (should succeed after retries) - const response = await dbp.collector.waitForMessage('actionCompleted'); - dbp.isSuccessMessage(response); - }); test('ensuring retry doesnt apply everywhere', async ({ page }, workerInfo) => { const dbp = BrokerProtectionPage.create(page, workerInfo.project.use); diff --git a/injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html b/injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html index c88e2fd817..37832aefd5 100644 --- a/injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html +++ b/injected/integration-test/test-pages/broker-protection/pages/captcha-retry.html @@ -76,7 +76,7 @@

Captcha Retry Test

-

This page simulates a captcha that fails the first few attempts before succeeding.

+

This page simulates a captcha for retry testing.

@@ -88,15 +88,8 @@

Captcha Retry Test

- -
-
-

Configuration

- - +
- - From 6e738eb8e949da2b1129ae8499a6e3dbbffbe7f1 Mon Sep 17 00:00:00 2001 From: Matteo Borgato Date: Tue, 22 Jul 2025 22:13:32 -0300 Subject: [PATCH 5/6] remove debugging code --- .../broker-protection-tests/broker-protection-captcha.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js index 011bcf0b18..82cccbf417 100644 --- a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js +++ b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js @@ -137,8 +137,6 @@ test.describe('Broker Protection Captcha', () => { dbp.getSuccessResponse(); await dbp.isCaptchaTokenFilled(imageCaptchaResponseSelector); - - await page.pause(); }); test('retry fails with permanently invalid element type', async ({ page, createConfiguredDbp }) => { From 0c68a35a7b1979759ff2b667c5f25e0862f52544 Mon Sep 17 00:00:00 2001 From: Matteo Borgato Date: Tue, 22 Jul 2025 22:28:23 -0300 Subject: [PATCH 6/6] remove unused page --- .../broker-protection-tests/broker-protection-captcha.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js index 82cccbf417..adb054420f 100644 --- a/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js +++ b/injected/integration-test/broker-protection-tests/broker-protection-captcha.spec.js @@ -130,7 +130,7 @@ test.describe('Broker Protection Captcha', () => { }); test.describe('solveCaptchaInfo', () => { - test('solves the captcha for the correct action data', async ({ page, createConfiguredDbp }) => { + test('solves the captcha for the correct action data', async ({ createConfiguredDbp }) => { const dbp = await createConfiguredDbp(BROKER_PROTECTION_CONFIGS.default); await dbp.navigatesTo(imageCaptchaTargetPage); await dbp.receivesInlineAction(createSolveImageCaptchaAction({ selector: imageCaptchaResponseSelector }));