Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 149 additions & 1 deletion client/modules/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ const mockFetchResponse = (results: string[][]) => {
};

describe("Search Module", () => {
beforeEach(() => {
beforeEach(async () => {
// Reset shared mutable state mutated by other test suites so every
// describe block runs with a clean, deterministic cache config.
searchModule.searchServiceInstance.updateCacheConfig({
ttl: 15 * 60 * 1000,
maxEntries: 100,
enabled: true,
});
// Clear the fake IndexedDB database so each test starts fresh.
await searchModule.searchServiceInstance.clearSearchCache();
// Clear mocks after clearSearchCache so addLogEntry starts empty.
vi.clearAllMocks();
});

Expand Down Expand Up @@ -360,6 +370,144 @@ describe("Search Module", () => {
});
});

describe("Cache Hit Path", () => {
it("should serve a repeated text query from the IndexedDB cache without a second fetch", async () => {
const mockResults: string[][] = [
["Title", "Snippet", "https://example.com"],
];
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResults),
});

// First call — cache miss, fetches from network.
const firstResults = await searchModule.searchText("cached query");
expect(firstResults).toEqual(mockResults);
expect(mockFetch).toHaveBeenCalledTimes(1);

// Second call with the same query — should be a cache hit.
const secondResults = await searchModule.searchText("cached query");
expect(secondResults).toEqual(mockResults);
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(addLogEntry).toHaveBeenCalledWith(
expect.stringContaining("Text search: Reused 1 results from the cache"),
);
expect(searchModule.searchServiceInstance.getCacheStats().textHits).toBe(
1,
);
});

it("should serve a repeated image query from the IndexedDB cache without a second fetch", async () => {
const mockResults: string[][] = [
["Image", "Alt", "https://example.com/img.jpg"],
];
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResults),
});

const firstResults = await searchModule.searchImages("cached images");
expect(firstResults).toEqual(mockResults);
expect(mockFetch).toHaveBeenCalledTimes(1);

const secondResults = await searchModule.searchImages("cached images");
expect(secondResults).toEqual(mockResults);
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(addLogEntry).toHaveBeenCalledWith(
expect.stringContaining(
"Image search: Reused 1 results from the cache",
),
);
expect(searchModule.searchServiceInstance.getCacheStats().imageHits).toBe(
1,
);
});

it("should treat different queries as separate cache entries", async () => {
const mockResults: string[][] = [
["Title", "Snippet", "https://example.com"],
];
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResults),
});

await searchModule.searchText("query A");
await searchModule.searchText("query B");
expect(mockFetch).toHaveBeenCalledTimes(2);
});

it("should treat different limits as separate cache entries", async () => {
const mockResults: string[][] = [
["Title", "Snippet", "https://example.com"],
];
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResults),
});

await searchModule.searchText("same query", 5);
await searchModule.searchText("same query", 10);
expect(mockFetch).toHaveBeenCalledTimes(2);
});

it("should refetch when TTL expires", async () => {
const mockResults: string[][] = [
["Title", "Snippet", "https://example.com"],
];
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResults),
});

await searchModule.searchText("ttl query");
expect(mockFetch).toHaveBeenCalledTimes(1);

// Expire the cache entry instantly.
searchModule.searchServiceInstance.updateCacheConfig({ ttl: 0 });

const secondResults = await searchModule.searchText("ttl query");
expect(secondResults).toEqual(mockResults);
expect(mockFetch).toHaveBeenCalledTimes(2);
});

it("should skip cache when caching is disabled", async () => {
const mockResults: string[][] = [
["Title", "Snippet", "https://example.com"],
];
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResults),
});

searchModule.searchServiceInstance.updateCacheConfig({ enabled: false });

await searchModule.searchText("disabled cache query");
await searchModule.searchText("disabled cache query");
expect(mockFetch).toHaveBeenCalledTimes(2);
});

it("should use separate caches for text and image stores", async () => {
const mockResults: string[][] = [
["Title", "Snippet", "https://example.com"],
];
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResults),
});

await searchModule.searchText("cross-store query");
expect(mockFetch).toHaveBeenCalledTimes(1);

await searchModule.searchImages("cross-store query");
expect(mockFetch).toHaveBeenCalledTimes(2);

// Repeat the image query — should hit the image cache, not the network.
await searchModule.searchImages("cross-store query");
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});

describe("URL Construction", () => {
it("should include the search token hash in the request URL", async () => {
mockFetchResponse([]);
Expand Down
4 changes: 4 additions & 0 deletions client/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// fake-indexeddb/auto installs all IndexedDB globals (indexedDB, IDBKeyRange,
// etc.) so that Dexie-based cache tests exercise the real cache code paths
// instead of silently erroring into catch blocks.
import "fake-indexeddb/auto";
import "@testing-library/jest-dom";
import { vi } from "vitest";

Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@vitejs/plugin-basic-ssl": "^2.0.0",
"@vitejs/plugin-react": "^6.0.0",
"@vitest/coverage-v8": "^4.0.18",
"fake-indexeddb": "^6.0.0",
"globals": "^17.0.0",
"husky": "^9.1.7",
"jscpd": "^5.0.0",
Expand Down