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.
74 lines
2.9 KiB
TypeScript
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",
|
|
]);
|
|
});
|
|
});
|