diff --git a/src/lib/stores/editor.test.ts b/src/lib/stores/editor.test.ts new file mode 100644 index 0000000..0154423 --- /dev/null +++ b/src/lib/stores/editor.test.ts @@ -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(); + }); +}); diff --git a/vitest.setup.ts b/vitest.setup.ts index 0263656..5090215 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -17,7 +17,10 @@ vi.mock("@tauri-apps/api/core", () => ({ if (command in mockInvokeResults) { const result = mockInvokeResults[command]; if (result instanceof Error) { - return Promise.reject(result); + const err = result; + return Promise.resolve().then(() => { + throw err; + }); } return Promise.resolve(result); }