Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"readme": "This config tests that uaChBrands can override navigator.userAgentData.brands",
"version": 1,
"unprotectedTemporary": [],
"features": {
"uaChBrands": {
"state": "enabled",
"exceptions": [],
"settings": {
"brands": [
{
"brand": "Chromium",
"version": "140"
},
{
"brand": "DuckDuckGo",
"version": "140"
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"readme": "This config tests that uaChBrands feature returns early when brands setting is missing (feature is enabled but settings.brands is undefined)",
"version": 1,
"unprotectedTemporary": [],
"features": {
"uaChBrands": {
"state": "enabled",
"exceptions": [],
"settings": {
}
}
}
}
18 changes: 18 additions & 0 deletions injected/integration-test/test-pages/ua-ch-brands/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>UA CH Brands</title>
<link rel="stylesheet" href="../shared/style.css">
</head>
<body>
<p><a href="../index.html">[Home]</a></p>

<p>UA CH Brands</p>
<ul>
<li><a href="./pages/brand-override.html">Brand Override</a> - <a href="./config/brand-override.json">Config</a></li>
<li><a href="./pages/brands-missing.html">Brands Missing</a> - <a href="./config/brands-missing.json">Config</a></li>
</ul>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>UA CH Brands - Brand Override</title>
<link rel="stylesheet" href="../../shared/style.css">
</head>
<body>
<script src="../../shared/utils.js"></script>
<p><a href="../index.html">[UA CH Brands]</a></p>

<p>This page verifies that navigator.userAgentData.brands uses the configured brands from the uaChBrands feature settings</p>

<script>
test('Brand override', async () => {
const results = [];

if (navigator.userAgentData && navigator.userAgentData.brands) {
const brands = navigator.userAgentData.brands;
const brandNames = new Set(brands.map(b => b.brand));

const hasGrease = brands.some(b => {
const name = b.brand;
return name.trim().startsWith('Not') || /[^\w\s.]/.test(name);
});

results.push({
name: 'contains Chromium brand',
result: brandNames.has('Chromium'),
expected: true
});

results.push({
name: 'contains DuckDuckGo brand',
result: brandNames.has('DuckDuckGo'),
expected: true
});

// Expect 3 brands if GREASE present, 2 if not
const expectedBrandCount = hasGrease ? 3 : 2;
results.push({
name: `has correct number of brands (${hasGrease ? 'with GREASE' : 'without GREASE'})`,
result: brands.length,
expected: expectedBrandCount
});

if (hasGrease) {
results.push({
name: 'GREASE value is preserved',
result: hasGrease,
expected: true
});
}

results.push({
name: 'does not contain Microsoft Edge brand',
result: !brandNames.has('Microsoft Edge'),
expected: true
});

results.push({
name: 'does not contain Microsoft Edge WebView2 brand',
result: !brandNames.has('Microsoft Edge WebView2'),
expected: true
});

if (navigator.userAgentData.getHighEntropyValues) {
try {
const highEntropyValues = await navigator.userAgentData.getHighEntropyValues(['brands']);
if (highEntropyValues.brands) {
const heBrands = highEntropyValues.brands;
const heBrandNames = new Set(heBrands.map(b => b.brand));

const heHasGrease = heBrands.some(b => {
const name = b.brand;
return name.trim().startsWith('Not') || /[^\w\s.]/.test(name);
});

results.push({
name: 'high entropy contains Chromium',
result: heBrandNames.has('Chromium'),
expected: true
});
results.push({
name: 'high entropy contains DuckDuckGo',
result: heBrandNames.has('DuckDuckGo'),
expected: true
});

if (heHasGrease) {
results.push({
name: 'high entropy GREASE value is preserved',
result: heHasGrease,
expected: true
});
}

results.push({
name: 'high entropy does not contain Microsoft Edge',
result: !heBrandNames.has('Microsoft Edge'),
expected: true
});

results.push({
name: 'high entropy does not contain Microsoft Edge WebView2',
result: !heBrandNames.has('Microsoft Edge WebView2'),
expected: true
});
}
} catch (error) {
results.push({
name: 'getHighEntropyValues works',
result: false,
expected: true
});
}
}
} else {
results.push({
name: 'navigator.userAgentData.brands available',
result: false,
expected: true
});
}

return results;
});

renderResults();
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>UA CH Brands - Brands Missing</title>
<link rel="stylesheet" href="../../shared/style.css">
</head>
<body>
<script src="../../shared/utils.js"></script>
<p><a href="../index.html">[UA CH Brands]</a></p>

<p>This page verifies that when brands setting is missing/null/empty, the feature returns early and original brands are preserved</p>

<script>
test('Feature returns early when brands missing', async () => {
const results = [];

if (navigator.userAgentData && navigator.userAgentData.brands) {
// When brands setting is missing, feature returns early in init()

const brands = navigator.userAgentData.brands;

// Verify brands still works normally
results.push({
name: 'brands array exists and has content',
result: Array.isArray(brands) && brands.length > 0,
expected: true
});

// Verify getHighEntropyValues also works and returns matching values
if (navigator.userAgentData.getHighEntropyValues) {
try {
const highEntropyValues = await navigator.userAgentData.getHighEntropyValues(['brands']);
const heBrands = highEntropyValues.brands;

if (heBrands) {
const brandsStr = JSON.stringify(brands);
const heBrandsStr = JSON.stringify(heBrands);

results.push({
name: 'getHighEntropyValues and direct access return matching values',
result: brandsStr === heBrandsStr,
expected: true
});
}
} catch (error) {
results.push({
name: 'getHighEntropyValues works',
result: false,
expected: true
});
}
}
} else {
results.push({
name: 'navigator.userAgentData.brands available',
result: false,
expected: true
});
}

return results;
});

renderResults();
</script>
</body>
</html>
36 changes: 36 additions & 0 deletions injected/integration-test/ua-ch-brands.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test, expect } from '@playwright/test';
import { ResultsCollector } from './page-objects/results-collector.js';

test('UA CH Brands override', async ({ page }, testInfo) => {
const collector = ResultsCollector.create(page, testInfo.project.use);
await collector.load(
'/ua-ch-brands/pages/brand-override.html',
'./integration-test/test-pages/ua-ch-brands/config/brand-override.json',
);
const results = await collector.results();

for (const key in results) {
for (const result of results[key]) {
await test.step(`${key}: ${result.name}`, () => {
expect(result.result).toEqual(result.expected);
});
}
}
});

test('UA CH Brands when brands missing', async ({ page }, testInfo) => {
const collector = ResultsCollector.create(page, testInfo.project.use);
await collector.load(
'/ua-ch-brands/pages/brands-missing.html',
'./integration-test/test-pages/ua-ch-brands/config/brands-missing.json',
);
const results = await collector.results();

for (const key in results) {
for (const result of results[key]) {
await test.step(`${key}: ${result.name}`, () => {
expect(result.result).toEqual(result.expected);
});
}
}
});
1 change: 1 addition & 0 deletions injected/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default defineConfig({
'integration-test/duckplayer-remote-config.spec.js',
'integration-test/harmful-apis.spec.js',
'integration-test/windows-permissions.spec.js',
'integration-test/ua-ch-brands.spec.js',
'integration-test/broker-protection-tests/**/*.spec.js',
'integration-test/breakage-reporting.spec.js',
'integration-test/duck-ai-data-clearing.spec.js',
Expand Down
2 changes: 2 additions & 0 deletions injected/src/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const otherFeatures = /** @type {const} */ ([
'harmfulApis',
'webCompat',
'windowsPermissionUsage',
'uaChBrands',
'brokerProtection',
'performanceMetrics',
'breakageReporting',
Expand Down Expand Up @@ -68,6 +69,7 @@ export const platformSupport = {
...baseFeatures,
'webTelemetry',
'windowsPermissionUsage',
'uaChBrands',
'duckPlayer',
'brokerProtection',
'breakageReporting',
Expand Down
Loading