| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { buildTree, canDo, citationHref, coverStyle, cursorColour, dayGroups, dayKey, flatten, linkedArtifacts, pathTo, readingTime, repoFilePath, repoFolders, snippetParts, spacePath } from "./folios.ts"; |
| 5 | |
| 6 | const node = (id: string, parent_id: string | null, position: number) => ({ id, kind: "doc" as const, parent_id, position, title: id.toUpperCase(), icon: null, slug: id, restricted: false }); |
| 7 | |
| 8 | const nodes = [node("b", null, 2), node("a", null, 1), node("c", "a", 1), node("orphan", "gone", 3)]; |
| 9 | |
| 10 | test("the tree orders each level and lifts folios whose parent isn't there", () => { |
| 11 | const tree = buildTree(nodes); |
| 12 | assert.deepEqual( |
| 13 | tree.map((n) => n.id), |
| 14 | ["a", "b", "orphan"], |
| 15 | ); |
| 16 | assert.equal(tree[0]!.children[0]!.id, "c"); |
| 17 | assert.equal(tree[0]!.children[0]!.depth, 1); |
| 18 | assert.deepEqual( |
| 19 | flatten(tree).map((n) => n.id), |
| 20 | ["a", "c", "b", "orphan"], |
| 21 | ); |
| 22 | assert.deepEqual(pathTo(nodes, "c"), ["a"]); |
| 23 | }); |
| 24 | |
| 25 | test("the home list groups by the viewer's day: Today, Yesterday, then dates", () => { |
| 26 | const now = new Date("2026-10-09T15:00:00Z"); |
| 27 | const items = ["2026-10-09T14:00:00Z", "2026-10-09T01:00:00Z", "2026-10-08T20:00:00Z", "2026-10-07T12:00:00Z", "2025-12-31T12:00:00Z"]; |
| 28 | const utc = dayGroups(items, (i) => i, "UTC", now); |
| 29 | assert.deepEqual( |
| 30 | utc.map((g) => [g.label, g.items.length]), |
| 31 | [ |
| 32 | ["Today", 2], |
| 33 | ["Yesterday", 1], |
| 34 | ["Oct 7", 1], |
| 35 | ["Dec 31, 2025", 1], |
| 36 | ], |
| 37 | ); |
| 38 | // At 01:00 UTC it is still the 8th in Chicago: that one is yesterday's there. |
| 39 | const chicago = dayGroups(items, (i) => i, "America/Chicago", now); |
| 40 | assert.deepEqual( |
| 41 | chicago.map((g) => [g.label, g.items.length]), |
| 42 | [ |
| 43 | ["Today", 1], |
| 44 | ["Yesterday", 2], |
| 45 | ["Oct 7", 1], |
| 46 | ["Dec 31, 2025", 1], |
| 47 | ], |
| 48 | ); |
| 49 | assert.equal(dayKey("2026-10-09T01:00:00Z", "America/Chicago"), "2026-10-08"); |
| 50 | }); |
| 51 | |
| 52 | test("chat messages name the artifacts they link to, in this workspace only", () => { |
| 53 | const id = "fol_01jb2k7x9hfq0b3zj0f5s2m8ra"; |
| 54 | const other = "fol_01jb2k7x9hfq0b3zj0f5s2m8rb"; |
| 55 | assert.deepEqual(linkedArtifacts(`See https://g1t.sh/acme/-/artifacts/q4-plan-${id} and /acme/-/artifacts/${other}, and again /Acme/-/artifacts/${id}`, "acme"), [id, other]); |
| 56 | assert.deepEqual(linkedArtifacts(`/other/-/artifacts/x-${id}`, "acme"), []); |
| 57 | assert.deepEqual(linkedArtifacts(`/acme/-/artifacts/spaces/general`, "acme"), []); |
| 58 | }); |
| 59 | |
| 60 | test("snippets mark their matches", () => { |
| 61 | assert.deepEqual(snippetParts("a [[b]] c"), [ |
| 62 | { text: "a ", match: false }, |
| 63 | { text: "b", match: true }, |
| 64 | { text: " c", match: false }, |
| 65 | ]); |
| 66 | }); |
| 67 | |
| 68 | test("covers, colours, roles, reading time", () => { |
| 69 | assert.match(coverStyle("gradient:1")!, /^linear-gradient/); |
| 70 | assert.match(coverStyle("https://g1tusercontent.com/docs-files/x")!, /url\("https:/); |
| 71 | assert.equal(coverStyle("javascript:alert(1)"), null); |
| 72 | assert.equal(cursorColour("ana"), cursorColour("ana")); |
| 73 | assert.equal(canDo("edit", "comment"), true); |
| 74 | assert.equal(canDo("comment", "edit"), false); |
| 75 | assert.deepEqual(readingTime("one two three"), { words: 3, minutes: 1 }); |
| 76 | }); |
| 77 | |
| 78 | test("a citation links to its code, a glob to the folder it starts from", () => { |
| 79 | assert.equal(citationHref({ repo: "acme/web", path: "src/export.ts", ref: "abc1234" }), "/acme/web/blob/abc1234/src/export.ts"); |
| 80 | assert.equal(citationHref({ repo: "acme/web", path: "src/jobs", ref: null }), "/acme/web/tree/HEAD/src/jobs"); |
| 81 | assert.equal(citationHref({ repo: "acme/web", path: "src/**/*.sql", ref: "abc" }), "/acme/web/tree/abc/src"); |
| 82 | }); |
| 83 | |
| 84 | test("a project's docs become folders, and spaces and files have addresses under Artifacts", () => { |
| 85 | const root = repoFolders([ |
| 86 | { path: "README.md", title: "Acme" }, |
| 87 | { path: "docs/setup.md", title: "Setup" }, |
| 88 | { path: "docs/guides/deploy.md", title: "Deploy" }, |
| 89 | ]); |
| 90 | assert.deepEqual( |
| 91 | root.files.map((f) => f.path), |
| 92 | ["README.md", "docs/setup.md"], |
| 93 | ); |
| 94 | assert.equal(root.folders[0]!.name, "guides"); |
| 95 | assert.deepEqual(root.folders[0]!.files.map((f) => f.title), ["Deploy"]); |
| 96 | assert.equal(repoFilePath("acme", "acme/web", "docs/a b.md"), "/acme/-/artifacts/repo/acme/web/docs/a%20b.md"); |
| 97 | assert.equal(spacePath("acme", "general"), "/acme/-/artifacts/spaces/general"); |
| 98 | }); |