mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
* 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
235 lines
10 KiB
TypeScript
235 lines
10 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: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");
|
||
});
|