The list is in diigo's updated order; each item now leads with its updated_at as Date#toISOString() (diigo sends epoch seconds; updatedISO converts, tested), or "(no date)". Generated-by: Claude Opus 5.5
84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { publicOnly, toRows, updatedISO, usernameOf } from "../src/items.js";
|
|
import type { Item } from "../src/items.js";
|
|
|
|
/** a load_user_items row: diigo's actual field names (observed
|
|
* 2026-10-07), made-up values */
|
|
function diigoItem(linkId: number, url: string): Item {
|
|
return {
|
|
link_id: linkId,
|
|
url,
|
|
title: `title ${linkId}`,
|
|
description: "",
|
|
private: false,
|
|
u_name: "alice_99",
|
|
first_by: "bob-the-first",
|
|
user_id: 4242,
|
|
real_name: "Alice A.",
|
|
tags: "",
|
|
created_at: 1700000000,
|
|
updated_at: 1700000100 + linkId,
|
|
annotations: [],
|
|
comments: [],
|
|
};
|
|
}
|
|
|
|
function makeLog() {
|
|
const entries: { level: string; args: unknown[] }[] = [];
|
|
const at = (level: string) => (...args: unknown[]) => entries.push({ level, args });
|
|
return { log: { warn: at("warn") }, entries };
|
|
}
|
|
|
|
describe("toRows", () => {
|
|
it("keys each item on diigo's link_id, so a page of distinct items stays distinct", () => {
|
|
const { log } = makeLog();
|
|
const rows = toRows([diigoItem(11, "https://a.example/"), diigoItem(12, "https://b.example/")], log);
|
|
expect(rows.map((r) => r.itemId)).toEqual(["11", "12"]);
|
|
});
|
|
|
|
it("logs, rather than silently drops or merges, items it cannot key", () => {
|
|
const { log, entries } = makeLog();
|
|
const noId = diigoItem(13, "https://c.example/");
|
|
delete noId.link_id;
|
|
const noUrl = diigoItem(14, "");
|
|
const rows = toRows([noId, noUrl, diigoItem(15, "https://d.example/")], log);
|
|
expect(rows.map((r) => r.itemId)).toEqual(["15"]);
|
|
expect(entries.map((e) => e.level)).toEqual(["warn", "warn"]);
|
|
// diagnostics name the fields present, never their values
|
|
expect(JSON.stringify(entries)).not.toContain("https://c.example/");
|
|
});
|
|
});
|
|
|
|
describe("usernameOf", () => {
|
|
it("reads the owner's username from diigo's u_name", () => {
|
|
expect(usernameOf(diigoItem(1, "https://a.example/"))).toBe("alice_99");
|
|
});
|
|
|
|
it("answers empty for an item without one", () => {
|
|
expect(usernameOf(undefined)).toBe("");
|
|
expect(usernameOf({ u_name: "has spaces" })).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("publicOnly", () => {
|
|
it("keeps shared rows and drops private ones, before anything is stored", () => {
|
|
const { log } = makeLog();
|
|
const shared = diigoItem(21, "https://shared.example/");
|
|
const secret = { ...diigoItem(22, "https://secret.example/"), private: true };
|
|
const rows = publicOnly(toRows([shared, secret], log));
|
|
expect(rows.map((r) => r.itemId)).toEqual(["21"]);
|
|
expect(JSON.stringify(rows)).not.toContain("secret.example");
|
|
});
|
|
});
|
|
|
|
describe("updatedISO", () => {
|
|
it("reads diigo's updated_at as epoch seconds", () => {
|
|
expect(updatedISO({ updated_at: 1700000000 })).toBe("2023-11-14T22:13:20.000Z");
|
|
});
|
|
|
|
it("answers empty when there is no usable date", () => {
|
|
expect(updatedISO({})).toBe("");
|
|
expect(updatedISO({ updated_at: "yesterday" })).toBe("");
|
|
});
|
|
});
|