-
Notifications
You must be signed in to change notification settings - Fork 31
Add new Theme Color content feature #1609
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| --- | ||
| title: Theme Color Monitor | ||
| --- | ||
|
|
||
| # Theme Color Monitor | ||
|
|
||
| Reports the presence of the theme-color meta tag on page load. | ||
|
|
||
| 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. | ||
|
|
||
| ## Notifications | ||
|
|
||
| ### `themeColorStatus` | ||
| - {@link "ThemeColor Messages".ThemeColorStatusNotification} | ||
| - Sends initial theme color value on page load | ||
| - If no theme-color meta tag is found, the themeColor value will be null | ||
|
|
||
| **Example** | ||
|
|
||
| ```json | ||
| { | ||
| "themeColor": "#ff0000", | ||
| "documentUrl": "https://example.com" | ||
| } | ||
| ``` | ||
|
|
||
| ## Remote Config | ||
|
|
||
| ### Enabled (default) | ||
| {@includeCode ../integration-test/test-pages/theme-color/config/theme-color-enabled.json} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| { | ||
| "features": { | ||
|
|
||
| "themeColor": { | ||
| "state": "disabled", | ||
| "exceptions": [] | ||
| } | ||
| }, | ||
| "unprotectedTemporary": [] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,10 @@ | |
| "favicon": { | ||
| "state": "disabled", | ||
| "exceptions": [] | ||
| }, | ||
| "themeColor": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was going to ask why this needs to be added in all other tests but then I found this https://github.com/duckduckgo/content-scope-scripts/pull/1561/files#r2007736395 . Let's follow up on this with @shakyShane when he's back as this means new features such as this will have ever larger PRs. |
||
| "state": "disabled", | ||
| "exceptions": [] | ||
| } | ||
| }, | ||
| "unprotectedTemporary": [] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "features": { | ||
|
|
||
| }, | ||
| "unprotectedTemporary": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "features": { | ||
| "themeColor": { | ||
| "state": "disabled", | ||
| "exceptions": [] | ||
| } | ||
| }, | ||
| "unprotectedTemporary": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "features": { | ||
| "themeColor": { | ||
| "state": "enabled", | ||
| "exceptions": [] | ||
| } | ||
| }, | ||
| "unprotectedTemporary": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| <title>Theme Color Test</title> | ||
| <meta name="theme-color" content="#ff0000"> | ||
| </head> | ||
| <body> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| <title>Theme Color Media Queries Test</title> | ||
| <meta name="theme-color" content="#ff0000"> | ||
| <meta name="theme-color" content="#00ff00" media="(min-width: 600px)"> | ||
| <meta name="theme-color" content="#0000ff" media="(prefers-color-scheme: dark)"> | ||
| </head> | ||
| <body> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| <title>No Theme Color Test</title> | ||
| </head> | ||
| <body> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { test, expect } from '@playwright/test'; | ||
| import { ResultsCollector } from './page-objects/results-collector.js'; | ||
|
|
||
| const HTML = '/theme-color/index.html'; | ||
| const CONFIG = './integration-test/test-pages/theme-color/config/theme-color-enabled.json'; | ||
|
|
||
| test('theme-color feature absent', async ({ page }, testInfo) => { | ||
| const CONFIG = './integration-test/test-pages/theme-color/config/theme-color-absent.json'; | ||
| const themeColor = ResultsCollector.create(page, testInfo.project.use); | ||
| await themeColor.load(HTML, CONFIG); | ||
|
|
||
| const messages = await themeColor.waitForMessage('themeColorStatus', 1); | ||
|
|
||
| expect(messages[0].payload.params).toStrictEqual({ | ||
| themeColor: '#ff0000', | ||
| documentUrl: 'http://localhost:3220/theme-color/index.html', | ||
| }); | ||
| }); | ||
|
|
||
| test('theme-color (no theme color)', async ({ page }, testInfo) => { | ||
| const HTML = '/theme-color/no-theme-color.html'; | ||
| const themeColor = ResultsCollector.create(page, testInfo.project.use); | ||
| await themeColor.load(HTML, CONFIG); | ||
|
|
||
| const messages = await themeColor.waitForMessage('themeColorStatus', 1); | ||
|
|
||
| expect(messages[0].payload.params).toStrictEqual({ | ||
| themeColor: null, | ||
| documentUrl: 'http://localhost:3220/theme-color/no-theme-color.html', | ||
| }); | ||
| }); | ||
|
|
||
| test('theme-color (viewport media query)', async ({ page }, testInfo) => { | ||
| // Use a desktop viewport | ||
| await page.setViewportSize({ width: 1280, height: 720 }); | ||
|
|
||
| const HTML = '/theme-color/media-queries.html'; | ||
| const themeColor = ResultsCollector.create(page, testInfo.project.use); | ||
| await themeColor.load(HTML, CONFIG); | ||
|
|
||
| const messages = await themeColor.waitForMessage('themeColorStatus', 1); | ||
|
|
||
| expect(messages[0].payload.params).toStrictEqual({ | ||
| themeColor: '#00ff00', | ||
| documentUrl: 'http://localhost:3220/theme-color/media-queries.html', | ||
| }); | ||
| }); | ||
|
|
||
| test('theme-color (color scheme media query)', async ({ page }, testInfo) => { | ||
| // Use a dark color scheme | ||
| await page.emulateMedia({ colorScheme: 'dark' }); | ||
|
|
||
| const HTML = '/theme-color/media-queries.html'; | ||
| const themeColor = ResultsCollector.create(page, testInfo.project.use); | ||
| await themeColor.load(HTML, CONFIG); | ||
|
|
||
| const messages = await themeColor.waitForMessage('themeColorStatus', 1); | ||
|
|
||
| expect(messages[0].payload.params).toStrictEqual({ | ||
| themeColor: '#0000ff', | ||
| documentUrl: 'http://localhost:3220/theme-color/media-queries.html', | ||
| }); | ||
| }); | ||
|
|
||
| test('theme-color feature disabled completely', async ({ page }, testInfo) => { | ||
| const CONFIG = './integration-test/test-pages/theme-color/config/theme-color-disabled.json'; | ||
| const themeColor = ResultsCollector.create(page, testInfo.project.use); | ||
| await themeColor.load(HTML, CONFIG); | ||
|
|
||
| // this is here purely to guard against a false positive in this test. | ||
| // without this manual `wait`, it might be possible for the following assertion to | ||
| // pass, but just because it was too quick (eg: the first message wasn't sent yet) | ||
| await page.waitForTimeout(100); | ||
|
|
||
| const messages = await themeColor.outgoingMessages(); | ||
| expect(messages).toHaveLength(0); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import ContentFeature from '../content-feature.js'; | ||
| import { isBeingFramed } from '../utils.js'; | ||
|
|
||
| export class ThemeColor extends ContentFeature { | ||
| init() { | ||
| /** | ||
| * This feature never operates in a frame | ||
| */ | ||
| if (isBeingFramed()) return; | ||
|
|
||
| window.addEventListener('DOMContentLoaded', () => { | ||
| // send once, immediately | ||
| this.send(); | ||
| }); | ||
| } | ||
|
|
||
| send() { | ||
| const themeColor = getThemeColor(); | ||
| this.notify('themeColorStatus', { themeColor, documentUrl: document.URL }); | ||
| } | ||
| } | ||
|
|
||
| export default ThemeColor; | ||
|
|
||
| /** | ||
| * Gets current theme color considering media queries | ||
| * Follows browser behavior by returning the last matching meta tag in document order | ||
| * @returns {string|null} The theme color value or null if not found | ||
| */ | ||
| function getThemeColor() { | ||
| const metaTags = document.head.querySelectorAll('meta[name="theme-color"]'); | ||
| if (metaTags.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| let lastMatchingTag = null; | ||
| for (const meta of metaTags) { | ||
| const mediaAttr = meta.getAttribute('media'); | ||
| if (!mediaAttr || window.matchMedia(mediaAttr).matches) { | ||
| lastMatchingTag = meta; | ||
| } | ||
| } | ||
| return lastMatchingTag ? lastMatchingTag.getAttribute('content') : null; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "$schema": "http://json-schema.org/draft-07/schema#", | ||
| "type": "object", | ||
| "title": "themeColorStatus", | ||
| "required": ["themeColor", "documentUrl"], | ||
| "properties": { | ||
| "themeColor": { | ||
| "type": ["string", "null"], | ||
| "description": "The theme color value, or null if not present" | ||
| }, | ||
| "documentUrl": { | ||
| "type": "string", | ||
| "description": "The URL of the document" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| /** | ||
| * These types are auto-generated from schema files. | ||
| * scripts/build-types.mjs is responsible for type generation. | ||
| * **DO NOT** edit this file directly as your changes will be lost. | ||
| * | ||
| * @module ThemeColor Messages | ||
| */ | ||
|
|
||
| /** | ||
| * Requests, Notifications and Subscriptions from the ThemeColor feature | ||
| */ | ||
| export interface ThemeColorMessages { | ||
| notifications: ThemeColorStatusNotification; | ||
| } | ||
| /** | ||
| * Generated from @see "../messages/theme-color/themeColorStatus.notify.json" | ||
| */ | ||
| export interface ThemeColorStatusNotification { | ||
| method: "themeColorStatus"; | ||
| params: ThemeColorStatus; | ||
| } | ||
| export interface ThemeColorStatus { | ||
| /** | ||
| * The theme color value, or null if not present | ||
| */ | ||
| themeColor: string | null; | ||
| /** | ||
| * The URL of the document | ||
| */ | ||
| documentUrl: string; | ||
| } | ||
|
|
||
| declare module "../features/theme-color.js" { | ||
| export interface ThemeColor { | ||
| notify: import("@duckduckgo/messaging/lib/shared-types").MessagingBase<ThemeColorMessages>['notify'] | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.