Files
openidea/e2e/themes.spec.ts
Walter WangandClaude 3b608355a3 feat(themes): Built-in Solarized and One light/dark themes (#85)
Adds Solarized Light, Solarized Dark, One Light and One Dark as built-in themes across the workbench, editor, terminal and Mermaid, keeping the original palettes (MIT notices in THIRD_PARTY_LICENSES.md, mapping in docs/themes.md).

- Native window colours are read from tokens.css at build time (`theme_frame.rs`) instead of a second colour table.
- Startup restores the built-in palette before first paint.
- Editor schemes, the terminal and Mermaid refresh on switches between themes of the same kind.
- The plugin packages of the same themes are removed.
- A test requires every built-in palette theme to define each token (only listed ones may inherit from the base theme), so new tokens can't silently miss them.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
2026-10-27 18:53:12 +08:00

189 lines
7.8 KiB
TypeScript

import { expect, openFromTree, test } from "./fixtures";
import type { Page } from "@playwright/test";
test.setTimeout(60_000);
const palettes = [
{
id: "solarized-light",
label: "Solarized Light",
kind: "light",
bg: "rgb(253, 246, 227)",
fg: "rgb(101, 123, 131)",
frame: [238, 232, 213, 255],
},
{
id: "solarized-dark",
label: "Solarized Dark",
kind: "dark",
bg: "rgb(0, 43, 54)",
fg: "rgb(131, 148, 150)",
frame: [7, 54, 66, 255],
},
{
id: "one-light",
label: "One Light",
kind: "light",
bg: "rgb(250, 250, 250)",
fg: "rgb(56, 58, 66)",
frame: [234, 234, 235, 255],
},
{
id: "one-dark",
label: "One Dark",
kind: "dark",
bg: "rgb(40, 44, 52)",
fg: "rgb(171, 178, 191)",
frame: [33, 37, 43, 255],
},
];
async function choose(page: Page, control: string, label: string) {
await page.getByRole("button", { name: control, exact: true }).click();
await page.getByRole("option", { name: label, exact: true }).click();
}
async function settings(page: Page) {
await page.keyboard.press("Meta+,");
const dialog = page.getByRole("dialog", { name: "Settings", exact: true });
await expect(dialog).toBeVisible();
await dialog
.locator('[role="treeitem"][aria-level="2"]')
.filter({ hasText: /^Appearance$/ })
.click();
return dialog;
}
for (const palette of palettes) {
test(`${palette.label}: settings, editor, terminal, popup and reload`, async ({ workbench: page }, info) => {
const editor = await openFromTree(page, "build.gradle.kts");
await page.keyboard.press("Alt+F12");
await expect(page.locator(".xterm")).toBeVisible();
// Use only synthetic ANSI output; no shell, network or native PTY is involved.
await page.evaluate(async () => {
const path = "/src/features/terminal/store.ts";
const { activeSession } = await import(/* @vite-ignore */ path);
const session = activeSession();
session.term.clear();
session.term.writeln("Synthetic theme regression — no PTY");
session.term.writeln("\x1b[31merror\x1b[0m \x1b[33mwarning\x1b[0m \x1b[32msuccess\x1b[0m \x1b[34minfo\x1b[0m");
});
const dialog = await settings(page);
await choose(page, "Theme", palette.label);
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-color-theme", palette.id);
await expect(page.locator("html")).toHaveAttribute("data-theme", palette.kind);
expect(
await page.evaluate(async () => {
const path = "/src/workbench/theme.ts";
const { windowBackgroundColor, windowBackgroundHex } = await import(/* @vite-ignore */ path);
return { rgba: windowBackgroundColor(), hex: windowBackgroundHex() };
}),
).toEqual({
rgba: palette.frame,
hex:
"#" +
palette.frame
.slice(0, 3)
.map((n) => n.toString(16).padStart(2, "0"))
.join(""),
});
await page.screenshot({ path: info.outputPath(`${palette.id}-settings.png`), animations: "disabled" });
await page.keyboard.press("Escape");
await expect(editor).toHaveCSS("background-color", palette.bg);
await expect(editor).toHaveCSS("color", palette.fg);
await expect(editor.locator(".cm-gutters")).toHaveCSS("background-color", palette.bg);
await expect
.poll(() =>
page.evaluate(async () => {
const path = "/src/features/terminal/store.ts";
const { activeSession } = await import(/* @vite-ignore */ path);
return activeSession().term.options.theme.background;
}),
)
.toBe(
palette.id === "solarized-light"
? "#fdf6e3"
: palette.id === "solarized-dark"
? "#002b36"
: palette.id === "one-light"
? "#fafafa"
: "#282c34",
);
await editor.locator(".cm-content").click();
await page.keyboard.press("Meta+a");
await expect(editor.locator(".cm-selectionBackground").first()).toBeVisible();
await page.keyboard.press("ArrowRight");
await page.keyboard.press("Meta+f");
await page.getByRole("textbox", { name: "Find", exact: true }).fill("Order");
await expect(editor.locator(".cm-openidea-findMatch").first()).toBeVisible();
await page.screenshot({ path: info.outputPath(`${palette.id}-workbench.png`), animations: "disabled" });
await page.keyboard.press("Escape");
await settings(page);
await page.getByRole("button", { name: "Theme", exact: true }).click();
await expect(page.getByRole("listbox", { name: "Theme", exact: true })).toBeVisible();
await page.screenshot({ path: info.outputPath(`${palette.id}-popup.png`), animations: "disabled" });
await page.keyboard.press("Escape");
await page.keyboard.press("Escape");
await page.reload();
await expect(page.getByRole("tree", { name: "Project" })).toBeVisible();
await expect(page.locator("html")).toHaveAttribute("data-color-theme", palette.id);
const cached = await page.evaluate(() => JSON.parse(localStorage.getItem("openidea.theme")!));
expect(cached).toMatchObject({ theme: palette.kind, colorTheme: palette.id });
});
}
test("same-kind switching, legacy themes, system mode and explicit editor schemes", async ({ workbench: page }) => {
await openFromTree(page, "build.gradle.kts");
const dialog = await settings(page);
for (const label of [
"Solarized Light",
"One Light",
"Solarized Dark",
"One Dark",
"Islands Light",
"Islands Dark",
"High Contrast",
]) {
await choose(page, "Theme", label);
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
if (label === "High Contrast") await expect(page.locator("html")).toHaveAttribute("data-contrast", "high");
else await expect(page.locator("html")).not.toHaveAttribute("data-contrast");
const added = palettes.find((p) => p.label === label);
if (added) await expect(page.locator("html")).toHaveAttribute("data-color-theme", added.id);
else await expect(page.locator("html")).not.toHaveAttribute("data-color-theme");
const bg =
added?.bg ??
(label === "Islands Light"
? "rgb(255, 255, 255)"
: label === "Islands Dark"
? "rgb(25, 26, 28)"
: "rgb(0, 0, 0)");
await expect(page.locator(".cm-editor").first()).toHaveCSS("background-color", bg);
}
await choose(page, "Theme", "One Light");
await choose(page, "Editor color scheme", "Solarized Dark");
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator(".cm-editor").first()).toHaveCSS("background-color", "rgb(0, 43, 54)");
await choose(page, "Editor color scheme", "Theme default");
await dialog.getByLabel("Sync with OS", { exact: true }).check();
await dialog.getByRole("button", { name: "Preferred themes", exact: true }).click();
await choose(page, "Preferred light theme", "Solarized Light");
await choose(page, "Preferred dark theme", "One Dark");
await page.keyboard.press("Escape");
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await page.emulateMedia({ colorScheme: "light" });
await expect(page.locator("html")).toHaveAttribute("data-color-theme", "solarized-light");
await page.emulateMedia({ colorScheme: "dark" });
await expect(page.locator("html")).toHaveAttribute("data-color-theme", "one-dark");
});
test("quick theme menu offers all four palettes and applies their kind", async ({ workbench: page }) => {
for (const palette of palettes) {
await page.getByRole("button", { name: "IDE and Project Settings", exact: true }).click();
await page.getByText("Theme", { exact: true }).click();
await page.getByText(palette.label, { exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-color-theme", palette.id);
await expect(page.locator("html")).toHaveAttribute("data-theme", palette.kind);
}
});