Files
flights_web/src/shared/hooks/useSearchHistory.test.ts
T
gnezim 031ad7c15d Fix lint warnings and test environment for Phase 5 tests
Remove unused type alias, unused variable, add jsdom environment
directive, and use container.textContent for cross-element text
assertions.
2026-04-15 10:09:56 +03:00

194 lines
5.4 KiB
TypeScript

/**
* @vitest-environment jsdom
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useSearchHistory } from "./useSearchHistory";
import type { SearchHistoryItem } from "./useSearchHistory";
// ---------------------------------------------------------------------------
// localStorage mock
// ---------------------------------------------------------------------------
function createMockLocalStorage() {
const store = new Map<string, string>();
return {
getItem: vi.fn((key: string) => store.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
store.set(key, value);
}),
removeItem: vi.fn((key: string) => {
store.delete(key);
}),
get length() {
return store.size;
},
key: vi.fn((index: number) => [...store.keys()][index] ?? null),
clear: vi.fn(() => store.clear()),
store,
} satisfies Storage & { store: Map<string, string> };
}
// ---------------------------------------------------------------------------
// Test data
// ---------------------------------------------------------------------------
const flightItem: SearchHistoryItem = {
type: "flight-number",
url: "/ru/onlineboard/flight/SU0001-20250528",
label: "SU 0001",
};
const routeItem: SearchHistoryItem = {
type: "board-route",
url: "/ru/onlineboard/route/SVO-JFK-20250528",
label: "SVO - JFK",
};
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("useSearchHistory", () => {
let mockStorage: ReturnType<typeof createMockLocalStorage>;
beforeEach(() => {
mockStorage = createMockLocalStorage();
vi.stubGlobal("localStorage", mockStorage);
});
it("starts with empty history when nothing in storage", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
expect(result.current.items).toEqual([]);
});
it("loads existing history from storage on mount", () => {
mockStorage.store.set(
"afl_history_ru",
JSON.stringify([flightItem]),
);
const { result } = renderHook(() => useSearchHistory("ru"));
expect(result.current.items).toEqual([flightItem]);
});
it("adds an item and persists to storage", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
act(() => {
result.current.add(flightItem);
});
expect(result.current.items).toEqual([flightItem]);
expect(mockStorage.setItem).toHaveBeenCalledWith(
"afl_history_ru",
JSON.stringify([flightItem]),
);
});
it("prepends new items (most recent first)", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
act(() => {
result.current.add(flightItem);
});
act(() => {
result.current.add(routeItem);
});
expect(result.current.items).toEqual([routeItem, flightItem]);
});
it("deduplicates by URL — skips if most recent matches", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
act(() => {
result.current.add(flightItem);
});
act(() => {
result.current.add(flightItem);
});
expect(result.current.items).toEqual([flightItem]);
});
it("moves duplicate to front when URL exists in older position", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
act(() => {
result.current.add(flightItem);
});
act(() => {
result.current.add(routeItem);
});
act(() => {
result.current.add(flightItem);
});
expect(result.current.items).toEqual([flightItem, routeItem]);
});
it("limits history to 10 items", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
for (let i = 0; i < 12; i++) {
act(() => {
result.current.add({
type: "flight-number",
url: `/flight-${i}`,
label: `Flight ${i}`,
});
});
}
expect(result.current.items).toHaveLength(10);
// Most recent should be first
expect(result.current.items[0]?.url).toBe("/flight-11");
});
it("namespaces by language — different langs have separate history", () => {
const { result: ruResult } = renderHook(() => useSearchHistory("ru"));
const { result: enResult } = renderHook(() => useSearchHistory("en"));
act(() => {
ruResult.current.add(flightItem);
});
expect(ruResult.current.items).toEqual([flightItem]);
expect(enResult.current.items).toEqual([]);
});
it("clear removes all items and deletes from storage", () => {
const { result } = renderHook(() => useSearchHistory("ru"));
act(() => {
result.current.add(flightItem);
result.current.add(routeItem);
});
act(() => {
result.current.clear();
});
expect(result.current.items).toEqual([]);
expect(mockStorage.removeItem).toHaveBeenCalledWith("afl_history_ru");
});
it("gracefully handles corrupted storage data", () => {
mockStorage.store.set("afl_history_ru", "not-valid-json{{{");
const { result } = renderHook(() => useSearchHistory("ru"));
expect(result.current.items).toEqual([]);
});
it("gracefully handles invalid schema in storage", () => {
mockStorage.store.set(
"afl_history_ru",
JSON.stringify([{ invalid: true }]),
);
const { result } = renderHook(() => useSearchHistory("ru"));
expect(result.current.items).toEqual([]);
});
});