import { describe, expect, it } from "vitest"; import { isValidElement, type ReactNode } from "react"; import { linkify } from "./linkify"; // linkify returns React nodes without rendering, so we can inspect them structurally in node env: // an element carries { href, children:label }, everything else is text held in a Fragment. function links(nodes: ReactNode[]): { href: string; label: string }[] { return nodes .filter( (n): n is React.ReactElement<{ href: string; children: string }> => isValidElement(n) && (n as React.ReactElement).type === "a", ) .map((n) => ({ href: n.props.href, label: n.props.children })); } describe("linkify", () => { it("leaves plain prose untouched (no anchors)", () => { expect(links(linkify("just some text, nothing to see"))).toEqual([]); }); it("links an explicit http(s) URL to itself", () => { expect(links(linkify("see https://example.com/watch?v=1 now"))).toEqual([ { href: "https://example.com/watch?v=1", label: "https://example.com/watch?v=1" }, ]); }); it("gives a www.* host an https:// href but keeps the bare label", () => { expect(links(linkify("at www.example.com/foo"))).toEqual([ { href: "https://www.example.com/foo", label: "www.example.com/foo" }, ]); }); it("links a bare domain.tld/path", () => { expect(links(linkify("go to example.com/foo please"))).toEqual([ { href: "https://example.com/foo", label: "example.com/foo" }, ]); }); it("does not mistake a version number for a bare URL", () => { // letters-only TLD + a required path keep 1.2.3 out (no path, numeric last segment). expect(links(linkify("upgraded to 1.2.3 today"))).toEqual([]); }); it("links only the handle of a ': @handle' mention, keeping the label as text", () => { const out = linkify("follow x: @jane please"); expect(links(out)).toEqual([{ href: "https://x.com/jane", label: "@jane" }]); // the "x: " prefix survives as text, not swallowed into the link. const text = out .filter((n) => typeof n === "string" || (isValidElement(n) && n.type !== "a")) .map((n) => (isValidElement(n) ? (n.props as { children?: string }).children : n)) .join(""); expect(text).toContain("x: "); }); it("maps a platform alias to its canonical base URL", () => { expect(links(linkify("ig: @foo"))).toEqual([ { href: "https://instagram.com/foo", label: "@foo" }, ]); }); it("leaves a bare @handle with no platform, and #hashtags, as plain text", () => { expect(links(linkify("hi @nobody and #hashtag"))).toEqual([]); }); it("handles several links in one string in order", () => { const out = links(linkify("https://a.com/x then youtube: @chan then b.org/y")); expect(out.map((l) => l.href)).toEqual([ "https://a.com/x", "https://www.youtube.com/@chan", "https://b.org/y", ]); }); });