/* Copyright 2026 Marimo. All rights reserved. */
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { cleanAnsiCodes, RenderTextWithLinks } from "../text-rendering";
// Mock the useInstallPackages hook
vi.mock("@/core/packages/useInstallPackage", () => ({
useInstallPackages: () => ({
handleInstallPackages: vi.fn(),
}),
}));
describe("RenderTextWithLinks", () => {
describe("URL detection", () => {
it("should render plain text without URLs", () => {
render();
expect(screen.getByText("Hello, world!")).toBeInTheDocument();
});
it("should make URLs clickable", () => {
render(
,
);
const link = screen.getByRole("link", { name: "https://marimo.io" });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "https://marimo.io");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
it("should handle multiple URLs in one line", () => {
render(
,
);
expect(
screen.getByRole("link", { name: "https://marimo.io" }),
).toBeInTheDocument();
expect(
screen.getByRole("link", {
name: "https://github.com/marimo-team/marimo",
}),
).toBeInTheDocument();
});
it("should handle http URLs", () => {
render();
const link = screen.getByRole("link", { name: "http://example.com" });
expect(link).toBeInTheDocument();
});
it("should handle URLs with query parameters", () => {
render(
,
);
const link = screen.getByRole("link", {
name: "https://marimo.io/docs?page=1§ion=intro",
});
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
"href",
"https://marimo.io/docs?page=1§ion=intro",
);
});
it("should handle URLs at the start of text", () => {
render();
expect(
screen.getByRole("link", { name: "https://marimo.io" }),
).toBeInTheDocument();
expect(screen.getByText("is awesome")).toBeInTheDocument();
});
it("should handle URLs at the end of text", () => {
render();
expect(screen.getByText("Check out")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "https://marimo.io" }),
).toBeInTheDocument();
});
it("should not break on text without protocols", () => {
render();
// These should be rendered as plain text, not links
expect(
screen.queryByRole("link", { name: "marimo.io" }),
).not.toBeInTheDocument();
expect(
screen.getByText(/Visit marimo.io or github.com/),
).toBeInTheDocument();
});
});
describe("XSS safety", () => {
it("should safely handle URLs with special characters", () => {
render(
,
);
// The URL should be rendered but script should not execute
expect(screen.getByText(/Visit/)).toBeInTheDocument();
// Should not have a script element in the DOM
const scripts = document.querySelectorAll("script");
const hasXSSScript = [...scripts].some((script) =>
script.textContent?.includes("alert('xss')"),
);
expect(hasXSSScript).toBe(false);
});
it("should handle javascript: protocol URLs safely", () => {
render();
// javascript: protocol should not be detected as a valid URL
expect(
screen.queryByRole("link", { name: /javascript:/ }),
).not.toBeInTheDocument();
});
});
});
describe("cleanAnsiCodes", () => {
it("should remove basic ANSI color codes", () => {
const text = "\u001B[31mRed text\u001B[0m";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("Red text");
});
it("should remove multiple ANSI codes", () => {
const text =
"\u001B[31mRed\u001B[0m \u001B[32mGreen\u001B[0m \u001B[34mBlue\u001B[0m";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("Red Green Blue");
});
it("should handle text without ANSI codes", () => {
const text = "Plain text";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("Plain text");
});
it("should remove ANSI codes with multiple parameters", () => {
const text = "\u001B[1;31mBold Red\u001B[0m";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("Bold Red");
});
it("should clean ANSI codes from URLs", () => {
const text = "https://marimo.io\u001B[0m";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("https://marimo.io");
});
it("should handle empty string", () => {
const text = "";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("");
});
it("should remove complex ANSI sequences", () => {
const text = "\u001B[38;5;208mOrange text\u001B[0m";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("Orange text");
});
it("should handle text with only ANSI codes", () => {
const text = "\u001B[31m\u001B[0m";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("");
});
it("should preserve special characters and whitespace", () => {
const text = "\u001B[31mSpecial: !@#$%^&*()\n\tTab and newline\u001B[0m";
const cleaned = cleanAnsiCodes(text);
expect(cleaned).toBe("Special: !@#$%^&*()\n\tTab and newline");
});
});
describe("RenderTextWithLinks - pip install detection", () => {
it("should render pip install command with install button", () => {
render();
expect(screen.getByText(/pip install pandas/)).toBeInTheDocument();
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("should handle pip install with package extras", () => {
render();
expect(
screen.getByText(/pip install package\[extra,dep]/),
).toBeInTheDocument();
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("should handle multiple packages", () => {
render();
expect(screen.getByText(/pip install pandas/)).toBeInTheDocument();
const button = screen.getByRole("button");
expect(button).toBeInTheDocument();
expect(button.textContent).toContain("pandas");
});
it("should handle pip install with surrounding text", () => {
render(
,
);
expect(screen.getByText(/Error: please run/)).toBeInTheDocument();
expect(screen.getByText(/pip install pandas/)).toBeInTheDocument();
expect(screen.getByText(/to fix this issue/)).toBeInTheDocument();
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("should not render button for non-pip install text", () => {
render();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
it("should handle pip install with ANSI codes", () => {
// ANSI codes create nested spans which makes the replacer logic complex
// For now, just verify it renders without crashing
const { container } = render(
,
);
expect(container).toBeInTheDocument();
});
it("should handle pip install with hyphens in package name", () => {
render();
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("should handle pip install with dots in package name", () => {
render();
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("should handle both pip install and URLs in the same text", () => {
render(
,
);
expect(screen.getByRole("button")).toBeInTheDocument();
expect(
screen.getByRole("link", { name: "https://marimo.io" }),
).toBeInTheDocument();
});
it("should handle multiple separate pip install commands in the same text", () => {
render(
,
);
// Should create button for the first package in the command
const buttons = screen.getAllByRole("button");
expect(buttons).toHaveLength(1);
expect(buttons[0].textContent).toContain("pip install polars");
});
it("should handle multiple distinct pip install commands", () => {
render(
,
);
// Should create separate buttons for each command
const buttons = screen.getAllByRole("button");
expect(buttons).toHaveLength(2);
expect(buttons[0].textContent).toContain("polars");
expect(buttons[1].textContent).toContain("pandas");
});
});