Files
buzz/desktop/tests/e2e/buzz-theme-screenshots.spec.ts
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

640 lines
24 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/buzz-theme";
const THEME_STORAGE_KEY = "buzz-theme";
const MOCK_PUBKEY = "deadbeef".repeat(8);
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
/**
* Seed the active theme into localStorage BEFORE the mock bridge installs so
* ThemeProvider reads it on first mount (init scripts run in registration
* order; React reads state on mount, which the bridge triggers).
*/
async function seedTheme(page: Page, theme: string) {
await page.addInitScript(
({ key, value }) => {
window.localStorage.setItem(key, value);
},
{ key: THEME_STORAGE_KEY, value: theme },
);
}
async function seedIconChannelSection(page: Page) {
await page.addInitScript(
({ channelId, pubkey }) => {
window.localStorage.setItem(
`buzz-channel-sections.v1:${pubkey}`,
JSON.stringify({
version: 1,
sections: [
{
id: "alignment-section",
name: "Team channels",
icon: "📌",
order: 0,
},
],
assignments: { [channelId]: "alignment-section" },
}),
);
},
{ channelId: GENERAL_CHANNEL_ID, pubkey: MOCK_PUBKEY },
);
}
async function openChannel(page: Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await expect(page.getByTestId("app-sidebar")).toBeVisible();
}
async function expectBuzzSidebarPalette(page: Page, mode: "light" | "dark") {
const mutedColor =
mode === "light" ? "rgba(0, 0, 0, 0.4)" : "rgba(255, 255, 255, 0.4)";
const searchSurface =
mode === "light" ? "rgba(0, 0, 0, 0.04)" : "rgba(255, 255, 255, 0.04)";
const hoverSurface =
mode === "light" ? "rgba(0, 0, 0, 0.04)" : "rgba(255, 255, 255, 0.04)";
const activeSurface =
mode === "light" ? "rgba(0, 0, 0, 0.07)" : "color(srgb 1 1 1 / 0.16)";
const chromeColor =
mode === "light" ? "rgba(0, 0, 0, 0.5)" : "rgba(255, 255, 255, 0.5)";
const search = page.getByTestId("open-search");
const pinnedHeader = page.getByTestId("sidebar-pinned-header");
const sidebarScroller = page.locator(".buzz-sidebar-scrollbar");
const scrollContent = page.getByTestId("sidebar-scroll-content");
const primaryMenu = page.getByTestId("sidebar-primary-menu");
const sectionLabel = page
.locator('[data-sidebar="group-label"]')
.filter({ hasText: "Channels" })
.first();
await expect(sectionLabel).toHaveCSS("color", mutedColor);
await expect(search).toHaveCSS("background-color", searchSurface);
await expect(search.locator("svg").first()).toHaveCSS("color", mutedColor);
await expect(search.locator("span").first()).toHaveCSS("color", mutedColor);
await expect(pinnedHeader).toHaveCSS("padding-bottom", "8px");
await expect(pinnedHeader).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
await expect(pinnedHeader).toHaveCSS("margin-left", "3px");
await expect(pinnedHeader).toHaveCSS("margin-right", "3px");
await expect(pinnedHeader).toHaveCSS("padding-right", "8px");
await expect(sidebarScroller).toHaveCSS("padding-left", "0px");
await expect(sidebarScroller).toHaveCSS("padding-right", "0px");
await expect(scrollContent).toHaveCSS("padding-left", "3px");
await expect(scrollContent).toHaveCSS("padding-right", "3px");
const pinnedSpacerColor = await pinnedHeader.evaluate(
(element) => getComputedStyle(element, "::before").backgroundColor,
);
expect(pinnedSpacerColor).toBe("rgba(0, 0, 0, 0)");
await expect(sidebarScroller.getByTestId("open-agents-view")).toBeVisible();
const searchBox = await search.boundingBox();
const pinnedHeaderBox = await pinnedHeader.boundingBox();
const primaryMenuBox = await primaryMenu.boundingBox();
const primaryRowBox = await page
.getByTestId("open-agents-view")
.boundingBox();
const activeRowBox = await page.getByTestId("channel-general").boundingBox();
const hoverRowBox = await page.getByTestId("channel-random").boundingBox();
const scrollContentBox = await scrollContent.evaluate((element) => {
const box = element.getBoundingClientRect();
return { left: box.left, right: box.right };
});
expect(searchBox).not.toBeNull();
expect(pinnedHeaderBox).not.toBeNull();
expect(primaryMenuBox).not.toBeNull();
expect(primaryRowBox).not.toBeNull();
expect(activeRowBox).not.toBeNull();
expect(hoverRowBox).not.toBeNull();
if (
!searchBox ||
!pinnedHeaderBox ||
!primaryMenuBox ||
!primaryRowBox ||
!activeRowBox ||
!hoverRowBox
) {
throw new Error("Sidebar search or primary navigation geometry is missing");
}
expect(primaryMenuBox.y - (searchBox.y + searchBox.height)).toBe(8);
expect(
pinnedHeaderBox.y +
pinnedHeaderBox.height -
(searchBox.y + searchBox.height),
).toBe(8);
expect(primaryMenuBox.y - (pinnedHeaderBox.y + pinnedHeaderBox.height)).toBe(
0,
);
for (const rowBox of [primaryRowBox, activeRowBox, hoverRowBox]) {
expect(Math.abs(rowBox.x - searchBox.x)).toBeLessThanOrEqual(0.5);
// Linux CI reserves a classic scrollbar gutter while macOS uses an
// overlay scrollbar. Compare each row to its usable scroll area so the
// alignment check remains platform-independent.
const rowLeftSpacing = rowBox.x - scrollContentBox.left;
const rowRightSpacing = scrollContentBox.right - (rowBox.x + rowBox.width);
expect(Math.abs(rowLeftSpacing - rowRightSpacing)).toBeLessThanOrEqual(0.5);
}
await expect(page.locator("[data-buzz-sidebar-secondary]").first()).toHaveCSS(
"color",
mutedColor,
);
await expect(page.locator('[data-sidebar="trigger"]')).toHaveCSS(
"color",
chromeColor,
);
await expect(page.getByTestId("global-back")).toHaveCSS("color", chromeColor);
await expect(page.getByTestId("global-forward")).toHaveCSS(
"color",
chromeColor,
);
await expect(page.getByTestId("channel-general")).not.toHaveCSS(
"color",
mutedColor,
);
await expect(page.getByTestId("channel-general")).toHaveCSS(
"background-color",
activeSurface,
);
const hoverChannel = page.getByTestId("channel-random");
await hoverChannel.hover();
await expect(hoverChannel).toHaveCSS("background-color", hoverSurface);
const firstDmItem = page
.getByTestId("dm-list")
.locator('[data-sidebar="menu-item"]')
.first();
const firstDmButton = firstDmItem.locator('[data-sidebar="menu-button"]');
const closeDmButton = firstDmItem.getByRole("button", {
name: "Close direct message",
});
await firstDmItem.hover();
await expect(closeDmButton).toBeVisible();
await closeDmButton.hover();
await expect(firstDmButton).toHaveCSS("background-color", hoverSurface);
const scrollbarThumbColor = await sidebarScroller.evaluate(
(element) =>
getComputedStyle(element, "::-webkit-scrollbar-thumb").backgroundColor,
);
expect(scrollbarThumbColor).toBe(hoverSurface);
}
async function expectIconlessSectionTitleAligned(
page: Page,
listTestId: "stream-list" | "dm-list",
) {
const titleBox = await page
.getByTestId(`${listTestId}-section-label`)
.locator("[data-sidebar-section-title]")
.boundingBox();
const firstRowIconX = await page
.getByTestId(listTestId)
.locator('[data-sidebar="menu-button"]')
.first()
.evaluate((element) => {
const box = element.getBoundingClientRect();
const paddingLeft = Number.parseFloat(
getComputedStyle(element).paddingLeft,
);
return box.x + paddingLeft;
});
expect(titleBox).not.toBeNull();
if (!titleBox) {
throw new Error(`Sidebar section ${listTestId} is missing label geometry`);
}
expect(Math.abs(titleBox.x - firstRowIconX)).toBeLessThanOrEqual(0.5);
}
async function expectBuzzContentShadow(page: Page, mode: "light" | "dark") {
const effects = await page.evaluate(() => {
const shell = document.querySelector(".buzz-huddle-shell");
const content = document.querySelector("[data-buzz-content-surface]");
const shadowViewport = document.querySelector(
"[data-buzz-shadow-viewport]",
);
return {
appStroke: shell ? getComputedStyle(shell, "::before").boxShadow : "",
contentShadow: content ? getComputedStyle(content).boxShadow : "",
shadowViewportOverflow: shadowViewport
? getComputedStyle(shadowViewport).overflow
: "",
};
});
expect(effects.appStroke).toBe("none");
if (mode === "light") {
expect(effects.contentShadow).toContain("4px");
expect(effects.contentShadow).toContain("rgba(0, 0, 0, 0.07)");
expect(effects.shadowViewportOverflow).toBe("visible");
} else {
expect(effects.contentShadow).not.toContain("4px");
expect(effects.contentShadow).not.toContain("rgba(255, 255, 255, 0.07)");
expect(effects.shadowViewportOverflow).toBe("hidden");
}
}
async function expectBuzzGradientPaint(
page: Page,
mode: "light" | "dark",
): Promise<string> {
const paint = await page.evaluate(() => {
const root = document.documentElement;
const appSurface = document.querySelector(".buzz-huddle-app-surface");
const lightLayer = document.querySelector('[data-buzz-gradient="light"]');
const darkLayer = document.querySelector('[data-buzz-gradient="dark"]');
const sidebarRoot = document.querySelector(
'[data-testid="app-sidebar"], [data-testid="settings-sidebar"]',
);
const sidebarSurface =
sidebarRoot?.querySelector('[data-sidebar="sidebar"]') ?? sidebarRoot;
const appStyles = appSurface ? getComputedStyle(appSurface) : null;
const lightStyles = lightLayer ? getComputedStyle(lightLayer) : null;
const darkStyles = darkLayer ? getComputedStyle(darkLayer) : null;
return {
isDark: root.classList.contains("dark"),
theme: root.getAttribute("data-buzz-theme"),
surfaceImage: appStyles?.backgroundImage ?? "",
lightImage: lightStyles?.backgroundImage ?? "",
lightOpacity: lightStyles?.opacity ?? "",
darkImage: darkStyles?.backgroundImage ?? "",
darkOpacity: darkStyles?.opacity ?? "",
sidebarImage: sidebarSurface
? getComputedStyle(sidebarSurface).backgroundImage
: "",
};
});
expect(paint.theme).toBe(mode === "light" ? "buzz" : "buzz-dark");
expect(paint.isDark).toBe(mode === "dark");
expect(paint.surfaceImage).toBe("none");
expect(paint.lightImage).not.toBe("");
expect(paint.lightImage).not.toBe("none");
expect(paint.darkImage).not.toBe("");
expect(paint.darkImage).not.toBe("none");
expect(paint.lightImage).not.toBe(paint.darkImage);
expect(paint.lightOpacity).toBe(mode === "light" ? "1" : "0");
expect(paint.darkOpacity).toBe(mode === "dark" ? "1" : "0");
expect(paint.sidebarImage).toBe("none");
return mode === "light" ? paint.lightImage : paint.darkImage;
}
async function expectBuzzSettingsPalette(page: Page, mode: "light" | "dark") {
const mutedColor =
mode === "light" ? "rgba(0, 0, 0, 0.4)" : "rgba(255, 255, 255, 0.4)";
const sidebar = page.getByTestId("settings-sidebar");
const sectionLabel = sidebar
.locator('[data-sidebar="group-label"]')
.filter({ hasText: "Personal" });
await expect(sectionLabel).toHaveCSS("color", mutedColor);
await expect(page.getByTestId("settings-nav-profile")).not.toHaveCSS(
"color",
mutedColor,
);
await expectBuzzGradientPaint(page, mode);
const version = page.getByTestId("settings-version");
if ((await version.count()) > 0) {
await expect(version).toHaveCSS("color", mutedColor);
}
}
async function expectAppliedBuzzTheme(
page: Page,
themeName: "buzz" | "buzz-dark",
storedTheme: "buzz" | "buzz-dark" = themeName,
) {
const isDark = themeName === "buzz-dark";
await expect
.poll(() =>
page.evaluate((storageKey) => {
const root = document.documentElement;
const styles = getComputedStyle(root);
return {
storedTheme: window.localStorage.getItem(storageKey),
isDark: root.classList.contains("dark"),
buzzTheme: root.getAttribute("data-buzz-theme"),
gradientTop: styles.getPropertyValue("--buzz-gradient-top").trim(),
gradientBottom: styles
.getPropertyValue("--buzz-gradient-bottom")
.trim(),
};
}, THEME_STORAGE_KEY),
)
.toEqual({
storedTheme,
isDark,
buzzTheme: themeName,
gradientTop: isDark ? "#4a4616" : "#e6e6b6",
gradientBottom: isDark ? "#0a1423" : "#c4d0da",
});
}
async function emitNativeThemeChange(page: Page, theme: "light" | "dark") {
await page.evaluate(async (nextTheme) => {
const tauriWindow = window as typeof window & {
__TAURI_INTERNALS__?: {
invoke?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
};
};
const invoke = tauriWindow.__TAURI_INTERNALS__?.invoke;
if (!invoke) throw new Error("Mock Tauri invoke bridge is unavailable.");
await invoke("plugin:event|emit", {
event: "tauri://theme-changed",
payload: nextTheme,
});
}, theme);
}
test("buzz light sidebar gradient", async ({ page }) => {
await seedTheme(page, "buzz");
await installMockBridge(page);
await openChannel(page);
await expectBuzzGradientPaint(page, "light");
await expectBuzzSidebarPalette(page, "light");
await expectBuzzContentShadow(page, "light");
await expectIconlessSectionTitleAligned(page, "stream-list");
await expectIconlessSectionTitleAligned(page, "dm-list");
await waitForAnimations(page);
await page
.getByTestId("app-sidebar")
.screenshot({ path: `${SHOTS}/01-buzz-light-sidebar.png` });
});
test("buzz dark sidebar gradient", async ({ page }) => {
await seedTheme(page, "buzz-dark");
await installMockBridge(page);
await openChannel(page);
await expectBuzzGradientPaint(page, "dark");
await expectBuzzSidebarPalette(page, "dark");
await expectBuzzContentShadow(page, "dark");
await expectIconlessSectionTitleAligned(page, "stream-list");
await expectIconlessSectionTitleAligned(page, "dm-list");
await expect(page.locator("[data-buzz-content-surface]")).toHaveCSS(
"background-color",
"rgb(26, 26, 26)",
);
await waitForAnimations(page);
await page
.getByTestId("app-sidebar")
.screenshot({ path: `${SHOTS}/02-buzz-dark-sidebar.png` });
});
test("custom section icon and name align with channel columns", async ({
page,
}) => {
await seedTheme(page, "buzz");
await seedIconChannelSection(page);
await installMockBridge(page);
await openChannel(page);
const sectionIconBox = await page
.getByTestId("section-icon-alignment-section")
.boundingBox();
const sectionTitleBox = await page
.getByTestId("section-title-alignment-section")
.boundingBox();
const channelButton = page.getByTestId("channel-general");
const channelIconBox = await channelButton
.locator("svg")
.first()
.boundingBox();
const channelTitleBox = await channelButton
.locator("[data-sidebar-row-label]")
.boundingBox();
expect(sectionIconBox).not.toBeNull();
expect(sectionTitleBox).not.toBeNull();
expect(channelIconBox).not.toBeNull();
expect(channelTitleBox).not.toBeNull();
if (
!sectionIconBox ||
!sectionTitleBox ||
!channelIconBox ||
!channelTitleBox
) {
throw new Error("Custom section alignment geometry is missing");
}
expect(Math.abs(sectionIconBox.x - channelIconBox.x)).toBeLessThanOrEqual(
0.5,
);
expect(Math.abs(sectionTitleBox.x - channelTitleBox.x)).toBeLessThanOrEqual(
0.5,
);
});
async function openAppearance(page: Page, mode: "system" | "light" | "dark") {
// Settings renders at the AppShell level; open it via the profile card
// button, then select the Appearance section.
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-settings").click();
await page.getByTestId("profile-popover-settings").click();
await page.getByTestId("settings-nav-appearance").click();
const panel = page.getByTestId("settings-theme");
await expect(panel).toBeVisible({ timeout: 10_000 });
await page.getByTestId(`appearance-mode-${mode}`).click();
await waitForAnimations(page);
return panel;
}
test("appearance picker — system tab (Buzz follows OS)", async ({ page }) => {
await seedTheme(page, "buzz");
await installMockBridge(page);
const panel = await openAppearance(page, "system");
await panel.screenshot({ path: `${SHOTS}/03-picker-system.png` });
});
test("appearance picker — light tab (Buzz)", async ({ page }) => {
await seedTheme(page, "buzz");
await installMockBridge(page);
const panel = await openAppearance(page, "light");
await panel.screenshot({ path: `${SHOTS}/04-picker-light.png` });
});
test("appearance picker — dark tab (Buzz Dark)", async ({ page }) => {
await seedTheme(page, "buzz-dark");
await installMockBridge(page);
const panel = await openAppearance(page, "dark");
await panel.screenshot({ path: `${SHOTS}/05-picker-dark.png` });
});
test("settings nav uses Buzz active pill + hover (light)", async ({ page }) => {
await seedTheme(page, "buzz");
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-settings").click();
await page.getByTestId("profile-popover-settings").click();
const sidebar = page.getByTestId("settings-sidebar");
await expect(sidebar).toBeVisible({ timeout: 10_000 });
const profileRow = page.getByTestId("settings-nav-profile");
const profileLabel = profileRow.locator('[data-sidebar="menu-label"]');
await expect(profileRow).toHaveAttribute("data-active", "true");
await expect(profileRow).toHaveCSS("font-weight", "600");
const selectedLabelBox = await profileLabel.boundingBox();
// Appearance is the active section here; its nav row should carry the Buzz
// white active pill (data-active=true), matching the Left Nav treatment.
await page.getByTestId("settings-nav-appearance").click();
await expect(profileRow).toHaveCSS("font-weight", "400");
const unselectedLabelBox = await profileLabel.boundingBox();
expect(selectedLabelBox).not.toBeNull();
expect(unselectedLabelBox).not.toBeNull();
if (!selectedLabelBox || !unselectedLabelBox) {
throw new Error("Settings nav label geometry is missing");
}
expect(Math.abs(selectedLabelBox.width - unselectedLabelBox.width)).toBe(0);
await expectBuzzSettingsPalette(page, "light");
const activeRow = page.getByTestId("settings-nav-appearance");
await expect(activeRow).toHaveAttribute("data-active", "true");
await waitForAnimations(page);
await sidebar.screenshot({ path: `${SHOTS}/06-settings-nav-light.png` });
});
test("settings nav uses Buzz active pill + hover (dark)", async ({ page }) => {
await seedTheme(page, "buzz-dark");
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-settings").click();
await page.getByTestId("profile-popover-settings").click();
const sidebar = page.getByTestId("settings-sidebar");
await expect(sidebar).toBeVisible({ timeout: 10_000 });
await page.getByTestId("settings-nav-appearance").click();
await expectBuzzSettingsPalette(page, "dark");
await expect(page.getByTestId("settings-content-surface")).toHaveCSS(
"background-color",
"rgb(26, 26, 26)",
);
await waitForAnimations(page);
await sidebar.screenshot({ path: `${SHOTS}/07-settings-nav-dark.png` });
await page.getByTestId("settings-view").screenshot({
path: `${SHOTS}/09-settings-content-dark.png`,
});
});
test("settings content uses the same inset surface as the main app", async ({
page,
}) => {
await seedTheme(page, "buzz");
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
const searchBox = await page.getByTestId("open-search").boundingBox();
await page.getByTestId("open-settings").click();
await page.getByTestId("profile-popover-settings").click();
const settingsView = page.getByTestId("settings-view");
const contentSurface = page.getByTestId("settings-content-surface");
const backToAppBox = await page
.getByTestId("settings-back-to-app")
.boundingBox();
await expect(contentSurface).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("settings-content-scroll")).toHaveCSS(
"padding-top",
"24px",
);
const viewBox = await settingsView.boundingBox();
const surfaceBox = await contentSurface.boundingBox();
expect(searchBox).not.toBeNull();
expect(backToAppBox).not.toBeNull();
expect(viewBox).not.toBeNull();
expect(surfaceBox).not.toBeNull();
if (!searchBox || !backToAppBox || !viewBox || !surfaceBox) {
throw new Error("Settings layout is missing");
}
expect(Math.abs(backToAppBox.y - searchBox.y)).toBeLessThanOrEqual(0.5);
// Match the normal app shell: a fixed 40px top chrome strip, then a 1px
// top/left inset and 8px right/bottom inset around the rounded content card.
expect(surfaceBox.y - viewBox.y).toBe(41);
expect(surfaceBox.x - viewBox.x).toBe(1);
expect(viewBox.x + viewBox.width - (surfaceBox.x + surfaceBox.width)).toBe(8);
expect(viewBox.y + viewBox.height - (surfaceBox.y + surfaceBox.height)).toBe(
8,
);
await waitForAnimations(page);
await settingsView.screenshot({
path: `${SHOTS}/08-settings-content-inset.png`,
});
});
test("appearance hides accent picker under Buzz", async ({ page }) => {
await seedTheme(page, "buzz");
await installMockBridge(page);
const panel = await openAppearance(page, "light");
// The accent picker is hidden while a Buzz theme is active. Its neutral
// swatch testid must not be present.
await expect(page.getByTestId("accent-color-neutral")).toHaveCount(0);
await panel.screenshot({ path: `${SHOTS}/10-appearance-no-accent.png` });
});
test("accent picker reveals/hides when toggling Buzz", async ({ page }) => {
// Start on a non-Buzz theme so the accent picker is present, then select the
// Buzz tile — the picker should animate out and unmount. Reselecting a
// non-Buzz tile brings it back. Asserts the presence toggle (the motion
// wrapper) works end to end.
await seedTheme(page, "github-light");
await installMockBridge(page);
await openAppearance(page, "light");
await expect(page.getByTestId("accent-color-neutral")).toBeVisible();
// Switch to Buzz — picker should leave (allow the exit animation to settle).
await page.getByTestId("theme-option-buzz").click();
await expect(page.getByTestId("accent-color-neutral")).toHaveCount(0);
// Back to a non-Buzz theme — picker returns.
await page.getByTestId("theme-option-github-light").click();
await expect(page.getByTestId("accent-color-neutral")).toBeVisible();
});
test("Buzz light and dark modes apply live without a reload", async ({
page,
}) => {
await seedTheme(page, "buzz");
await installMockBridge(page);
await openAppearance(page, "light");
await expectAppliedBuzzTheme(page, "buzz");
const lightGradient = await expectBuzzGradientPaint(page, "light");
await page.getByTestId("appearance-mode-dark").click();
await expectAppliedBuzzTheme(page, "buzz-dark");
const darkGradient = await expectBuzzGradientPaint(page, "dark");
expect(darkGradient).not.toBe(lightGradient);
await page.getByTestId("appearance-mode-light").click();
await expectAppliedBuzzTheme(page, "buzz");
await expectBuzzGradientPaint(page, "light");
// Exercise the overlap that previously let a slower, stale theme load win.
await page.getByTestId("appearance-mode-dark").click();
await page.getByTestId("appearance-mode-light").click();
await expectAppliedBuzzTheme(page, "buzz");
});
test("Buzz follows native system theme changes without a reload", async ({
page,
}) => {
await seedTheme(page, "buzz");
await page.addInitScript(() => {
(window as typeof window & { isTauri?: boolean }).isTauri = true;
});
await installMockBridge(page);
await openAppearance(page, "system");
await emitNativeThemeChange(page, "dark");
await expectAppliedBuzzTheme(page, "buzz-dark", "buzz");
await expectBuzzGradientPaint(page, "dark");
await emitNativeThemeChange(page, "light");
await expectAppliedBuzzTheme(page, "buzz", "buzz");
await expectBuzzGradientPaint(page, "light");
});