/* 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"); }); });