// @vitest-environment node // TASK-0103 — the file picker's pure filter/sort logic (Requirements §4, // Tests: "picker filter/sort logic"). The component (FilePicker.tsx) calls // filterByName + sortPickerFiles on the server-ordered list; the logic is // tested here without a DOM. import { describe, expect, it } from "vitest"; import { filterByName, sortPickerFiles, type PickerSort } from "../picker"; import type { FileListItem } from "../api"; function file(overrides: Partial): FileListItem { return { id: "f1", workspaceId: "ws", blockId: null, pageId: "p1", name: "file", size: 100, mimeType: "image/png", status: "READY", metadata: {}, thumbnailKey: null, thumbnailUrl: null, scanReport: null, uploadedBy: "u1", version: 1, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z", uploadedByName: "Owner", pageName: "Page", versionCount: 1, ...overrides, }; } const FIXTURES: FileListItem[] = [ file({ id: "a", name: "alpha.png", size: 10, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-05T00:00:00.000Z" }), file({ id: "b", name: "Bravo.pdf", size: 30, createdAt: "2026-01-03T00:00:00.000Z", updatedAt: "2026-01-02T00:00:00.000Z" }), file({ id: "c", name: "charlie.mp4", size: 20, createdAt: "2026-01-02T00:00:00.000Z", updatedAt: "2026-01-04T00:00:00.000Z" }), file({ id: "d", name: "delta-notes.txt", size: 40, createdAt: "2026-01-04T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" }), ]; describe("filterByName (client search, spec §4)", () => { it("empty / whitespace query returns everything, unmodified", () => { expect(filterByName(FIXTURES, "")).toHaveLength(4); expect(filterByName(FIXTURES, " ")).toHaveLength(4); // same array (no copy) for the empty case expect(filterByName(FIXTURES, "")).toBe(FIXTURES); }); it("is a case-insensitive substring match on the name", () => { expect(filterByName(FIXTURES, "BRAVO").map((f) => f.id)).toEqual(["b"]); expect(filterByName(FIXTURES, "br").map((f) => f.id)).toEqual(["b"]); expect(filterByName(FIXTURES, ".png").map((f) => f.id)).toEqual(["a"]); }); it("returns [] when nothing matches (the picker shows its empty state)", () => { expect(filterByName(FIXTURES, "zzz")).toEqual([]); }); it("trims the query before matching", () => { expect(filterByName(FIXTURES, " charlie ").map((f) => f.id)).toEqual(["c"]); }); }); describe("sortPickerFiles (picker order)", () => { const ids = (arr: FileListItem[]) => arr.map((f) => f.id); it("updated (default): most recently updated first", () => { expect(ids(sortPickerFiles(FIXTURES, "updated"))).toEqual(["a", "c", "b", "d"]); }); it("date: newest createdAt first", () => { expect(ids(sortPickerFiles(FIXTURES, "date"))).toEqual(["d", "b", "c", "a"]); }); it("size: largest first", () => { expect(ids(sortPickerFiles(FIXTURES, "size"))).toEqual(["d", "b", "c", "a"]); }); it("name: locale-aware ascending (case-insensitive)", () => { expect(ids(sortPickerFiles(FIXTURES, "name"))).toEqual(["a", "b", "c", "d"]); }); it("returns a NEW array — the caller's list is not mutated", () => { const before = [...FIXTURES]; sortPickerFiles(FIXTURES, "size"); expect(FIXTURES.map((f) => f.id)).toEqual(before.map((f) => f.id)); }); it("composes with filterByName: filter first, then sort (the picker pipeline)", () => { const pipeline = (q: string, sort: PickerSort) => sortPickerFiles(filterByName(FIXTURES, q), sort); // only Bravo.pdf matches "pdf" → single item, order irrelevant expect(ids(pipeline("pdf", "size"))).toEqual(["b"]); // "e" matches charlie + delta-notes → by size, delta(40) before charlie(20) expect(ids(pipeline("e", "size"))).toEqual(["d", "c"]); // "e" matches the same two → by name ascending, charlie before delta expect(ids(pipeline("e", "name"))).toEqual(["c", "d"]); }); });