memos/web/tests/memo-body-compact.test.tsx
johnnyjoygh 620db61f7c chore(web): unify compact truncation into a single ClampedSection
Compact cards previously had two truncation systems taped together: a
text-only clamp inside MemoContent (144px, no re-measure on image load,
fade tinted to the wrong surface) and a whole-tile cap in ColumnGrid
(hard clip mid-image, no affordance), later joined by a content-shape
rule choosing an owner per card — which could still stack two Show-more
buttons and silently broke footnote navigation in clamped cards.

Now there is exactly one mechanism: ClampedSection measures its content
and folds anything taller than 420px to a 360px preview with a fade and
a Show more/less toggle. MemoBody wraps the whole body in it (reactions
stay outside, never hidden); ColumnGrid is pure packing again and never
clips; MemoContent is a stateless renderer whose `compact` prop only
informs footnote-link behavior; MemoPreview uses a static CSS bound in
place of its previously inert toggle.

Deltas: text-only compact previews grow from ~6 rows to the same 360px
image cards get, and pinned memos clamp like any card (pinned means
ordering and a badge, not size).
2026-07-07 21:01:19 +08:00

100 lines
3.1 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { CLAMP_PREVIEW_HEIGHT_PX, CLAMP_TRIGGER_HEIGHT_PX } from "@/components/ClampedSection";
import MemoBody from "@/components/MemoView/components/MemoBody";
const mockState = vi.hoisted(() => ({
memo: {
name: "memos/1",
content: "",
relations: [],
attachments: [],
reactions: [],
},
}));
vi.mock("@/utils/i18n", () => ({
useTranslate: () => (key: string) => key,
}));
vi.mock("@/components/MemoContent/MentionResolutionContext", () => ({
useResolvedMentionUsernames: () => new Set<string>(),
}));
vi.mock("@/components/MemoContent/MemoMarkdownRenderer", () => ({
MemoMarkdownRenderer: ({ content }: { content: string }) => <div>{content}</div>,
}));
vi.mock("@/components/MemoMetadata", () => ({
AttachmentListView: () => null,
LocationDisplayView: () => null,
RelationListView: () => null,
}));
vi.mock("@/components/MemoReactionListView", () => ({
MemoReactionListView: () => null,
}));
vi.mock("@/components/MemoView/hooks", () => ({
useMemoHandlers: () => ({
handleMemoContentClick: vi.fn(),
handleMemoContentDoubleClick: vi.fn(),
}),
}));
vi.mock("@/components/MemoView/MemoViewContext", () => ({
useMemoViewContext: () => ({
memo: mockState.memo,
parentPage: "",
showBlurredContent: false,
blurred: false,
readonly: false,
openEditor: vi.fn(),
openPreview: vi.fn(),
toggleBlurVisibility: vi.fn(),
}),
}));
const createMemo = (content: string) => ({
name: "memos/1",
content,
relations: [],
attachments: [],
reactions: [],
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("<MemoBody /> compact body clamp", () => {
it("keeps a buffer between the preview height and the fold trigger", () => {
expect(CLAMP_TRIGGER_HEIGHT_PX).toBeGreaterThan(CLAMP_PREVIEW_HEIGHT_PX);
});
it("folds tall compact bodies and keeps them expanded across content changes", async () => {
// jsdom has no layout; report every element as taller than the fold trigger.
vi.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(CLAMP_TRIGGER_HEIGHT_PX + 100);
mockState.memo = createMemo("line 1\nline 2");
const { rerender } = render(<MemoBody compact={true} />);
await waitFor(() => expect(screen.getByRole("button", { name: /memo\.show-more/ })).toBeInTheDocument());
fireEvent.click(screen.getByRole("button", { name: /memo\.show-more/ }));
expect(screen.getByRole("button", { name: /memo\.show-less/ })).toBeInTheDocument();
mockState.memo = createMemo("line 1\nline 2 updated");
rerender(<MemoBody compact={true} />);
expect(screen.getByRole("button", { name: /memo\.show-less/ })).toBeInTheDocument();
});
it("renders no clamp when compact is off", () => {
vi.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(CLAMP_TRIGGER_HEIGHT_PX + 100);
mockState.memo = createMemo("tall content");
render(<MemoBody compact={false} />);
expect(screen.queryByRole("button", { name: /memo\.show-more/ })).toBeNull();
});
});