fix(spellcheck): custom dictionary not actually saved due to CLS

This commit is contained in:
Elian Doran
2026-04-06 20:16:02 +03:00
parent ad97071862
commit ef72d89172
2 changed files with 56 additions and 32 deletions

View File

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

View File

@@ -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<string> {
* Saves the given words to the custom dictionary note, one per line.
*/
function saveWords(words: Set<string>) {
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"));
});
}
/**