98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
create: vi.fn(),
|
|
getFullList: vi.fn(),
|
|
authStore: {
|
|
isValid: true,
|
|
record: { id: 'user-1' } as { id: string } | null,
|
|
onChange: () => () => {},
|
|
},
|
|
}));
|
|
|
|
vi.mock('../lib/pocketbase', () => ({
|
|
default: {
|
|
authStore: mocks.authStore,
|
|
collection: () => ({ getFullList: mocks.getFullList, create: mocks.create }),
|
|
},
|
|
}));
|
|
|
|
import { useClickedListings } from './useClickedListings';
|
|
|
|
describe('useClickedListings', () => {
|
|
beforeEach(() => {
|
|
mocks.create.mockReset().mockResolvedValue({});
|
|
mocks.getFullList.mockReset().mockResolvedValue([]);
|
|
mocks.authStore.isValid = true;
|
|
mocks.authStore.record = { id: 'user-1' };
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('hydrates the visited set from PocketBase for the signed-in user', async () => {
|
|
mocks.getFullList.mockResolvedValue([{ url: 'https://example.com/a' }]);
|
|
const { result } = renderHook(() => useClickedListings());
|
|
|
|
await waitFor(() => expect(result.current.clickedUrls.has('https://example.com/a')).toBe(true));
|
|
});
|
|
|
|
it('records a click optimistically and persists it', async () => {
|
|
const { result } = renderHook(() => useClickedListings());
|
|
await waitFor(() => expect(mocks.getFullList).toHaveBeenCalled());
|
|
|
|
act(() => result.current.markClicked('https://example.com/b'));
|
|
|
|
// Visible immediately. No need to await the network round-trip.
|
|
expect(result.current.clickedUrls.has('https://example.com/b')).toBe(true);
|
|
expect(mocks.create).toHaveBeenCalledWith({
|
|
user: 'user-1',
|
|
url: 'https://example.com/b',
|
|
});
|
|
});
|
|
|
|
it('produces a new Set instance for a new url so deck.gl re-colours', async () => {
|
|
const { result } = renderHook(() => useClickedListings());
|
|
await waitFor(() => expect(mocks.getFullList).toHaveBeenCalled());
|
|
|
|
const before = result.current.clickedUrls;
|
|
act(() => result.current.markClicked('https://example.com/c'));
|
|
expect(result.current.clickedUrls).not.toBe(before);
|
|
});
|
|
|
|
it('does not re-persist an already visited url', async () => {
|
|
mocks.getFullList.mockResolvedValue([{ url: 'https://example.com/a' }]);
|
|
const { result } = renderHook(() => useClickedListings());
|
|
await waitFor(() => expect(result.current.clickedUrls.has('https://example.com/a')).toBe(true));
|
|
|
|
const before = result.current.clickedUrls;
|
|
act(() => result.current.markClicked('https://example.com/a'));
|
|
|
|
expect(mocks.create).not.toHaveBeenCalled();
|
|
expect(result.current.clickedUrls).toBe(before); // identity stable → no needless recompute
|
|
});
|
|
|
|
it('ignores empty urls', async () => {
|
|
const { result } = renderHook(() => useClickedListings());
|
|
await waitFor(() => expect(mocks.getFullList).toHaveBeenCalled());
|
|
|
|
act(() => result.current.markClicked(''));
|
|
act(() => result.current.markClicked(undefined));
|
|
|
|
expect(mocks.create).not.toHaveBeenCalled();
|
|
expect(result.current.clickedUrls.size).toBe(0);
|
|
});
|
|
|
|
it('recolours for anonymous users in-memory but does not persist', async () => {
|
|
mocks.authStore.isValid = false;
|
|
mocks.authStore.record = null;
|
|
const { result } = renderHook(() => useClickedListings());
|
|
|
|
act(() => result.current.markClicked('https://example.com/d'));
|
|
|
|
expect(result.current.clickedUrls.has('https://example.com/d')).toBe(true);
|
|
expect(mocks.create).not.toHaveBeenCalled();
|
|
});
|
|
});
|