import { describe, expect, it } from "vitest"; import { readFileSync } from "node:fs"; import { DefinitionEngine, hasPotentialDefinitionForUrl, matchesUrlPattern, selectDefinition, selectDefinitionForUrl } from "../src/content/definition-engine"; import { BUILTIN_DEFINITIONS } from "../src/content/definitions"; import type { SiteDefinition } from "../src/shared/types"; const definition: SiteDefinition = { id: "example-social", name: "Example Social", urlPatterns: ["https://*.example.test/*"], requiredSelectors: [".feed"], post: { rootSelectors: ["article.post"], textSelectors: [".content"], excludedSelectors: [".exclude"], idAttributes: ["data-id"], permalinkSelectors: ["a.permalink[href]"] } }; describe("DefinitionEngine", () => { it("discovers the supplied root, extracts only visible text, and uses a stable ID", () => { document.body.innerHTML = '
Visible skiphidden
'; const post = new DefinitionEngine(definition).discover(document.querySelector("article")!)[0]; expect(post).toMatchObject({ id: "example-social:42", text: "visible" }); }); it("uses permalink IDs and selects only definitions with URL and marker matches", () => { document.body.innerHTML = '
Hello
'; const engine = new DefinitionEngine(definition); expect(engine.discover(document)[0]?.id).toContain("/users/a/statuses/1"); expect(selectDefinition([definition], "https://social.example.test/home")).toBe(definition); expect(selectDefinition([definition], "https://other.test/home")).toBeUndefined(); }); it("matches browser-style URL patterns", () => { expect(matchesUrlPattern("https://*.example.test/*", "https://a.example.test/path?q=1")).toBe(true); expect(matchesUrlPattern("https://*.example.test/*", "http://a.example.test/path")).toBe(false); }); it("covers Mastodon timeline, profile, and detailed reply status containers", () => { document.body.innerHTML = `
Timeline post
Profile post
Reply in thread
`; const posts = new DefinitionEngine(BUILTIN_DEFINITIONS[0]!).discover(document); expect(posts.map((post) => post.id)).toEqual(["mastodon:timeline", "mastodon:profile", "mastodon:http://localhost:3000/users/a/statuses/3"]); expect(posts.map((post) => post.text)).toEqual(["timeline post", "profile post", "reply in thread"]); }); it("does not include hidden Mastodon content-warning text until it is revealed", () => { document.body.innerHTML = '

Content warning Show more

'; const engine = new DefinitionEngine(BUILTIN_DEFINITIONS[0]!); expect(engine.discover(document)[0]?.text).toBe("content warning"); document.querySelector("#hidden")?.removeAttribute("style"); expect(engine.discover(document)[0]?.text).toBe("content warning hidden post text"); }); it("rediscovers a Mastodon status when a content warning is revealed", async () => { document.body.innerHTML = '

Content warning

'; const engine = new DefinitionEngine(BUILTIN_DEFINITIONS[0]!); const observed: string[] = []; const stop = engine.observe((posts) => observed.push(...posts.map((post) => post.text))); document.querySelector("#hidden")?.removeAttribute("style"); await new Promise((resolve) => setTimeout(resolve, 0)); stop(); expect(observed).toContain("content warning revealed text"); }); it("discovers Threads posts appended after observation starts", async () => { document.body.innerHTML = '
Initial post
'; const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; const engine = new DefinitionEngine(threads); const observed: string[] = []; const stop = engine.observe((posts) => observed.push(...posts.map((post) => post.text))); const post = document.createElement("div"); post.setAttribute("role", "article"); post.innerHTML = 'Scrolled-in post'; document.querySelector("main")?.append(post); await new Promise((resolve) => setTimeout(resolve, 0)); stop(); expect(observed).toContain("scrolled-in post"); }); it("discovers Threads posts from stable semantic/data selectors and permalink IDs", () => { document.body.innerHTML = readFileSync("tests/fixtures/threads-feed.html", "utf8"); const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; const posts = new DefinitionEngine(threads).discover(document); expect(posts.map((post) => post.id)).toEqual(["threads:http://localhost:3000/@alice/post/ABC123", "threads:http://localhost:3000/@bob/post/DEF456"]); expect(posts.map((post) => post.text)).toEqual(["first threads post with visible text", "a reply in the conversation"]); }); it("discovers Threads detail-page posts from pagelet containers", () => { document.body.innerHTML = readFileSync("tests/fixtures/threads-post-page.html", "utf8"); const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; const posts = new DefinitionEngine(threads).discover(document); expect(posts.map((post) => post.id)).toEqual([ "threads:http://localhost:3000/@alice/post/ABC123", "threads:http://localhost:3000/@bob/post/DEF456" ]); expect(posts.map((post) => post.text)).toEqual(["the main post on the threads detail page", "a reply on the threads detail page"]); }); it("discovers posts from the old Reddit interface using fullname IDs", () => { document.body.innerHTML = readFileSync("tests/fixtures/reddit-old-feed.html", "utf8"); const reddit = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "reddit-old")!; const posts = new DefinitionEngine(reddit).discover(document); expect(posts.map((post) => post.id)).toEqual(["reddit-old:t3_abc123", "reddit-old:t1_def456"]); expect(posts.map((post) => post.text)).toEqual(["the post body with visible text.", "a useful comment with visible text."]); }); it("discovers X posts using semantic tweet markers and permalink IDs", () => { document.body.innerHTML = readFileSync("tests/fixtures/x-feed.html", "utf8"); const x = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "x")!; const posts = new DefinitionEngine(x).discover(document); expect(posts.map((post) => post.id)).toEqual(["x:http://localhost:3000/alice/status/1234567890"]); expect(posts[0]?.text).toBe("a useful post on x."); }); it("selects Threads on current and legacy domains", () => { const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; document.body.innerHTML = '
'; for (const url of ["https://threads.com/home", "https://www.threads.com/@alice/post/ABC123", "https://threads.net/home", "https://www.threads.net/@alice/post/ABC123"]) { expect(selectDefinition([threads], url)).toBe(threads); } expect(selectDefinition([threads], "https://example.com/home")).toBeUndefined(); }); it("selects Reddit old and X only on their supported domains", () => { const reddit = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "reddit-old")!; const x = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "x")!; document.body.innerHTML = '
'; expect(selectDefinition([reddit], "https://old.reddit.com/r/all")).toBe(reddit); expect(selectDefinition([reddit], "https://www.reddit.com/r/all")).toBeUndefined(); expect(selectDefinition([x], "https://x.com/home")).toBe(x); expect(selectDefinition([x], "https://twitter.com/alice/status/123")).toBe(x); expect(selectDefinition([x], "https://example.com/home")).toBeUndefined(); }); it("can identify a specific site while its SPA markers are still loading", () => { const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; document.body.innerHTML = ""; expect(selectDefinition([threads], "https://www.threads.com/")).toBeUndefined(); expect(selectDefinitionForUrl([BUILTIN_DEFINITIONS[0]!, threads], "https://www.threads.com/")).toBe(threads); }); it("keeps broad definitions pending until their hydration markers appear", () => { const mastodon = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "mastodon")!; const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; document.body.innerHTML = ""; expect(selectDefinition([mastodon], "https://mastodon.example/@user")).toBeUndefined(); expect(selectDefinitionForUrl([mastodon], "https://mastodon.example/@user")).toBeUndefined(); expect(hasPotentialDefinitionForUrl([mastodon], "https://mastodon.example/@user")).toBe(true); expect(hasPotentialDefinitionForUrl([threads], "https://example.com/")).toBe(false); document.body.innerHTML = '
Ready
'; expect(selectDefinition([mastodon], "https://mastodon.example/@user")).toBe(mastodon); }); });