mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
* fix(theme-color): emit media-scoped dark variant for iOS Safari (#7606) The theme-color meta only had a single light value rendered server-side; dark mode was applied purely by JS (skin_variants.ts) after page load. iOS Safari colors the address bar at parse time and does not reliably repaint when JS mutates the meta later, so dark-mode iPhone users kept a white address bar above a dark toolbar (the green Chromium Playwright test masked this because Chrome does honor the dynamic update). Emit a prefers-color-scheme media-scoped pair server-side so the correct color is chosen at first paint without JS: <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#485365" media="(prefers-color-scheme: dark)"> - Add SkinColors.darkToolbarColor() (reuses toolbarColorForTokens). - Expose enableDarkMode via getPublicSettings so the templates can gate the dark variant on it (no dark variant when dark mode can't be reached). - Apply to both pad.html and timeslider.html. - updateThemeColorMeta now updates every theme-color meta so a manual #options-darkmode toggle still wins over the media scoping on desktop/Android. - Backend + frontend tests updated to assert the media-scoped pair and the enableDarkMode-off case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(theme-color): prevent the light-mode flash on dark-OS load (#7606) The theme-color meta fix corrects the address-bar tint, but dark-OS users still saw the whole page painted light before the JS bundle ran and applied the dark skin classes in postAceInit — a visible flash on every browser, not just the mobile address bar. Add a tiny blocking inline script in <head>, before the stylesheet, that applies the dark skin classes to <html> synchronously during parse when the client is in dark mode (matchMedia + no localStorage white-mode override). The condition mirrors pad.ts's auto-switch, which still runs on init to wire up the #options-darkmode toggle and theme the editor iframes (those don't exist yet at parse time). Gated on the same enableDarkMode + colibris check as the dark theme-color variant. Applied to pad.html and timeslider.html. Verified in Chromium: at domcontentloaded a dark-OS client's <html> already carries super-dark-editor/dark-background/super-dark-toolbar (no flash), and a light-OS client is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note the dark-mode address-bar + flash fix (#7606) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(theme-color): guard pre-paint script on #skinvariantsbuilder; ignore updater state Address PR review: - Copilot: the inline pre-paint dark-mode script must skip the auto-dark switch on the #skinvariantsbuilder hash, matching pad.ts — otherwise it forces super-dark classes on a dark-OS client and fights the variants builder UI. Added the guard to pad.html and timeslider.html and a backend assertion so it can't regress. - Qodo: ignore var/update-state.json (runtime updater cache) so the server run that regenerates it can't dirty the tree or be committed accidentally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c8350782dd
commit
ef205bf2ac
9 changed files with 217 additions and 41 deletions
|
|
@ -27,6 +27,7 @@ A defence-in-depth pass across the API, token, export, and deployment surfaces:
|
|||
- **URL-encode pad names in the admin 'Open' button and recent pads (#7865 / #7895).** Pad names are `encodeURIComponent`-d in the admin `PadPage` Open href and the colibris recent-pads href, and `decodeURIComponent`-d when read back from the URL pathname; legacy URL-encoded recent-pads names are normalised before re-encoding to prevent double-encoding (`%2F` → `%252F`). The admin Open `window.open` gains `noopener,noreferrer`.
|
||||
- **OIDC — fix broken `OIDCAdapter` flows (#7837).** Repairs the adapter flows and widens the storage type to include `string` for the `userCode` index; adds regression tests.
|
||||
- **Accessibility — dialog titles/descriptions and a missing l10n key (#7835 / #7836).** Adds the `index.code` key referenced by `index.html` but never defined (which produced a "Couldn't find translation key" console error on the landing page), and gives every admin `@radix-ui/react-dialog` `Dialog.Content` a `Dialog.Title` and `Dialog.Description` (visually hidden where there's no visible heading), silencing Radix's a11y warnings. A new backend spec fails CI if any `data-l10n-id` in `src/templates/*.html` is missing from `en.json`.
|
||||
- **Dark mode — fix the white address bar and the light-flash on load (#7909, issue #7606).** Dark-mode users still saw a white mobile address bar above the dark toolbar, and the whole page flashed light before going dark. Both came from rendering the light state server-side and switching to dark only after the JS bundle ran: iOS Safari reads `theme-color` at parse time and doesn't reliably repaint on a later JS mutation, and the page painted light before the bundle applied the dark skin classes. The server now emits a `prefers-color-scheme`-scoped `theme-color` pair so the address bar is correct at first paint, plus a small blocking `<head>` script that applies the dark skin classes before the stylesheet paints. Both are gated on `enableDarkMode` (default on) and the colibris skin; `pad.ts` still runs on init to wire up the `#options-darkmode` toggle (which now updates every `theme-color` meta) and theme the editor iframes. Applies to the pad and timeslider views.
|
||||
|
||||
### Internal / contributor-facing
|
||||
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ export type SettingsType = {
|
|||
from: string | null;
|
||||
auth: {user: string; pass: string} | null;
|
||||
},
|
||||
getPublicSettings: () => Pick<SettingsType, "title" | "skinVariants"|"randomVersionString"|"skinName"|"toolbar"| "exposeVersion"| "gitVersion" | "enablePadWideSettings" | "enablePluginPadOptions" | "privacyBanner">,
|
||||
getPublicSettings: () => Pick<SettingsType, "title" | "skinVariants"|"randomVersionString"|"skinName"|"toolbar"| "exposeVersion"| "gitVersion" | "enableDarkMode" | "enablePadWideSettings" | "enablePluginPadOptions" | "privacyBanner">,
|
||||
}
|
||||
|
||||
const settings: SettingsType = {
|
||||
|
|
@ -871,6 +871,9 @@ const settings: SettingsType = {
|
|||
title: settings.title,
|
||||
skinName: settings.skinName,
|
||||
skinVariants: settings.skinVariants,
|
||||
// Needed so pad.html / timeslider.html only emit the dark theme-color
|
||||
// variant when dark mode can actually be reached client-side (#7606).
|
||||
enableDarkMode: settings.enableDarkMode,
|
||||
enablePadWideSettings: settings.enablePadWideSettings,
|
||||
enablePluginPadOptions: settings.enablePluginPadOptions,
|
||||
privacyBanner: getPublicPrivacyBanner(),
|
||||
|
|
|
|||
|
|
@ -14,3 +14,18 @@ export const configuredToolbarColor = (
|
|||
if (skinName !== 'colibris') return null;
|
||||
return toolbarColorForTokens((skinVariants || '').split(/\s+/).filter(Boolean));
|
||||
};
|
||||
|
||||
// The toolbar color colibris auto-switches to on a dark-OS client (pad.ts
|
||||
// forces 'super-dark-toolbar' when enableDarkMode is on and the system is in
|
||||
// dark mode). Templates emit this in a `media="(prefers-color-scheme: dark)"`
|
||||
// theme-color meta so iOS Safari — which colors the address bar at parse time
|
||||
// and does not reliably repaint when JS mutates the tag later — picks the
|
||||
// right color at first paint instead of staying on the light baseline
|
||||
// (issue #7606). Returns null for non-colibris skins, matching
|
||||
// configuredToolbarColor.
|
||||
export const darkToolbarColor = (
|
||||
skinName: string | undefined | null,
|
||||
): string | null => {
|
||||
if (skinName !== 'colibris') return null;
|
||||
return toolbarColorForTokens(['super-dark-toolbar']);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,19 +7,21 @@ const containers = ['editor', 'background', 'toolbar'];
|
|||
const colors = ['super-light', 'light', 'dark', 'super-dark'];
|
||||
|
||||
// Keep <meta name="theme-color"> in sync with the toolbar the user actually
|
||||
// sees. The server emits a baseline derived from settings.skinVariants, but
|
||||
// pad.ts may flip the toolbar to super-dark on first paint (enableDarkMode
|
||||
// + prefers-color-scheme:dark + no localStorage white-mode override) and
|
||||
// the user can toggle via #options-darkmode. Without this, dark-mode users
|
||||
// keep the light meta and see a white address bar above a dark toolbar
|
||||
// (issue #7606 follow-up). Color resolution lives in skin_toolbar_colors so
|
||||
// the server-rendered baseline and the client updates share one source of
|
||||
// truth — Qodo flagged the prior duplicated table as a drift hazard.
|
||||
// sees. The server emits a media-scoped light + dark pair so iOS Safari picks
|
||||
// the right color at first paint (issue #7606); on desktop/Android the user
|
||||
// can still override the system scheme via #options-darkmode. When that
|
||||
// happens we point EVERY theme-color meta at the chosen color so the explicit
|
||||
// choice wins regardless of which media query the browser is currently
|
||||
// matching — otherwise toggling light while the OS is dark (or vice versa)
|
||||
// would update a tag the browser is ignoring. Color resolution lives in
|
||||
// skin_toolbar_colors so the server-rendered baseline and the client updates
|
||||
// share one source of truth — Qodo flagged the prior duplicated table as a
|
||||
// drift hazard.
|
||||
const updateThemeColorMeta = (newClasses: string[]) => {
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (!meta) return;
|
||||
meta.setAttribute('content',
|
||||
toolbarColorForTokens(newClasses.join(' ').split(/\s+/).filter(Boolean)));
|
||||
const metas = document.querySelectorAll('meta[name="theme-color"]');
|
||||
if (!metas.length) return;
|
||||
const color = toolbarColorForTokens(newClasses.join(' ').split(/\s+/).filter(Boolean));
|
||||
metas.forEach((meta) => { meta.setAttribute('content', color); });
|
||||
};
|
||||
|
||||
// add corresponding classes when config change
|
||||
|
|
|
|||
|
|
@ -7,11 +7,16 @@
|
|||
&& req.acceptsLanguages(Object.keys(langs))) || 'en';
|
||||
var renderDir = (langs[renderLang] && langs[renderLang].direction === 'rtl') ? 'rtl' : 'ltr';
|
||||
// theme-color matches the configured toolbar so mobile address bars don't
|
||||
// paint a mismatched system color above the toolbar on first paint. We do
|
||||
// not emit a prefers-color-scheme: dark variant: the client-side dark-mode
|
||||
// auto-switch is gated on enableDarkMode, matchMedia, and a localStorage
|
||||
// white-mode override, none of which a media query can express.
|
||||
// paint a mismatched system color above the toolbar on first paint. We emit
|
||||
// a prefers-color-scheme: dark variant scoped with the `media` attribute so
|
||||
// iOS Safari — which colors the address bar at parse time and does not
|
||||
// reliably repaint when JS mutates the tag later — picks the dark toolbar
|
||||
// color at first paint on a dark-OS client (issue #7606). The dark variant
|
||||
// is only emitted when enableDarkMode is on, since that is what gates the
|
||||
// client-side auto-switch; the manual #options-darkmode toggle and the
|
||||
// localStorage white-mode override are still handled by skin_variants.ts.
|
||||
var configuredColor = skinColors.configuredToolbarColor(settings.skinName, settings.skinVariants);
|
||||
var darkColor = settings.enableDarkMode ? skinColors.darkToolbarColor(settings.skinName) : null;
|
||||
%>
|
||||
<!doctype html>
|
||||
<html lang="<%=renderLang%>" dir="<%=renderDir%>" translate="no" class="pad <%=pluginUtils.clientPluginNames().join(' '); %> <%=settings.skinVariants%>">
|
||||
|
|
@ -49,9 +54,38 @@
|
|||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
|
||||
<% if (configuredColor) { %><meta name="theme-color" content="<%=configuredColor%>"><% } %>
|
||||
<% if (configuredColor) { %><meta name="theme-color" content="<%=configuredColor%>"<% if (darkColor) { %> media="(prefers-color-scheme: light)"<% } %>><% } %>
|
||||
<% if (darkColor) { %><meta name="theme-color" content="<%=darkColor%>" media="(prefers-color-scheme: dark)"><% } %>
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
|
||||
<% if (darkColor) { %>
|
||||
<script>
|
||||
// Apply the dark skin classes before the stylesheet paints so dark-OS
|
||||
// users don't see a white flash while the JS bundle loads (issue #7606).
|
||||
// enableDarkMode being on is implied by this script being emitted; the
|
||||
// matchMedia + localStorage white-mode checks mirror the auto-switch in
|
||||
// pad.ts, which still runs on init to wire up the #options-darkmode toggle
|
||||
// and theme the editor iframes (those don't exist yet at parse time).
|
||||
(function () {
|
||||
try {
|
||||
// The skin-variants builder lets the user pick variants by hand; pad.ts
|
||||
// skips the auto-dark switch there, so we must too.
|
||||
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') return;
|
||||
if (localStorage.getItem('ep_darkMode') === 'false') return;
|
||||
if (!(window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)) return;
|
||||
var el = document.documentElement;
|
||||
['super-light', 'light', 'dark', 'super-dark'].forEach(function (c) {
|
||||
['editor', 'background', 'toolbar'].forEach(function (k) {
|
||||
el.classList.remove(c + '-' + k);
|
||||
});
|
||||
});
|
||||
el.classList.add('super-dark-editor', 'dark-background', 'super-dark-toolbar');
|
||||
} catch (e) { /* localStorage/matchMedia unavailable — fall back to light */ }
|
||||
})();
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
<% e.begin_block("styles"); %>
|
||||
<link href="../static/css/pad.css?v=<%=settings.randomVersionString%>" rel="stylesheet">
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
&& req.acceptsLanguages(Object.keys(langs))) || 'en';
|
||||
var renderDir = (langs[renderLang] && langs[renderLang].direction === 'rtl') ? 'rtl' : 'ltr';
|
||||
var themeColor = skinColors.configuredToolbarColor(settings.skinName, settings.skinVariants);
|
||||
// See pad.html: emit a media-scoped dark variant so iOS Safari picks the
|
||||
// dark toolbar color at first paint on a dark-OS client (issue #7606).
|
||||
var darkThemeColor = settings.enableDarkMode ? skinColors.darkToolbarColor(settings.skinName) : null;
|
||||
%>
|
||||
<!doctype html>
|
||||
<html lang="<%=renderLang%>" dir="<%=renderDir%>" translate="no" class="pad <%=settings.skinVariants%>">
|
||||
|
|
@ -39,8 +42,30 @@
|
|||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
|
||||
<% if (themeColor) { %><meta name="theme-color" content="<%=themeColor%>"><% } %>
|
||||
<% if (themeColor) { %><meta name="theme-color" content="<%=themeColor%>"<% if (darkThemeColor) { %> media="(prefers-color-scheme: light)"<% } %>><% } %>
|
||||
<% if (darkThemeColor) { %><meta name="theme-color" content="<%=darkThemeColor%>" media="(prefers-color-scheme: dark)"><% } %>
|
||||
<link rel="shortcut icon" href="../../favicon.ico">
|
||||
<% if (darkThemeColor) { %>
|
||||
<script>
|
||||
// See pad.html: apply the dark skin classes before the stylesheet paints
|
||||
// so dark-OS users don't see a white flash on load (issue #7606).
|
||||
(function () {
|
||||
try {
|
||||
if (window.location.hash.toLowerCase() === '#skinvariantsbuilder') return;
|
||||
if (localStorage.getItem('ep_darkMode') === 'false') return;
|
||||
if (!(window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)) return;
|
||||
var el = document.documentElement;
|
||||
['super-light', 'light', 'dark', 'super-dark'].forEach(function (c) {
|
||||
['editor', 'background', 'toolbar'].forEach(function (k) {
|
||||
el.classList.remove(c + '-' + k);
|
||||
});
|
||||
});
|
||||
el.classList.add('super-dark-editor', 'dark-background', 'super-dark-toolbar');
|
||||
} catch (e) { /* localStorage/matchMedia unavailable — fall back to light */ }
|
||||
})();
|
||||
</script>
|
||||
<% } %>
|
||||
<% e.begin_block("timesliderStyles"); %>
|
||||
<link rel="stylesheet" href="../../static/css/pad.css?v=<%=settings.randomVersionString%>">
|
||||
<link rel="stylesheet" href="../../static/css/iframe_editor.css?v=<%=settings.randomVersionString%>">
|
||||
|
|
|
|||
|
|
@ -60,19 +60,36 @@ describe(__filename, function () {
|
|||
beforeEach(function () {
|
||||
backups.skinName = settings.skinName;
|
||||
backups.skinVariants = settings.skinVariants;
|
||||
backups.enableDarkMode = settings.enableDarkMode;
|
||||
});
|
||||
afterEach(function () {
|
||||
settings.skinName = backups.skinName;
|
||||
settings.skinVariants = backups.skinVariants;
|
||||
settings.enableDarkMode = backups.enableDarkMode;
|
||||
});
|
||||
|
||||
it('pad page emits theme-color matching the configured colibris toolbar', async function () {
|
||||
it('pad page emits a light baseline and a media-scoped dark variant', async function () {
|
||||
// Issue #7606: iOS Safari colors the address bar at parse time and does
|
||||
// not reliably repaint when JS mutates the meta later, so the dark
|
||||
// toolbar color must be selectable at first paint via a media query.
|
||||
settings.skinName = 'colibris';
|
||||
settings.skinVariants = 'super-light-toolbar super-light-editor light-background';
|
||||
settings.enableDarkMode = true;
|
||||
const res = await agent.get('/p/testpad').expect(200);
|
||||
assert.match(res.text,
|
||||
/<meta name="theme-color" content="#ffffff" media="\(prefers-color-scheme: light\)">/);
|
||||
assert.match(res.text,
|
||||
/<meta name="theme-color" content="#485365" media="\(prefers-color-scheme: dark\)">/);
|
||||
});
|
||||
|
||||
it('pad page omits the dark variant when enableDarkMode is off', async function () {
|
||||
// With dark mode disabled the client never auto-switches, so the address
|
||||
// bar should stay on the unscoped light baseline.
|
||||
settings.skinName = 'colibris';
|
||||
settings.skinVariants = 'super-light-toolbar super-light-editor light-background';
|
||||
settings.enableDarkMode = false;
|
||||
const res = await agent.get('/p/testpad').expect(200);
|
||||
assert.match(res.text, /<meta name="theme-color" content="#ffffff">/);
|
||||
// No media-query variants — runtime dark-mode also depends on localStorage,
|
||||
// which a server-rendered media query cannot account for.
|
||||
assert.doesNotMatch(res.text, /prefers-color-scheme/);
|
||||
});
|
||||
|
||||
|
|
@ -80,7 +97,7 @@ describe(__filename, function () {
|
|||
settings.skinName = 'colibris';
|
||||
settings.skinVariants = 'dark-toolbar dark-editor dark-background';
|
||||
const res = await agent.get('/p/testpad').expect(200);
|
||||
assert.match(res.text, /<meta name="theme-color" content="#576273">/);
|
||||
assert.match(res.text, /<meta name="theme-color" content="#576273"/);
|
||||
});
|
||||
|
||||
it('pad page omits theme-color for non-colibris skins', async function () {
|
||||
|
|
@ -90,14 +107,17 @@ describe(__filename, function () {
|
|||
assert.doesNotMatch(res.text, /theme-color/);
|
||||
});
|
||||
|
||||
it('timeslider page emits theme-color matching the configured toolbar', async function () {
|
||||
it('timeslider page emits a light baseline and a media-scoped dark variant', async function () {
|
||||
settings.skinName = 'colibris';
|
||||
settings.skinVariants = 'super-dark-toolbar super-dark-editor dark-background';
|
||||
settings.skinVariants = 'super-light-toolbar super-light-editor light-background';
|
||||
settings.enableDarkMode = true;
|
||||
// Issue #7659: /p/:pad/timeslider redirects unless ?embed=1 — that
|
||||
// query is the iframe path that still serves the timeslider HTML.
|
||||
const res = await agent.get('/p/testpad/timeslider?embed=1').expect(200);
|
||||
assert.match(res.text, /<meta name="theme-color" content="#485365">/);
|
||||
assert.doesNotMatch(res.text, /prefers-color-scheme/);
|
||||
assert.match(res.text,
|
||||
/<meta name="theme-color" content="#ffffff" media="\(prefers-color-scheme: light\)">/);
|
||||
assert.match(res.text,
|
||||
/<meta name="theme-color" content="#485365" media="\(prefers-color-scheme: dark\)">/);
|
||||
});
|
||||
|
||||
it('timeslider page omits theme-color for non-colibris skins', async function () {
|
||||
|
|
@ -107,4 +127,59 @@ describe(__filename, function () {
|
|||
assert.doesNotMatch(res.text, /theme-color/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dark-mode flash prevention', function () {
|
||||
const backups:MapArrayType<any> = {};
|
||||
beforeEach(function () {
|
||||
backups.skinName = settings.skinName;
|
||||
backups.enableDarkMode = settings.enableDarkMode;
|
||||
});
|
||||
afterEach(function () {
|
||||
settings.skinName = backups.skinName;
|
||||
settings.enableDarkMode = backups.enableDarkMode;
|
||||
});
|
||||
|
||||
const PREPAINT = "classList.add('super-dark-editor', 'dark-background', 'super-dark-toolbar')";
|
||||
|
||||
it('pad page inlines the pre-paint dark-mode script before the stylesheet', async function () {
|
||||
// Issue #7606: without this, dark-OS users see the page painted light
|
||||
// until the JS bundle runs and flips the skin classes. The inline script
|
||||
// must come before pad.css so the dark classes are on <html> at first
|
||||
// paint.
|
||||
settings.skinName = 'colibris';
|
||||
settings.enableDarkMode = true;
|
||||
const res = await agent.get('/p/testpad').expect(200);
|
||||
const scriptIdx = res.text.indexOf(PREPAINT);
|
||||
const cssIdx = res.text.indexOf('css/pad.css');
|
||||
assert(scriptIdx !== -1, 'expected the pre-paint dark-mode script in the pad page');
|
||||
assert(cssIdx !== -1 && scriptIdx < cssIdx,
|
||||
'pre-paint dark-mode script must come before pad.css so it applies before first paint');
|
||||
// Must skip the auto-dark switch on the skin-variants builder, matching
|
||||
// pad.ts (so it can't fight the builder UI on a dark-OS client).
|
||||
assert(res.text.includes('#skinvariantsbuilder'),
|
||||
'pre-paint script must guard against the #skinvariantsbuilder hash like pad.ts');
|
||||
});
|
||||
|
||||
it('timeslider page inlines the pre-paint dark-mode script', async function () {
|
||||
settings.skinName = 'colibris';
|
||||
settings.enableDarkMode = true;
|
||||
const res = await agent.get('/p/testpad/timeslider?embed=1').expect(200);
|
||||
assert(res.text.includes(PREPAINT),
|
||||
'expected the pre-paint dark-mode script in the timeslider page');
|
||||
});
|
||||
|
||||
it('omits the pre-paint script when dark mode is disabled', async function () {
|
||||
settings.skinName = 'colibris';
|
||||
settings.enableDarkMode = false;
|
||||
const res = await agent.get('/p/testpad').expect(200);
|
||||
assert(!res.text.includes(PREPAINT));
|
||||
});
|
||||
|
||||
it('omits the pre-paint script for non-colibris skins', async function () {
|
||||
settings.skinName = 'no-skin';
|
||||
settings.enableDarkMode = true;
|
||||
const res = await agent.get('/p/testpad').expect(200);
|
||||
assert(!res.text.includes(PREPAINT));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
import {expect, test, Page} from '@playwright/test';
|
||||
import {goToNewPad} from '../helper/padHelper';
|
||||
|
||||
const themeColor = (page: Page) =>
|
||||
page.locator('meta[name="theme-color"]').getAttribute('content');
|
||||
// Issue #7606: the server emits a media-scoped pair of theme-color metas so
|
||||
// iOS Safari can pick the right address-bar color at first paint without JS.
|
||||
// Read each by its media attribute; the browser applies whichever matches the
|
||||
// active color scheme.
|
||||
const lightThemeColor = (page: Page) =>
|
||||
page.locator('meta[name="theme-color"][media="(prefers-color-scheme: light)"]')
|
||||
.getAttribute('content');
|
||||
const darkThemeColor = (page: Page) =>
|
||||
page.locator('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]')
|
||||
.getAttribute('content');
|
||||
|
||||
test.describe('light color scheme', () => {
|
||||
test.use({colorScheme: 'light'});
|
||||
|
||||
test('theme-color meta tracks the dark-mode toggle', async ({page}) => {
|
||||
await goToNewPad(page);
|
||||
// Server emits the light baseline derived from settings.skinVariants.
|
||||
expect(await themeColor(page)).toBe('#ffffff');
|
||||
// First paint: light baseline is active, and the dark variant is present
|
||||
// so a dark-OS client would have rendered #485365 without any JS.
|
||||
expect(await lightThemeColor(page)).toBe('#ffffff');
|
||||
expect(await darkThemeColor(page)).toBe('#485365');
|
||||
|
||||
await page.locator('button[data-l10n-id="pad.toolbar.settings.title"]').click();
|
||||
await expect(page.locator('#theme-toggle-row')).toBeVisible();
|
||||
|
|
@ -18,25 +28,35 @@ test.describe('light color scheme', () => {
|
|||
// Colibris styles the native checkbox via a sibling label; click the label
|
||||
// so the toggle fires the real change event the production code listens on.
|
||||
await page.locator('label[for="options-darkmode"]').click();
|
||||
// pad.ts forces super-dark-toolbar (#485365) regardless of the configured
|
||||
// light skinVariants, so the meta must follow the client-applied class.
|
||||
await expect.poll(() => themeColor(page)).toBe('#485365');
|
||||
// The explicit toggle points every theme-color meta at the dark toolbar
|
||||
// color, so the address bar goes dark even though the OS is in light mode.
|
||||
await expect.poll(() => lightThemeColor(page)).toBe('#485365');
|
||||
|
||||
await page.locator('label[for="options-darkmode"]').click();
|
||||
await expect.poll(() => themeColor(page)).toBe('#ffffff');
|
||||
await expect.poll(() => lightThemeColor(page)).toBe('#ffffff');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('dark color scheme', () => {
|
||||
test.use({colorScheme: 'dark'});
|
||||
|
||||
test('theme-color meta follows the auto dark-mode switch on dark-OS clients',
|
||||
test('theme-color meta is dark at first paint on dark-OS clients',
|
||||
async ({page}) => {
|
||||
await goToNewPad(page);
|
||||
// pad.ts auto-switches to super-dark-toolbar when enableDarkMode is on,
|
||||
// matchMedia(prefers-color-scheme:dark) matches, and no localStorage
|
||||
// white-mode override is set. The meta must follow the applied class —
|
||||
// this is the case stffen reported on issue #7606.
|
||||
await expect.poll(() => themeColor(page)).toBe('#485365');
|
||||
// The media-scoped dark variant is what fixes stffen's iPhone: it is
|
||||
// present and dark before any JS runs, so iOS Safari colors the address
|
||||
// bar correctly at parse time (issue #7606).
|
||||
await expect.poll(() => darkThemeColor(page)).toBe('#485365');
|
||||
});
|
||||
|
||||
test('page paints dark without a light flash on dark-OS clients',
|
||||
async ({page}) => {
|
||||
await goToNewPad(page);
|
||||
// The inline pre-paint script in <head> adds the dark skin classes to
|
||||
// <html> before the stylesheet paints, so a dark-OS user never sees the
|
||||
// light page (issue #7606). Asserting the class is present confirms the
|
||||
// dark skin is applied; the backend test verifies the script is ordered
|
||||
// before pad.css so it takes effect at first paint.
|
||||
await expect(page.locator('html')).toHaveClass(/super-dark-editor/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
1
var/.gitignore
vendored
1
var/.gitignore
vendored
|
|
@ -3,3 +3,4 @@ minified*
|
|||
installed_plugins.json
|
||||
dirty.db
|
||||
rusty.db
|
||||
update-state.json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue