Files
siftlode/frontend/src/lib/linkify.test.ts
T
peter bdb0185db8 test(frontend): unit-test columnSort and linkify
22 vitest cases for two pure helpers that had none:
- columnSort: numeric-vs-locale comparison, asc/desc, no-mutation, the null/junk
  coercion in parseSortState, and the unsorted→asc→desc→unsorted click cycle.
- linkify: http/www/bare-domain URLs, the version-number non-match, the
  "<platform>: @handle" mention (handle linked, prefix kept as text), alias→canonical
  base URL, bare @handle/#hashtag left plain, and multi-link ordering. Nodes are
  inspected structurally, so the existing node-env vitest needs no DOM.

useCardPager (a hook) is deferred to when component/hook test infra lands with the
backend pytest sprint.
2026-07-21 03:13:26 +02:00

74 lines
2.9 KiB
TypeScript

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 <a> 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 '<platform>: @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",
]);
});
});