Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions special-pages/pages/special-error/app/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ function PageTitle() {

export function App() {
const { messaging } = useMessaging();
const { isDarkMode } = useEnv();

/**
* @param {Error} error
Expand All @@ -60,7 +59,7 @@ export function App() {
}

return (
<main className={styles.main} data-theme={isDarkMode ? 'dark' : 'light'}>
<main className={styles.main}>
<PageTitle />
<ErrorBoundary didCatch={({ error }) => didCatch(error)} fallback={<ErrorFallback />}>
<SpecialErrorView />
Expand Down
25 changes: 15 additions & 10 deletions special-pages/pages/special-error/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Components } from './components/Components.jsx';
import enStrings from '../public/locales/en/special-error.json';
import { TranslationProvider } from '../../../shared/components/TranslationsProvider.js';
import { MessagingProvider } from './providers/MessagingProvider.js';
import { ThemeProvider } from './providers/ThemeProvider.js';
import { SettingsProvider } from './providers/SettingsProvider.jsx';
import { SpecialErrorProvider } from './providers/SpecialErrorProvider.js';
import { callWithRetry } from '../../../shared/call-with-retry.js';
Expand Down Expand Up @@ -65,11 +66,13 @@ export async function init(messaging, baseEnvironment) {
<UpdateEnvironment search={window.location.search} />
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
<MessagingProvider messaging={messaging}>
<SettingsProvider settings={settings}>
<SpecialErrorProvider specialError={specialError}>
<App />
</SpecialErrorProvider>
</SettingsProvider>
<ThemeProvider initialTheme={init.theme} initialThemeVariant={init.themeVariant}>
<SettingsProvider settings={settings}>
<SpecialErrorProvider specialError={specialError}>
<App />
</SpecialErrorProvider>
</SettingsProvider>
</ThemeProvider>
</MessagingProvider>
</TranslationProvider>
</EnvironmentProvider>,
Expand All @@ -79,11 +82,13 @@ export async function init(messaging, baseEnvironment) {
render(
<EnvironmentProvider debugState={false} injectName={environment.injectName}>
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
<SettingsProvider settings={settings}>
<SpecialErrorProvider specialError={specialError}>
<Components />
</SpecialErrorProvider>
</SettingsProvider>
<ThemeProvider initialTheme={init.theme} initialThemeVariant={init.themeVariant}>
<SettingsProvider settings={settings}>
<SpecialErrorProvider specialError={specialError}>
<Components />
</SpecialErrorProvider>
</SettingsProvider>
</ThemeProvider>
</TranslationProvider>
</EnvironmentProvider>,
root,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { createContext, h } from 'preact';
import { useContext, useEffect, useState } from 'preact/hooks';
import { useMessaging } from './MessagingProvider.js';
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';

/**
* @typedef {import('../../types/special-error').BrowserTheme} BrowserTheme
* @typedef {import('../../types/special-error').ThemeVariant} ThemeVariant
*/

const ThemeContext = createContext({
/** @type {BrowserTheme} */
theme: 'light',
/** @type {ThemeVariant} */
themeVariant: 'default',
});

/**
* @param {object} props
* @param {import('preact').ComponentChild} props.children
* @param {BrowserTheme | undefined} props.initialTheme
* @param {ThemeVariant | undefined} props.initialThemeVariant
*/
export function ThemeProvider({ children, initialTheme, initialThemeVariant }) {
const { isDarkMode } = useEnv();
const { messaging } = useMessaging();

// Track explicit theme updates from onThemeUpdate subscription
const [explicitTheme, setExplicitTheme] = useState(/** @type {BrowserTheme | undefined} */ (undefined));
const [explicitThemeVariant, setExplicitThemeVariant] = useState(/** @type {ThemeVariant | undefined} */ (undefined));

useEffect(() => {
if (!messaging) return;
const unsubscribe = messaging.onThemeUpdate((data) => {
setExplicitTheme(data.theme);
setExplicitThemeVariant(data.themeVariant);
});
return unsubscribe;
}, [messaging]);

// Derive theme from explicit updates, initial theme, or system preference (in that order)
const theme = explicitTheme ?? initialTheme ?? (isDarkMode ? 'dark' : 'light');
const themeVariant = explicitThemeVariant ?? initialThemeVariant ?? 'default';

// Sync theme attributes to <body>
useEffect(() => {
document.body.dataset.theme = theme;
}, [theme]);
useEffect(() => {
document.body.dataset.themeVariant = themeVariant;
}, [themeVariant]);

return <ThemeContext.Provider value={{ theme, themeVariant }}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
return useContext(ThemeContext);
}
68 changes: 52 additions & 16 deletions special-pages/pages/special-error/app/styles/variables.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
:root {
/* Light theme colors */
--theme-background-color: var(--color-gray-20);
--default-light-background-color: var(--color-gray-20);
--default-dark-background-color: var(--color-gray-85);
}

body[data-theme='light'] {
--theme-background-color: var(--default-light-background-color);
--theme-text-primary-color: var(--color-black-at-84);
--link-color: var(--color-black);
--border-color: rgba(0, 0, 0, 0.1);
Expand All @@ -11,23 +15,55 @@
--visit-site-color: var(--color-black);
}

@media (prefers-color-scheme: dark) {
:root {
/* Dark theme colors */
--theme-background-color: var(--color-gray-85);
--theme-text-primary-color: var(--color-white-at-84);
--link-color: var(--color-gray-40);
--border-color: var(--color-white-at-18);
body[data-theme='dark'] {
--theme-background-color: var(--default-dark-background-color);
--theme-text-primary-color: var(--color-white-at-84);
--link-color: var(--color-gray-40);
--border-color: var(--color-white-at-18);

--container-bg: var(--color-gray-90);
--advanced-info-bg: #2f2f2f;

--visit-site-color: var(--color-gray-40);
}

body[data-theme='dark'][data-platform-name='ios'][data-theme-variant='default'] {
--theme-background-color: #222;
}

/* TODO: Use colour variables from design-tokens */

--container-bg: var(--color-gray-90);
--advanced-info-bg: #2f2f2f;
body[data-theme-variant='coolGray'] {
--default-light-background-color: #d2d5e3;
--default-dark-background-color: #2b2f45;
}

body[data-theme-variant='slateBlue'] {
--default-light-background-color: #d2e5f3;
--default-dark-background-color: #1e3347;
}

body[data-theme-variant='green'] {
--default-light-background-color: #e3eee1;
--default-dark-background-color: #203b30;
}

--visit-site-color: var(--color-gray-40);
}
body[data-theme-variant='violet'] {
--default-light-background-color: #e7e4f5;
--default-dark-background-color: #2e2158;
}

[data-platform-name="ios"] {
--theme-background-color: #222;
}
body[data-theme-variant='rose'] {
--default-light-background-color: #f8ebf5;
--default-dark-background-color: #5b194b;
}

body[data-theme-variant='orange'] {
--default-light-background-color: #fcedd8;
--default-dark-background-color: #54240c;
}

body[data-theme-variant='desert'] {
--default-light-background-color: #eee9e1;
--default-dark-background-color: #3c3833;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { test } from '@playwright/test';
import { SpecialErrorPage } from './special-error.js';

test.describe('special-error theme and theme variants', () => {
test('setting theme = dark and themeVariant via initialSetup', async ({ page }, workerInfo) => {
const sp = SpecialErrorPage.create(page, workerInfo);
await sp.openPage({ additional: { theme: 'dark', themeVariant: 'violet' } });
await sp.hasTheme('dark', 'violet');
await sp.hasBackgroundColor({ hex: '#2e2158' });
});

test('setting theme = light and themeVariant via initialSetup', async ({ page }, workerInfo) => {
const sp = SpecialErrorPage.create(page, workerInfo);
await sp.openPage({ additional: { theme: 'light', themeVariant: 'coolGray' } });
await sp.hasTheme('light', 'coolGray');
await sp.hasBackgroundColor({ hex: '#d2d5e3' });
});

test('light theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
const sp = SpecialErrorPage.create(page, workerInfo);
await sp.openPage();
await sp.hasTheme('light', 'default');
await sp.hasBackgroundColor({ hex: '#eeeeee' });
});

test('dark theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
const sp = SpecialErrorPage.create(page, workerInfo);
await sp.darkMode();
await sp.openPage();
await sp.hasTheme('dark', 'default');
const isIOS = sp.platform.name === 'ios'; // iOS has a different default background color
await sp.hasBackgroundColor({ hex: isIOS ? '#222222' : '#333333' });
});

test('changing theme to dark and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
const sp = SpecialErrorPage.create(page, workerInfo);
await sp.openPage({ additional: { theme: 'light', themeVariant: 'desert' } });
await sp.hasTheme('light', 'desert');
await sp.hasBackgroundColor({ hex: '#eee9e1' });
await sp.acceptsThemeUpdate('dark', 'slateBlue');
await sp.hasTheme('dark', 'slateBlue');
await sp.hasBackgroundColor({ hex: '#1e3347' });
});

test('changing theme to light and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
const sp = SpecialErrorPage.create(page, workerInfo);
await sp.openPage({ additional: { theme: 'dark', themeVariant: 'rose' } });
await sp.hasTheme('dark', 'rose');
await sp.hasBackgroundColor({ hex: '#5b194b' });
await sp.acceptsThemeUpdate('light', 'green');
await sp.hasTheme('light', 'green');
await sp.hasBackgroundColor({ hex: '#e3eee1' });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,16 @@ export class SpecialErrorPage {
* @param {keyof sampleData} [params.errorId] - ID of the error to be mocked (see sampleData.js)
* @param {PlatformInfo['name']} [params.platformName] - platform name
* @param {string} [params.locale] - locale
* @param {Record<string, any>} [params.additional] - Optional map of key/values to add to initialSetup
*/
async openPage({ env = 'app', willThrow = false, errorId = 'ssl.expired', platformName = this.platform.name, locale } = {}) {
async openPage({
env = 'app',
willThrow = false,
errorId = 'ssl.expired',
platformName = this.platform.name,
locale,
additional,
} = {}) {
if (platformName === 'extension') {
throw new Error(`Unsupported platform ${platformName}`);
}
Expand All @@ -67,6 +75,10 @@ export class SpecialErrorPage {
initialSetup.locale = locale;
}

if (additional) {
Object.assign(initialSetup, additional);
}

this.mocks.defaultResponses({
initialSetup,
});
Expand Down Expand Up @@ -396,4 +408,34 @@ export class SpecialErrorPage {
}),
);
}

/**
* @param {object} params
* @param {string} params.hex
* @returns {Promise<void>}
*/
async hasBackgroundColor({ hex }) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const rgb = `rgb(${[r, g, b].join(', ')})`;
await expect(this.page.locator('body')).toHaveCSS('background-color', rgb, { timeout: 50 });
}

/**
* @param {import('../types/special-error.ts').BrowserTheme} theme
* @param {import('../types/special-error.ts').ThemeVariant} themeVariant
*/
async acceptsThemeUpdate(theme, themeVariant) {
await this.mocks.simulateSubscriptionMessage('onThemeUpdate', { theme, themeVariant });
}

/**
* @param {import('../types/special-error.ts').BrowserTheme} theme
* @param {import('../types/special-error.ts').ThemeVariant} themeVariant
*/
async hasTheme(theme, themeVariant) {
await expect(this.page.locator('body')).toHaveAttribute('data-theme', theme);
await expect(this.page.locator('body')).toHaveAttribute('data-theme-variant', themeVariant);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@
"localeStrings": {
"type": "string",
"description": "Optional locale-specific strings"
},
"theme": {
"$ref": "./types/browser-theme.json"
},
"themeVariant": {
"$ref": "./types/theme-variant.json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["theme", "themeVariant"],
"properties": {
"theme": {
"$ref": "./types/browser-theme.json"
},
"themeVariant": {
"$ref": "./types/theme-variant.json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Browser Theme",
"enum": [
"light",
"dark"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Theme Variant",
"enum": [
"default",
"coolGray",
"slateBlue",
"green",
"violet",
"rose",
"orange",
"desert"
]
}
7 changes: 7 additions & 0 deletions special-pages/pages/special-error/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
<title>Error</title>
<meta name="robots" content="noindex,nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
/* Token is replaced by native browser so background isn't white while Special Error initializes */
--loading-color: $LOADING_COLOR$;
background: var(--loading-color);
}
</style>
<script src="./dist/inline.js"></script>
<link rel="stylesheet" href="./dist/index.css" />
</head>
Expand Down
Loading
Loading