Skip to content

Commit fb7d436

Browse files
committed
Add new Theme Color content feature
1 parent f8c5a43 commit fb7d436

File tree

14 files changed

+272
-4
lines changed

14 files changed

+272
-4
lines changed

injected/docs/theme-color.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
title: Theme Color Monitor
3+
---
4+
5+
# Theme Color Monitor
6+
7+
Reports the presence of the theme-color meta tag on page load.
8+
9+
The theme-color meta tag is used by browsers to customize the UI color to match the website's branding. This feature reports the theme color value when it's found on initial page load.
10+
11+
## Notifications
12+
13+
### `themeColorFound`
14+
- {@link "Theme Color Messages".ThemeColorFoundNotification}
15+
- Sends initial theme color value on page load
16+
- If no theme-color meta tag is found, the themeColor value will be null
17+
18+
**Example**
19+
20+
```json
21+
{
22+
"themeColor": "#ff0000",
23+
"documentUrl": "https://example.com"
24+
}
25+
```
26+
27+
## Remote Config
28+
29+
### Enabled (default)
30+
{@includeCode ../integration-test/test-pages/theme-color/config/theme-color-enabled.json}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"features": {
3+
4+
},
5+
"unprotectedTemporary": []
6+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"features": {
3+
"themeColor": {
4+
"state": "disabled",
5+
"exceptions": []
6+
}
7+
},
8+
"unprotectedTemporary": []
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"features": {
3+
"themeColor": {
4+
"state": "enabled",
5+
"exceptions": []
6+
}
7+
},
8+
"unprotectedTemporary": []
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Theme Color Test</title>
7+
<meta name="theme-color" content="#ff0000">
8+
</head>
9+
<body>
10+
</body>
11+
</html>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Theme Color Media Queries Test</title>
7+
<meta name="theme-color" content="#ff0000">
8+
<meta name="theme-color" content="#00ff00" media="(min-width: 600px)">
9+
<meta name="theme-color" content="#0000ff" media="(prefers-color-scheme: dark)">
10+
</head>
11+
<body>
12+
</body>
13+
</html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>No Theme Color Test</title>
7+
</head>
8+
<body>
9+
</body>
10+
</html>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { test, expect } from '@playwright/test';
2+
import { ResultsCollector } from './page-objects/results-collector.js';
3+
4+
const HTML = '/theme-color/index.html';
5+
const CONFIG = './integration-test/test-pages/theme-color/config/theme-color-enabled.json';
6+
7+
test('theme-color feature absent', async ({ page }, testInfo) => {
8+
const CONFIG = './integration-test/test-pages/theme-color/config/theme-color-absent.json';
9+
const themeColor = ResultsCollector.create(page, testInfo.project.use);
10+
await themeColor.load(HTML, CONFIG);
11+
12+
const messages = await themeColor.waitForMessage('themeColorFound', 1);
13+
14+
expect(messages[0].payload.params).toStrictEqual({
15+
themeColor: '#ff0000',
16+
documentUrl: 'http://localhost:3220/theme-color/index.html',
17+
});
18+
});
19+
20+
test('theme-color (no theme color)', async ({ page }, testInfo) => {
21+
const HTML = '/theme-color/no-theme-color.html';
22+
const themeColor = ResultsCollector.create(page, testInfo.project.use);
23+
await themeColor.load(HTML, CONFIG);
24+
25+
const messages = await themeColor.waitForMessage('themeColorFound', 1);
26+
27+
expect(messages[0].payload.params).toStrictEqual({
28+
themeColor: null,
29+
documentUrl: 'http://localhost:3220/theme-color/no-theme-color.html',
30+
});
31+
});
32+
33+
test('theme-color (viewport media query)', async ({ page }, testInfo) => {
34+
// Use a desktop viewport
35+
await page.setViewportSize({ width: 1280, height: 720 });
36+
37+
const HTML = '/theme-color/media-queries.html';
38+
const themeColor = ResultsCollector.create(page, testInfo.project.use);
39+
await themeColor.load(HTML, CONFIG);
40+
41+
const messages = await themeColor.waitForMessage('themeColorFound', 1);
42+
43+
expect(messages[0].payload.params).toStrictEqual({
44+
themeColor: '#00ff00',
45+
documentUrl: 'http://localhost:3220/theme-color/media-queries.html',
46+
});
47+
});
48+
49+
test('theme-color (color scheme media query)', async ({ page }, testInfo) => {
50+
// Use a dark color scheme
51+
await page.emulateMedia({ colorScheme: 'dark' });
52+
53+
const HTML = '/theme-color/media-queries.html';
54+
const themeColor = ResultsCollector.create(page, testInfo.project.use);
55+
await themeColor.load(HTML, CONFIG);
56+
57+
const messages = await themeColor.waitForMessage('themeColorFound', 1);
58+
59+
expect(messages[0].payload.params).toStrictEqual({
60+
themeColor: '#0000ff',
61+
documentUrl: 'http://localhost:3220/theme-color/media-queries.html',
62+
});
63+
});
64+
65+
test('theme-color feature disabled completely', async ({ page }, testInfo) => {
66+
const CONFIG = './integration-test/test-pages/theme-color/config/theme-color-disabled.json';
67+
const themeColor = ResultsCollector.create(page, testInfo.project.use);
68+
await themeColor.load(HTML, CONFIG);
69+
70+
// this is here purely to guard against a false positive in this test.
71+
// without this manual `wait`, it might be possible for the following assertion to
72+
// pass, but just because it was too quick (eg: the first message wasn't sent yet)
73+
await page.waitForTimeout(100);
74+
75+
const messages = await themeColor.outgoingMessages();
76+
expect(messages).toHaveLength(0);
77+
});

injected/playwright.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export default defineConfig({
3737
},
3838
{
3939
name: 'ios',
40-
testMatch: ['integration-test/duckplayer-mobile.spec.js', 'integration-test/duckplayer-mobile-drawer.spec.js'],
40+
testMatch: [
41+
'integration-test/duckplayer-mobile.spec.js',
42+
'integration-test/duckplayer-mobile-drawer.spec.js',
43+
'integration-test/theme-color.spec.js',
44+
],
4145
use: { injectName: 'apple-isolated', platform: 'ios', ...devices['iPhone 13'] },
4246
},
4347
{
@@ -47,6 +51,7 @@ export default defineConfig({
4751
'integration-test/duckplayer-mobile-drawer.spec.js',
4852
'integration-test/web-compat-android.spec.js',
4953
'integration-test/message-bridge-android.spec.js',
54+
'integration-test/theme-color.spec.js',
5055
],
5156
use: { injectName: 'android', platform: 'android', ...devices['Galaxy S5'] },
5257
},

injected/src/features.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ const otherFeatures = /** @type {const} */ ([
2727
'breakageReporting',
2828
'autofillPasswordImport',
2929
'favicon',
30+
'themeColor',
3031
]);
3132

3233
/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
3334
/** @type {Record<string, FeatureName[]>} */
3435
export const platformSupport = {
3536
apple: ['webCompat', ...baseFeatures],
36-
'apple-isolated': ['duckPlayer', 'brokerProtection', 'performanceMetrics', 'clickToLoad', 'messageBridge', 'favicon'],
37-
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge'],
37+
'apple-isolated': ['duckPlayer', 'brokerProtection', 'performanceMetrics', 'clickToLoad', 'messageBridge', 'favicon', 'themeColor'],
38+
android: [...baseFeatures, 'webCompat', 'breakageReporting', 'duckPlayer', 'messageBridge', 'themeColor'],
3839
'android-broker-protection': ['brokerProtection'],
3940
'android-autofill-password-import': ['autofillPasswordImport'],
4041
windows: ['cookie', ...baseFeatures, 'windowsPermissionUsage', 'duckPlayer', 'brokerProtection', 'breakageReporting'],

0 commit comments

Comments
 (0)