import { test } from "node:test"; import assert from "node:assert/strict"; import { cursorFromShowMore, nitterHandleFromHref, nitterIdFromHref, normalizeNitterItem, parseNitterDate, parseNitterStat, xNitterFetcher, type NitterRawItem, } from "./xNitterFetcher"; import { parsePost } from "../lib/posts"; // Every literal below was captured from a real nitter.poast.org render of // /TheQuartering — no network in tests. test("parses nitter's UTC timestamp format", () => { assert.equal(parseNitterDate("Aug 6, 2026 · 8:51 PM UTC"), "2026-08-06T20:51:00.000Z"); assert.equal(parseNitterDate("Aug 5, 2026 · 1:13 AM UTC"), "2026-08-05T01:13:00.000Z"); // Midnight/noon are the classic 12-hour conversion traps. assert.equal(parseNitterDate("Jan 1, 2024 · 12:00 AM UTC"), "2024-01-01T00:00:00.000Z"); assert.equal(parseNitterDate("Jan 1, 2024 · 12:30 PM UTC"), "2024-01-01T12:30:00.000Z"); // A drifted format must be rejected, not mis-dated. assert.equal(parseNitterDate("6 August 2026 20:51"), null); assert.equal(parseNitterDate(""), null); }); test("extracts a 64-bit id from the tweet link, as a string", () => { const href = "/TheQuartering/status/2085468926012702808#m"; assert.equal(nitterIdFromHref(href), "2085468926012702808"); assert.equal(nitterHandleFromHref(href), "TheQuartering"); // Never through a Number — X ids exceed 2^53. assert.ok(!Number.isSafeInteger(Number("2085468926012702808"))); assert.equal(nitterIdFromHref("/TheQuartering"), null); }); test("parses stat counts, including thousands separators", () => { assert.equal(parseNitterStat("7,602"), 7602); assert.equal(parseNitterStat("47"), 47); assert.equal(parseNitterStat(""), undefined); assert.equal(parseNitterStat(null), undefined); assert.equal(parseNitterStat("—"), undefined); }); test("reads the pagination cursor off the Load-more link", () => { assert.equal( cursorFromShowMore("?cursor=DAAHCgABHPEsST3__-sLAAIAAAATMjA4NDgx"), "DAAHCgABHPEsST3__-sLAAIAAAATMjA4NDgx", ); assert.equal(cursorFromShowMore(null), null); assert.equal(cursorFromShowMore("/TheQuartering"), null); }); const REAL_ITEM: NitterRawItem = { href: "/TheQuartering/status/2085468926012702808#m", dateTitle: "Aug 6, 2026 · 8:51 PM UTC", text: "Coffee Brand Coffee has just shipped our first shipment to Walmart. seethe losers.", fullname: "TheQuartering", username: "@TheQuartering", isRetweet: false, isReply: false, replyingTo: [], quoteHref: null, stats: [ { icon: "icon-comment", value: "47" }, { icon: "icon-retweet", value: "6" }, { icon: "icon-heart", value: "194" }, { icon: "icon-quote", value: "7,602" }, ], mediaCount: 0, }; test("normalizes a real nitter item into a valid Post", () => { const post = normalizeNitterItem(REAL_ITEM, "thequartering-X")!; assert.equal(post.id, "2085468926012702808"); assert.equal(post.slug, "thequartering-X/2085468926012702808"); assert.equal(post.platform, "twitter"); assert.equal(post.author, "TheQuartering"); assert.equal(post.authorName, "TheQuartering"); assert.equal(post.createdAt, "2026-08-06T20:51:00.000Z"); assert.equal(post.uploadDate, "20260806"); assert.match(post.text, /^Coffee Brand Coffee/); assert.deepEqual(post.engagement, { replies: 47, reposts: 6, likes: 194, quotes: 7602, }); // Survives the on-disk round trip. assert.deepEqual(parsePost(JSON.parse(JSON.stringify(post))), post); }); test("links to x.com, never to the nitter instance", () => { // The instance is a transport detail and is likely dead by the time anyone // clicks the citation. const post = normalizeNitterItem(REAL_ITEM, "c")!; assert.equal(post.url, "https://x.com/TheQuartering/status/2085468926012702808"); assert.ok(!post.url.includes("nitter")); }); test("carries retweet / reply / quote structure", () => { const rt = normalizeNitterItem({ ...REAL_ITEM, isRetweet: true }, "c")!; assert.equal(rt.isRepost, true); const reply = normalizeNitterItem( { ...REAL_ITEM, isReply: true, replyingTo: ["someoneelse"] }, "c", )!; assert.equal(reply.isReply, true); assert.equal(reply.replyTo?.author, "someoneelse"); const quote = normalizeNitterItem( { ...REAL_ITEM, quoteHref: "/OtherUser/status/1788248211448004625#m" }, "c", )!; assert.equal(quote.quoted?.id, "1788248211448004625"); assert.equal(quote.quoted?.author, "OtherUser"); }); test("rejects an item with no id or an unparseable date", () => { assert.equal(normalizeNitterItem({ ...REAL_ITEM, href: "/TheQuartering" }, "c"), null); assert.equal(normalizeNitterItem({ ...REAL_ITEM, dateTitle: "yesterday" }, "c"), null); }); test("claims nitter URLs only, so gallery-dl stays the default X path", () => { assert.equal(xNitterFetcher.detect("https://nitter.poast.org/TheQuartering"), true); assert.equal(xNitterFetcher.detect("https://nitter.net/nasa"), true); // An x.com channel must opt in explicitly via postFetcher: "x-nitter". assert.equal(xNitterFetcher.detect("https://x.com/TheQuartering"), false); assert.equal(xNitterFetcher.platform, "twitter"); });