Файли
openidea/e2e/themes.spec.ts
l0ng-ai 1186cdce64 fix(editor): keep selections whole over line backgrounds and indent guides (#1568)
* fix(editor): keep selections whole over line backgrounds and indent guides

Multi-line selections could show a 1px seam between their stacked
rectangles, indent guides showed as short dashes inside a selection,
and a breakpoint or execution line hid the selection past its text.

- Snap selection rectangle edges to device pixels so stacked pieces
  share one edge.
- Paint caret-row, breakpoint and execution line backgrounds on a
  pseudo-element below the selection layer.
- Draw indent guides on a pseudo-element above the text.

* test(e2e): check the visible selection layer in the theme flow
2026-11-05 23:31:14 +08:00

235 рядки
10 KiB
TypeScript
Неформатований Постійне посилання Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
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:visible").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);
}
});
test("gear › Theme previews the theme under the pointer and keeps the one chosen", async ({ workbench: page }) => {
const html = page.locator("html");
const gear = page.getByRole("button", { name: "IDE and Project Settings", exact: true });
const theme = (label: string) => page.getByRole("menuitemcheckbox", { name: label, exact: true });
await gear.click();
await page.getByRole("menuitem", { name: "Theme", exact: true }).hover();
await theme("Solarized Dark").hover();
await expect(html).toHaveAttribute("data-color-theme", "solarized-dark");
// A plugin's theme previews too.
await theme("Harbor Night").hover();
await expect(html).toHaveAttribute("data-plugin-theme", /harbor-night$/);
await expect(html).not.toHaveAttribute("data-color-theme");
await page.keyboard.press("Escape");
await page.keyboard.press("Escape");
await expect(html).not.toHaveAttribute("data-plugin-theme");
await expect(html).toHaveAttribute("data-theme", "light");
await gear.click();
await page.getByRole("menuitem", { name: "Theme", exact: true }).hover();
await theme("One Dark").hover();
await expect(html).toHaveAttribute("data-color-theme", "one-dark");
await theme("One Dark").click();
await expect(html).toHaveAttribute("data-color-theme", "one-dark");
// Chosen, not previewed: the next start-up opens with it.
await expect
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem("openidea.theme") ?? "{}").colorTheme))
.toBe("one-dark");
});
test("Settings › Appearance › Theme previews the highlighted theme until Cancel", async ({ workbench: page }) => {
const html = page.locator("html");
const dialog = await settings(page);
await page.getByRole("button", { name: "Theme", exact: true }).click();
await page.getByRole("option", { name: "Solarized Dark", exact: true }).hover();
await expect(html).toHaveAttribute("data-color-theme", "solarized-dark");
await page.keyboard.press("Escape");
await expect(html).toHaveAttribute("data-theme", "light");
await expect(html).not.toHaveAttribute("data-color-theme");
// Picked: it shows until Apply or Cancel.
await choose(page, "Theme", "One Dark");
await expect(html).toHaveAttribute("data-color-theme", "one-dark");
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
await expect(html).toHaveAttribute("data-theme", "light");
await expect(html).not.toHaveAttribute("data-color-theme");
});