Files
Sybil-2/web/tests/workspace-search.test.mjs
T
buzzert dd6cd1ea7b
TestFlight / Build and upload (push) Successful in 1m55s
Add indexed history search across iOS and web
2026-10-07 22:05:49 -07:00

81 lines
3.6 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { scheduleWorkspaceSearch } from "../src/lib/workspace-search.ts";
import { retainSelectedHistoryItem, resolveSidebarSelectionAfterRefresh } from "../src/lib/sidebar-selection.ts";
const flush = async () => { await Promise.resolve(); await Promise.resolve(); };
test("typing is debounced and superseded requests never start", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
const queries = [];
const results = [];
const request = async (query) => { queries.push(query); return { items: [], hasMore: false }; };
const reportError = (error) => assert.fail(String(error));
const cancelFirst = scheduleWorkspaceSearch("ne", request, (result) => results.push(result), reportError);
t.mock.timers.tick(299);
assert.deepEqual(queries, []);
cancelFirst();
const cancelLast = scheduleWorkspaceSearch("nebula", request, (result) => results.push(result), reportError);
t.mock.timers.tick(300);
await flush();
assert.deepEqual(queries, ["nebula"]);
assert.deepEqual(results, [{ items: [], hasMore: false }]);
cancelLast();
});
test("cancellation aborts the transport and discards stale results and errors", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
let resolveFirst;
let rejectSecond;
let signal;
const results = [];
const errors = [];
const first = scheduleWorkspaceSearch("first", (_query, value) => {
signal = value;
return new Promise((resolve) => { resolveFirst = resolve; });
}, (result) => results.push(result), (error) => errors.push(error));
t.mock.timers.tick(300);
first();
assert.equal(signal.aborted, true);
const second = scheduleWorkspaceSearch("second", () => new Promise((_resolve, reject) => { rejectSecond = reject; }),
(result) => results.push(result), (error) => errors.push(error));
t.mock.timers.tick(300);
second();
resolveFirst({ items: [{ id: "stale" }], hasMore: true });
rejectSecond(new Error("stale failure"));
await flush();
assert.deepEqual(results, []);
assert.deepEqual(errors, []);
});
test("search results retain excerpts and truncation info; failures reach the UI", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
const expected = { items: [{ type: "chat", id: "old", searchSnippet: "A nebula in the sky" }], hasMore: true };
let received;
let error;
const cancel = scheduleWorkspaceSearch("nebula", async () => expected, (result) => { received = result; }, assert.fail);
t.mock.timers.tick(300);
await flush();
assert.deepEqual(received, expected);
cancel();
const cancelFailure = scheduleWorkspaceSearch("nebula", async () => { throw new Error("Offline"); }, assert.fail, (result) => { error = result; });
t.mock.timers.tick(300);
await flush();
assert.equal(error.message, "Offline");
cancelFailure();
});
test("opening older chat/search results survives collection refresh without restoring deleted or unrelated items", () => {
const recent = [{ type: "chat", id: "recent" }];
for (const kind of ["chat", "search"]) {
const selection = { kind, id: "old" };
const retained = { type: kind, id: "old" };
const refreshed = retainSelectedHistoryItem(recent, selection, retained);
assert.deepEqual(resolveSidebarSelectionAfterRefresh(selection, refreshed), selection);
assert.equal(retainSelectedHistoryItem(refreshed, selection, retained), refreshed);
assert.equal(retainSelectedHistoryItem(recent, { kind, id: "different" }, retained), recent);
assert.equal(retainSelectedHistoryItem(recent, selection, null), recent);
assert.equal(retainSelectedHistoryItem(recent, null, retained), recent);
}
});