mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +03:00
Merge 7ce152141f into 37cfd2144b
This commit is contained in:
commit
20074f8f71
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 { SumEngine } from "../engine";
|
||||||
import { parseResultQuery, searchAllSheets, SearchDeps, SearchDoc } from "./search";
|
import { initSearch, parseResultQuery, searchAllSheets, SearchDeps, SearchDoc } from "./search";
|
||||||
import { Workspace } from "../workspace";
|
import { Workspace } from "../workspace";
|
||||||
import Decimal from "decimal.js";
|
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(() => 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