/** * RED PHASE Tests for TanStack Store Factory * * Tests for the main TanStack store implementation. * These tests define expected behavior for createTanStackStore. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { createTanStackStore } from './store' import type { BaseRecord, QueryCollectionOptions, SyncCollectionOptions, PGLiteCollectionOptions } from './types' interface Todo extends BaseRecord { id: string title: string completed: boolean } interface User extends BaseRecord { id: number name: string email: string } describe('createTanStackStore', () => { let mockFetch: ReturnType beforeEach(() => { mockFetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([]), }) vi.stubGlobal('fetch', mockFetch) }) afterEach(() => { vi.unstubAllGlobals() }) describe('Factory function', () => { it('should create a store with no options', () => { const store = createTanStackStore() expect(store).toBeDefined() }) it('should create a store with options', () => { const store = createTanStackStore({ defaultStaleTime: 5000, defaultSyncUrl: 'http://localhost:3000', }) expect(store).toBeDefined() }) }) describe('getCollection()', () => { it('should return undefined for unregistered collection', () => { const store = createTanStackStore() expect(store.getCollection('unknown')).toBeUndefined() }) it('should return registered collection', () => { const store = createTanStackStore() const options: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], } store.registerCollection(options) expect(store.getCollection('todos')).toBeDefined() }) it('should return correctly typed collection', () => { const store = createTanStackStore() const options: QueryCollectionOptions = { id: 'users', table: 'users', queryFn: async () => [{ id: 1, name: 'Alice', email: 'alice@test.com' }], } store.registerCollection(options) const collection = store.getCollection('users') expect(collection?.id).toBe('users') }) }) describe('registerCollection()', () => { describe('QueryCollection registration', () => { it('should register a QueryCollection with queryFn', () => { const store = createTanStackStore() const options: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], } const collection = store.registerCollection(options) expect(collection).toBeDefined() expect(collection.id).toBe('todos') }) it('should apply default stale time to QueryCollection', () => { const store = createTanStackStore({ defaultStaleTime: 10000 }) const options: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], } const collection = store.registerCollection(options) expect(collection).toBeDefined() }) it('should use explicit stale time over default', () => { const store = createTanStackStore({ defaultStaleTime: 10000 }) const options: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], staleTime: 5000, } const collection = store.registerCollection(options) expect(collection).toBeDefined() }) }) describe('SyncCollection registration', () => { it('should register a SyncCollection with syncUrl', () => { const store = createTanStackStore() const options: SyncCollectionOptions = { id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', } const collection = store.registerCollection(options) expect(collection).toBeDefined() expect(collection.id).toBe('todos') }) it('should register SyncCollection with shapeParams', () => { const store = createTanStackStore() const options: SyncCollectionOptions = { id: 'active-todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', shapeParams: { where: 'completed = false' }, } const collection = store.registerCollection(options) expect(collection).toBeDefined() }) it('should register SyncCollection with pollInterval', () => { const store = createTanStackStore() const options: SyncCollectionOptions = { id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', pollInterval: 5000, } const collection = store.registerCollection(options) expect(collection).toBeDefined() }) }) describe('PGLiteCollection registration', () => { it('should register a PGLiteCollection with pglite instance', () => { const mockPGLite = { query: vi.fn().mockResolvedValue({ rows: [] }), exec: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } const store = createTanStackStore() const options: PGLiteCollectionOptions = { id: 'todos', table: 'todos', pglite: mockPGLite, } const collection = store.registerCollection(options) expect(collection).toBeDefined() expect(collection.id).toBe('todos') }) it('should register a PGLiteCollection with pglite factory', () => { const mockPGLite = { query: vi.fn().mockResolvedValue({ rows: [] }), exec: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } const store = createTanStackStore() const options: PGLiteCollectionOptions = { id: 'todos', table: 'todos', pglite: async () => mockPGLite, } const collection = store.registerCollection(options) expect(collection).toBeDefined() }) it('should register a PGLiteCollection with autoCreateTable', () => { const mockPGLite = { query: vi.fn().mockResolvedValue({ rows: [] }), exec: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } const store = createTanStackStore() const options: PGLiteCollectionOptions = { id: 'todos', table: 'todos', pglite: mockPGLite, autoCreateTable: true, tableSchema: 'id TEXT PRIMARY KEY, title TEXT, completed BOOLEAN', } const collection = store.registerCollection(options) expect(collection).toBeDefined() }) }) describe('Invalid options', () => { it('should throw for invalid collection options', () => { const store = createTanStackStore() const invalidOptions = { id: 'invalid', table: 'invalid', // Missing queryFn, syncUrl, or pglite } as unknown as QueryCollectionOptions expect(() => store.registerCollection(invalidOptions)).toThrow('Invalid collection options') }) }) }) describe('getCollectionIds()', () => { it('should return empty array for new store', () => { const store = createTanStackStore() expect(store.getCollectionIds()).toEqual([]) }) it('should return ids of registered collections', () => { const store = createTanStackStore() const queryOptions: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], } store.registerCollection(queryOptions) const syncOptions: SyncCollectionOptions = { id: 'users', table: 'users', syncUrl: 'http://localhost:3000/v1/shape', } store.registerCollection(syncOptions) const ids = store.getCollectionIds() expect(ids).toContain('todos') expect(ids).toContain('users') expect(ids).toHaveLength(2) }) }) describe('dispose()', () => { it('should clear all collections', async () => { const store = createTanStackStore() const options: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], } store.registerCollection(options) await store.dispose() expect(store.getCollectionIds()).toEqual([]) }) it('should stop auto-refetch on QueryCollections', async () => { vi.useFakeTimers() const store = createTanStackStore() const queryFn = vi.fn().mockResolvedValue([]) const options: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn, refetchInterval: 1000, } const collection = store.registerCollection(options) // Start auto-refetch (if the implementation supports it) if ('startAutoRefetch' in collection && typeof collection.startAutoRefetch === 'function') { (collection as { startAutoRefetch: () => void }).startAutoRefetch() } await store.dispose() // Verify collection was stopped queryFn.mockClear() await vi.advanceTimersByTimeAsync(3000) // After dispose, no more calls should happen expect(queryFn).not.toHaveBeenCalled() vi.useRealTimers() }) it('should disconnect SyncCollections', async () => { const store = createTanStackStore() const options: SyncCollectionOptions = { id: 'todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', } store.registerCollection(options) // Should not throw await store.dispose() expect(store.getCollectionIds()).toEqual([]) }) it('should be idempotent', async () => { const store = createTanStackStore() const options: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], } store.registerCollection(options) await store.dispose() await store.dispose() // Second call should not throw expect(store.getCollectionIds()).toEqual([]) }) }) describe('Multiple collections', () => { it('should handle multiple collections of same type', () => { const store = createTanStackStore() const todos: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [], } const completedTodos: QueryCollectionOptions = { id: 'completed-todos', table: 'todos', queryFn: async () => [], } store.registerCollection(todos) store.registerCollection(completedTodos) expect(store.getCollection('todos')).toBeDefined() expect(store.getCollection('completed-todos')).toBeDefined() }) it('should handle mixed collection types', () => { const mockPGLite = { query: vi.fn().mockResolvedValue({ rows: [] }), exec: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined), } const store = createTanStackStore() const queryOptions: QueryCollectionOptions = { id: 'remote-todos', table: 'todos', queryFn: async () => [], } const syncOptions: SyncCollectionOptions = { id: 'synced-todos', table: 'todos', syncUrl: 'http://localhost:3000/v1/shape', } const pgliteOptions: PGLiteCollectionOptions = { id: 'local-todos', table: 'todos', pglite: mockPGLite, } store.registerCollection(queryOptions) store.registerCollection(syncOptions) store.registerCollection(pgliteOptions) expect(store.getCollectionIds()).toHaveLength(3) }) it('should replace collection with same id', () => { const store = createTanStackStore() const options1: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [{ id: '1', title: 'First', completed: false }], } const options2: QueryCollectionOptions = { id: 'todos', table: 'todos', queryFn: async () => [{ id: '2', title: 'Second', completed: true }], } store.registerCollection(options1) const collection2 = store.registerCollection(options2) // Should have replaced the first collection expect(store.getCollectionIds()).toHaveLength(1) expect(store.getCollection('todos')).toBe(collection2) }) }) })