diff --git a/packages/views/editor/attachment.test.tsx b/packages/views/editor/attachment.test.tsx index dbe0d4b57..f477962df 100644 --- a/packages/views/editor/attachment.test.tsx +++ b/packages/views/editor/attachment.test.tsx @@ -10,6 +10,7 @@ const { downloadMock, openExternalMock, openByUrlMock, + copyTextMock, } = vi.hoisted(() => ({ getAttachmentTextContentMock: vi.fn(), // Default: empty base URL so existing tests render site-relative URLs @@ -20,6 +21,7 @@ const { downloadMock: vi.fn(), openExternalMock: vi.fn(), openByUrlMock: vi.fn(), + copyTextMock: vi.fn().mockResolvedValue(true), })); vi.mock("@multica/core/api", () => ({ @@ -31,6 +33,10 @@ vi.mock("@multica/core/api", () => ({ PreviewUnsupportedError: class extends Error {}, })); +vi.mock("@multica/ui/lib/clipboard", () => ({ + copyText: copyTextMock, +})); + vi.mock("./use-download-attachment", () => ({ useDownloadAttachment: () => downloadMock, })); @@ -474,3 +480,85 @@ describe("Attachment — absolutize site-relative URLs (MUL-3192)", () => { expect(img?.getAttribute("src")).toBe("/api/attachments/abc-3/download"); }); }); + +// MUL-3192 — "Copy link" copies the rendered . When the +// surrounding AttachmentDownloadProvider has the attachment record in +// scope, pickInlineMediaURL swaps to the freshly-signed CloudFront URL +// for inline rendering, and copy reflects that. The user's request was +// to keep the description and comment surfaces consistent — they now +// both copy whatever pickInlineMediaURL chose. The earlier inconsistency +// (description copying the API endpoint while comment copied the signed +// CDN URL) was caused by `issueAttachments` not being available to the +// description's resolver in time; with the resolver populated on both +// surfaces (descEditorAttachments + comment.attachments), both surfaces +// copy the same shape. +describe("Attachment — Copy link copies the rendered src so it stays consistent across surfaces (MUL-3192)", () => { + beforeEach(() => { + copyTextMock.mockClear(); + copyTextMock.mockResolvedValue(true); + }); + + it("copies the freshly-signed CloudFront download_url when the resolver returned a record (comment / description surfaces with attachments resolved)", async () => { + const att = makeRecord({ + url: "https://prod.s3.amazonaws.com/private.png", + markdown_url: "https://api.multica.test/api/attachments/att-1/download", + download_url: + "https://cdn.multica.test/private.png?Signature=fresh&Expires=999", + }); + renderWithQuery(); + + // Sanity: is the signed URL — same value the user sees in DOM. + const renderedSrc = document.querySelector("img")?.getAttribute("src"); + expect(renderedSrc).toBe(att.download_url); + + fireEvent.click(screen.getByTitle("Copy link")); + await Promise.resolve(); + await Promise.resolve(); + expect(copyTextMock).toHaveBeenCalledWith(renderedSrc); + }); + + it("copies the persisted markdown URL when no resolver context is available (URL-only attachment)", async () => { + // No surrounding AttachmentDownloadProvider — the kind=url path + // with no resolved record. Without a swap, `` stays on the + // input URL and copy mirrors it. This is the description-surface + // behavior before the issueAttachments query has resolved. + renderWithQuery( + , + ); + + const renderedSrc = document.querySelector("img")?.getAttribute("src"); + fireEvent.click(screen.getByTitle("Copy link")); + await Promise.resolve(); + await Promise.resolve(); + expect(copyTextMock).toHaveBeenCalledWith(renderedSrc); + }); + + it("copies the public CDN URL when the record's storage URL is itself a durable public URL (CloudFront public mode)", async () => { + // markdown_url == record.url == public CDN URL, download_url is the + // bare API endpoint (not signed). pickInlineMediaURL falls through + // to record.markdown_url; copy mirrors the rendered src. + const att = makeRecord({ + url: "https://cdn.multica.test/uploads/abc.png", + markdown_url: "https://cdn.multica.test/uploads/abc.png", + download_url: "/api/attachments/att-1/download", + }); + renderWithQuery(); + + const renderedSrc = document.querySelector("img")?.getAttribute("src"); + expect(renderedSrc).toBe("https://cdn.multica.test/uploads/abc.png"); + + fireEvent.click(screen.getByTitle("Copy link")); + await Promise.resolve(); + await Promise.resolve(); + expect(copyTextMock).toHaveBeenCalledWith( + "https://cdn.multica.test/uploads/abc.png", + ); + }); +});