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(); }); }); 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); }); });