fix(theme-color): dark address bar + no light-flash on dark-OS load (#7606) (#7909)

* 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:
John McLear 2026-06-08 20:54:08 +01:00 committed by GitHub
parent c8350782dd
commit ef205bf2ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 217 additions and 41 deletions

View file

@ -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

View file

@ -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(),

View file

@ -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']);
};

View file

@ -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

View file

@ -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">

View file

@ -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%>">

View file

@ -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));
});
});
});

View file

@ -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
View file

@ -3,3 +3,4 @@ minified*
installed_plugins.json
dirty.db
rusty.db
update-state.json