import { afterEach, beforeEach, describe, expect, it, rs } from "@rstest/core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { cleanup, render } from "@testing-library/react";
import type { PropsWithChildren } from "react";
const mocks = rs.hoisted(() => ({
artifactContent: {
content: undefined as string | undefined,
url: undefined as string | undefined,
sha256: undefined as string | undefined,
truncated: false,
previewBytes: undefined as number | undefined,
totalBytes: undefined as number | undefined,
fullContentRequested: false,
loadFullContent: rs.fn(),
isLoading: false,
error: undefined as unknown,
},
}));
rs.mock("@/core/artifacts/hooks", () => ({
useArtifactContent: () => mocks.artifactContent,
}));
rs.mock("@/components/workspace/messages/context", () => ({
useThread: () => ({ thread: { isLoading: false }, isMock: false }),
}));
rs.mock("@/components/workspace/artifacts/context", () => ({
useArtifacts: () => ({
artifacts: [],
setArtifacts: rs.fn(),
selectedArtifact: null,
autoSelect: false,
select: rs.fn(),
deselect: rs.fn(),
open: true,
autoOpen: false,
setOpen: rs.fn(),
drafts: {},
setDrafts: rs.fn(),
editingPath: null,
setEditingPath: rs.fn(),
}),
}));
rs.mock("@/core/auth/AuthProvider", () => ({
useAuth: () => ({ user: null }),
}));
// The real editor pulls CodeMirror into the worker bundle; these tests only
// exercise the browser-preview iframe branch, which never mounts it.
rs.mock("@/components/workspace/code-editor", () => ({
CodeEditor: () => null,
}));
rs.mock("@/core/config", () => ({
getBackendBaseURL: () => "/backend",
}));
rs.mock("@/env", () => ({
env: { NEXT_PUBLIC_STATIC_WEBSITE_ONLY: "false" },
}));
import { ArtifactFileDetail } from "@/components/workspace/artifacts/artifact-file-detail";
import { I18nProvider } from "@/core/i18n/context";
const THREAD_ID = "7cfa5f8f-a2f8-47ad-acbd-da7137baf990";
function Wrapper({ children }: PropsWithChildren) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return (
{children}
);
}
function renderDetail(filepath: string) {
return render(
,
);
}
beforeEach(() => {
mocks.artifactContent.content = undefined;
});
afterEach(() => {
cleanup();
rs.clearAllMocks();
});
describe("ArtifactFileDetail browser-preview iframe", () => {
it("renders a PDF in an iframe WITHOUT the sandbox attribute", () => {
// Chromium blocks its built-in PDF viewer inside sandbox=""; the bytes
// are safe inline because the endpoint declares application/pdf with
// X-Content-Type-Options: nosniff.
const { container } = renderDetail("/mnt/user-data/outputs/report.pdf");
const frame = container.querySelector("iframe");
if (!(frame instanceof HTMLIFrameElement)) {
throw new Error("preview iframe not rendered");
}
expect(frame.getAttribute("sandbox")).toBeNull();
expect(frame.getAttribute("src")).toBe(
`/backend/api/threads/${THREAD_ID}/artifacts/mnt/user-data/outputs/report.pdf`,
);
// Untitled frames have no accessible name (the sibling preview iframe
// keeps "Artifact preview"); dropping this regressed WCAG frame titles.
expect(frame.getAttribute("title")).toBe("report.pdf");
});
it("keeps the empty sandbox for images and other passive binaries", () => {
const { container } = renderDetail("/mnt/user-data/outputs/chart.png");
const frame = container.querySelector("iframe");
if (!(frame instanceof HTMLIFrameElement)) {
throw new Error("preview iframe not rendered");
}
expect(frame.getAttribute("sandbox")).toBe("");
expect(frame.getAttribute("src")).toBe(
`/backend/api/threads/${THREAD_ID}/artifacts/mnt/user-data/outputs/chart.png`,
);
});
});