diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer.test.tsx
new file mode 100644
index 000000000..591cf9307
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ArtifactVisualizer.test.tsx
@@ -0,0 +1,296 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import { userEvent } from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import type { ArtifactNodeResponse } from "@/api/types.gen";
+
+import ArtifactVisualizer from "./ArtifactVisualizer";
+
+vi.mock("@/providers/BackendProvider", () => ({
+ useBackend: () => ({ backendUrl: "http://localhost:8000" }),
+}));
+
+vi.mock("@/services/executionService", () => ({
+ getArtifactSignedUrl: vi.fn().mockResolvedValue({
+ signed_url: "https://storage.example.com/signed",
+ }),
+}));
+
+vi.mock("./TextVisualizer", () => ({
+ TextVisualizerValue: ({ value }: { value: string }) => (
+
+ ),
+ TextVisualizerRemote: ({ signedUrl }: { signedUrl: string }) => (
+
+ ),
+}));
+
+vi.mock("./ImageVisualizer", () => ({
+ default: ({ src, name }: { src: string; name: string }) => (
+
+ ),
+}));
+
+vi.mock("./CsvVisualizer", () => ({
+ CsvVisualizerValue: ({ value }: { value: string }) => (
+
+ ),
+ CsvVisualizerRemote: ({ signedUrl }: { signedUrl: string }) => (
+
+ ),
+}));
+
+vi.mock("./JsonVisualizer", () => ({
+ JsonVisualizerValue: ({ value, name }: { value: string; name: string }) => (
+
+ ),
+ JsonVisualizerRemote: ({
+ signedUrl,
+ name,
+ }: {
+ signedUrl: string;
+ name: string;
+ }) => (
+
+ ),
+}));
+
+vi.mock("./ParquetVisualizer", () => ({
+ default: ({ signedUrl }: { signedUrl: string }) => (
+
+ ),
+}));
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+});
+
+const renderWithQuery = (ui: React.ReactElement) =>
+ render({ui});
+
+const makeArtifact = (
+ overrides?: Partial,
+): ArtifactNodeResponse => ({
+ id: "artifact-1",
+ artifact_data: { total_size: 1024, is_dir: false },
+ ...overrides,
+});
+
+beforeEach(() => {
+ queryClient.clear();
+});
+
+describe("ArtifactVisualizer", () => {
+ describe("trigger button", () => {
+ it("renders Eye + Preview button when no value is provided", () => {
+ renderWithQuery(
+ ,
+ );
+
+ expect(screen.getByText("Preview")).toBeInTheDocument();
+ });
+
+ it("renders Maximize2 button when value is provided", () => {
+ renderWithQuery(
+ ,
+ );
+
+ expect(screen.queryByText("Preview")).not.toBeInTheDocument();
+ // The maximize button should be present (ghost button without text)
+ expect(screen.getByRole("button")).toBeInTheDocument();
+ });
+ });
+
+ it("returns null for non-visualizable types", () => {
+ const { container } = renderWithQuery(
+ ,
+ );
+
+ expect(container.innerHTML).toBe("");
+ });
+
+ describe("inline value rendering", () => {
+ it("renders TextVisualizerValue for text type", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button"));
+
+ await waitFor(() => {
+ const viz = screen.getByTestId("text-visualizer");
+ expect(viz).toHaveAttribute("data-value", "hello");
+ });
+ });
+
+ it("renders CsvVisualizerValue for csv type", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button"));
+
+ await waitFor(() => {
+ const viz = screen.getByTestId("csv-visualizer");
+ expect(viz).toHaveAttribute("data-value", "a,b\n1,2");
+ });
+ });
+
+ it("renders CsvVisualizerValue for tsv type", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button"));
+
+ await waitFor(() => {
+ const viz = screen.getByTestId("csv-visualizer");
+ expect(viz).toHaveAttribute("data-value", "a\tb\n1\t2");
+ });
+ });
+
+ it("renders JsonVisualizerValue for jsonobject type", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button"));
+
+ await waitFor(() => {
+ const viz = screen.getByTestId("json-visualizer");
+ expect(viz).toHaveAttribute("data-value", '{"key":"val"}');
+ });
+ });
+ });
+
+ describe("signed URL rendering", () => {
+ it("renders TextVisualizerRemote with signedUrl for text type", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByText("Preview"));
+
+ await waitFor(() => {
+ const viz = screen.getByTestId("text-visualizer");
+ expect(viz).toHaveAttribute(
+ "data-signed-url",
+ "https://storage.example.com/signed",
+ );
+ });
+ });
+
+ it("renders ImageVisualizer for image type", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByText("Preview"));
+
+ await waitFor(() => {
+ expect(screen.getByTestId("image-visualizer")).toBeInTheDocument();
+ });
+ });
+
+ it("renders ParquetVisualizer for apacheparquet type", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByText("Preview"));
+
+ await waitFor(() => {
+ expect(screen.getByTestId("parquet-visualizer")).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe("dialog header", () => {
+ it("shows artifact name and type in dialog", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button"));
+
+ await waitFor(() => {
+ expect(screen.getByText("my-output")).toBeInTheDocument();
+ expect(screen.getByText("CSV")).toBeInTheDocument();
+ });
+ });
+
+ it("shows artifact URI when available", async () => {
+ renderWithQuery(
+ ,
+ );
+
+ await userEvent.click(screen.getByRole("button"));
+
+ await waitFor(() => {
+ expect(screen.getByText("Copy URI")).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/CsvVisualizer.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/CsvVisualizer.test.tsx
new file mode 100644
index 000000000..4f2f0b0c5
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/CsvVisualizer.test.tsx
@@ -0,0 +1,161 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import { type ReactElement, Suspense } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { CsvVisualizerRemote, CsvVisualizerValue } from "./CsvVisualizer";
+
+vi.mock("./TableVisualizer", () => ({
+ default: ({
+ data,
+ isFullscreen,
+ }: {
+ data: { headers: string[]; rows: string[][] };
+ isFullscreen: boolean;
+ }) => (
+
+ ),
+}));
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+});
+
+const renderWithQuery = (ui: ReactElement) =>
+ render({ui});
+
+const renderWithSuspense = (ui: ReactElement) =>
+ render(
+
+ (
+
+ {error instanceof Error ? error.message : "Unknown error"}
+
+ )}
+ >
+ Loading}>
+ {ui}
+
+
+ ,
+ );
+
+beforeEach(() => {
+ queryClient.clear();
+});
+
+describe("CsvVisualizerValue", () => {
+ it("parses CSV and renders TableVisualizer", () => {
+ renderWithQuery(
+ ,
+ );
+
+ const table = screen.getByTestId("table-visualizer");
+ expect(table).toHaveAttribute("data-headers", "Name,Age");
+ expect(table).toHaveAttribute("data-row-count", "2");
+ });
+
+ it("parses TSV with tab delimiter", () => {
+ renderWithQuery(
+ ,
+ );
+
+ const table = screen.getByTestId("table-visualizer");
+ expect(table).toHaveAttribute("data-headers", "Name,Age");
+ });
+
+ it("shows 'No data' for empty CSV", () => {
+ renderWithQuery();
+ expect(screen.getByText("No data")).toBeInTheDocument();
+ });
+
+ it("does not fetch", () => {
+ const fetchSpy = vi.spyOn(globalThis, "fetch");
+ renderWithQuery(
+ ,
+ );
+ expect(fetchSpy).not.toHaveBeenCalled();
+ fetchSpy.mockRestore();
+ });
+});
+
+describe("CsvVisualizerRemote", () => {
+ it("fetches and renders CSV data", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ text: () => Promise.resolve("X,Y\n1,2\n3,4"),
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ const table = screen.getByTestId("table-visualizer");
+ expect(table).toHaveAttribute("data-headers", "X,Y");
+ expect(table).toHaveAttribute("data-row-count", "2");
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("renders error boundary on fetch failure", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: false,
+ status: 403,
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toBeInTheDocument();
+ expect(screen.getByText(/Failed to fetch artifact/)).toBeInTheDocument();
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("passes isFullscreen to TableVisualizer", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ text: () => Promise.resolve("A,B\n1,2"),
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("table-visualizer")).toHaveAttribute(
+ "data-fullscreen",
+ "true",
+ );
+ });
+
+ vi.restoreAllMocks();
+ });
+});
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ImageVisualizer.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ImageVisualizer.test.tsx
new file mode 100644
index 000000000..472cd78dc
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ImageVisualizer.test.tsx
@@ -0,0 +1,22 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import ImageVisualizer from "./ImageVisualizer";
+
+describe("ImageVisualizer", () => {
+ it("renders an image with the correct src and alt", () => {
+ render(
+ ,
+ );
+ const img = screen.getByRole("img", { name: "test-image" });
+ expect(img).toHaveAttribute("src", "https://example.com/image.png");
+ });
+
+ it("applies object-contain styling", () => {
+ render(
+ ,
+ );
+ const img = screen.getByRole("img");
+ expect(img).toHaveClass("object-contain");
+ });
+});
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/JsonVisualizer.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/JsonVisualizer.test.tsx
new file mode 100644
index 000000000..09e9a2606
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/JsonVisualizer.test.tsx
@@ -0,0 +1,105 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import { type ReactElement, Suspense } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { JsonVisualizerRemote, JsonVisualizerValue } from "./JsonVisualizer";
+
+vi.mock("../IOCodeViewer", () => ({
+ default: ({ title, value }: { title: string; value: string }) => (
+
+ {value}
+
+ ),
+}));
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+});
+
+const renderWithQuery = (ui: ReactElement) =>
+ render({ui});
+
+const renderWithSuspense = (ui: ReactElement) =>
+ render(
+
+ (
+
+ {error instanceof Error ? error.message : "Unknown error"}
+
+ )}
+ >
+ Loading}>
+ {ui}
+
+
+ ,
+ );
+
+beforeEach(() => {
+ queryClient.clear();
+});
+
+describe("JsonVisualizerValue", () => {
+ it("renders the JSON value via IOCodeViewer", () => {
+ const json = '{"key": "value"}';
+ renderWithQuery();
+
+ const viewer = screen.getByTestId("io-code-viewer");
+ expect(viewer).toHaveAttribute("data-title", "output.json");
+ expect(viewer).toHaveTextContent(json);
+ });
+
+ it("does not fetch", () => {
+ const fetchSpy = vi.spyOn(globalThis, "fetch");
+ renderWithQuery();
+ expect(fetchSpy).not.toHaveBeenCalled();
+ fetchSpy.mockRestore();
+ });
+});
+
+describe("JsonVisualizerRemote", () => {
+ it("renders fetched JSON content", async () => {
+ const json = '{"fetched": true}';
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ text: () => Promise.resolve(json),
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("io-code-viewer")).toHaveTextContent(json);
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("renders error boundary on fetch failure", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: false,
+ status: 500,
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toBeInTheDocument();
+ expect(screen.getByText(/Failed to fetch artifact/)).toBeInTheDocument();
+ });
+
+ vi.restoreAllMocks();
+ });
+});
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ParquetVisualizer.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ParquetVisualizer.test.tsx
new file mode 100644
index 000000000..36a29c89d
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/ParquetVisualizer.test.tsx
@@ -0,0 +1,153 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import { type ReactElement, Suspense } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import ParquetVisualizer from "./ParquetVisualizer";
+
+vi.mock("hyparquet", () => ({
+ parquetReadObjects: vi.fn(),
+}));
+
+vi.mock("./TableVisualizer", () => ({
+ default: ({
+ data,
+ isFullscreen,
+ }: {
+ data: { headers: string[]; rows: string[][] };
+ isFullscreen: boolean;
+ }) => (
+
+ ),
+}));
+
+const { parquetReadObjects } = await import("hyparquet");
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+});
+
+const renderWithSuspense = (ui: ReactElement) =>
+ render(
+
+ (
+
+ {error instanceof Error ? error.message : "Unknown error"}
+
+ )}
+ >
+ Loading}>
+ {ui}
+
+
+ ,
+ );
+
+beforeEach(() => {
+ queryClient.clear();
+});
+
+describe("ParquetVisualizer", () => {
+ it("fetches, parses parquet, and renders TableVisualizer", async () => {
+ const buffer = new ArrayBuffer(8);
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ arrayBuffer: () => Promise.resolve(buffer),
+ } as Response);
+
+ vi.mocked(parquetReadObjects).mockResolvedValue([
+ { name: "Alice", score: 100 },
+ { name: "Bob", score: 90 },
+ ]);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ const table = screen.getByTestId("table-visualizer");
+ expect(table).toHaveAttribute("data-headers", "name,score");
+ expect(table).toHaveAttribute("data-row-count", "2");
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("renders error boundary on fetch failure", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: false,
+ status: 500,
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toBeInTheDocument();
+ expect(screen.getByText(/Failed to fetch artifact/)).toBeInTheDocument();
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("shows 'No data' for empty parquet files", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)),
+ } as Response);
+
+ vi.mocked(parquetReadObjects).mockResolvedValue([]);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("No data")).toBeInTheDocument();
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("passes isFullscreen to TableVisualizer", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
+ } as Response);
+
+ vi.mocked(parquetReadObjects).mockResolvedValue([{ col: "val" }]);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("table-visualizer")).toHaveAttribute(
+ "data-fullscreen",
+ "true",
+ );
+ });
+
+ vi.restoreAllMocks();
+ });
+});
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TableVisualizer.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TableVisualizer.test.tsx
new file mode 100644
index 000000000..b24bca2b7
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TableVisualizer.test.tsx
@@ -0,0 +1,87 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import TableVisualizer from "./TableVisualizer";
+import type { ArtifactTableData } from "./utils";
+
+const makeData = (rowCount: number): ArtifactTableData => ({
+ headers: ["Name", "Score"],
+ rows: Array.from({ length: rowCount }, (_, i) => [
+ `row-${i}`,
+ String(i * 10),
+ ]),
+});
+
+describe("TableVisualizer", () => {
+ it("renders headers and rows", () => {
+ const data = makeData(3);
+ render();
+
+ expect(screen.getByText("Name")).toBeInTheDocument();
+ expect(screen.getByText("Score")).toBeInTheDocument();
+ expect(screen.getByText("row-0")).toBeInTheDocument();
+ expect(screen.getByText("row-2")).toBeInTheDocument();
+ });
+
+ it("limits rows to DEFAULT_PREVIEW_ROWS (10) when not fullscreen", () => {
+ const data = makeData(20);
+ render();
+
+ expect(screen.getByText("row-9")).toBeInTheDocument();
+ expect(screen.queryByText("row-10")).not.toBeInTheDocument();
+ expect(screen.getByText("Showing first 10 rows")).toBeInTheDocument();
+ });
+
+ it("shows up to MAX_PREVIEW_ROWS (30) when fullscreen", () => {
+ const data = makeData(35);
+ render();
+
+ expect(screen.getByText("row-29")).toBeInTheDocument();
+ expect(screen.queryByText("row-30")).not.toBeInTheDocument();
+ expect(screen.getByText("Showing first 30 rows")).toBeInTheDocument();
+ });
+
+ it("shows 'Showing all N rows' when all rows fit", () => {
+ const data = makeData(5);
+ render();
+
+ expect(screen.getByText("Showing all 5 rows")).toBeInTheDocument();
+ });
+
+ it("renders 'See all' link when remoteLink is provided and rows are truncated", () => {
+ const data = makeData(20);
+ render(
+ ,
+ );
+
+ const link = screen.getByRole("link", { name: "See all" });
+ expect(link).toHaveAttribute(
+ "href",
+ "https://storage.example.com/file.csv",
+ );
+ });
+
+ it("does not render 'See all' link when remoteLink is not provided", () => {
+ const data = makeData(20);
+ render();
+
+ expect(screen.queryByText("See all")).not.toBeInTheDocument();
+ });
+
+ it("does not render 'See all' link when all rows are shown", () => {
+ const data = makeData(3);
+ render(
+ ,
+ );
+
+ expect(screen.queryByText("See all")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TextVisualizer.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TextVisualizer.test.tsx
new file mode 100644
index 000000000..e003109a2
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/ArtifactVisualizer/TextVisualizer.test.tsx
@@ -0,0 +1,103 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen, waitFor } from "@testing-library/react";
+import { type ReactElement, Suspense } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { TextVisualizerRemote, TextVisualizerValue } from "./TextVisualizer";
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+});
+
+const renderWithQuery = (ui: ReactElement) =>
+ render({ui});
+
+const renderWithSuspense = (ui: ReactElement) =>
+ render(
+
+ (
+
+ {error instanceof Error ? error.message : "Unknown error"}
+
+ )}
+ >
+ Loading}>
+ {ui}
+
+
+ ,
+ );
+
+beforeEach(() => {
+ queryClient.clear();
+});
+
+describe("TextVisualizerValue", () => {
+ it("renders the value directly", () => {
+ renderWithQuery();
+ expect(screen.getByText("Hello world")).toBeInTheDocument();
+ });
+
+ it("does not fetch", () => {
+ const fetchSpy = vi.spyOn(globalThis, "fetch");
+ renderWithQuery();
+ expect(fetchSpy).not.toHaveBeenCalled();
+ fetchSpy.mockRestore();
+ });
+});
+
+describe("TextVisualizerRemote", () => {
+ it("renders fetched text content", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ text: () => Promise.resolve("Remote content"),
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("Remote content")).toBeInTheDocument();
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("renders error boundary on fetch failure", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: false,
+ status: 404,
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByTestId("error")).toBeInTheDocument();
+ expect(screen.getByText(/Failed to fetch artifact/)).toBeInTheDocument();
+ });
+
+ vi.restoreAllMocks();
+ });
+
+ it("shows 'No data' when fetched text is empty", async () => {
+ vi.spyOn(globalThis, "fetch").mockResolvedValue({
+ ok: true,
+ text: () => Promise.resolve(""),
+ } as Response);
+
+ renderWithSuspense(
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("No data")).toBeInTheDocument();
+ });
+
+ vi.restoreAllMocks();
+ });
+});
diff --git a/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.test.tsx b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.test.tsx
new file mode 100644
index 000000000..fe203a753
--- /dev/null
+++ b/src/components/shared/ReactFlow/FlowCanvas/TaskNode/TaskOverview/IOSection/IOCell/IOCell.test.tsx
@@ -0,0 +1,215 @@
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import type { ArtifactNodeResponse } from "@/api/types.gen";
+
+import IOCell from "./IOCell";
+
+vi.mock("@/providers/BackendProvider", () => ({
+ useBackend: () => ({ backendUrl: "http://localhost:8000" }),
+}));
+
+vi.mock("./ArtifactVisualizer/ArtifactVisualizer", () => ({
+ default: ({
+ name,
+ type,
+ value,
+ }: {
+ name: string;
+ type: string;
+ value?: string;
+ }) => (
+
+ ),
+}));
+
+const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+});
+
+const renderWithQuery = (ui: React.ReactElement) =>
+ render({ui});
+
+const makeArtifact = (
+ overrides?: Partial,
+): ArtifactNodeResponse => ({
+ id: "artifact-1",
+ artifact_data: { total_size: 0, is_dir: false },
+ ...overrides,
+});
+
+describe("IOCell", () => {
+ it("renders the artifact name", () => {
+ renderWithQuery();
+ expect(screen.getByText("my_input")).toBeInTheDocument();
+ });
+
+ it("shows the explicit type when provided", () => {
+ renderWithQuery(
+ ,
+ );
+ expect(screen.getByText("CSV")).toBeInTheDocument();
+ });
+
+ it("falls back to type_name from artifact", () => {
+ renderWithQuery(
+ ,
+ );
+ expect(screen.getByText("JsonObject")).toBeInTheDocument();
+ });
+
+ it("shows 'Directory' when artifact is a directory", () => {
+ renderWithQuery(
+ ,
+ );
+ expect(screen.getByText("Directory")).toBeInTheDocument();
+ });
+
+ it("shows 'Any' as default type", () => {
+ renderWithQuery();
+ expect(screen.getByText("Any")).toBeInTheDocument();
+ });
+
+ it("displays formatted file size", () => {
+ renderWithQuery(
+ ,
+ );
+ expect(screen.getByText(/2/)).toBeInTheDocument();
+ });
+
+ it("renders ArtifactVisualizer with value for inline values", () => {
+ renderWithQuery(
+ ,
+ );
+
+ const viz = screen.getByTestId("artifact-visualizer");
+ expect(viz).toHaveAttribute("data-value", "hello world");
+ expect(viz).toHaveAttribute("data-type", "text");
+ });
+
+ it("renders ArtifactVisualizer without value for remote artifacts", () => {
+ renderWithQuery(
+ ,
+ );
+
+ const viz = screen.getByTestId("artifact-visualizer");
+ expect(viz).not.toHaveAttribute("data-value");
+ expect(viz).toHaveAttribute("data-type", "CSV");
+ });
+
+ it("renders inline value text with line-clamp", () => {
+ renderWithQuery(
+ ,
+ );
+
+ expect(screen.getByText("some inline value")).toBeInTheDocument();
+ });
+
+ it("does not render inline value for whitespace-only strings", () => {
+ renderWithQuery(
+ ,
+ );
+
+ expect(screen.queryByTestId("artifact-visualizer")).not.toBeInTheDocument();
+ });
+
+ it("renders artifact URI when available", () => {
+ renderWithQuery(
+ ,
+ );
+
+ expect(screen.getByText("Copy URI")).toBeInTheDocument();
+ });
+
+ it("does not render ArtifactVisualizer when artifact is null", () => {
+ renderWithQuery();
+ expect(screen.queryByTestId("artifact-visualizer")).not.toBeInTheDocument();
+ });
+
+ it("defaults type to 'text' for inline values without explicit type", () => {
+ renderWithQuery(
+ ,
+ );
+
+ const viz = screen.getByTestId("artifact-visualizer");
+ expect(viz).toHaveAttribute("data-type", "text");
+ });
+});