generated from nhcarrigan/template
test: add coverage for editorStore methods and file system operations
This commit is contained in:
@@ -0,0 +1,362 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import { editorStore } from "$lib/stores/editor";
|
||||||
|
import { setMockInvokeResult } from "../../../vitest.setup";
|
||||||
|
|
||||||
|
const FILE_CONTENT = "// test file content";
|
||||||
|
|
||||||
|
// Reset tabs between tests
|
||||||
|
afterEach(async () => {
|
||||||
|
const tabs = get(editorStore.tabs);
|
||||||
|
for (const tab of tabs) {
|
||||||
|
editorStore.closeTab(tab.id);
|
||||||
|
}
|
||||||
|
editorStore.hideEditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - openFile (tests getLanguageFromPath)", () => {
|
||||||
|
const testCases: [string, string][] = [
|
||||||
|
["file.ts", "typescript"],
|
||||||
|
["file.tsx", "typescript"],
|
||||||
|
["file.js", "javascript"],
|
||||||
|
["file.jsx", "javascript"],
|
||||||
|
["file.py", "python"],
|
||||||
|
["file.rs", "rust"],
|
||||||
|
["file.go", "go"],
|
||||||
|
["file.java", "java"],
|
||||||
|
["file.c", "c"],
|
||||||
|
["file.cpp", "cpp"],
|
||||||
|
["file.h", "c"],
|
||||||
|
["file.hpp", "cpp"],
|
||||||
|
["file.cs", "csharp"],
|
||||||
|
["file.rb", "ruby"],
|
||||||
|
["file.php", "php"],
|
||||||
|
["file.swift", "swift"],
|
||||||
|
["file.kt", "kotlin"],
|
||||||
|
["file.scala", "scala"],
|
||||||
|
["file.html", "html"],
|
||||||
|
["file.htm", "html"],
|
||||||
|
["file.css", "css"],
|
||||||
|
["file.scss", "scss"],
|
||||||
|
["file.sass", "sass"],
|
||||||
|
["file.less", "less"],
|
||||||
|
["file.json", "json"],
|
||||||
|
["file.xml", "xml"],
|
||||||
|
["file.yaml", "yaml"],
|
||||||
|
["file.yml", "yaml"],
|
||||||
|
["file.toml", "toml"],
|
||||||
|
["file.md", "markdown"],
|
||||||
|
["file.markdown", "markdown"],
|
||||||
|
["file.sql", "sql"],
|
||||||
|
["file.sh", "shell"],
|
||||||
|
["file.bash", "shell"],
|
||||||
|
["file.zsh", "shell"],
|
||||||
|
["file.ps1", "powershell"],
|
||||||
|
["file.svelte", "svelte"],
|
||||||
|
["file.vue", "vue"],
|
||||||
|
["file.graphql", "graphql"],
|
||||||
|
["file.gql", "graphql"],
|
||||||
|
["file.lua", "lua"],
|
||||||
|
["file.r", "r"],
|
||||||
|
["file.dart", "dart"],
|
||||||
|
["file.elm", "elm"],
|
||||||
|
["file.ex", "elixir"],
|
||||||
|
["file.exs", "elixir"],
|
||||||
|
["file.erl", "erlang"],
|
||||||
|
["file.hs", "haskell"],
|
||||||
|
["file.clj", "clojure"],
|
||||||
|
["file.lisp", "lisp"],
|
||||||
|
["file.ml", "ocaml"],
|
||||||
|
["file.fs", "fsharp"],
|
||||||
|
["file.zig", "zig"],
|
||||||
|
["file.nim", "nim"],
|
||||||
|
["file.v", "v"],
|
||||||
|
["file.wasm", "wasm"],
|
||||||
|
["file.wat", "wasm"],
|
||||||
|
["dockerfile", "plaintext"],
|
||||||
|
["file.unknown", "plaintext"],
|
||||||
|
["file", "plaintext"],
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(testCases)("maps %s to language %s", async (filename, expectedLanguage) => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile(`/path/to/${filename}`);
|
||||||
|
const activeTab = get(editorStore.activeTab);
|
||||||
|
expect(activeTab?.language).toBe(expectedLanguage);
|
||||||
|
if (activeTab) editorStore.closeTab(activeTab.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens a new tab and makes it active", async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/path/to/file.ts");
|
||||||
|
const tabs = get(editorStore.tabs);
|
||||||
|
const activeTab = get(editorStore.activeTab);
|
||||||
|
expect(tabs).toHaveLength(1);
|
||||||
|
expect(activeTab?.fileName).toBe("file.ts");
|
||||||
|
expect(activeTab?.content).toBe(FILE_CONTENT);
|
||||||
|
expect(activeTab?.isDirty).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("switches to existing tab instead of opening a duplicate", async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/path/to/file.ts");
|
||||||
|
const firstTabId = get(editorStore.activeTabId);
|
||||||
|
|
||||||
|
// Open another file to change active tab
|
||||||
|
await editorStore.openFile("/path/to/other.ts");
|
||||||
|
const secondTabId = get(editorStore.activeTabId);
|
||||||
|
expect(secondTabId).not.toBe(firstTabId);
|
||||||
|
|
||||||
|
// Re-open first file — should switch to existing tab
|
||||||
|
await editorStore.openFile("/path/to/file.ts");
|
||||||
|
expect(get(editorStore.activeTabId)).toBe(firstTabId);
|
||||||
|
expect(get(editorStore.tabs)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles open file errors gracefully", async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("read_file_content", new Error("File not found"));
|
||||||
|
await editorStore.openFile("/path/to/missing.ts");
|
||||||
|
expect(get(editorStore.tabs)).toHaveLength(0);
|
||||||
|
expect(get(editorStore.saveError)).toContain("Failed to open file");
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts filename from path correctly", async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/deep/nested/path/component.svelte");
|
||||||
|
expect(get(editorStore.activeTab)?.fileName).toBe("component.svelte");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - saveFile", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/path/to/file.ts");
|
||||||
|
// Make it dirty first
|
||||||
|
editorStore.updateTabContent(get(editorStore.activeTabId)!, "modified content");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves the active tab successfully", async () => {
|
||||||
|
setMockInvokeResult("write_file_content", undefined);
|
||||||
|
await editorStore.saveFile();
|
||||||
|
expect(get(editorStore.activeTab)?.isDirty).toBe(false);
|
||||||
|
expect(get(editorStore.activeTab)?.originalContent).toBe("modified content");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saves a specific tab by ID", async () => {
|
||||||
|
setMockInvokeResult("write_file_content", undefined);
|
||||||
|
const tabId = get(editorStore.activeTabId)!;
|
||||||
|
await editorStore.saveFile(tabId);
|
||||||
|
expect(get(editorStore.activeTab)?.isDirty).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does nothing when no tab ID matches", async () => {
|
||||||
|
await editorStore.saveFile("non-existent-tab");
|
||||||
|
// No error = pass
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - saveFile error", () => {
|
||||||
|
it("sets saveError when write fails", async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/path/to/file-err.ts");
|
||||||
|
editorStore.updateTabContent(get(editorStore.activeTabId)!, "modified content");
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("write_file_content", new Error("Permission denied"));
|
||||||
|
await editorStore.saveFile().catch(() => {});
|
||||||
|
expect(get(editorStore.saveError)).toContain("Failed to save file");
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - updateTabContent", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/path/to/file.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates content and marks tab as dirty", () => {
|
||||||
|
const tabId = get(editorStore.activeTabId)!;
|
||||||
|
editorStore.updateTabContent(tabId, "new content");
|
||||||
|
const tab = get(editorStore.activeTab);
|
||||||
|
expect(tab?.content).toBe("new content");
|
||||||
|
expect(tab?.isDirty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks tab as clean when content matches original", () => {
|
||||||
|
const tabId = get(editorStore.activeTabId)!;
|
||||||
|
editorStore.updateTabContent(tabId, "modified");
|
||||||
|
expect(get(editorStore.activeTab)?.isDirty).toBe(true);
|
||||||
|
editorStore.updateTabContent(tabId, FILE_CONTENT);
|
||||||
|
expect(get(editorStore.activeTab)?.isDirty).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hasDirtyTabs is true when any tab is dirty", () => {
|
||||||
|
const tabId = get(editorStore.activeTabId)!;
|
||||||
|
expect(get(editorStore.hasDirtyTabs)).toBe(false);
|
||||||
|
editorStore.updateTabContent(tabId, "dirty content");
|
||||||
|
expect(get(editorStore.hasDirtyTabs)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - closeTab", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/path/to/file1.ts");
|
||||||
|
await editorStore.openFile("/path/to/file2.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes the specified tab", () => {
|
||||||
|
const tabs = get(editorStore.tabs);
|
||||||
|
editorStore.closeTab(tabs[0].id);
|
||||||
|
expect(get(editorStore.tabs)).toHaveLength(1);
|
||||||
|
expect(get(editorStore.tabs)[0].filePath).toBe("/path/to/file2.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets active tab to remaining tab when active tab is closed", () => {
|
||||||
|
const activeTabId = get(editorStore.activeTabId)!;
|
||||||
|
editorStore.closeTab(activeTabId);
|
||||||
|
expect(get(editorStore.activeTabId)).not.toBe(activeTabId);
|
||||||
|
expect(get(editorStore.tabs)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - setActiveTab", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
setMockInvokeResult("read_file_content", FILE_CONTENT);
|
||||||
|
await editorStore.openFile("/path/to/file1.ts");
|
||||||
|
await editorStore.openFile("/path/to/file2.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the active tab to the specified ID", () => {
|
||||||
|
const tabs = get(editorStore.tabs);
|
||||||
|
editorStore.setActiveTab(tabs[0].id);
|
||||||
|
expect(get(editorStore.activeTabId)).toBe(tabs[0].id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - visibility controls", () => {
|
||||||
|
it("showEditor sets isEditorVisible to true", () => {
|
||||||
|
expect(get(editorStore.isEditorVisible)).toBe(false);
|
||||||
|
editorStore.showEditor();
|
||||||
|
expect(get(editorStore.isEditorVisible)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hideEditor sets isEditorVisible to false", () => {
|
||||||
|
editorStore.showEditor();
|
||||||
|
editorStore.hideEditor();
|
||||||
|
expect(get(editorStore.isEditorVisible)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggleEditor toggles isEditorVisible", () => {
|
||||||
|
expect(get(editorStore.isEditorVisible)).toBe(false);
|
||||||
|
editorStore.toggleEditor();
|
||||||
|
expect(get(editorStore.isEditorVisible)).toBe(true);
|
||||||
|
editorStore.toggleEditor();
|
||||||
|
expect(get(editorStore.isEditorVisible)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - toggleFileBrowser", () => {
|
||||||
|
it("toggles the file browser visibility", () => {
|
||||||
|
const initial = get(editorStore.isFileBrowserOpen);
|
||||||
|
editorStore.toggleFileBrowser();
|
||||||
|
expect(get(editorStore.isFileBrowserOpen)).toBe(!initial);
|
||||||
|
editorStore.toggleFileBrowser();
|
||||||
|
expect(get(editorStore.isFileBrowserOpen)).toBe(initial);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - file system operations", () => {
|
||||||
|
it("createFile invokes backend and refreshes directory", async () => {
|
||||||
|
setMockInvokeResult("create_file", undefined);
|
||||||
|
setMockInvokeResult("list_directory", []);
|
||||||
|
await editorStore.createFile("/path/to", "new-file.ts");
|
||||||
|
// No error = success
|
||||||
|
});
|
||||||
|
|
||||||
|
it("createFile handles errors gracefully", async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("create_file", new Error("Permission denied"));
|
||||||
|
await editorStore.createFile("/path/to", "new-file.ts");
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to create file:", expect.any(Error));
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("createDirectory invokes backend and refreshes", async () => {
|
||||||
|
setMockInvokeResult("create_directory", undefined);
|
||||||
|
setMockInvokeResult("list_directory", []);
|
||||||
|
await editorStore.createDirectory("/path/to", "new-dir");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("createDirectory handles errors gracefully", async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("create_directory", new Error("Failed"));
|
||||||
|
await editorStore.createDirectory("/path/to", "new-dir");
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to create directory:", expect.any(Error));
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deleteFile invokes backend and refreshes", async () => {
|
||||||
|
setMockInvokeResult("delete_file", undefined);
|
||||||
|
setMockInvokeResult("list_directory", []);
|
||||||
|
await editorStore.deleteFile("/path/to/file.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deleteFile handles errors gracefully", async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("delete_file", new Error("Failed"));
|
||||||
|
await editorStore.deleteFile("/path/to/file.ts");
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to delete file:", expect.any(Error));
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deleteDirectory invokes backend and refreshes", async () => {
|
||||||
|
setMockInvokeResult("delete_directory", undefined);
|
||||||
|
setMockInvokeResult("list_directory", []);
|
||||||
|
await editorStore.deleteDirectory("/path/to/dir");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deleteDirectory handles errors gracefully", async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("delete_directory", new Error("Failed"));
|
||||||
|
await editorStore.deleteDirectory("/path/to/dir");
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to delete directory:", expect.any(Error));
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renamePath invokes backend and refreshes", async () => {
|
||||||
|
setMockInvokeResult("rename_path", undefined);
|
||||||
|
setMockInvokeResult("list_directory", []);
|
||||||
|
await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renamePath handles errors gracefully", async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("rename_path", new Error("Failed"));
|
||||||
|
await editorStore.renamePath("/path/to/old-name.ts", "new-name.ts");
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to rename:", expect.any(Error));
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("editorStore - initializeFileTree", () => {
|
||||||
|
it("loads directory entries and updates file tree", async () => {
|
||||||
|
const entries = [
|
||||||
|
{ path: "/path/file.ts", name: "file.ts", isDirectory: false, children: undefined },
|
||||||
|
];
|
||||||
|
setMockInvokeResult("list_directory", entries);
|
||||||
|
await editorStore.initializeFileTree("/path");
|
||||||
|
expect(get(editorStore.fileTree)).toEqual(entries);
|
||||||
|
expect(get(editorStore.currentDirectory)).toBe("/path");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles errors gracefully", async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
setMockInvokeResult("list_directory", new Error("Failed"));
|
||||||
|
await editorStore.initializeFileTree("/path");
|
||||||
|
expect(get(editorStore.isLoadingTree)).toBe(false);
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
+4
-1
@@ -17,7 +17,10 @@ vi.mock("@tauri-apps/api/core", () => ({
|
|||||||
if (command in mockInvokeResults) {
|
if (command in mockInvokeResults) {
|
||||||
const result = mockInvokeResults[command];
|
const result = mockInvokeResults[command];
|
||||||
if (result instanceof Error) {
|
if (result instanceof Error) {
|
||||||
return Promise.reject(result);
|
const err = result;
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve(result);
|
return Promise.resolve(result);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user