Skip to content

Commit c0a1c71

Browse files
authored
Merge branch 'main' into dbajpeyi/fix/animate-export-once
2 parents 702544f + a418217 commit c0a1c71

File tree

214 files changed

+10421
-1221
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

214 files changed

+10421
-1221
lines changed

injected/integration-test/broker-protection.spec.js

+40
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,46 @@ test.describe('Broker Protection communications', () => {
469469
await page.waitForURL((url) => url.hash === '#no', { timeout: 2000 });
470470
});
471471

472+
test('clicking selectors that do not exists should fail', async ({ page }, workerInfo) => {
473+
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
474+
await dbp.enabled();
475+
await dbp.navigatesTo('clicks.html');
476+
await dbp.receivesAction('click-nonexistent-selector.json');
477+
const response = await dbp.collector.waitForMessage('actionCompleted');
478+
479+
dbp.isErrorMessage(response);
480+
});
481+
482+
test('clicking buttons that are disabled should fail', async ({ page }, workerInfo) => {
483+
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
484+
await dbp.enabled();
485+
await dbp.navigatesTo('clicks.html');
486+
await dbp.receivesAction('click-disabled-button.json');
487+
const response = await dbp.collector.waitForMessage('actionCompleted');
488+
489+
dbp.isErrorMessage(response);
490+
});
491+
492+
test('clicking selectors that do not exist when failSilently is enabled should not fail', async ({ page }, workerInfo) => {
493+
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
494+
await dbp.enabled();
495+
await dbp.navigatesTo('clicks.html');
496+
await dbp.receivesAction('click-nonexistent-selector-failSilently.json');
497+
const response = await dbp.collector.waitForMessage('actionCompleted');
498+
499+
dbp.isSuccessMessage(response);
500+
});
501+
502+
test('clicking buttons that are disabled when failSilently is enabled should not fail', async ({ page }, workerInfo) => {
503+
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
504+
await dbp.enabled();
505+
await dbp.navigatesTo('clicks.html');
506+
await dbp.receivesAction('click-disabled-button-failSilently.json');
507+
const response = await dbp.collector.waitForMessage('actionCompleted');
508+
509+
dbp.isSuccessMessage(response);
510+
});
511+
472512
test('getCaptchaInfo', async ({ page }, workerInfo) => {
473513
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
474514
await dbp.enabled();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { test, expect } from '@playwright/test';
2+
import { ResultsCollector } from './page-objects/results-collector.js';
3+
import { readOutgoingMessages } from '@duckduckgo/messaging/lib/test-utils.mjs';
4+
5+
const ENABLED_CONFIG = 'integration-test/test-pages/message-bridge/config/message-bridge-enabled.json';
6+
const DISABLED_CONFIG = 'integration-test/test-pages/message-bridge/config/message-bridge-disabled.json';
7+
const ENABLED_HTML = '/message-bridge/pages/enabled.html';
8+
const DISABLED_HTML = '/message-bridge/pages/disabled.html';
9+
10+
test('message bridge when enabled (android)', async ({ page }, testInfo) => {
11+
const pageWorld = ResultsCollector.create(page, testInfo.project.use);
12+
13+
// seed the request->re
14+
pageWorld.withMockResponse({
15+
sampleData: /** @type {any} */ ({
16+
ghi: 'jkl',
17+
}),
18+
});
19+
20+
pageWorld.withUserPreferences({
21+
messageSecret: 'ABC',
22+
javascriptInterface: 'javascriptInterface',
23+
messageCallback: 'messageCallback',
24+
});
25+
26+
// now load the page
27+
await pageWorld.load(ENABLED_HTML, ENABLED_CONFIG);
28+
29+
// simulate a push event
30+
await pageWorld.simulateSubscriptionMessage('exampleFeature', 'onUpdate', { abc: 'def' });
31+
32+
// get all results
33+
const results = await pageWorld.results();
34+
expect(results['Creating the bridge']).toStrictEqual([
35+
{ name: 'bridge.notify', result: 'function', expected: 'function' },
36+
{ name: 'bridge.request', result: 'function', expected: 'function' },
37+
{ name: 'bridge.subscribe', result: 'function', expected: 'function' },
38+
{ name: 'data', result: [{ abc: 'def' }, { ghi: 'jkl' }], expected: [{ abc: 'def' }, { ghi: 'jkl' }] },
39+
]);
40+
41+
// verify messaging calls
42+
const calls = await page.evaluate(readOutgoingMessages);
43+
expect(calls.length).toBe(2);
44+
const pixel = calls[0].payload;
45+
const request = calls[1].payload;
46+
47+
expect(pixel).toStrictEqual({
48+
context: 'contentScopeScripts',
49+
featureName: 'exampleFeature',
50+
method: 'pixel',
51+
params: {},
52+
});
53+
54+
const { id, ...rest } = /** @type {import("@duckduckgo/messaging").RequestMessage} */ (request);
55+
56+
expect(rest).toStrictEqual({
57+
context: 'contentScopeScripts',
58+
featureName: 'exampleFeature',
59+
method: 'sampleData',
60+
params: {},
61+
});
62+
63+
if (!('id' in request)) throw new Error('unreachable');
64+
65+
expect(typeof request.id).toBe('string');
66+
expect(request.id.length).toBeGreaterThan(10);
67+
});
68+
69+
test('message bridge when disabled (android)', async ({ page }, testInfo) => {
70+
const pageWorld = ResultsCollector.create(page, testInfo.project.use);
71+
72+
// now load the main page
73+
await pageWorld.load(DISABLED_HTML, DISABLED_CONFIG);
74+
75+
// verify no outgoing calls were made
76+
const calls = await page.evaluate(readOutgoingMessages);
77+
expect(calls).toHaveLength(0);
78+
79+
// get all results
80+
const results = await pageWorld.results();
81+
expect(results['Creating the bridge, but it is unavailable']).toStrictEqual([
82+
{ name: 'error', result: 'Did not install Message Bridge', expected: 'Did not install Message Bridge' },
83+
]);
84+
});

injected/integration-test/page-objects/duckplayer-overlays.js

+5
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ export class DuckplayerOverlays {
8080
},
8181
sendDuckPlayerPixel: {},
8282
});
83+
this.collector.withUserPreferences({
84+
messageSecret: 'ABC',
85+
javascriptInterface: 'javascriptInterface',
86+
messageCallback: 'messageCallback',
87+
});
8388
page.on('console', (msg) => {
8489
console.log(msg.type(), msg.text());
8590
});

injected/integration-test/page-objects/results-collector.js

+3
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export class ResultsCollector {
172172
messagingContext: this.messagingContext('n/a'),
173173
responses: this.#mockResponses,
174174
messageCallback: 'messageCallback',
175+
javascriptInterface: this.#userPreferences.javascriptInterface,
175176
});
176177

177178
const wrapFn = this.build.switch({
@@ -234,6 +235,8 @@ export class ResultsCollector {
234235
name,
235236
payload,
236237
injectName: this.build.name,
238+
messageCallback: this.#userPreferences.messageCallback,
239+
messageSecret: this.#userPreferences.messageSecret,
237240
});
238241
}
239242

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "click",
5+
"id": "1",
6+
"elements": [
7+
{
8+
"type": "button",
9+
"selector": ".btn",
10+
"failSilently": true
11+
}
12+
]
13+
}
14+
}
15+
}
16+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "click",
5+
"id": "1",
6+
"elements": [
7+
{
8+
"type": "button",
9+
"selector": ".btn"
10+
}
11+
]
12+
}
13+
}
14+
}
15+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "click",
5+
"id": "1",
6+
"elements": [
7+
{
8+
"type": "button",
9+
"selector": ".test",
10+
"failSilently": true
11+
}
12+
]
13+
}
14+
}
15+
}
16+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"state": {
3+
"action": {
4+
"actionType": "click",
5+
"id": "1",
6+
"elements": [
7+
{
8+
"type": "button",
9+
"selector": ".test"
10+
}
11+
]
12+
}
13+
}
14+
}
15+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>Broker Protection</title>
7+
</head>
8+
<body>
9+
<div class="result">
10+
<div class="name">John Doe</div>
11+
<div class="age">32</div>
12+
<div class="locations" data-id="1">
13+
<span>New York, NY</span>
14+
<span>Los Angeles, CA</span>
15+
<a href="#" class="view-more">View More</a>
16+
</div>
17+
<button disabled class="btn">View More</button>
18+
</div>
19+
</body>
20+
</html>

injected/integration-test/type-helpers.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class Build {
6262
apple: () => '../Sources/ContentScopeScripts/dist/contentScope.js',
6363
'apple-isolated': () => '../Sources/ContentScopeScripts/dist/contentScopeIsolated.js',
6464
'android-autofill-password-import': () => '../build/android/autofillPasswordImport.js',
65+
'android-broker-protection': () => '../build/android/brokerProtection.js',
6566
});
6667
return readFileSync(path, 'utf8');
6768
}

injected/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"build-chrome": "node scripts/entry-points.js --platform chrome",
1111
"build-chrome-mv3": "node scripts/entry-points.js --platform chrome-mv3",
1212
"build-apple": "node scripts/entry-points.js --platform apple && node scripts/entry-points.js --platform apple-isolated",
13-
"build-android": "node scripts/entry-points.js --platform android && node scripts/entry-points.js --platform android-autofill-password-import",
13+
"build-android": "node scripts/entry-points.js --platform android && node scripts/entry-points.js --platform android-autofill-password-import && node scripts/entry-points.js --platform android-broker-protection",
1414
"build-windows": "node scripts/entry-points.js --platform windows",
1515
"build-integration": "node scripts/entry-points.js --platform integration",
1616
"build-types": "node scripts/types.mjs",
@@ -42,9 +42,9 @@
4242
"@rollup/plugin-commonjs": "^28.0.2",
4343
"@rollup/plugin-node-resolve": "^16.0.0",
4444
"@rollup/plugin-replace": "^6.0.2",
45-
"@types/chrome": "^0.0.297",
45+
"@types/chrome": "^0.0.304",
4646
"@types/jasmine": "^5.1.5",
47-
"@types/node": "^22.10.7",
47+
"@types/node": "^22.13.1",
4848
"@typescript-eslint/eslint-plugin": "^8.20.0",
4949
"fast-check": "^3.23.2",
5050
"jasmine": "^5.5.0",

injected/playwright.config.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export default defineConfig({
4141
},
4242
{
4343
name: 'android',
44-
testMatch: ['integration-test/duckplayer-mobile.spec.js', 'integration-test/web-compat-android.spec.js'],
44+
testMatch: [
45+
'integration-test/duckplayer-mobile.spec.js',
46+
'integration-test/web-compat-android.spec.js',
47+
'integration-test/message-bridge-android.spec.js',
48+
],
4549
use: { injectName: 'android', platform: 'android', ...devices['Galaxy S5'] },
4650
},
4751
{

injected/scripts/entry-points.js

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const builds = {
3333
input: 'entry-points/android.js',
3434
output: ['../build/android/contentScope.js'],
3535
},
36+
'android-broker-protection': {
37+
input: 'entry-points/android',
38+
output: ['../build/android/brokerProtection.js'],
39+
},
3640
'android-autofill-password-import': {
3741
input: 'entry-points/android',
3842
output: ['../build/android/autofillPasswordImport.js'],

injected/src/features.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const otherFeatures = /** @type {const} */ ([
3333
export const platformSupport = {
3434
apple: ['webCompat', ...baseFeatures],
3535
'apple-isolated': ['duckPlayer', 'brokerProtection', 'performanceMetrics', 'clickToLoad', 'messageBridge'],
36-
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer'],
36+
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge'],
37+
'android-broker-protection': ['brokerProtection'],
3738
'android-autofill-password-import': ['autofillPasswordImport'],
3839
windows: ['cookie', ...baseFeatures, 'windowsPermissionUsage', 'duckPlayer', 'brokerProtection', 'breakageReporting'],
3940
firefox: ['cookie', ...baseFeatures, 'clickToLoad'],

injected/src/features/broker-protection/actions/click.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export function click(action, userData, root = document) {
5151
const elements = getElements(rootElement, element.selector);
5252

5353
if (!elements?.length) {
54+
if (element.failSilently) {
55+
return new SuccessResponse({ actionID: action.id, actionType: action.actionType, response: null });
56+
}
57+
5458
return new ErrorResponse({
5559
actionID: action.id,
5660
message: `could not find element to click with selector '${element.selector}'!`,
@@ -63,7 +67,7 @@ export function click(action, userData, root = document) {
6367
const elem = elements[i];
6468

6569
if ('disabled' in elem) {
66-
if (elem.disabled) {
70+
if (elem.disabled && !element.failSilently) {
6771
return new ErrorResponse({ actionID: action.id, message: `could not click disabled element ${element.selector}'!` });
6872
}
6973
}

injected/src/features/broker-protection/actions/extract.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,26 @@ export function createProfile(elementFactory, extractData) {
160160
}
161161

162162
/**
163-
* @param {{innerText: string}[]} elements
163+
* @param {({ textContent: string } | { innerText: string })[]} elements
164164
* @param {string} key
165165
* @param {ExtractProfileProperty} extractField
166166
* @return {string[]}
167167
*/
168-
function stringValuesFromElements(elements, key, extractField) {
168+
export function stringValuesFromElements(elements, key, extractField) {
169169
return elements.map((element) => {
170-
// todo: should we use textContent here?
171-
let elementValue = rules[key]?.(element) ?? element?.innerText ?? null;
170+
let elementValue;
171+
172+
if ('innerText' in element) {
173+
elementValue = rules[key]?.(element) ?? element?.innerText ?? null;
174+
175+
// In instances where we use the text() node test, innerText will be undefined, and we fall back to textContent
176+
} else if ('textContent' in element) {
177+
elementValue = rules[key]?.(element) ?? element?.textContent ?? null;
178+
}
179+
180+
if (!elementValue) {
181+
return elementValue;
182+
}
172183

173184
if (extractField?.afterText) {
174185
elementValue = elementValue?.split(extractField.afterText)[1]?.trim() || elementValue;

injected/src/features/broker-protection/extractors/address.js

+3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ function getCityStateCombos(inputList) {
5353
// Strip out the zip code since we're only interested in city/state here.
5454
item = item.replace(/,?\s*\d{5}(-\d{4})?/, '');
5555

56+
// Replace any commas at the end of the string that could confuse the city/state split.
57+
item = item.replace(/,$/, '');
58+
5659
if (item.includes(',')) {
5760
words = item.split(',').map((item) => item.trim());
5861
} else {

injected/src/globals.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface ImportMeta {
2020
| 'integration'
2121
| 'chrome-mv3'
2222
| 'chrome'
23+
| 'android-broker-protection'
2324
| 'android-autofill-password-import';
2425
trackerLookup?: Record<string, unknown>;
2526
pageName?: string;

0 commit comments

Comments
 (0)