mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 11:10:23 +02:00
* feat(editor): HTML attachments render like images (MUL-2345 v4) HTML attachments no longer wear the file-card chrome (icon + filename row). They now render as a sandboxed iframe with a hover-revealed right-top toolbar (Open / Download / Copy code), mirroring the image attachment visual model. - New HtmlAttachmentPreview owns the iframe + hover toolbar plus three states (loading / success / error). Failure mode keeps the toolbar pinned open and Open/Download enabled so the user is never stranded without an escape hatch — Copy code disables when the text body is unavailable. - New AttachmentBlock thin dispatcher picks the renderer per kind: html + attachmentId + !uploading -> HtmlAttachmentPreview, else AttachmentCard. All three entry points (file-card NodeView, readonly file-card, standalone AttachmentList) call AttachmentBlock, so feature work on a new kind only touches one place. - AttachmentCard collapses back to a pure file-card row UI: the inline HTML iframe branch (InlineHtmlIframe + inlineHtmlEnabled + showInlineHtml) is removed. - AttachmentBlock added to the editor barrel export. Sandbox/server-side defenses unchanged: sandbox="allow-scripts" (no allow-same-origin), srcDoc, server still returns text/plain + nosniff on the /content proxy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * 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> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
|
|
vi.mock("../i18n", () => ({
|
|
useT: () => ({
|
|
t: (sel: (s: Record<string, Record<string, string>>) => string) =>
|
|
sel({
|
|
image: { download: "Download" },
|
|
attachment: {
|
|
preview: "Preview",
|
|
preview_loading: "Loading preview…",
|
|
},
|
|
file_card: { uploading: "Uploading {{filename}}" },
|
|
}),
|
|
}),
|
|
}));
|
|
|
|
import { AttachmentCard } from "./attachment-card";
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
describe("AttachmentCard — chrome row", () => {
|
|
it("renders chrome only and never an inline iframe (HTML rich preview lives in HtmlAttachmentPreview)", () => {
|
|
render(
|
|
<AttachmentCard
|
|
filename="report.html"
|
|
contentType="text/html"
|
|
attachmentId="att-1"
|
|
href="https://cdn.example/report.html"
|
|
onPreview={() => {}}
|
|
onDownload={() => {}}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("report.html")).toBeTruthy();
|
|
expect(document.querySelector("iframe")).toBeNull();
|
|
});
|
|
|
|
it("hides the Eye button for an html URL-only source (the modal's /content proxy is ID-keyed)", () => {
|
|
// Regression: a cross-comment / copy-pasted `!file[report.html](url)`
|
|
// used to surface a dead Eye button — text kinds need an attachmentId,
|
|
// otherwise tryOpen rejects and the click becomes a silent no-op.
|
|
render(
|
|
<AttachmentCard
|
|
filename="report.html"
|
|
contentType="text/html"
|
|
href="https://cdn.example/report.html"
|
|
onPreview={() => {}}
|
|
onDownload={() => {}}
|
|
/>,
|
|
);
|
|
expect(screen.queryByTitle("Preview")).toBeNull();
|
|
// Download stays available — the underlying URL is still reachable.
|
|
expect(screen.getByTitle("Download")).toBeTruthy();
|
|
});
|
|
|
|
it("shows the Eye button for an html source when an attachmentId is available", () => {
|
|
render(
|
|
<AttachmentCard
|
|
filename="report.html"
|
|
contentType="text/html"
|
|
attachmentId="att-1"
|
|
href="https://cdn.example/report.html"
|
|
onPreview={() => {}}
|
|
onDownload={() => {}}
|
|
/>,
|
|
);
|
|
expect(screen.getByTitle("Preview")).toBeTruthy();
|
|
});
|
|
|
|
it("shows the Eye button for a URL-only pdf source (modal renders pdfs directly from URL)", () => {
|
|
// Counterpart to the html regression: media kinds (pdf/video/audio)
|
|
// ARE URL-previewable because the modal renders them via
|
|
// <iframe src=url>/<video>/<audio>, not via the /content proxy.
|
|
render(
|
|
<AttachmentCard
|
|
filename="manual.pdf"
|
|
contentType="application/pdf"
|
|
href="https://cdn.example/manual.pdf"
|
|
onPreview={() => {}}
|
|
onDownload={() => {}}
|
|
/>,
|
|
);
|
|
expect(screen.getByTitle("Preview")).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("AttachmentCard — Eye / Download buttons", () => {
|
|
it("invokes onPreview when Eye is clicked", () => {
|
|
const onPreview = vi.fn();
|
|
render(
|
|
<AttachmentCard
|
|
filename="manual.pdf"
|
|
contentType="application/pdf"
|
|
attachmentId="att-1"
|
|
href="https://cdn.example/manual.pdf"
|
|
onPreview={onPreview}
|
|
onDownload={() => {}}
|
|
/>,
|
|
);
|
|
fireEvent.mouseDown(screen.getByTitle("Preview"));
|
|
expect(onPreview).toHaveBeenCalled();
|
|
});
|
|
|
|
it("invokes onDownload when Download is clicked", () => {
|
|
const onDownload = vi.fn();
|
|
render(
|
|
<AttachmentCard
|
|
filename="manual.pdf"
|
|
contentType="application/pdf"
|
|
attachmentId="att-1"
|
|
href="https://cdn.example/manual.pdf"
|
|
onPreview={() => {}}
|
|
onDownload={onDownload}
|
|
/>,
|
|
);
|
|
fireEvent.mouseDown(screen.getByTitle("Download"));
|
|
expect(onDownload).toHaveBeenCalled();
|
|
});
|
|
|
|
it("hides Eye and Download buttons while uploading", () => {
|
|
render(
|
|
<AttachmentCard
|
|
filename="report.html"
|
|
contentType="text/html"
|
|
attachmentId="att-1"
|
|
href="https://cdn.example/report.html"
|
|
uploading
|
|
onPreview={() => {}}
|
|
onDownload={() => {}}
|
|
/>,
|
|
);
|
|
expect(screen.queryByTitle("Preview")).toBeNull();
|
|
expect(screen.queryByTitle("Download")).toBeNull();
|
|
// The mock `t()` returns the i18n template as-is; the production t-fn
|
|
// interpolates {{filename}} → "report.html". Asserting the template
|
|
// proves the uploading branch was selected without depending on the
|
|
// interpolation behavior of the mock.
|
|
expect(screen.getByText("Uploading {{filename}}")).toBeTruthy();
|
|
});
|
|
});
|