Decouple conversation font size and density

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-08-12 10:42:42 +01:00
parent 74f74e3d37
commit 2a24f8e5a0
10 changed files with 394 additions and 63 deletions

View File

@@ -20,6 +20,12 @@ import {
useConversationDensity,
type ConversationDensity,
} from "@/shared/lib/conversationDensityPreference";
import {
previewConversationFontSize,
setConversationFontSize,
useConversationFontSize,
type ConversationFontSize,
} from "@/shared/lib/conversationFontSizePreference";
import {
ACCENT_COLORS,
DEFAULT_GLASS_OPACITY,
@@ -105,6 +111,24 @@ const CONVERSATION_DENSITY_OPTIONS: readonly {
},
];
const CONVERSATION_FONT_SIZE_OPTIONS: readonly {
value: ConversationFontSize;
label: string;
}[] = [
{
value: "smaller",
label: "Smaller",
},
{
value: "default",
label: "Default",
},
{
value: "larger",
label: "Larger",
},
];
function ConversationDensityPreviewMessage({
avatar,
author,
@@ -138,18 +162,18 @@ function ConversationDensityPreviewMessage({
);
}
function ConversationDensityPreview() {
function ConversationPreview() {
return (
<div className="px-4 py-3" data-testid="conversation-density-preview">
<div className="px-4 py-3" data-testid="conversation-preview">
<div
aria-hidden="true"
className="relative overflow-hidden rounded-xl border border-border/65 bg-muted/45"
data-testid="conversation-density-preview-surface"
data-testid="conversation-preview-surface"
>
<span className="absolute right-3 top-3 inline-flex rounded-full border border-border/65 bg-background/70 px-2.5 py-1 text-xs font-medium text-muted-foreground">
Preview
</span>
<div className="p-4" data-testid="conversation-density-preview-content">
<div className="p-4" data-testid="conversation-preview-content">
<ConversationDensityPreviewMessage
avatar="M"
author="Maya"
@@ -176,12 +200,34 @@ function ConversationDensityPreview() {
);
}
/** Message sizing and spacing shared by every conversation surface. */
export function ConversationDensitySetting() {
/** Independent message type and spacing controls shared by every conversation. */
export function ConversationDisplaySettings() {
const density = useConversationDensity();
const fontSize = useConversationFontSize();
return (
<>
<SettingsOptionRow data-testid="conversation-font-size-row">
<div className="min-w-0">
<p className="text-sm font-medium">Font size</p>
<p
className="text-sm font-normal text-muted-foreground/70"
data-settings-subcopy
>
Adjust message text and line height.
</p>
</div>
<SettingsSegmentedControl
className="w-72"
legend="Font size"
onPreviewChange={previewConversationFontSize}
onValueChange={setConversationFontSize}
optionTestIdPrefix="conversation-font-size"
options={CONVERSATION_FONT_SIZE_OPTIONS}
testId="conversation-font-size-control"
value={fontSize}
/>
</SettingsOptionRow>
<SettingsOptionRow data-testid="conversation-density-row">
<div className="min-w-0">
<p className="text-sm font-medium">Conversation density</p>
@@ -189,7 +235,7 @@ export function ConversationDensitySetting() {
className="text-sm font-normal text-muted-foreground/70"
data-settings-subcopy
>
Adjust message size and spacing.
Adjust message and paragraph spacing.
</p>
</div>
<SettingsSegmentedControl
@@ -203,7 +249,7 @@ export function ConversationDensitySetting() {
value={density}
/>
</SettingsOptionRow>
<ConversationDensityPreview />
<ConversationPreview />
</>
);
}

View File

@@ -55,7 +55,7 @@ import {
import { appearanceCommunityLabel } from "../lib/appearanceScopeCopy";
import {
AccentPickerContent,
ConversationDensitySetting,
ConversationDisplaySettings,
GlassBackgroundSetting,
LinkPreviewStyleSetting,
ProminentActiveTabSetting,
@@ -782,7 +782,7 @@ function ThemeSettingsCard() {
data-testid="appearance-preferences-card"
title="Preferences"
>
<ConversationDensitySetting />
<ConversationDisplaySettings />
<LinkPreviewStyleSetting />
<ThreadLayoutSetting />
</SettingsOptionGroup>

View File

@@ -21,6 +21,7 @@ import { TooltipProvider } from "@/shared/ui/tooltip";
import { recoverLocalStorageQuotaOnStartup } from "@/shared/lib/localStorageQuota";
import { startLocalStorageSweep } from "@/shared/lib/localStorageSweep";
import { initializeConversationDensityPreference } from "@/shared/lib/conversationDensityPreference";
import { initializeConversationFontSizePreference } from "@/shared/lib/conversationFontSizePreference";
type E2eWindow = Window & {
__BUZZ_E2E__?: unknown;
@@ -126,6 +127,7 @@ async function bootstrap() {
configureDevE2eBridgeFromUrl();
recoverLocalStorageQuotaOnStartup();
initializeConversationDensityPreference();
initializeConversationFontSizePreference();
startLocalStorageSweep();
await installE2eBridgeIfConfigured();
await migrateLegacyCommunityStorageBeforeRender();

View File

@@ -1,6 +1,6 @@
import * as React from "react";
/** Device-level message sizing and spacing used across conversation surfaces. */
/** Device-level spacing used across conversation surfaces. */
export type ConversationDensity = "compact" | "comfortable" | "spacious";
export const CONVERSATION_DENSITY_STORAGE_KEY =

View File

@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import test from "node:test";
const values = new Map();
const attributes = new Map();
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("./conversationFontSizePreference.ts");
test("defaults invalid and missing conversation font sizes to default", () => {
assert.equal(preference.parseConversationFontSize(null), "default");
assert.equal(preference.parseConversationFontSize("medium"), "default");
assert.equal(preference.parseConversationFontSize("smaller"), "smaller");
assert.equal(preference.parseConversationFontSize("default"), "default");
assert.equal(preference.parseConversationFontSize("larger"), "larger");
});
test("persists and applies the selected conversation font size", () => {
preference.setConversationFontSize("smaller");
assert.equal(preference.getConversationFontSize(), "smaller");
assert.equal(
values.get(preference.CONVERSATION_FONT_SIZE_STORAGE_KEY),
"smaller",
);
assert.equal(attributes.get("data-conversation-font-size"), "smaller");
});
test("previews a font size without changing the saved preference", () => {
preference.setConversationFontSize("smaller");
preference.previewConversationFontSize("larger");
assert.equal(preference.getConversationFontSize(), "smaller");
assert.equal(
values.get(preference.CONVERSATION_FONT_SIZE_STORAGE_KEY),
"smaller",
);
assert.equal(attributes.get("data-conversation-font-size"), "larger");
preference.previewConversationFontSize(null);
assert.equal(attributes.get("data-conversation-font-size"), "smaller");
});
test("initializes from the stored conversation font size", () => {
values.set(preference.CONVERSATION_FONT_SIZE_STORAGE_KEY, "larger");
preference.initializeConversationFontSizePreference();
assert.equal(preference.getConversationFontSize(), "larger");
assert.equal(attributes.get("data-conversation-font-size"), "larger");
});

View File

@@ -0,0 +1,84 @@
import * as React from "react";
/** Device-level text sizing used across conversation surfaces. */
export type ConversationFontSize = "smaller" | "default" | "larger";
export const CONVERSATION_FONT_SIZE_STORAGE_KEY =
"buzz.appearance.conversationFontSize";
export const DEFAULT_CONVERSATION_FONT_SIZE: ConversationFontSize = "default";
const listeners = new Set<() => void>();
let conversationFontSize: ConversationFontSize = DEFAULT_CONVERSATION_FONT_SIZE;
export function parseConversationFontSize(
value: string | null | undefined,
): ConversationFontSize {
return value === "smaller" || value === "default" || value === "larger"
? value
: DEFAULT_CONVERSATION_FONT_SIZE;
}
function readStoredConversationFontSize(): ConversationFontSize {
try {
return parseConversationFontSize(
globalThis.localStorage?.getItem(CONVERSATION_FONT_SIZE_STORAGE_KEY),
);
} catch {
return DEFAULT_CONVERSATION_FONT_SIZE;
}
}
function applyConversationFontSize(size: ConversationFontSize): void {
globalThis.document?.documentElement?.setAttribute(
"data-conversation-font-size",
size,
);
}
function notifyListeners(): void {
for (const listener of listeners) listener();
}
/** Apply the persisted preference before React renders to avoid a layout jump. */
export function initializeConversationFontSizePreference(): void {
const nextSize = readStoredConversationFontSize();
const changed = nextSize !== conversationFontSize;
conversationFontSize = nextSize;
applyConversationFontSize(nextSize);
if (changed) notifyListeners();
}
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function getConversationFontSize(): ConversationFontSize {
return conversationFontSize;
}
export function setConversationFontSize(size: ConversationFontSize): void {
conversationFontSize = size;
applyConversationFontSize(size);
try {
globalThis.localStorage?.setItem(CONVERSATION_FONT_SIZE_STORAGE_KEY, size);
} catch {
// Persistence is best-effort; the live preference still applies.
}
notifyListeners();
}
/** Temporarily apply a size without changing the saved preference. */
export function previewConversationFontSize(
size: ConversationFontSize | null,
): void {
applyConversationFontSize(size ?? conversationFontSize);
}
export function useConversationFontSize(): ConversationFontSize {
return React.useSyncExternalStore(
subscribe,
getConversationFontSize,
() => DEFAULT_CONVERSATION_FONT_SIZE,
);
}

View File

@@ -3,9 +3,9 @@
/* Catppuccin Latte (mauve accent) */
--radius: 0.625rem;
/*
* Comfortable conversation rhythm for channels, DMs, threads, Inbox, and
* the composer. The Appearance preference overrides this complete scale
* below, while rem units keep Cmd +/- text scaling intact.
* Default conversation type and comfy spacing for channels, DMs, threads,
* Inbox, and the composer. Independent Appearance preferences override the
* two scales below, while rem units keep Cmd +/- text scaling intact.
*/
--conversation-message-font-size: 0.9375rem;
--conversation-message-line-height: 1.25rem;
@@ -80,25 +80,31 @@
}
:root[data-conversation-density="compact"] {
--conversation-message-font-size: 0.875rem;
--conversation-message-line-height: 1.1875rem;
--conversation-author-line-height: 1rem;
--conversation-body-gap: 0rem;
--conversation-row-padding-block: 0.25rem;
--conversation-paragraph-gap: 0.375rem;
--conversation-list-item-gap: 0.25rem;
--conversation-timestamp-font-size: 0.625rem;
--conversation-timestamp-line-height: 0.875rem;
}
:root[data-conversation-density="spacious"] {
--conversation-message-font-size: 1rem;
--conversation-message-line-height: 1.375rem;
--conversation-author-line-height: 1.125rem;
--conversation-body-gap: 0.25rem;
--conversation-row-padding-block: 0.5rem;
--conversation-paragraph-gap: 0.625rem;
--conversation-list-item-gap: 0.5rem;
}
:root[data-conversation-font-size="smaller"] {
--conversation-message-font-size: 0.875rem;
--conversation-message-line-height: 1.1875rem;
--conversation-author-line-height: 1rem;
--conversation-timestamp-font-size: 0.625rem;
--conversation-timestamp-line-height: 0.875rem;
}
:root[data-conversation-font-size="larger"] {
--conversation-message-font-size: 1rem;
--conversation-message-line-height: 1.375rem;
--conversation-author-line-height: 1.125rem;
--conversation-timestamp-font-size: 0.75rem;
--conversation-timestamp-line-height: 1.125rem;
}

View File

@@ -12,8 +12,8 @@ export default {
"3xs": "0.5rem", // 8px — tiny glyphs / micro labels
badge: "0.625rem", // 10px — compact status badges
// Shared channel, DM, thread, and composer type. Backed by CSS
// variables so the Appearance preference can change the complete
// conversation rhythm without branching individual components.
// variables so font size and density can change independently without
// branching individual components.
message: [
"var(--conversation-message-font-size)",
{ lineHeight: "var(--conversation-message-line-height)" },

View File

@@ -9,6 +9,8 @@ const GLASS_BACKGROUND_STORAGE_KEY = "buzz-glass-background";
const GLASS_OPACITY_STORAGE_KEY = "buzz-glass-opacity";
const PROMINENT_ACTIVE_TAB_STORAGE_KEY = "buzz-prominent-active-tab";
const CONVERSATION_DENSITY_STORAGE_KEY = "buzz.appearance.conversationDensity";
const CONVERSATION_FONT_SIZE_STORAGE_KEY =
"buzz.appearance.conversationFontSize";
const MOCK_PUBKEY = "deadbeef".repeat(8);
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
@@ -617,7 +619,7 @@ test("appearance groups theme and preferences into labeled rows", async ({
).toBeLessThanOrEqual(0.5);
});
test("conversation density applies and persists all three scales", async ({
test("conversation font size and density apply independently", async ({
page,
}) => {
await seedTheme(page, "buzz");
@@ -625,27 +627,35 @@ test("conversation density applies and persists all three scales", async ({
await openAppearance(page, "light");
const root = page.locator("html");
const control = page.getByTestId("conversation-density-control");
const densityControl = page.getByTestId("conversation-density-control");
const compact = page.getByTestId("conversation-density-compact");
const comfortable = page.getByTestId("conversation-density-comfortable");
const spacious = page.getByTestId("conversation-density-spacious");
const indicator = page.getByTestId("conversation-density-control-indicator");
const preview = page.getByTestId("conversation-density-preview");
const previewSurface = page.getByTestId(
"conversation-density-preview-surface",
const densityIndicator = page.getByTestId(
"conversation-density-control-indicator",
);
const previewContent = page.getByTestId(
"conversation-density-preview-content",
const fontSizeControl = page.getByTestId("conversation-font-size-control");
const smaller = page.getByTestId("conversation-font-size-smaller");
const defaultSize = page.getByTestId("conversation-font-size-default");
const larger = page.getByTestId("conversation-font-size-larger");
const fontSizeIndicator = page.getByTestId(
"conversation-font-size-control-indicator",
);
const preview = page.getByTestId("conversation-preview");
const previewSurface = page.getByTestId("conversation-preview-surface");
const previewContent = page.getByTestId("conversation-preview-content");
const previewChip = preview.getByText("Preview");
const firstPreviewMessage = previewSurface.locator("article").first();
const previewMessage = preview.getByText(
"The revised conversation layout is ready to review.",
);
const previewTimestamp = preview.getByText("9:41");
const description = page
const densityDescription = page
.getByTestId("conversation-density-row")
.locator("[data-settings-subcopy]");
const fontSizeDescription = page
.getByTestId("conversation-font-size-row")
.locator("[data-settings-subcopy]");
const readScale = () =>
root.evaluate((element) => {
const style = window.getComputedStyle(element);
@@ -710,12 +720,21 @@ test("conversation density applies and persists all three scales", async ({
await expect(
page.getByRole("group", { name: "Conversation density" }),
).toBeVisible();
await expect(control).toHaveAccessibleName("Conversation density");
await expect(page.getByRole("group", { name: "Font size" })).toBeVisible();
await expect(densityControl).toHaveAccessibleName("Conversation density");
await expect(fontSizeControl).toHaveAccessibleName("Font size");
await expect(comfortable).toHaveText("Comfy");
await expect(defaultSize).toHaveText("Default");
await expect(preview).toContainText("Preview");
await expect(preview).not.toContainText("Message #design");
await expect(comfortable).toHaveAttribute("aria-pressed", "true");
await expect(description).toHaveText("Adjust message size and spacing.");
await expect(defaultSize).toHaveAttribute("aria-pressed", "true");
await expect(densityDescription).toHaveText(
"Adjust message and paragraph spacing.",
);
await expect(fontSizeDescription).toHaveText(
"Adjust message text and line height.",
);
await expect.poll(readScale).toEqual({
authorLineHeight: 1.0625,
bodyGap: 0.125,
@@ -729,13 +748,18 @@ test("conversation density applies and persists all three scales", async ({
await expect
.poll(() =>
Promise.all([
control.evaluate((element) => element.getBoundingClientRect().width),
densityControl.evaluate(
(element) => element.getBoundingClientRect().width,
),
fontSizeControl.evaluate(
(element) => element.getBoundingClientRect().width,
),
page
.getByTestId("appearance-color-mode-control")
.evaluate((element) => element.getBoundingClientRect().width),
]),
)
.toEqual([288, 240]);
.toEqual([288, 288, 240]);
await expect
.poll(() =>
previewMessage.evaluate((element) => {
@@ -752,15 +776,17 @@ test("conversation density applies and persists all three scales", async ({
});
await expect.poll(readPreviewTimestampScale).toEqual(["11px", "16px"]);
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
await expect(indicator).toHaveCSS("transition-duration", "0.2s");
await expect(indicator).toHaveCSS("transition-property", /transform/);
await expect(densityIndicator).toHaveCSS("transition-duration", "0.2s");
await expect(densityIndicator).toHaveCSS("transition-property", /transform/);
await expect(fontSizeIndicator).toHaveCSS("transition-duration", "0.2s");
await expect(fontSizeIndicator).toHaveCSS("transition-property", /transform/);
await expect
.poll(async () => {
const [previewBackground, controlBackground] = await Promise.all([
previewSurface.evaluate(
(element) => window.getComputedStyle(element).backgroundColor,
),
control.evaluate(
densityControl.evaluate(
(element) => window.getComputedStyle(element).backgroundColor,
),
]);
@@ -774,7 +800,7 @@ test("conversation density applies and persists all three scales", async ({
expect(previewChipBox).not.toBeNull();
expect(firstPreviewMessageBox).not.toBeNull();
if (!previewSurfaceBox || !previewChipBox || !firstPreviewMessageBox) {
throw new Error("Conversation density preview geometry is missing");
throw new Error("Conversation preview geometry is missing");
}
const previewChipRightInset =
previewSurfaceBox.x +
@@ -804,7 +830,7 @@ test("conversation density applies and persists all three scales", async ({
await expect(previewChip).toHaveCSS("padding-top", "4px");
await expect(previewChip).toHaveCSS("padding-bottom", "4px");
await indicator.evaluate((element) => {
await densityIndicator.evaluate((element) => {
element.addEventListener(
"transitionrun",
() => element.setAttribute("data-transition-ran", "true"),
@@ -813,10 +839,10 @@ test("conversation density applies and persists all three scales", async ({
});
await compact.click();
await expect(indicator).toHaveAttribute("data-transition-ran", "true");
await expect(densityIndicator).toHaveAttribute("data-transition-ran", "true");
await expect(root).toHaveAttribute("data-conversation-density", "compact");
await expect(root).toHaveAttribute("data-conversation-font-size", "default");
await expect(compact).toHaveAttribute("aria-pressed", "true");
await expect(description).toHaveText("Adjust message size and spacing.");
await expect
.poll(() =>
page.evaluate(
@@ -826,14 +852,14 @@ test("conversation density applies and persists all three scales", async ({
)
.toBe("compact");
await expect.poll(readScale).toEqual({
authorLineHeight: 1,
authorLineHeight: 1.0625,
bodyGap: 0,
fontSize: 0.875,
lineHeight: 1.1875,
fontSize: 0.9375,
lineHeight: 1.25,
paragraphGap: 0.375,
rowPadding: 0.25,
timestampFontSize: 0.625,
timestampLineHeight: 0.875,
timestampFontSize: 0.6875,
timestampLineHeight: 1,
});
await expect.poll(readSettingsScale).toEqual({
fontSize: "14px",
@@ -841,17 +867,41 @@ test("conversation density applies and persists all three scales", async ({
minHeight: "64px",
paddingBlock: "12px",
});
await expect.poll(readPreviewTimestampScale).toEqual(["10px", "14px"]);
await expect.poll(readPreviewTimestampScale).toEqual(["11px", "16px"]);
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
await larger.click();
await expect(root).toHaveAttribute("data-conversation-density", "compact");
await expect(root).toHaveAttribute("data-conversation-font-size", "larger");
await expect(larger).toHaveAttribute("aria-pressed", "true");
await expect
.poll(() =>
page.evaluate(
(key) => window.localStorage.getItem(key),
CONVERSATION_FONT_SIZE_STORAGE_KEY,
),
)
.toBe("larger");
await expect.poll(readScale).toEqual({
authorLineHeight: 1.125,
bodyGap: 0,
fontSize: 1,
lineHeight: 1.375,
paragraphGap: 0.375,
rowPadding: 0.25,
timestampFontSize: 0.75,
timestampLineHeight: 1.125,
});
await expect.poll(readPreviewTimestampScale).toEqual(["12px", "18px"]);
await waitForAnimations(page);
await page.getByTestId("appearance-preferences-card").screenshot({
path: `${SHOTS}/15-conversation-density-compact.png`,
path: `${SHOTS}/15-conversation-compact-larger.png`,
});
await spacious.click();
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
await expect(root).toHaveAttribute("data-conversation-font-size", "larger");
await expect(spacious).toHaveAttribute("aria-pressed", "true");
await expect(description).toHaveText("Adjust message size and spacing.");
await expect.poll(readScale).toEqual({
authorLineHeight: 1.125,
bodyGap: 0.25,
@@ -870,19 +920,36 @@ test("conversation density applies and persists all three scales", async ({
});
await expect.poll(readPreviewTimestampScale).toEqual(["12px", "18px"]);
await expect.poll(readSettingsChromeScale).toEqual(["24px", "14px", "18px"]);
await smaller.click();
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
await expect(root).toHaveAttribute("data-conversation-font-size", "smaller");
await expect(smaller).toHaveAttribute("aria-pressed", "true");
await expect.poll(readScale).toEqual({
authorLineHeight: 1,
bodyGap: 0.25,
fontSize: 0.875,
lineHeight: 1.1875,
paragraphGap: 0.625,
rowPadding: 0.5,
timestampFontSize: 0.625,
timestampLineHeight: 0.875,
});
await expect.poll(readPreviewTimestampScale).toEqual(["10px", "14px"]);
await waitForAnimations(page);
await page.getByTestId("appearance-preferences-card").screenshot({
path: `${SHOTS}/16-conversation-density-spacious.png`,
path: `${SHOTS}/16-conversation-spacious-smaller.png`,
});
await comfortable.click();
await defaultSize.click();
await expect(root).toHaveAttribute(
"data-conversation-density",
"comfortable",
);
await expect(comfortable).toHaveAttribute("aria-pressed", "true");
const controlBox = await control.boundingBox();
const controlBox = await densityControl.boundingBox();
const compactBox = await compact.boundingBox();
const spaciousBox = await spacious.boundingBox();
expect(controlBox).not.toBeNull();
@@ -901,6 +968,7 @@ test("conversation density applies and persists all three scales", async ({
controlBox.y + controlBox.height / 2,
);
await expect(root).toHaveAttribute("data-conversation-density", "spacious");
await expect(root).toHaveAttribute("data-conversation-font-size", "default");
await expect
.poll(() =>
page.evaluate(
@@ -909,14 +977,16 @@ test("conversation density applies and persists all three scales", async ({
),
)
.toBe("comfortable");
await expect
.poll(() =>
previewMessage.evaluate((element) => {
const style = window.getComputedStyle(element);
return [style.fontSize, style.lineHeight];
}),
)
.toEqual(["16px", "22px"]);
await expect.poll(readScale).toEqual({
authorLineHeight: 1.0625,
bodyGap: 0.25,
fontSize: 0.9375,
lineHeight: 1.25,
paragraphGap: 0.625,
rowPadding: 0.5,
timestampFontSize: 0.6875,
timestampLineHeight: 1,
});
await expect.poll(readSettingsScale).toEqual({
fontSize: "14px",
lineHeight: "20px",
@@ -934,9 +1004,60 @@ test("conversation density applies and persists all three scales", async ({
)
.toBe("spacious");
await comfortable.click();
const fontSizeControlBox = await fontSizeControl.boundingBox();
const smallerBox = await smaller.boundingBox();
const largerBox = await larger.boundingBox();
expect(fontSizeControlBox).not.toBeNull();
expect(smallerBox).not.toBeNull();
expect(largerBox).not.toBeNull();
if (!fontSizeControlBox || !smallerBox || !largerBox) {
throw new Error("Conversation font size control geometry is missing");
}
await page.mouse.move(
smallerBox.x + smallerBox.width / 2,
fontSizeControlBox.y + fontSizeControlBox.height / 2,
);
await page.mouse.down();
await page.mouse.move(
largerBox.x + largerBox.width / 2,
fontSizeControlBox.y + fontSizeControlBox.height / 2,
);
await expect(root).toHaveAttribute("data-conversation-font-size", "larger");
await expect(root).toHaveAttribute(
"data-conversation-density",
"comfortable",
);
await expect
.poll(() =>
page.evaluate(
(key) => window.localStorage.getItem(key),
CONVERSATION_FONT_SIZE_STORAGE_KEY,
),
)
.toBe("default");
await expect
.poll(() =>
previewMessage.evaluate((element) => {
const style = window.getComputedStyle(element);
return [style.fontSize, style.lineHeight];
}),
)
.toEqual(["16px", "22px"]);
await page.mouse.up();
await expect(larger).toHaveAttribute("aria-pressed", "true");
await expect
.poll(() =>
page.evaluate(
(key) => window.localStorage.getItem(key),
CONVERSATION_FONT_SIZE_STORAGE_KEY,
),
)
.toBe("larger");
await defaultSize.click();
await waitForAnimations(page);
await page.getByTestId("appearance-preferences-card").screenshot({
path: `${SHOTS}/14-conversation-density.png`,
path: `${SHOTS}/14-conversation-preferences.png`,
});
});
@@ -1352,6 +1473,8 @@ test("glass background keeps the content panel solid", async ({ page }) => {
const matchingRadiusControls = [
page.getByTestId("appearance-color-mode-control"),
page.getByTestId("appearance-color-mode-indicator"),
page.getByTestId("conversation-font-size-control"),
page.getByTestId("conversation-font-size-control-indicator"),
page.getByTestId("conversation-density-control"),
page.getByTestId("conversation-density-control-indicator"),
page.getByTestId("theme-style-trigger"),

View File

@@ -427,6 +427,18 @@ test.describe("inbox refactor screenshots", () => {
await page.locator("html").evaluate((root) => {
root.setAttribute("data-conversation-density", "compact");
});
await expect.poll(readConversationMetrics).toEqual([
{ fontSize: "15px", lineHeight: "20px" },
{ paddingBottom: "4px", paddingTop: "4px" },
{ fontSize: "15px", lineHeight: "17px" },
{ fontSize: "15px", lineHeight: "20px" },
{ fontSize: "11px", lineHeight: "16px" },
{ fontSize: "15px", lineHeight: "20px" },
]);
await page.locator("html").evaluate((root) => {
root.setAttribute("data-conversation-font-size", "smaller");
});
await expect.poll(readConversationMetrics).toEqual([
{ fontSize: "14px", lineHeight: "19px" },
{ paddingBottom: "4px", paddingTop: "4px" },
@@ -440,6 +452,7 @@ test.describe("inbox refactor screenshots", () => {
await page.locator("html").evaluate((root) => {
root.setAttribute("data-conversation-density", "spacious");
root.setAttribute("data-conversation-font-size", "larger");
});
await expect.poll(readConversationMetrics).toEqual([
{ fontSize: "16px", lineHeight: "22px" },
@@ -454,6 +467,7 @@ test.describe("inbox refactor screenshots", () => {
await page.locator("html").evaluate((root) => {
root.setAttribute("data-conversation-density", "comfortable");
root.setAttribute("data-conversation-font-size", "default");
});
await page.evaluate(() => {