mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-10 22:41:58 +03:00
test: Add tests for initSearch function
Added unit tests for the previously untested initSearch function in src/ui/search.ts. A manual DOM mock was implemented to verify UI behaviors such as opening, closing, debouncing inputs, rendering results, and reacting to user actions without relying on jsdom. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
37cfd2144b
commit
440794e6e2
1 changed files with 199 additions and 2 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { SumEngine } from "../engine";
|
||||
import { parseResultQuery, searchAllSheets, SearchDeps, SearchDoc } from "./search";
|
||||
import { initSearch, parseResultQuery, searchAllSheets, SearchDeps, SearchDoc } from "./search";
|
||||
import { Workspace } from "../workspace";
|
||||
import Decimal from "decimal.js";
|
||||
|
||||
|
|
@ -241,3 +241,200 @@ describe("searchAllSheets", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("initSearch", () => {
|
||||
class MockElement {
|
||||
className: string = "";
|
||||
textContent: string = "";
|
||||
value: string = "";
|
||||
classList = {
|
||||
add: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
children: MockElement[] = [];
|
||||
listeners: Record<string, Function[]> = {};
|
||||
|
||||
replaceChildren() {
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
appendChild(child: MockElement) {
|
||||
if (child.isFragment) {
|
||||
this.children.push(...child.children);
|
||||
} else {
|
||||
this.children.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
append(...children: MockElement[]) {
|
||||
this.children.push(...children);
|
||||
}
|
||||
|
||||
addEventListener(event: string, handler: Function) {
|
||||
if (!this.listeners[event]) this.listeners[event] = [];
|
||||
this.listeners[event].push(handler);
|
||||
}
|
||||
|
||||
dispatchEvent(event: any) {
|
||||
const handlers = this.listeners[event.type] || [];
|
||||
for (const h of handlers) h(event);
|
||||
}
|
||||
|
||||
focus = vi.fn();
|
||||
|
||||
click() {
|
||||
this.dispatchEvent({ type: "click" });
|
||||
}
|
||||
|
||||
querySelector(sel: string): MockElement | undefined {
|
||||
if (sel === ".search-item") {
|
||||
return this.children.find(c => c.className === "search-item");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isFragment = false;
|
||||
}
|
||||
|
||||
let elements: Record<string, MockElement>;
|
||||
|
||||
beforeEach(() => {
|
||||
elements = {
|
||||
"#search-overlay": new MockElement(),
|
||||
"#search-input": new MockElement(),
|
||||
"#search-results": new MockElement(),
|
||||
};
|
||||
|
||||
vi.stubGlobal("document", {
|
||||
querySelector: vi.fn((sel: string) => elements[sel]),
|
||||
createElement: vi.fn((tag: string) => new MockElement()),
|
||||
createDocumentFragment: vi.fn(() => {
|
||||
const frag = new MockElement();
|
||||
frag.isFragment = true;
|
||||
return frag;
|
||||
}),
|
||||
});
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("opens search and focuses input", () => {
|
||||
const deps = { t: vi.fn(), onOpen: vi.fn() } as unknown as SearchDeps;
|
||||
const ctrl = initSearch(deps);
|
||||
|
||||
ctrl.open();
|
||||
|
||||
expect(elements["#search-overlay"].classList.remove).toHaveBeenCalledWith("hidden");
|
||||
expect(elements["#search-input"].value).toBe("");
|
||||
expect(elements["#search-input"].focus).toHaveBeenCalled();
|
||||
expect(elements["#search-results"].children).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("closes search on Escape key", () => {
|
||||
const deps = { t: vi.fn(), onOpen: vi.fn() } as unknown as SearchDeps;
|
||||
initSearch(deps);
|
||||
|
||||
elements["#search-input"].dispatchEvent({ type: "keydown", key: "Escape" });
|
||||
|
||||
expect(elements["#search-overlay"].classList.add).toHaveBeenCalledWith("hidden");
|
||||
});
|
||||
|
||||
it("closes search on clicking overlay", () => {
|
||||
const deps = { t: vi.fn(), onOpen: vi.fn() } as unknown as SearchDeps;
|
||||
initSearch(deps);
|
||||
|
||||
elements["#search-overlay"].dispatchEvent({ type: "mousedown", target: elements["#search-overlay"] });
|
||||
|
||||
expect(elements["#search-overlay"].classList.add).toHaveBeenCalledWith("hidden");
|
||||
});
|
||||
|
||||
it("renders empty state", () => {
|
||||
const deps = {
|
||||
t: vi.fn().mockReturnValue("No results"),
|
||||
docs: () => [],
|
||||
} as unknown as SearchDeps;
|
||||
initSearch(deps);
|
||||
|
||||
elements["#search-input"].value = "test query";
|
||||
elements["#search-input"].dispatchEvent({ type: "input" });
|
||||
|
||||
vi.runAllTimers();
|
||||
|
||||
const results = elements["#search-results"];
|
||||
expect(results.children).toHaveLength(1);
|
||||
expect(results.children[0].className).toBe("search-empty");
|
||||
expect(results.children[0].textContent).toBe("No results");
|
||||
});
|
||||
|
||||
it("renders search hits and opens them on click", () => {
|
||||
const deps = {
|
||||
docs: () => [{ id: "doc1", title: "Doc 1", text: "test match" }],
|
||||
onOpen: vi.fn(),
|
||||
engine: new SumEngine(),
|
||||
} as unknown as SearchDeps;
|
||||
initSearch(deps);
|
||||
|
||||
elements["#search-input"].value = "test";
|
||||
elements["#search-input"].dispatchEvent({ type: "input" });
|
||||
|
||||
vi.runAllTimers();
|
||||
|
||||
const results = elements["#search-results"];
|
||||
expect(results.children).toHaveLength(1);
|
||||
|
||||
const item = results.children[0];
|
||||
expect(item.className).toBe("search-item");
|
||||
expect(item.children).toHaveLength(2);
|
||||
expect(item.children[0].className).toBe("doc");
|
||||
expect(item.children[0].textContent).toBe("Doc 1");
|
||||
expect(item.children[1].className).toBe("line");
|
||||
expect(item.children[1].textContent).toBe("test match");
|
||||
|
||||
item.click();
|
||||
expect(deps.onOpen).toHaveBeenCalledWith("doc1", 1);
|
||||
expect(elements["#search-overlay"].classList.add).toHaveBeenCalledWith("hidden");
|
||||
});
|
||||
|
||||
it("opens first result on Enter key", () => {
|
||||
const deps = {
|
||||
docs: () => [{ id: "doc1", title: "Doc 1", text: "test match" }],
|
||||
onOpen: vi.fn(),
|
||||
engine: new SumEngine(),
|
||||
} as unknown as SearchDeps;
|
||||
initSearch(deps);
|
||||
|
||||
elements["#search-input"].value = "test";
|
||||
elements["#search-input"].dispatchEvent({ type: "input" });
|
||||
|
||||
vi.runAllTimers();
|
||||
|
||||
elements["#search-input"].dispatchEvent({ type: "keydown", key: "Enter" });
|
||||
|
||||
expect(deps.onOpen).toHaveBeenCalledWith("doc1", 1);
|
||||
});
|
||||
|
||||
it("debounces search inputs", () => {
|
||||
const deps = {
|
||||
docs: vi.fn().mockReturnValue([]),
|
||||
t: vi.fn(),
|
||||
} as unknown as SearchDeps;
|
||||
initSearch(deps);
|
||||
|
||||
elements["#search-input"].value = "t";
|
||||
elements["#search-input"].dispatchEvent({ type: "input" });
|
||||
elements["#search-input"].value = "te";
|
||||
elements["#search-input"].dispatchEvent({ type: "input" });
|
||||
elements["#search-input"].value = "tes";
|
||||
elements["#search-input"].dispatchEvent({ type: "input" });
|
||||
|
||||
expect(deps.docs).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(150);
|
||||
|
||||
expect(deps.docs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue