690 lines
18 KiB
TypeScript
690 lines
18 KiB
TypeScript
/**
|
|
* ============================================================================
|
|
* 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<TData = unknown, TError = Error> {
|
|
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<TData = unknown, TError = Error> {
|
|
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<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 });
|
|
}
|
|
|
|
// FONT STORE MOCKS
|
|
|
|
/**
|
|
* Mock UnifiedFontStore state
|
|
*/
|
|
export interface MockFontStoreState {
|
|
/** All cached fonts */
|
|
fonts: Record<string, UnifiedFont>;
|
|
/** 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> = {},
|
|
): 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<T>(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<UnifiedFont[]>({
|
|
isLoading: true,
|
|
data: undefined,
|
|
}),
|
|
|
|
/** Font store with fonts loaded */
|
|
successFontStore: createMockStore<UnifiedFont[]>({
|
|
data: Object.values(UNIFIED_FONTS),
|
|
isLoading: false,
|
|
isError: false,
|
|
}),
|
|
|
|
/** Font store with error */
|
|
errorFontStore: createMockStore<UnifiedFont[]>({
|
|
data: undefined,
|
|
isLoading: false,
|
|
isError: true,
|
|
error: new Error('Failed to load fonts'),
|
|
}),
|
|
|
|
/** Font store with empty results */
|
|
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
|
|
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<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,
|
|
},
|
|
};
|
|
}
|