Files
edgeever/tests/e2e/notebook-appearance.spec.ts
T
edgeever-updater[bot] 05e53dcba3 chore: update EdgeEver from v2.7.0
EdgeEver-Upstream-Commit: 30fcb3fc65c419000a3e6f8cfb70eca47ae53914
2026-10-26 07:48:43 +00:00

147 lines
9.9 KiB
TypeScript

import { expect, test } from "@playwright/test";
test("saves notebook appearance, keeps drafts on cancel, and resets across reloads and themes", async ({ page }) => {
await page.addInitScript(() => {
localStorage.setItem("edgeever.locale.preference", "en-US");
if (!localStorage.getItem("edgeever.notebook-color-palette")) localStorage.setItem("edgeever.notebook-color-palette", JSON.stringify(["#2563eb", "#16a34a", "#ca8a04", "#dc2626", "#7c3aed"]));
});
await page.setViewportSize({ width: 1280, height: 900 });
await page.goto("/");
const name = `Appearance ${Date.now()}`;
const created = await page.request.post("/api/v1/notebooks", { data: { name } });
expect(created.ok()).toBeTruthy();
const { notebook } = await created.json();
const childResponse = await page.request.post("/api/v1/notebooks", { data: { name: "Child", parentId: notebook.id } });
const { notebook: child } = await childResponse.json();
const readNotebook = async () => (await (await page.request.get("/api/v1/notebooks")).json()).notebooks.find((item: { id: string }) => item.id === notebook.id);
const row = page.locator(`[data-notebook-id="${notebook.id}"]`);
const dialog = page.getByRole("dialog", { name: "Appearance", exact: true });
const open = async () => {
await row.hover();
await row.getByRole("button", { name: `Open actions for ${name}`, exact: true }).click();
await page.getByRole("button", { name: "Appearance", exact: true }).click();
await expect(dialog).toBeVisible();
};
const save = async () => {
await dialog.getByRole("button", { name: "Save", exact: true }).click();
await expect(dialog).toBeHidden();
};
try {
await page.reload();
await expect(row).toBeVisible();
await row.locator("[data-notebook-tree-button]").click();
await open();
await dialog.getByRole("button", { name: "Icon", exact: true }).click();
await dialog.getByPlaceholder("Search icons (English or German)").fill("alarm-clock");
await dialog.getByRole("option", { name: "alarm clock", exact: true }).click();
await expect(dialog.locator('[data-notebook-color-picker="Icon color"] button')).toHaveCount(5);
await expect(dialog.locator('[data-notebook-color-picker="Text color"] button')).toHaveCount(5);
await dialog.getByRole("button", { name: "Icon color 1: #2563eb", exact: true }).click();
await dialog.getByLabel("Icon color", { exact: true }).fill("#0d9488");
await expect(dialog.getByRole("button", { name: "Icon color 1: #0d9488", exact: true })).toHaveAttribute("aria-pressed", "true");
await expect(dialog.getByRole("button", { name: "Text color 1: #0d9488", exact: true })).toBeVisible();
await dialog.getByLabel("Icon color", { exact: true }).evaluate((element) => {
const input = element as HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
setter?.call(input, "#2563eb");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
});
await dialog.getByLabel("Text color", { exact: true }).evaluate((element) => {
const input = element as HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
setter?.call(input, "#dc2626");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
});
await dialog.getByRole("switch", { name: "Bold", exact: true }).check();
await dialog.getByLabel("Invert", { exact: true }).selectOption("icon");
await expect(dialog.locator("[data-notebook-icon]")).toHaveAttribute("data-notebook-icon-inverted", "true");
await expect(dialog.locator("[data-notebook-name]")).toHaveCSS("font-weight", "700");
await expect(dialog.locator("[data-notebook-appearance-preview] [data-notebook-icon]")).toHaveAttribute("data-notebook-icon", "alarm-clock");
await expect(dialog.locator("[data-notebook-name]")).toHaveCSS("color", "rgb(196, 34, 34)");
await expect(dialog.locator("[data-notebook-icon]")).toHaveCSS("width", "24px");
await expect(dialog.locator("[data-notebook-icon]")).toHaveCSS("padding", "4px");
const previewColor = await dialog.locator("[data-notebook-name]").evaluate(element => getComputedStyle(element).color);
await page.screenshot({ path: "test-results/notebook-appearance-dialog.png" });
await save();
await expect.poll(readNotebook).toMatchObject({ icon: "alarm-clock", color: "#2563eb", textColor: "#dc2626", isBold: true, iconInverted: true });
await page.reload();
await expect(row.locator("[data-notebook-icon]")).toHaveAttribute("data-notebook-icon", "alarm-clock");
await expect(row.locator('[data-notebook-icon] circle[cx="12"][cy="13"]')).toHaveCount(1);
await expect(row.locator("[data-notebook-icon]")).toHaveAttribute("data-notebook-icon-inverted", "true");
await expect(row.locator("[data-notebook-name]")).toHaveCSS("color", previewColor);
await expect(row.locator("[data-notebook-name]")).toHaveCSS("font-weight", "700");
await row.locator("[data-notebook-tree-button]").click();
await expect(row.locator("[data-notebook-name]")).toHaveCSS("font-weight", "700");
// Children have independent settings.
await expect(page.locator(`[data-notebook-id="${child.id}"] [data-notebook-icon]`)).toHaveAttribute("data-notebook-icon", "notebook");
await expect(page.locator(`[data-notebook-id="${child.id}"] [data-notebook-name]`)).toHaveCSS("font-weight", "400");
await page.evaluate(() => document.documentElement.classList.add("dark"));
await expect(row.locator("[data-notebook-name]")).not.toHaveCSS("color", previewColor);
await page.screenshot({ path: "test-results/notebook-appearance-dark.png" });
await open();
await dialog.getByLabel("Text color", { exact: true }).evaluate((element) => {
const input = element as HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
setter?.call(input, "#ffffff");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
});
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
expect(await readNotebook()).toMatchObject({ color: "#2563eb", textColor: "#dc2626", isBold: true, iconInverted: true });
// Reset is a draft until Save; Escape discards it.
await row.click({ button: "right" });
await page.getByRole("menuitem", { name: "Appearance", exact: true }).click();
await dialog.getByRole("button", { name: "Reset to default", exact: true }).click();
await page.keyboard.press("Escape");
await expect(dialog).toBeHidden();
expect(await readNotebook()).toMatchObject({ icon: "alarm-clock", color: "#2563eb", textColor: "#dc2626", isBold: true, iconInverted: true });
await open();
await dialog.getByLabel("Invert", { exact: true }).selectOption("row");
await expect(dialog.locator("[data-notebook-name]")).toHaveCSS("color", "rgb(255, 255, 255)");
await expect(dialog.locator("[data-notebook-icon]")).not.toHaveAttribute("data-notebook-icon-inverted");
const rowBackground = await dialog.locator("[data-notebook-appearance-preview]").evaluate(element => getComputedStyle(element).backgroundColor);
await save();
await expect.poll(readNotebook).toMatchObject({ rowInverted: true, iconInverted: false });
await page.reload();
await expect(row).toHaveAttribute("data-notebook-row-inverted", "true");
await expect(row).toHaveCSS("background-color", rowBackground);
await expect(row.locator("[data-notebook-name]")).toHaveCSS("color", "rgb(255, 255, 255)");
await expect(row.locator("[data-notebook-icon]")).toHaveCSS("color", "rgb(255, 255, 255)");
// The active row stays recognizable; dark mode preserves the selected background.
await row.locator("[data-notebook-tree-button]").click();
await expect(row).toHaveClass(/notebook-inverted-row-selected/);
await page.evaluate(() => document.documentElement.classList.add("dark"));
await expect(row).toHaveCSS("background-color", rowBackground);
// A nested row spans the same width, with its content still indented.
expect((await page.request.patch(`/api/v1/notebooks/${child.id}`, { data: { rowInverted: true, color: "#2563eb" } })).ok()).toBeTruthy();
await page.reload();
const childRow = page.locator(`[data-notebook-id="${child.id}"]`);
await expect(row).toHaveAttribute("data-notebook-row-inverted", "true");
const expand = row.getByRole("button", { name: `Expand ${name}`, exact: true });
if (await expand.isVisible()) await expand.click();
await expect(childRow).toBeVisible();
await expect(childRow).toHaveAttribute("data-notebook-row-inverted", "true");
const parentBox = await row.boundingBox();
const childBox = await childRow.boundingBox();
expect(childBox?.x).toBe(parentBox?.x);
expect(childBox?.width).toBe(parentBox?.width);
await expect(childRow).toHaveCSS("padding-left", "22px");
await expect(childRow.locator("[data-notebook-name]")).toHaveCSS("color", "rgb(255, 255, 255)");
await page.screenshot({ path: "test-results/notebook-row-inverted.png" });
await open();
await expect(dialog.getByLabel("Invert", { exact: true })).toHaveValue("row");
await dialog.getByRole("button", { name: "Reset to default", exact: true }).click();
await save();
await expect.poll(readNotebook).toMatchObject({ icon: null, color: null, textColor: null, isBold: false, iconInverted: false, rowInverted: false });
await page.reload();
await expect(row.locator("[data-notebook-icon]")).toHaveAttribute("data-notebook-icon", "notebook");
await expect(row.locator(".notebook-custom-color")).toHaveCount(0);
await expect(row.locator("[data-notebook-name]")).toHaveCSS("font-weight", "400");
} finally {
await page.request.delete(`/api/v1/notebooks/${child.id}`);
await page.request.delete(`/api/v1/notebooks/${notebook.id}`);
}
});