From ef72d89172222f739d044baef517b4481cc6f6ff Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 20:16:02 +0300 Subject: [PATCH] fix(spellcheck): custom dictionary not actually saved due to CLS --- .../src/services/custom_dictionary.spec.ts | 71 ++++++++++++------- apps/server/src/services/custom_dictionary.ts | 17 +++-- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/apps/server/src/services/custom_dictionary.spec.ts b/apps/server/src/services/custom_dictionary.spec.ts index 56b54bad2b..9d94dc79ab 100644 --- a/apps/server/src/services/custom_dictionary.spec.ts +++ b/apps/server/src/services/custom_dictionary.spec.ts @@ -1,4 +1,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; +import becca from "../becca/becca.js"; +import { buildNote } from "../test/becca_easy_mocking.js"; +import customDictionary from "./custom_dictionary.js"; vi.mock("./log.js", () => ({ default: { @@ -7,20 +10,17 @@ vi.mock("./log.js", () => ({ } })); -const mockNote = { - getContent: vi.fn(), - setContent: vi.fn() -}; - -vi.mock("../becca/becca.js", () => ({ +vi.mock("./sql.js", () => ({ default: { - getNote: vi.fn() + transactional: (cb: Function) => cb(), + execute: () => {}, + replace: () => {}, + getMap: () => {}, + getValue: () => null, + upsert: () => {} } })); -import becca from "../becca/becca.js"; -import customDictionary from "./custom_dictionary.js"; - function mockSession(localWords: string[] = []) { return { listWordsInSpellCheckerDictionary: vi.fn().mockResolvedValue(localWords), @@ -31,67 +31,89 @@ function mockSession(localWords: string[] = []) { describe("custom_dictionary", () => { beforeEach(() => { vi.clearAllMocks(); - vi.mocked(becca.getNote).mockReturnValue(mockNote as any); + becca.reset(); + buildNote({ + id: "_customDictionary", + title: "Custom Dictionary", + type: "code", + content: "" + }); }); describe("loadForSession", () => { it("does nothing when note is empty and no local words", async () => { - mockNote.getContent.mockReturnValue(""); const session = mockSession(); await customDictionary.loadForSession(session); expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled(); - expect(mockNote.setContent).not.toHaveBeenCalled(); }); it("imports local words when note is empty (one-time import)", async () => { - mockNote.getContent.mockReturnValue(""); const session = mockSession(["hello", "world"]); await customDictionary.loadForSession(session); - expect(mockNote.setContent).toHaveBeenCalledWith("hello\nworld"); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("hello"); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("world"); }); it("loads note words into session when no local words exist", async () => { - mockNote.getContent.mockReturnValue("apple\nbanana"); + becca.reset(); + buildNote({ + id: "_customDictionary", + title: "Custom Dictionary", + type: "code", + content: "apple\nbanana" + }); const session = mockSession(); await customDictionary.loadForSession(session); - expect(mockNote.setContent).not.toHaveBeenCalled(); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple"); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("banana"); }); it("merges note and local words when both have content", async () => { - mockNote.getContent.mockReturnValue("apple\nbanana"); + becca.reset(); + buildNote({ + id: "_customDictionary", + title: "Custom Dictionary", + type: "code", + content: "apple\nbanana" + }); const session = mockSession(["banana", "cherry"]); await customDictionary.loadForSession(session); - // Should save the merged set (apple + banana + cherry), sorted - expect(mockNote.setContent).toHaveBeenCalledWith("apple\nbanana\ncherry"); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(3); }); it("does not save when local words are a subset of note words", async () => { - mockNote.getContent.mockReturnValue("apple\nbanana\ncherry"); + becca.reset(); + buildNote({ + id: "_customDictionary", + title: "Custom Dictionary", + type: "code", + content: "apple\nbanana\ncherry" + }); const session = mockSession(["apple", "banana"]); await customDictionary.loadForSession(session); - expect(mockNote.setContent).not.toHaveBeenCalled(); expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(3); }); it("handles note with whitespace and blank lines", async () => { - mockNote.getContent.mockReturnValue(" apple \n\n banana \n\n"); + becca.reset(); + buildNote({ + id: "_customDictionary", + title: "Custom Dictionary", + type: "code", + content: " apple \n\n banana \n\n" + }); const session = mockSession(); await customDictionary.loadForSession(session); @@ -102,12 +124,11 @@ describe("custom_dictionary", () => { }); it("handles missing dictionary note gracefully", async () => { - vi.mocked(becca.getNote).mockReturnValue(null as any); + becca.reset(); // no note created const session = mockSession(["hello"]); await customDictionary.loadForSession(session); - // Can't save, but shouldn't crash expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled(); }); }); diff --git a/apps/server/src/services/custom_dictionary.ts b/apps/server/src/services/custom_dictionary.ts index 1b48b2210d..c8f2748fdd 100644 --- a/apps/server/src/services/custom_dictionary.ts +++ b/apps/server/src/services/custom_dictionary.ts @@ -1,6 +1,7 @@ import type { Session } from "electron"; import becca from "../becca/becca.js"; +import cls from "./cls.js"; import log from "./log.js"; const DICTIONARY_NOTE_ID = "_customDictionary"; @@ -30,14 +31,16 @@ function getWords(): Set { * Saves the given words to the custom dictionary note, one per line. */ function saveWords(words: Set) { - const note = becca.getNote(DICTIONARY_NOTE_ID); - if (!note) { - log.error("Custom dictionary note not found."); - return; - } + cls.init(() => { + const note = becca.getNote(DICTIONARY_NOTE_ID); + if (!note) { + log.error("Custom dictionary note not found."); + return; + } - const sorted = [...words].sort((a, b) => a.localeCompare(b)); - note.setContent(sorted.join("\n")); + const sorted = [...words].sort((a, b) => a.localeCompare(b)); + note.setContent(sorted.join("\n")); + }); } /**