/** * Factory functions and preset mock data for TanStack Query stores and state management. * Used in Storybook stories for components that use reactive stores. * * ## Usage * * ```ts * import { * createMockQueryState, * MOCK_STORES, * } from '$entities/Font/lib/mocks'; * * // Create a mock query state * const loadingState = createMockQueryState({ status: 'pending' }); * const errorState = createMockQueryState({ status: 'error', error: 'Failed to load' }); * const successState = createMockQueryState({ status: 'success', data: mockFonts }); * * // Use preset stores * const mockFontStore = createMockFontStore(); * ``` */ import type { UnifiedFont } from '$entities/Font/model/types'; import type { QueryKey, QueryObserverResult, QueryStatus, } from '@tanstack/svelte-query'; import { UNIFIED_FONTS, generateMockFonts, } from './fonts.mock'; /** * Mock TanStack Query state */ export interface MockQueryState { /** * Primary query status (pending, success, error) */ status: QueryStatus; /** * Payload data (present on success) */ data?: TData; /** * Caught error object (present on error) */ error?: TError; /** * True if initial load is in progress */ isLoading?: boolean; /** * True if background fetch is in progress */ isFetching?: boolean; /** * True if query resolved successfully */ isSuccess?: boolean; /** * True if query failed */ isError?: boolean; /** * True if query is waiting to be executed */ isPending?: boolean; /** * Timestamp of last successful data retrieval */ dataUpdatedAt?: number; /** * Timestamp of last recorded error */ errorUpdatedAt?: number; /** * Total number of consecutive failures */ failureCount?: number; /** * Detailed reason for the last failure */ failureReason?: TError; /** * Number of times an error has been caught */ errorUpdateCount?: number; /** * True if currently refetching in background */ isRefetching?: boolean; /** * True if refetch attempt failed */ isRefetchError?: boolean; /** * True if query is paused (e.g. offline) */ isPaused?: boolean; } /** * Mock TanStack Query observer result */ export interface MockQueryObserverResult { /** * Current observer status */ status?: QueryStatus; /** * Cached or active data payload */ data?: TData; /** * Caught error from the observer */ error?: TError; /** * Loading flag for the observer */ isLoading?: boolean; /** * Fetching flag for the observer */ isFetching?: boolean; /** * Success flag for the observer */ isSuccess?: boolean; /** * Error flag for the observer */ isError?: boolean; /** * Pending flag for the observer */ isPending?: boolean; /** * Last update time for data */ dataUpdatedAt?: number; /** * Last update time for error */ errorUpdatedAt?: number; /** * Consecutive failure count */ failureCount?: number; /** * Failure reason object */ failureReason?: TError; /** * Error count for the observer */ errorUpdateCount?: number; /** * Refetching flag */ isRefetching?: boolean; /** * Refetch error flag */ isRefetchError?: boolean; /** * Paused flag */ isPaused?: boolean; } /** * Create a mock query state for TanStack Query */ export function createMockQueryState( options: MockQueryState, ): MockQueryObserverResult { const { status, data, error, } = options; return { status: status ?? 'success', data, error, isLoading: status === 'pending' ? true : false, isFetching: status === 'pending' ? true : false, isSuccess: status === 'success', isError: status === 'error', isPending: status === 'pending', dataUpdatedAt: status === 'success' ? Date.now() : undefined, errorUpdatedAt: status === 'error' ? Date.now() : undefined, failureCount: status === 'error' ? 1 : 0, failureReason: status === 'error' ? error : undefined, errorUpdateCount: status === 'error' ? 1 : 0, isRefetching: false, isRefetchError: false, isPaused: false, }; } /** * Create a loading query state */ export function createLoadingState(): MockQueryObserverResult { return createMockQueryState({ status: 'pending', data: undefined, error: undefined }); } /** * Create an error query state */ export function createErrorState( error: TError, ): MockQueryObserverResult { return createMockQueryState({ status: 'error', data: undefined, error }); } /** * Create a success query state */ export function createSuccessState(data: TData): MockQueryObserverResult { return createMockQueryState({ status: 'success', data, error: undefined }); } /** * Mock UnifiedFontStore state */ export interface MockFontStoreState { /** * Map of mock fonts indexed by ID */ fonts: Record; /** * Currently active page number */ page: number; /** * Total number of pages calculated from limit */ totalPages: number; /** * Number of items per page */ limit: number; /** * Total number of available fonts */ total: number; /** * Store-level loading status */ isLoading: boolean; /** * Caught error object */ error: Error | null; /** * Mock search filter string */ searchQuery: string; /** * Mock provider filter selection */ provider: 'google' | 'fontshare' | 'all'; /** * Mock category filter selection */ category: string | null; /** * Mock subset filter selection */ subset: string | null; } /** * Create a mock font store state */ export function createMockFontStoreState( options: Partial = {}, ): MockFontStoreState { const { page = 1, limit = 24, isLoading = false, error = null, searchQuery = '', provider = 'all', category = null, subset = null, } = options; // Generate mock fonts if not provided const mockFonts = options.fonts ?? Object.fromEntries( Object.values(UNIFIED_FONTS).map(font => [font.id, font]), ); const fontArray = Object.values(mockFonts); const total = options.total ?? fontArray.length; const totalPages = options.totalPages ?? Math.ceil(total / limit); return { fonts: mockFonts, page, totalPages, limit, total, isLoading, error, searchQuery, provider, category, subset, }; } /** * Preset font store states for UI testing */ export const MOCK_FONT_STORE_STATES = { /** * Initial loading state with no data */ loading: createMockFontStoreState({ isLoading: true, fonts: {}, total: 0, page: 1, }), /** * State with no fonts matching filters */ empty: createMockFontStoreState({ fonts: {}, total: 0, page: 1, isLoading: false, }), /** * First page of results (10 items) */ firstPage: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(0, 10).map(font => [font.id, font]), ), total: 50, page: 1, limit: 10, totalPages: 5, isLoading: false, }), /** * Second page of results (10 items) */ secondPage: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(10, 20).map(font => [font.id, font]), ), total: 50, page: 2, limit: 10, totalPages: 5, isLoading: false, }), /** * Final page of results (5 items) */ lastPage: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(0, 5).map(font => [font.id, font]), ), total: 25, page: 3, limit: 10, totalPages: 3, isLoading: false, }), /** * Terminal failure state */ error: createMockFontStoreState({ fonts: {}, error: new Error('Failed to load fonts'), total: 0, page: 1, isLoading: false, }), /** * State with active search query */ withSearch: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS).slice(0, 3).map(font => [font.id, font]), ), total: 3, page: 1, isLoading: false, searchQuery: 'Roboto', }), /** * State with active category filter */ filteredByCategory: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS) .filter(f => f.category === 'serif') .slice(0, 5) .map(font => [font.id, font]), ), total: 5, page: 1, isLoading: false, category: 'serif', }), /** * State with active provider filter */ filteredByProvider: createMockFontStoreState({ fonts: Object.fromEntries( Object.values(UNIFIED_FONTS) .filter(f => f.provider === 'google') .slice(0, 5) .map(font => [font.id, font]), ), total: 5, page: 1, isLoading: false, provider: 'google', }), /** * Large collection for performance testing (50 items) */ largeDataset: createMockFontStoreState({ fonts: Object.fromEntries( generateMockFonts(50).map(font => [font.id, font]), ), total: 500, page: 1, limit: 50, totalPages: 10, isLoading: false, }), }; /** * Create a mock store object that mimics TanStack Query behavior * Useful for components that subscribe to store properties */ export function createMockStore(config: { /** * Reactive data payload */ data?: T; /** * Loading status flag */ isLoading?: boolean; /** * Error status flag */ isError?: boolean; /** * Catch-all error object */ error?: Error; /** * Background fetching flag */ isFetching?: boolean; }) { const { data, isLoading = false, isError = false, error, isFetching = false, } = config; return { /** * Returns the active data payload */ get data() { return data; }, /** * True if initially loading */ get isLoading() { return isLoading; }, /** * True if last request failed */ get isError() { return isError; }, /** * Returns the caught error object */ get error() { return error; }, /** * True if fetching in background */ get isFetching() { return isFetching; }, /** * True if query is stable and has data */ get isSuccess() { return !isLoading && !isError && data !== undefined; }, /** * Returns semantic status string */ get status() { if (isLoading) { return 'pending'; } if (isError) { return 'error'; } return 'success'; }, }; } /** * Preset mock stores for common UI states */ export const MOCK_STORES = { /** * Initial loading state */ loadingFontStore: createMockStore({ isLoading: true, data: undefined, }), /** * Successful data load state */ successFontStore: createMockStore({ data: Object.values(UNIFIED_FONTS), isLoading: false, isError: false, }), /** * API error state */ errorFontStore: createMockStore({ data: undefined, isLoading: false, isError: true, error: new Error('Failed to load fonts'), }), /** * Empty result set state */ emptyFontStore: createMockStore({ data: [], isLoading: false, isError: false, }), /** * Create a mock UnifiedFontStore-like object * Note: This is a simplified mock for Storybook use */ unifiedFontStore: (state: Partial = {}) => { const mockState = createMockFontStoreState(state); return { // State properties /** * Collection of mock fonts */ get fonts() { return mockState.fonts; }, /** * Current mock page */ get page() { return mockState.page; }, /** * Total mock pages */ get totalPages() { return mockState.totalPages; }, /** * Mock items per page */ get limit() { return mockState.limit; }, /** * Total mock items */ get total() { return mockState.total; }, /** * Mock loading status */ get isLoading() { return mockState.isLoading; }, /** * Mock error status */ get error() { return mockState.error; }, /** * Mock search string */ get searchQuery() { return mockState.searchQuery; }, /** * Mock provider filter */ get provider() { return mockState.provider; }, /** * Mock category filter */ get category() { return mockState.category; }, /** * Mock subset filter */ get subset() { return mockState.subset; }, // Methods (no-op for Storybook) nextPage: () => {}, prevPage: () => {}, goToPage: (_page: number) => {}, setLimit: (_limit: number) => {}, setProvider: (_provider: typeof mockState.provider) => {}, setCategory: (_category: string | null) => {}, setSubset: (_subset: string | null) => {}, setSearch: (_query: string) => {}, resetFilters: () => {}, }; }, /** * Create a mock FontStore object * Matches FontStore's public API for Storybook use */ fontStore: (config: { /** * Preset font list */ fonts?: UnifiedFont[]; /** * Total item count */ total?: number; /** * Items per page */ limit?: number; /** * Pagination offset */ offset?: number; /** * Loading flag */ isLoading?: boolean; /** * Fetching flag */ isFetching?: boolean; /** * Error flag */ isError?: boolean; /** * Catch-all error object */ error?: Error | null; /** * Has more pages flag */ hasMore?: boolean; /** * Current page number */ page?: number; } = {}) => { const { fonts: mockFonts = Object.values(UNIFIED_FONTS).slice(0, 5), total: mockTotal = mockFonts.length, limit = 50, offset = 0, isLoading = false, isFetching = false, isError = false, error = null, hasMore = false, page = 1, } = config; const totalPages = Math.ceil(mockTotal / limit); const state = { params: { limit }, }; return { // State getters /** * Current mock parameters */ get params() { return state.params; }, /** * Mock font list */ get fonts() { return mockFonts; }, /** * Mock loading state */ get isLoading() { return isLoading; }, /** * Mock fetching state */ get isFetching() { return isFetching; }, /** * Mock error state */ get isError() { return isError; }, /** * Mock error object */ get error() { return error; }, /** * Mock empty state check */ get isEmpty() { return !isLoading && !isFetching && mockFonts.length === 0; }, /** * Mock pagination metadata */ get pagination() { return { total: mockTotal, limit, offset, hasMore, page, totalPages, }; }, // Category getters /** * Derived sans-serif filter */ get sansSerifFonts() { return mockFonts.filter(f => f.category === 'sans-serif'); }, /** * Derived serif filter */ get serifFonts() { return mockFonts.filter(f => f.category === 'serif'); }, /** * Derived display filter */ get displayFonts() { return mockFonts.filter(f => f.category === 'display'); }, /** * Derived handwriting filter */ get handwritingFonts() { return mockFonts.filter(f => f.category === 'handwriting'); }, /** * Derived monospace filter */ get monospaceFonts() { return mockFonts.filter(f => f.category === 'monospace'); }, // Lifecycle destroy() {}, // Param management setParams(_updates: Record) {}, invalidate() {}, // Async operations (no-op for Storybook) refetch() {}, prefetch() {}, cancel() {}, getCachedData() { return mockFonts.length > 0 ? mockFonts : undefined; }, setQueryData() {}, // Filter shortcuts setProviders() {}, setCategories() {}, setSubsets() {}, setSearch() {}, setSort() {}, // Pagination navigation nextPage() {}, prevPage() {}, goToPage() {}, setLimit(_limit: number) { state.params.limit = _limit; }, }; }, }; // REACTIVE STATE MOCKS /** * Create a reactive state object using Svelte 5 runes pattern * Useful for stories that need reactive state * * Note: This uses plain JavaScript objects since Svelte runes * only work in .svelte files. For Storybook, this provides * a similar API for testing. */ export function createMockReactiveState(initialValue: T) { let value = initialValue; return { get value() { return value; }, set value(newValue: T) { value = newValue; }, update(fn: (current: T) => T) { value = fn(value); }, }; } /** * Mock comparison store for ComparisonSlider component */ export function createMockComparisonStore(config: { fontA?: UnifiedFont; fontB?: UnifiedFont; text?: string; } = {}) { const { fontA, fontB, text = 'The quick brown fox jumps over the lazy dog.' } = config; return { get fontA() { return fontA ?? UNIFIED_FONTS.roboto; }, get fontB() { return fontB ?? UNIFIED_FONTS.openSans; }, get text() { return text; }, // Methods (no-op for Storybook) setFontA: (_font: UnifiedFont | undefined) => {}, setFontB: (_font: UnifiedFont | undefined) => {}, setText: (_text: string) => {}, swapFonts: () => {}, }; } // MOCK DATA GENERATORS /** * Generate paginated font data */ export function generatePaginatedFonts( totalCount: number, page: number, limit: number, ): { fonts: UnifiedFont[]; page: number; totalPages: number; total: number; hasNextPage: boolean; hasPrevPage: boolean; } { const totalPages = Math.ceil(totalCount / limit); const startIndex = (page - 1) * limit; const endIndex = Math.min(startIndex + limit, totalCount); return { fonts: generateMockFonts(endIndex - startIndex).map((font, i) => ({ ...font, id: `font-${startIndex + i + 1}`, name: `Font ${startIndex + i + 1}`, })), page, totalPages, total: totalCount, hasNextPage: page < totalPages, hasPrevPage: page > 1, }; } /** * Create mock API response for fonts */ export function createMockFontApiResponse(config: { fonts?: UnifiedFont[]; total?: number; page?: number; limit?: number; } = {}) { const fonts = config.fonts ?? Object.values(UNIFIED_FONTS); const total = config.total ?? fonts.length; const page = config.page ?? 1; const limit = config.limit ?? fonts.length; return { data: fonts, meta: { total, page, limit, totalPages: Math.ceil(total / limit), hasNextPage: page < Math.ceil(total / limit), hasPrevPage: page > 1, }, }; }