/** * ============================================================================ * MOCK FONT STORE HELPERS * ============================================================================ * * 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'; // TANSTACK QUERY MOCK TYPES /** * Mock TanStack Query state */ export interface MockQueryState { status: QueryStatus; data?: TData; error?: TError; isLoading?: boolean; isFetching?: boolean; isSuccess?: boolean; isError?: boolean; isPending?: boolean; dataUpdatedAt?: number; errorUpdatedAt?: number; failureCount?: number; failureReason?: TError; errorUpdateCount?: number; isRefetching?: boolean; isRefetchError?: boolean; isPaused?: boolean; } /** * Mock TanStack Query observer result */ export interface MockQueryObserverResult { status?: QueryStatus; data?: TData; error?: TError; isLoading?: boolean; isFetching?: boolean; isSuccess?: boolean; isError?: boolean; isPending?: boolean; dataUpdatedAt?: number; errorUpdatedAt?: number; failureCount?: number; failureReason?: TError; errorUpdateCount?: number; isRefetching?: boolean; isRefetchError?: boolean; isPaused?: boolean; } // TANSTACK QUERY MOCK FACTORIES /** * 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 }); } // FONT STORE MOCKS /** * Mock UnifiedFontStore state */ export interface MockFontStoreState { /** All cached fonts */ fonts: Record; /** Current page */ page: number; /** Total pages available */ totalPages: number; /** Items per page */ limit: number; /** Total font count */ total: number; /** Loading state */ isLoading: boolean; /** Error state */ error: Error | null; /** Search query */ searchQuery: string; /** Selected provider */ provider: 'google' | 'fontshare' | 'all'; /** Selected category */ category: string | null; /** Selected subset */ 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 */ export const MOCK_FONT_STORE_STATES = { /** Initial loading state */ loading: createMockFontStoreState({ isLoading: true, fonts: {}, total: 0, page: 1, }), /** Empty state (no fonts found) */ empty: createMockFontStoreState({ fonts: {}, total: 0, page: 1, isLoading: false, }), /** First page with fonts */ 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 with fonts */ 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, }), /** Last page with fonts */ 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, }), /** Error state */ error: createMockFontStoreState({ fonts: {}, error: new Error('Failed to load fonts'), total: 0, page: 1, isLoading: false, }), /** With 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', }), /** Filtered by category */ 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', }), /** Filtered by provider */ 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 dataset */ largeDataset: createMockFontStoreState({ fonts: Object.fromEntries( generateMockFonts(50).map(font => [font.id, font]), ), total: 500, page: 1, limit: 50, totalPages: 10, isLoading: false, }), }; // MOCK STORE OBJECT /** * Create a mock store object that mimics TanStack Query behavior * Useful for components that subscribe to store properties */ export function createMockStore(config: { data?: T; isLoading?: boolean; isError?: boolean; error?: Error; isFetching?: boolean; }) { const { data, isLoading = false, isError = false, error, isFetching = false, } = config; return { get data() { return data; }, get isLoading() { return isLoading; }, get isError() { return isError; }, get error() { return error; }, get isFetching() { return isFetching; }, get isSuccess() { return !isLoading && !isError && data !== undefined; }, get status() { if (isLoading) return 'pending'; if (isError) return 'error'; return 'success'; }, }; } /** * Preset mock stores */ export const MOCK_STORES = { /** Font store in loading state */ loadingFontStore: createMockStore({ isLoading: true, data: undefined, }), /** Font store with fonts loaded */ successFontStore: createMockStore({ data: Object.values(UNIFIED_FONTS), isLoading: false, isError: false, }), /** Font store with error */ errorFontStore: createMockStore({ data: undefined, isLoading: false, isError: true, error: new Error('Failed to load fonts'), }), /** Font store with empty results */ 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 get fonts() { return mockState.fonts; }, get page() { return mockState.page; }, get totalPages() { return mockState.totalPages; }, get limit() { return mockState.limit; }, get total() { return mockState.total; }, get isLoading() { return mockState.isLoading; }, get error() { return mockState.error; }, get searchQuery() { return mockState.searchQuery; }, get provider() { return mockState.provider; }, get category() { return mockState.category; }, 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: { fonts?: UnifiedFont[]; total?: number; limit?: number; offset?: number; isLoading?: boolean; isFetching?: boolean; isError?: boolean; error?: Error | null; hasMore?: boolean; 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 get params() { return state.params; }, get fonts() { return mockFonts; }, get isLoading() { return isLoading; }, get isFetching() { return isFetching; }, get isError() { return isError; }, get error() { return error; }, get isEmpty() { return !isLoading && !isFetching && mockFonts.length === 0; }, get pagination() { return { total: mockTotal, limit, offset, hasMore, page, totalPages, }; }, // Category getters get sansSerifFonts() { return mockFonts.filter(f => f.category === 'sans-serif'); }, get serifFonts() { return mockFonts.filter(f => f.category === 'serif'); }, get displayFonts() { return mockFonts.filter(f => f.category === 'display'); }, get handwritingFonts() { return mockFonts.filter(f => f.category === 'handwriting'); }, 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, }, }; }