generated from nhcarrigan/template
feat: multiple UI improvements, font settings, and memory file display names #175
@@ -552,6 +552,48 @@ describe("clipboardStore - store actions", () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to toggle pin:", expect.any(Error));
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("exercises the unpinned-before-pinned sort branch", async () => {
|
||||
// Three entries so sort compares (entry-1 unpinned, entry-2 pinned) — hits line 144
|
||||
const e1 = { ...mockEntry, id: "e1", is_pinned: false };
|
||||
const e2 = { ...mockEntry, id: "e2", is_pinned: false };
|
||||
const e3 = { ...mockEntry, id: "e3", is_pinned: false };
|
||||
clipboardStore.entries.set([e1, e2, e3]);
|
||||
const pinned = { ...e2, is_pinned: true };
|
||||
setMockInvokeResult("toggle_pin_clipboard_entry", pinned);
|
||||
await clipboardStore.togglePin("e2");
|
||||
expect(get(clipboardStore.entries)[0].id).toBe("e2");
|
||||
});
|
||||
|
||||
it("sorts same-pin-status entries by timestamp descending", async () => {
|
||||
// Toggle entry to pinned while two others remain unpinned — sort compares unpinned pair by timestamp
|
||||
const older = {
|
||||
...mockEntry,
|
||||
id: "older",
|
||||
is_pinned: false,
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
const newer = {
|
||||
...mockEntry,
|
||||
id: "newer",
|
||||
is_pinned: false,
|
||||
timestamp: "2026-01-02T00:00:00.000Z",
|
||||
};
|
||||
const pinned = {
|
||||
...mockEntry,
|
||||
id: "pinned",
|
||||
is_pinned: false,
|
||||
timestamp: "2026-01-03T00:00:00.000Z",
|
||||
};
|
||||
clipboardStore.entries.set([older, newer, pinned]);
|
||||
const pinnedResult = { ...pinned, is_pinned: true };
|
||||
setMockInvokeResult("toggle_pin_clipboard_entry", pinnedResult);
|
||||
await clipboardStore.togglePin("pinned");
|
||||
const entries = get(clipboardStore.entries);
|
||||
expect(entries[0].id).toBe("pinned");
|
||||
expect(entries[1].id).toBe("newer");
|
||||
expect(entries[2].id).toBe("older");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearHistory", () => {
|
||||
@@ -587,6 +629,16 @@ describe("clipboardStore - store actions", () => {
|
||||
expect(get(clipboardStore.entries)[0].language).toBe("javascript");
|
||||
});
|
||||
|
||||
it("leaves non-matching entries unchanged", async () => {
|
||||
const other = { ...mockEntry, id: "other", language: "python" };
|
||||
clipboardStore.entries.set([mockEntry, other]);
|
||||
const updated = { ...mockEntry, language: "javascript" };
|
||||
setMockInvokeResult("update_clipboard_language", updated);
|
||||
await clipboardStore.updateLanguage("entry-1", "javascript");
|
||||
const entries = get(clipboardStore.entries);
|
||||
expect(entries[1].language).toBe("python");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("update_clipboard_language", new Error("Failed"));
|
||||
|
||||
@@ -360,3 +360,280 @@ describe("editorStore - initializeFileTree", () => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorStore - toggleDirectory", () => {
|
||||
beforeEach(async () => {
|
||||
const entries = [
|
||||
{
|
||||
path: "/path/dir",
|
||||
name: "dir",
|
||||
isDirectory: true,
|
||||
isExpanded: false,
|
||||
children: undefined,
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("list_directory", entries);
|
||||
await editorStore.initializeFileTree("/path");
|
||||
});
|
||||
|
||||
it("does nothing when entry is not a directory", async () => {
|
||||
const nonDirEntry = {
|
||||
path: "/path/file.ts",
|
||||
name: "file.ts",
|
||||
isDirectory: false,
|
||||
};
|
||||
const treeBefore = get(editorStore.fileTree);
|
||||
await editorStore.toggleDirectory(nonDirEntry);
|
||||
expect(get(editorStore.fileTree)).toEqual(treeBefore);
|
||||
});
|
||||
|
||||
it("expands a collapsed directory and loads children asynchronously", async () => {
|
||||
const children = [
|
||||
{ path: "/path/dir/child.ts", name: "child.ts", isDirectory: false, children: undefined },
|
||||
];
|
||||
setMockInvokeResult("list_directory", children);
|
||||
|
||||
const dirEntry = get(editorStore.fileTree)[0];
|
||||
await editorStore.toggleDirectory(dirEntry);
|
||||
|
||||
const updatedEntry = get(editorStore.fileTree)[0];
|
||||
expect(updatedEntry.isExpanded).toBe(true);
|
||||
expect(updatedEntry.children).toEqual(children);
|
||||
expect(updatedEntry.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("collapses an expanded directory", async () => {
|
||||
const children = [
|
||||
{ path: "/path/dir/child.ts", name: "child.ts", isDirectory: false, children: undefined },
|
||||
];
|
||||
setMockInvokeResult("list_directory", children);
|
||||
|
||||
// Expand first
|
||||
const dirEntry = get(editorStore.fileTree)[0];
|
||||
await editorStore.toggleDirectory(dirEntry);
|
||||
expect(get(editorStore.fileTree)[0].isExpanded).toBe(true);
|
||||
|
||||
// Now collapse
|
||||
const expandedEntry = get(editorStore.fileTree)[0];
|
||||
await editorStore.toggleDirectory(expandedEntry);
|
||||
expect(get(editorStore.fileTree)[0].isExpanded).toBe(false);
|
||||
});
|
||||
|
||||
it("expands a directory that already has children without reloading", async () => {
|
||||
// Re-init with a directory that already has children
|
||||
const entries = [
|
||||
{
|
||||
path: "/path/dir",
|
||||
name: "dir",
|
||||
isDirectory: true,
|
||||
isExpanded: false,
|
||||
children: [
|
||||
{
|
||||
path: "/path/dir/child.ts",
|
||||
name: "child.ts",
|
||||
isDirectory: false,
|
||||
children: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("list_directory", entries);
|
||||
await editorStore.initializeFileTree("/path");
|
||||
|
||||
// Toggle should expand without loading (children already present)
|
||||
const dirEntry = get(editorStore.fileTree)[0];
|
||||
await editorStore.toggleDirectory(dirEntry);
|
||||
|
||||
const updatedEntry = get(editorStore.fileTree)[0];
|
||||
expect(updatedEntry.isExpanded).toBe(true);
|
||||
expect(updatedEntry.children).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("handles tree with multiple entries when toggling — exercises non-matching leaf fallback", async () => {
|
||||
// Tree has dir + a sibling file; toggling dir exercises the `return e` branch for the file
|
||||
const entries = [
|
||||
{
|
||||
path: "/path/dir",
|
||||
name: "dir",
|
||||
isDirectory: true,
|
||||
isExpanded: false,
|
||||
children: undefined,
|
||||
},
|
||||
{ path: "/path/file.ts", name: "file.ts", isDirectory: false, children: undefined },
|
||||
];
|
||||
setMockInvokeResult("list_directory", entries);
|
||||
await editorStore.initializeFileTree("/path");
|
||||
|
||||
const children: never[] = [];
|
||||
setMockInvokeResult("list_directory", children);
|
||||
const dirEntry = get(editorStore.fileTree)[0];
|
||||
await editorStore.toggleDirectory(dirEntry);
|
||||
|
||||
// The sibling file should remain unchanged
|
||||
expect(get(editorStore.fileTree)[1].path).toBe("/path/file.ts");
|
||||
expect(get(editorStore.fileTree)[0].isExpanded).toBe(true);
|
||||
});
|
||||
|
||||
it("handles nested directory toggle — exercises recursive updateTree branches", async () => {
|
||||
// Tree: outer_dir → [inner_dir (no children), sibling_file]
|
||||
// Toggling inner_dir covers the recursive path through outer's children
|
||||
const entries = [
|
||||
{
|
||||
path: "/path/outer",
|
||||
name: "outer",
|
||||
isDirectory: true,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
path: "/path/outer/inner",
|
||||
name: "inner",
|
||||
isDirectory: true,
|
||||
isExpanded: false,
|
||||
children: undefined,
|
||||
},
|
||||
{
|
||||
path: "/path/outer/sibling.ts",
|
||||
name: "sibling.ts",
|
||||
isDirectory: false,
|
||||
children: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("list_directory", entries);
|
||||
await editorStore.initializeFileTree("/path");
|
||||
|
||||
const innerChildren = [
|
||||
{
|
||||
path: "/path/outer/inner/deep.ts",
|
||||
name: "deep.ts",
|
||||
isDirectory: false,
|
||||
children: undefined,
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("list_directory", innerChildren);
|
||||
|
||||
const innerEntry = get(editorStore.fileTree)[0].children![0];
|
||||
await editorStore.toggleDirectory(innerEntry);
|
||||
|
||||
const outer = get(editorStore.fileTree)[0];
|
||||
expect(outer.children![0].isExpanded).toBe(true);
|
||||
expect(outer.children![0].children).toEqual(innerChildren);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorStore - deleteFile closes open tab", () => {
|
||||
it("closes the tab when the deleted file is open", async () => {
|
||||
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||
await editorStore.openFile("/path/to/file.ts");
|
||||
expect(get(editorStore.tabs)).toHaveLength(1);
|
||||
|
||||
setMockInvokeResult("delete_file", undefined);
|
||||
setMockInvokeResult("list_directory", []);
|
||||
await editorStore.deleteFile("/path/to/file.ts");
|
||||
expect(get(editorStore.tabs)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorStore - deleteDirectory closes open tabs", () => {
|
||||
it("closes all tabs inside the deleted directory", async () => {
|
||||
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||
await editorStore.openFile("/path/to/dir/file1.ts");
|
||||
await editorStore.openFile("/path/to/dir/nested/file2.ts");
|
||||
await editorStore.openFile("/path/to/other.ts");
|
||||
expect(get(editorStore.tabs)).toHaveLength(3);
|
||||
|
||||
setMockInvokeResult("delete_directory", undefined);
|
||||
setMockInvokeResult("list_directory", []);
|
||||
await editorStore.deleteDirectory("/path/to/dir");
|
||||
|
||||
const remainingTabs = get(editorStore.tabs);
|
||||
expect(remainingTabs).toHaveLength(1);
|
||||
expect(remainingTabs[0].filePath).toBe("/path/to/other.ts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorStore - renamePath updates open tabs", () => {
|
||||
it("updates filePath and fileName when the renamed file is open in a tab", async () => {
|
||||
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||
await editorStore.openFile("/path/to/old-name.ts");
|
||||
|
||||
setMockInvokeResult("rename_path", undefined);
|
||||
setMockInvokeResult("list_directory", []);
|
||||
await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");
|
||||
|
||||
const tab = get(editorStore.activeTab);
|
||||
expect(tab?.filePath).toBe("/path/to/new-name.ts");
|
||||
expect(tab?.fileName).toBe("new-name.ts");
|
||||
});
|
||||
|
||||
it("updates filePaths for tabs inside a renamed directory", async () => {
|
||||
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||
await editorStore.openFile("/path/to/old-dir/file.ts");
|
||||
|
||||
setMockInvokeResult("rename_path", undefined);
|
||||
setMockInvokeResult("list_directory", []);
|
||||
await editorStore.renamePath("/path/to/old-dir", "new-dir");
|
||||
|
||||
const tab = get(editorStore.activeTab);
|
||||
expect(tab?.filePath).toBe("/path/to/new-dir/file.ts");
|
||||
});
|
||||
|
||||
it("leaves unrelated tabs unchanged when renaming — exercises return-t fallback", async () => {
|
||||
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||
await editorStore.openFile("/path/to/unrelated.ts");
|
||||
await editorStore.openFile("/path/to/old-name.ts");
|
||||
|
||||
setMockInvokeResult("rename_path", undefined);
|
||||
setMockInvokeResult("list_directory", []);
|
||||
await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");
|
||||
|
||||
const tabs = get(editorStore.tabs);
|
||||
const unrelated = tabs.find((t) => t.filePath === "/path/to/unrelated.ts");
|
||||
expect(unrelated).toBeDefined();
|
||||
expect(unrelated?.fileName).toBe("unrelated.ts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("editorStore - refreshDirectory", () => {
|
||||
it("reloads the entire tree when refreshing the root directory", async () => {
|
||||
const initialEntries = [
|
||||
{ path: "/root/file.ts", name: "file.ts", isDirectory: false, children: undefined },
|
||||
];
|
||||
setMockInvokeResult("list_directory", initialEntries);
|
||||
await editorStore.initializeFileTree("/root");
|
||||
expect(get(editorStore.fileTree)).toHaveLength(1);
|
||||
|
||||
const updatedEntries = [
|
||||
{ path: "/root/file.ts", name: "file.ts", isDirectory: false, children: undefined },
|
||||
{ path: "/root/new.ts", name: "new.ts", isDirectory: false, children: undefined },
|
||||
];
|
||||
setMockInvokeResult("list_directory", updatedEntries);
|
||||
await editorStore.refreshDirectory("/root");
|
||||
expect(get(editorStore.fileTree)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("updates a subdirectory entry in the tree", async () => {
|
||||
const initialEntries = [
|
||||
{
|
||||
path: "/root/subdir",
|
||||
name: "subdir",
|
||||
isDirectory: true,
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
setMockInvokeResult("list_directory", initialEntries);
|
||||
await editorStore.initializeFileTree("/root");
|
||||
|
||||
const subChildren = [
|
||||
{ path: "/root/subdir/file.ts", name: "file.ts", isDirectory: false, children: undefined },
|
||||
];
|
||||
setMockInvokeResult("list_directory", subChildren);
|
||||
await editorStore.refreshDirectory("/root/subdir");
|
||||
|
||||
const tree = get(editorStore.fileTree);
|
||||
expect(tree[0].children).toEqual(subChildren);
|
||||
expect(tree[0].isExpanded).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,6 +220,16 @@ describe("sessionsStore - scheduleAutoSave and cancelAutoSave", () => {
|
||||
it("handles cancel when no auto-save is pending", () => {
|
||||
sessionsStore.cancelAutoSave("non-existent-id");
|
||||
});
|
||||
|
||||
it("clears an existing pending auto-save when rescheduled for the same conversation", async () => {
|
||||
setMockInvokeResult("save_session", undefined);
|
||||
setMockInvokeResult("list_sessions", []);
|
||||
const conv = makeConversation();
|
||||
sessionsStore.scheduleAutoSave(conv as never);
|
||||
// Schedule again before timer fires — hits clearTimeout branch (line 487)
|
||||
sessionsStore.scheduleAutoSave(conv as never);
|
||||
await vi.advanceTimersByTimeAsync(2001);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionsStore - exportSessionAsJson", () => {
|
||||
@@ -254,6 +264,17 @@ describe("sessionsStore - exportSessionAsJson", () => {
|
||||
expect(await sessionsStore.exportSessionAsJson("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns false when writeTextFile throws", async () => {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("load_session", makeSavedSession());
|
||||
vi.mocked(save).mockResolvedValue("/output/session.json");
|
||||
vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
|
||||
expect(await sessionsStore.exportSessionAsJson("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionsStore - exportSessionAsMarkdown", () => {
|
||||
@@ -318,6 +339,17 @@ describe("sessionsStore - exportSessionAsMarkdown", () => {
|
||||
expect(await sessionsStore.exportSessionAsMarkdown("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns false when writeTextFile throws", async () => {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("load_session", makeSavedSession());
|
||||
vi.mocked(save).mockResolvedValue("/output/session.md");
|
||||
vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
|
||||
expect(await sessionsStore.exportSessionAsMarkdown("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionsStore - exportSessionAsHtml (tests escapeHtml + generateHtmlExport)", () => {
|
||||
@@ -424,6 +456,17 @@ describe("sessionsStore - exportSessionAsHtml (tests escapeHtml + generateHtmlEx
|
||||
expect(await sessionsStore.exportSessionAsHtml("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns false when writeTextFile throws", async () => {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("load_session", makeSavedSession());
|
||||
vi.mocked(save).mockResolvedValue("/output/session.html");
|
||||
vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
|
||||
expect(await sessionsStore.exportSessionAsHtml("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionsStore - exportSessionAsPdf (tests generatePrintableHtml)", () => {
|
||||
@@ -472,6 +515,17 @@ describe("sessionsStore - exportSessionAsPdf (tests generatePrintableHtml)", ()
|
||||
expect(await sessionsStore.exportSessionAsPdf("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns false when writeTextFile throws", async () => {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("load_session", makeSavedSession());
|
||||
vi.mocked(save).mockResolvedValue("/output/session-print.html");
|
||||
vi.mocked(writeTextFile).mockRejectedValueOnce(new Error("Disk full"));
|
||||
expect(await sessionsStore.exportSessionAsPdf("session-1")).toBe(false);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessionsStore - importSession", () => {
|
||||
|
||||
Reference in New Issue
Block a user