Files
buzz/desktop/src/shared/lib/fontSizePreference.test.mjs
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

113 lines
4.0 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { readFileSync } from "node:fs";
import config from "../../../tailwind.config.js";
const typographyCss = readFileSync(
new URL("../styles/globals/typography.css", import.meta.url),
"utf8",
);
const values = new Map();
const attributes = new Map();
const windowListeners = new Map();
globalThis.window = {
addEventListener: (type, listener) => windowListeners.set(type, listener),
};
globalThis.localStorage = {
getItem: (key) => values.get(key) ?? null,
setItem: (key, value) => values.set(key, String(value)),
};
globalThis.document = {
documentElement: {
setAttribute: (name, value) => attributes.set(name, value),
},
};
const preference = await import("./fontSizePreference.ts");
test("scales fixed line-height utilities with the typography rem", () => {
assert.deepEqual(config.theme.extend.lineHeight, {
3: "calc(var(--buzz-type-rem) * 0.75)",
4: "var(--buzz-type-rem)",
5: "calc(var(--buzz-type-rem) * 1.25)",
6: "calc(var(--buzz-type-rem) * 1.5)",
7: "calc(var(--buzz-type-rem) * 1.75)",
8: "calc(var(--buzz-type-rem) * 2)",
"message-author": "var(--conversation-author-line-height)",
});
});
test("derives the typography rem from the real root so zoom scales layout too", () => {
// Cmd +/- scales the root font-size; the type rem must stay rem-relative
// (never an absolute px) or text would zoom while containers froze.
assert.match(
typographyCss,
/--buzz-type-rem:\s*calc\(1rem \* var\(--buzz-type-scale\)\);/,
);
assert.doesNotMatch(typographyCss, /--buzz-type-rem:\s*[\d.]+px/);
});
test("maps the font size attribute to the 13 / 14 / 15px type contract", () => {
assert.match(typographyCss, /:root\s*\{[^}]*--buzz-type-scale:\s*1;/s);
assert.match(
typographyCss,
/:root\[data-font-size="smaller"\]\s*\{\s*--buzz-type-scale:\s*calc\(13 \/ 14\);/,
);
assert.match(
typographyCss,
/:root\[data-font-size="larger"\]\s*\{\s*--buzz-type-scale:\s*calc\(15 \/ 14\);/,
);
});
test("defaults invalid and missing font sizes to default", () => {
assert.equal(preference.parseFontSize(null), "default");
assert.equal(preference.parseFontSize("medium"), "default");
assert.equal(preference.parseFontSize("smaller"), "smaller");
assert.equal(preference.parseFontSize("default"), "default");
assert.equal(preference.parseFontSize("larger"), "larger");
});
test("persists and applies the selected font size across the app", () => {
preference.setFontSize("smaller");
assert.equal(preference.getFontSize(), "smaller");
assert.equal(values.get(preference.FONT_SIZE_STORAGE_KEY), "smaller");
assert.equal(attributes.get("data-font-size"), "smaller");
});
test("previews a font size without changing the saved preference", () => {
preference.setFontSize("smaller");
preference.previewFontSize("larger");
assert.equal(preference.getFontSize(), "smaller");
assert.equal(values.get(preference.FONT_SIZE_STORAGE_KEY), "smaller");
assert.equal(attributes.get("data-font-size"), "larger");
preference.previewFontSize(null);
assert.equal(attributes.get("data-font-size"), "smaller");
});
test("initializes from the stored font size", () => {
values.set(preference.FONT_SIZE_STORAGE_KEY, "larger");
preference.initializeFontSizePreference();
assert.equal(preference.getFontSize(), "larger");
assert.equal(attributes.get("data-font-size"), "larger");
});
test("applies font size changes from another window", () => {
values.set(preference.FONT_SIZE_STORAGE_KEY, "smaller");
windowListeners.get("storage")({ key: preference.FONT_SIZE_STORAGE_KEY });
assert.equal(preference.getFontSize(), "smaller");
assert.equal(attributes.get("data-font-size"), "smaller");
});
test("returns to the default when another window clears storage", () => {
preference.setFontSize("larger");
values.clear();
windowListeners.get("storage")({ key: null });
assert.equal(preference.getFontSize(), "default");
assert.equal(attributes.get("data-font-size"), "default");
});