mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
test(editor): pin three entry points to AttachmentBlock HTML route (MUL-2345)
Reviewer flagged that the v4 dispatcher refactor only had tests on the shared AttachmentBlock + HtmlAttachmentPreview; the three real call sites at file-card.tsx:59, readonly-content.tsx:279, and comment-card.tsx:152 had no regression coverage. Reverting any one would silently lose the inline HTML iframe path — the exact MUL-2330 regression we're meant to be locking down. Each new test renders the real entry point with an HTML+attachmentId fixture and asserts the dispatched iframe (sandbox=allow-scripts, srcdoc) shows up while the AttachmentCard chrome (filename row) does not. FileCardView and AttachmentList are exported from their files for direct rendering, mirroring the existing CodeBlockView test pattern. Mutation-tested locally: temporarily flipping each site back to <AttachmentCard> turns its corresponding test red. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
105
packages/views/editor/extensions/file-card.test.tsx
Normal file
105
packages/views/editor/extensions/file-card.test.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
// Tiptap NodeView primitives can't be instantiated without a full editor.
|
||||
// Stub the wrapper so FileCardView renders as a plain React component and
|
||||
// the DOM can be inspected directly.
|
||||
vi.mock("@tiptap/react", () => ({
|
||||
NodeViewWrapper: ({ children, ...rest }: any) => <div {...rest}>{children}</div>,
|
||||
}));
|
||||
|
||||
const { getAttachmentTextContentMock, resolveAttachmentMock, openByUrlMock, tryOpenMock } =
|
||||
vi.hoisted(() => ({
|
||||
getAttachmentTextContentMock: vi.fn(),
|
||||
resolveAttachmentMock: vi.fn(),
|
||||
openByUrlMock: vi.fn(),
|
||||
tryOpenMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/api", () => ({
|
||||
api: { getAttachmentTextContent: getAttachmentTextContentMock },
|
||||
PreviewTooLargeError: class extends Error {},
|
||||
PreviewUnsupportedError: class extends Error {},
|
||||
}));
|
||||
|
||||
vi.mock("../attachment-download-context", () => ({
|
||||
useAttachmentDownloadResolver: () => ({
|
||||
openByUrl: openByUrlMock,
|
||||
resolveAttachment: resolveAttachmentMock,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../attachment-preview-modal", () => ({
|
||||
useAttachmentPreview: () => ({ tryOpen: tryOpenMock, open: vi.fn(), modal: null }),
|
||||
}));
|
||||
|
||||
vi.mock("../i18n", () => ({
|
||||
useT: () => ({
|
||||
t: (sel: (s: Record<string, Record<string, string>>) => string) =>
|
||||
sel({
|
||||
image: { download: "Download" },
|
||||
attachment: {
|
||||
preview: "Preview",
|
||||
preview_loading: "Loading preview…",
|
||||
preview_failed: "Couldn't load preview",
|
||||
},
|
||||
code_block: { copy_code: "Copy code" },
|
||||
file_card: { uploading: "Uploading {{filename}}" },
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
import { FileCardView } from "./file-card";
|
||||
|
||||
function renderWithQuery(ui: ReactElement) {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||
});
|
||||
return render(<QueryClientProvider client={qc}>{ui}</QueryClientProvider>);
|
||||
}
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("FileCardView — HTML attachment routes through AttachmentBlock to iframe", () => {
|
||||
// Regression pin for file-card.tsx:59. The NodeView must render through
|
||||
// <AttachmentBlock>, not the older <AttachmentCard>. If someone reverts that
|
||||
// line, the dispatcher's html+attachmentId branch is bypassed and the user
|
||||
// is left with the file-card chrome — exactly the bug MUL-2330 surfaced.
|
||||
it("renders an iframe (no file-card chrome) when the node resolves to an HTML attachment", async () => {
|
||||
resolveAttachmentMock.mockReturnValue({
|
||||
id: "att-1",
|
||||
content_type: "text/html",
|
||||
url: "/uploads/report.html",
|
||||
filename: "report.html",
|
||||
});
|
||||
getAttachmentTextContentMock.mockResolvedValueOnce({
|
||||
text: "<p>chart</p>",
|
||||
originalContentType: "text/html",
|
||||
});
|
||||
|
||||
const node = {
|
||||
attrs: {
|
||||
href: "/uploads/report.html",
|
||||
filename: "report.html",
|
||||
uploading: false,
|
||||
},
|
||||
} as any;
|
||||
|
||||
renderWithQuery(<FileCardView node={node} {...({} as any)} />);
|
||||
|
||||
const frame = await waitFor(() => {
|
||||
const f = document.querySelector("iframe") as HTMLIFrameElement | null;
|
||||
expect(f).toBeTruthy();
|
||||
return f!;
|
||||
});
|
||||
expect(frame.getAttribute("sandbox")).toBe("allow-scripts");
|
||||
expect(frame.getAttribute("srcdoc")).toContain("<p>chart</p>");
|
||||
// The AttachmentCard chrome surfaces the filename as text inside its row.
|
||||
// HtmlAttachmentPreview replaces the chrome entirely, so the filename
|
||||
// must not appear as visible text.
|
||||
expect(screen.queryByText("report.html")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,7 @@ const FILE_CARD_MARKDOWN_RE = new RegExp(
|
||||
// React NodeView
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function FileCardView({ node }: NodeViewProps) {
|
||||
export function FileCardView({ node }: NodeViewProps) {
|
||||
const href = (node.attrs.href as string) || "";
|
||||
const filename = (node.attrs.filename as string) || "";
|
||||
const uploading = node.attrs.uploading as boolean;
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const { getAttachmentTextContentMock } = vi.hoisted(() => ({
|
||||
getAttachmentTextContentMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/api", () => ({
|
||||
api: { getAttachmentTextContent: getAttachmentTextContentMock },
|
||||
PreviewTooLargeError: class extends Error {},
|
||||
PreviewUnsupportedError: class extends Error {},
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/paths", () => ({
|
||||
useWorkspacePaths: () => ({
|
||||
@@ -253,3 +265,47 @@ describe("ReadonlyContent HTML block rendering", () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ReadonlyContent file-card → AttachmentBlock HTML routing", () => {
|
||||
// Regression pin for readonly-content.tsx:279. The `div data-type=fileCard`
|
||||
// branch must render through <AttachmentBlock>, not the older
|
||||
// <AttachmentCard>. Reverting that line would skip the html+attachmentId
|
||||
// dispatcher branch and surface the bare file-card chrome (filename row)
|
||||
// instead of the rendered iframe — the exact regression MUL-2330 fixed.
|
||||
function renderWithQuery(ui: ReactElement) {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||
});
|
||||
return render(<QueryClientProvider client={qc}>{ui}</QueryClientProvider>);
|
||||
}
|
||||
|
||||
it("renders the !file[](url) HTML attachment as an iframe (no file-card chrome)", async () => {
|
||||
getAttachmentTextContentMock.mockResolvedValueOnce({
|
||||
text: "<p>chart</p>",
|
||||
originalContentType: "text/html",
|
||||
});
|
||||
const attachment = {
|
||||
id: "att-1",
|
||||
url: "/uploads/report.html",
|
||||
filename: "report.html",
|
||||
content_type: "text/html",
|
||||
size_bytes: 0,
|
||||
} as any;
|
||||
const { container, queryByText } = renderWithQuery(
|
||||
<ReadonlyContent
|
||||
content="!file[report.html](/uploads/report.html)"
|
||||
attachments={[attachment]}
|
||||
/>,
|
||||
);
|
||||
const frame = await waitFor(() => {
|
||||
const f = container.querySelector<HTMLIFrameElement>("iframe");
|
||||
expect(f).not.toBeNull();
|
||||
return f!;
|
||||
});
|
||||
expect(frame.getAttribute("sandbox")).toBe("allow-scripts");
|
||||
expect(frame.getAttribute("srcdoc")).toContain("<p>chart</p>");
|
||||
// AttachmentCard chrome surfaces the filename as visible text in a
|
||||
// <p class="truncate"> row. HtmlAttachmentPreview replaces it entirely.
|
||||
expect(queryByText("report.html")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
64
packages/views/issues/components/comment-card.test.tsx
Normal file
64
packages/views/issues/components/comment-card.test.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import type { ReactElement } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
const { getAttachmentTextContentMock } = vi.hoisted(() => ({
|
||||
getAttachmentTextContentMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/api", () => ({
|
||||
api: {
|
||||
getAttachmentTextContent: getAttachmentTextContentMock,
|
||||
getAttachment: vi.fn(),
|
||||
},
|
||||
PreviewTooLargeError: class extends Error {},
|
||||
PreviewUnsupportedError: class extends Error {},
|
||||
}));
|
||||
|
||||
import { AttachmentList } from "./comment-card";
|
||||
|
||||
function renderWithQuery(ui: ReactElement) {
|
||||
const qc = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||
});
|
||||
return render(<QueryClientProvider client={qc}>{ui}</QueryClientProvider>);
|
||||
}
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("AttachmentList — standalone HTML attachment routes through AttachmentBlock", () => {
|
||||
// Regression pin for comment-card.tsx:152. This is the entry point
|
||||
// MUL-2330 originally regressed on: standalone HTML attachments (not
|
||||
// referenced inline in the markdown body) MUST render through
|
||||
// <AttachmentBlock> so the html+attachmentId dispatch fires. Reverting to
|
||||
// <AttachmentCard> here re-introduces the "report.html shows as a bare
|
||||
// file card row instead of the rendered chart" bug.
|
||||
it("renders an iframe (no file-card chrome) for a standalone HTML attachment", async () => {
|
||||
getAttachmentTextContentMock.mockResolvedValueOnce({
|
||||
text: "<p>chart</p>",
|
||||
originalContentType: "text/html",
|
||||
});
|
||||
const attachment = {
|
||||
id: "att-1",
|
||||
url: "/uploads/report.html",
|
||||
filename: "report.html",
|
||||
content_type: "text/html",
|
||||
size_bytes: 0,
|
||||
} as any;
|
||||
|
||||
renderWithQuery(<AttachmentList attachments={[attachment]} content="" />);
|
||||
|
||||
const frame = await waitFor(() => {
|
||||
const f = document.querySelector("iframe") as HTMLIFrameElement | null;
|
||||
expect(f).toBeTruthy();
|
||||
return f!;
|
||||
});
|
||||
expect(frame.getAttribute("sandbox")).toBe("allow-scripts");
|
||||
expect(frame.getAttribute("srcdoc")).toContain("<p>chart</p>");
|
||||
// AttachmentCard chrome would render the filename as visible <p> text;
|
||||
// HtmlAttachmentPreview replaces the row entirely.
|
||||
expect(screen.queryByText("report.html")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -121,7 +121,7 @@ function DeleteCommentDialog({
|
||||
// Standalone attachment list — renders attachments not already in the markdown
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function AttachmentList({ attachments, content, className }: { attachments?: Attachment[]; content?: string; className?: string }) {
|
||||
export function AttachmentList({ attachments, content, className }: { attachments?: Attachment[]; content?: string; className?: string }) {
|
||||
const download = useDownloadAttachment();
|
||||
const preview = useAttachmentPreview();
|
||||
if (!attachments?.length) return null;
|
||||
|
||||
Reference in New Issue
Block a user