Files
frontend-svelte/src/entities/Font/lib/mocks/stores.mock.ts
T

966 lines
23 KiB
TypeScript

/**
* 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<TData = unknown, TError = Error> {
/**
* 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<TData = unknown, TError = Error> {
/**
* 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<TData = unknown, TError = Error>(
options: MockQueryState<TData, TError>,
): MockQueryObserverResult<TData, TError> {
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<TData = unknown>(): MockQueryObserverResult<TData> {
return createMockQueryState<TData>({ status: 'pending', data: undefined, error: undefined });
}
/**
* Create an error query state
*/
export function createErrorState<TError = Error>(
error: TError,
): MockQueryObserverResult<unknown, TError> {
return createMockQueryState<unknown, TError>({ status: 'error', data: undefined, error });
}
/**
* Create a success query state
*/
export function createSuccessState<TData>(data: TData): MockQueryObserverResult<TData> {
return createMockQueryState<TData>({ status: 'success', data, error: undefined });
}
/**
* Mock UnifiedFontStore state
*/
export interface MockFontStoreState {
/**
* Map of mock fonts indexed by ID
*/
fonts: Record<string, UnifiedFont>;
/**
* 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> = {},
): 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<T>(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<UnifiedFont[]>({
isLoading: true,
data: undefined,
}),
/**
* Successful data load state
*/
successFontStore: createMockStore<UnifiedFont[]>({
data: Object.values(UNIFIED_FONTS),
isLoading: false,
isError: false,
}),
/**
* API error state
*/
errorFontStore: createMockStore<UnifiedFont[]>({
data: undefined,
isLoading: false,
isError: true,
error: new Error('Failed to load fonts'),
}),
/**
* Empty result set state
*/
emptyFontStore: createMockStore<UnifiedFont[]>({
data: [],
isLoading: false,
isError: false,
}),
/**
* Create a mock UnifiedFontStore-like object
* Note: This is a simplified mock for Storybook use
*/
unifiedFontStore: (state: Partial<MockFontStoreState> = {}) => {
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<string, unknown>) {},
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<T>(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,
},
};
}