Files
buzz/desktop/tests/e2e/top-chrome-zoom-clearance.spec.ts
morgmart 97b1fee5c3 fix(desktop): restore true zoom by scaling the root rem (#6514)
## Summary

Follow-up to #5644. Cmd +/- had become a text-only zoom: type scaled
while rem-based padding, gaps, widths, avatars, and controls stayed
frozen, which produced cramped layouts (see [#buzz-frontend
thread](buzz://message?channel=a410ffde-c61f-416a-96e0-c296b5f5ecc9&id=1a758115cf07b00c097f6e988553908c045165325a57637519cfa7ed9c9accec)).

Root cause: #5644 introduced a virtual typography rem so the **Font
size** preference could change text without moving layout — a good
decoupling — but it also routed **Cmd +/- zoom** through that same
px-valued token and pinned the real root at 16px. One decision ("freeze
layout") was applied to two dials that shouldn't share it.

This PR gives each dial one owner and lets CSS compose them:

| Control | Changes | How |
|---|---|---|
| **Cmd +/- zoom** | Everything — true zoom | Scales the real `<html>`
font-size again (`useWebviewZoomShortcuts`) |
| **Font size preference** | Text only | Sets `data-font-size`;
`typography.css` maps it to a unitless `--buzz-type-scale`, mirroring
how density already works |

`--buzz-type-rem` becomes `calc(1rem * var(--buzz-type-scale))` —
rem-relative, so it rides on zoom automatically. Resulting text px = `16
× zoom × scale × token-ratio`. The 13 / 14 / 15px conversation contract
is unchanged at default zoom. Density and the type ramp from #5644 are
untouched.

The preference module no longer does px math or knows about zoom; the
zoom hook no longer imports the preference module. Net deletion in
production code.

## Validation

- `pnpm test` — 5,308 desktop unit tests
- `pnpm check:px-text`, `tsc --noEmit`, biome
- Playwright: `top-chrome-zoom-clearance.spec.ts` (native-chrome
clearance stays fixed under root zoom),
`inbox-refactor-screenshots.spec.ts` (zoomed row padding now asserts
`4.4px` instead of the frozen `4px`), and both `profile.spec.ts` zoom
tests (composed zoom × preference, cross-window storage reset)
- Before/after screenshots at 140% zoom in the comment below

---------

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-21 17:21:36 -07:00

176 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { installMockBridge } from "../helpers/bridge";
type TauriConfig = {
app: {
windows: Array<{
trafficLightPosition?: { x: number; y: number };
}>;
};
};
const tauriConfig = JSON.parse(
readFileSync(
new URL("../../src-tauri/tauri.conf.json", import.meta.url),
"utf8",
),
) as TauriConfig;
const EXPECTED_TRAFFIC_LIGHT_POSITION = { x: 16, y: 25 };
const EXPECTED_NAV_CENTER_Y = 23;
// The macOS traffic lights are native chrome: with `trafficLightPosition`
// x:16 they occupy roughly x 16–68 regardless of the app's Cmd +/- text
// zoom. The top-chrome nav row must clear that band in fixed px, so the
// clearance cannot change when text scales.
const TRAFFIC_LIGHT_RIGHT_EDGE = 72;
async function spoofMacPlatform(page: import("@playwright/test").Page) {
await page.addInitScript(() => {
Object.defineProperty(navigator, "platform", { get: () => "MacIntel" });
});
}
async function firstNavButtonX(page: import("@playwright/test").Page) {
const toggle = page.locator('[data-testid="app-top-chrome"] button').first();
await expect(toggle).toBeVisible();
const box = await toggle.boundingBox();
expect(box).not.toBeNull();
return box?.x ?? 0;
}
// The chrome buttons are styled to visually match the fixed-size native
// controls, so their box must not follow the rem text scale either. The
// sidebar toggle is 28px square; the back/forward history buttons share the
// height but are deliberately narrower (24px).
const NAV_BUTTON_SIZE = 28;
const HISTORY_BUTTON_WIDTH = 24;
// The grabber/drag strip hosting the buttons must hold its height too —
// otherwise Cmd+ balloons the bar around the fixed-size buttons and Cmd-
// collapses it.
const TOP_CHROME_BAR_HEIGHT = 40;
async function expectTopChromeFixedHeight(
page: import("@playwright/test").Page,
) {
const bar = page.getByTestId("app-top-chrome");
await expect(bar).toBeVisible();
const box = await bar.boundingBox();
expect(box).not.toBeNull();
expect(box?.height ?? 0).toBe(TOP_CHROME_BAR_HEIGHT);
}
async function expectNavButtonsFixedSize(
page: import("@playwright/test").Page,
) {
const buttons = page.locator('[data-testid="app-top-chrome"] button');
const count = await buttons.count();
expect(count).toBeGreaterThan(0);
for (let i = 0; i < count; i += 1) {
const button = buttons.nth(i);
const label = await button.getAttribute("aria-label");
const isHistoryButton = label === "Go back" || label === "Go forward";
const box = await button.boundingBox();
expect(box).not.toBeNull();
expect(box?.width ?? 0).toBe(
isHistoryButton ? HISTORY_BUTTON_WIDTH : NAV_BUTTON_SIZE,
);
expect(box?.height ?? 0).toBe(NAV_BUTTON_SIZE);
}
}
async function seedTextScale(
page: import("@playwright/test").Page,
scale: number,
) {
await page.addInitScript((value) => {
window.localStorage.setItem("buzz:text-scale", String(value));
}, scale);
}
// Cmd +/- scales the real root font-size so every rem in the app zooms. The
// top chrome below must stay fixed *despite* that, which is what these tests
// guard.
async function expectRootFontSize(
page: import("@playwright/test").Page,
fontSize: string,
) {
await expect
.poll(() =>
page.evaluate(() => getComputedStyle(document.documentElement).fontSize),
)
.toBe(fontSize);
}
test.describe("top chrome macOS traffic-light clearance under text zoom", () => {
test("nav buttons clear the traffic lights at default zoom", async ({
page,
}) => {
await spoofMacPlatform(page);
await installMockBridge(page);
await page.goto("/");
// Lock the native and webview placements together: removing this explicit
// Tauri inset or shifting the nav row regresses the macOS chrome alignment.
expect(tauriConfig.app.windows[0]?.trafficLightPosition).toEqual(
EXPECTED_TRAFFIC_LIGHT_POSITION,
);
const toggleBox = await page
.getByRole("button", { name: "Toggle Sidebar", exact: true })
.boundingBox();
expect(toggleBox).not.toBeNull();
// Tauri interprets y:25 as a native titlebar inset, not the literal
// traffic-light center. The native controls use a small optical correction
// while the adjacent web controls remain centered at y:23.
expect((toggleBox?.y ?? 0) + (toggleBox?.height ?? 0) / 2).toBe(
EXPECTED_NAV_CENTER_Y,
);
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
TRAFFIC_LIGHT_RIGHT_EDGE,
);
await expectNavButtonsFixedSize(page);
await expectTopChromeFixedHeight(page);
});
test("nav buttons still clear the traffic lights when zoomed out", async ({
page,
}) => {
await spoofMacPlatform(page);
// Seed the minimum Cmd- text scale (0.75). The old rem-based clearance
// (pl-20 = 5rem) shrank to 60px here, sliding the buttons under the
// fixed-position native controls.
await seedTextScale(page, 0.75);
await installMockBridge(page);
await page.goto("/");
// Confirm the zoomed-out scale reached the root.
await expectRootFontSize(page, "12px");
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
TRAFFIC_LIGHT_RIGHT_EDGE,
);
await expectNavButtonsFixedSize(page);
await expectTopChromeFixedHeight(page);
});
test("nav buttons keep their fixed size when zoomed in", async ({ page }) => {
await spoofMacPlatform(page);
// Seed the maximum Cmd+ text scale (1.5). Rem-sized buttons (h-7 = 42px
// here) grew visibly taller than the fixed-size traffic lights.
await seedTextScale(page, 1.5);
await installMockBridge(page);
await page.goto("/");
await expectRootFontSize(page, "24px");
expect(await firstNavButtonX(page)).toBeGreaterThanOrEqual(
TRAFFIC_LIGHT_RIGHT_EDGE,
);
await expectNavButtonsFixedSize(page);
await expectTopChromeFixedHeight(page);
});
});