031ad7c15d
Remove unused type alias, unused variable, add jsdom environment directive, and use container.textContent for cross-element text assertions.
194 lines
5.4 KiB
TypeScript
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([]);
|
|
});
|
|
});
|