refactor(Font): consolidate API layer and update type structure
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* Proxy API filters
|
||||
*
|
||||
* Fetches filter metadata from GlyphDiff proxy API.
|
||||
* Provides type-safe response handling.
|
||||
*
|
||||
* @see https://api.glyphdiff.com/api/v1/filters
|
||||
*/
|
||||
|
||||
import { api } from '$shared/api/api';
|
||||
|
||||
/**
|
||||
* Filter metadata type from backend
|
||||
*/
|
||||
export interface FilterMetadata {
|
||||
/** Filter ID (e.g., "providers", "categories", "subsets") */
|
||||
id: string;
|
||||
|
||||
/** Display name (e.g., "Font Providers", "Categories", "Character Subsets") */
|
||||
name: string;
|
||||
|
||||
/** Filter description */
|
||||
description: string;
|
||||
|
||||
/** Filter type */
|
||||
type: 'enum' | 'string' | 'array';
|
||||
|
||||
/** Available filter options */
|
||||
options: FilterOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter option type
|
||||
*/
|
||||
export interface FilterOption {
|
||||
/** Option ID (e.g., "google", "serif", "latin") */
|
||||
id: string;
|
||||
|
||||
/** Display name (e.g., "Google Fonts", "Serif", "Latin") */
|
||||
name: string;
|
||||
|
||||
/** Option value (e.g., "google", "serif", "latin") */
|
||||
value: string;
|
||||
|
||||
/** Number of fonts with this value */
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy filters API response
|
||||
*/
|
||||
export interface ProxyFiltersResponse {
|
||||
/** Array of filter metadata */
|
||||
filters: FilterMetadata[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch filters from proxy API
|
||||
*
|
||||
* @returns Promise resolving to array of filter metadata
|
||||
* @throws ApiError when request fails
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Fetch all filters
|
||||
* const filters = await fetchProxyFilters();
|
||||
*
|
||||
* console.log(filters); // [
|
||||
* // { id: "providers", name: "Font Providers", options: [...] },
|
||||
* // { id: "categories", name: "Categories", options: [...] },
|
||||
* // { id: "subsets", name: "Character Subsets", options: [...] }
|
||||
* // ]
|
||||
* ```
|
||||
*/
|
||||
export async function fetchProxyFilters(): Promise<FilterMetadata[]> {
|
||||
const response = await api.get<FilterMetadata[]>('/api/v1/filters');
|
||||
|
||||
if (!response.data || !Array.isArray(response.data)) {
|
||||
throw new Error('Proxy API returned invalid response');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
171
src/entities/Font/api/proxy/proxyFonts.test.ts
Normal file
171
src/entities/Font/api/proxy/proxyFonts.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Tests for proxy API client
|
||||
*/
|
||||
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
test,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import type { UnifiedFont } from '../../model/types';
|
||||
import type { ProxyFontsResponse } from './proxyFonts';
|
||||
|
||||
vi.mock('$shared/api/api', () => ({
|
||||
api: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { api } from '$shared/api/api';
|
||||
import {
|
||||
fetchFontsByIds,
|
||||
fetchProxyFontById,
|
||||
fetchProxyFonts,
|
||||
} from './proxyFonts';
|
||||
|
||||
const PROXY_API_URL = 'https://api.glyphdiff.com/api/v1/fonts';
|
||||
|
||||
function createMockFont(overrides: Partial<UnifiedFont> = {}): UnifiedFont {
|
||||
return {
|
||||
id: 'roboto',
|
||||
family: 'Roboto',
|
||||
provider: 'google',
|
||||
category: 'sans-serif',
|
||||
variants: [],
|
||||
subsets: [],
|
||||
...overrides,
|
||||
} as UnifiedFont;
|
||||
}
|
||||
|
||||
function mockApiGet<T>(data: T) {
|
||||
vi.mocked(api.get).mockResolvedValueOnce({ data, status: 200 });
|
||||
}
|
||||
|
||||
describe('proxyFonts', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(api.get).mockReset();
|
||||
});
|
||||
|
||||
describe('fetchProxyFonts', () => {
|
||||
test('should fetch fonts with no params', async () => {
|
||||
const mockResponse: ProxyFontsResponse = {
|
||||
fonts: [createMockFont()],
|
||||
total: 1,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
};
|
||||
mockApiGet(mockResponse);
|
||||
|
||||
const result = await fetchProxyFonts();
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(PROXY_API_URL);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
test('should build URL with query params', async () => {
|
||||
const mockResponse: ProxyFontsResponse = {
|
||||
fonts: [createMockFont()],
|
||||
total: 1,
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
};
|
||||
mockApiGet(mockResponse);
|
||||
|
||||
await fetchProxyFonts({ provider: 'google', category: 'sans-serif', limit: 20, offset: 0 });
|
||||
|
||||
const calledUrl = vi.mocked(api.get).mock.calls[0][0];
|
||||
expect(calledUrl).toContain('provider=google');
|
||||
expect(calledUrl).toContain('category=sans-serif');
|
||||
expect(calledUrl).toContain('limit=20');
|
||||
expect(calledUrl).toContain('offset=0');
|
||||
});
|
||||
|
||||
test('should throw on invalid response (missing fonts array)', async () => {
|
||||
mockApiGet({ total: 0 });
|
||||
|
||||
await expect(fetchProxyFonts()).rejects.toThrow('Proxy API returned invalid response');
|
||||
});
|
||||
|
||||
test('should throw on null response data', async () => {
|
||||
vi.mocked(api.get).mockResolvedValueOnce({ data: null, status: 200 });
|
||||
|
||||
await expect(fetchProxyFonts()).rejects.toThrow('Proxy API returned invalid response');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchProxyFontById', () => {
|
||||
test('should return font matching the ID', async () => {
|
||||
const targetFont = createMockFont({ id: 'satoshi', name: 'Satoshi' });
|
||||
const mockResponse: ProxyFontsResponse = {
|
||||
fonts: [createMockFont(), targetFont],
|
||||
total: 2,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
};
|
||||
mockApiGet(mockResponse);
|
||||
|
||||
const result = await fetchProxyFontById('satoshi');
|
||||
|
||||
expect(result).toEqual(targetFont);
|
||||
});
|
||||
|
||||
test('should return undefined when font not found', async () => {
|
||||
const mockResponse: ProxyFontsResponse = {
|
||||
fonts: [createMockFont()],
|
||||
total: 1,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
};
|
||||
mockApiGet(mockResponse);
|
||||
|
||||
const result = await fetchProxyFontById('nonexistent');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should search with the ID as query param', async () => {
|
||||
const mockResponse: ProxyFontsResponse = {
|
||||
fonts: [],
|
||||
total: 0,
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
};
|
||||
mockApiGet(mockResponse);
|
||||
|
||||
await fetchProxyFontById('Roboto');
|
||||
|
||||
const calledUrl = vi.mocked(api.get).mock.calls[0][0];
|
||||
expect(calledUrl).toContain('limit=1000');
|
||||
expect(calledUrl).toContain('q=Roboto');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFontsByIds', () => {
|
||||
test('should return empty array for empty input', async () => {
|
||||
const result = await fetchFontsByIds([]);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(api.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should call batch endpoint with comma-separated IDs', async () => {
|
||||
const fonts = [createMockFont({ id: 'roboto' }), createMockFont({ id: 'satoshi' })];
|
||||
mockApiGet(fonts);
|
||||
|
||||
const result = await fetchFontsByIds(['roboto', 'satoshi']);
|
||||
|
||||
expect(api.get).toHaveBeenCalledWith(`${PROXY_API_URL}/batch?ids=roboto,satoshi`);
|
||||
expect(result).toEqual(fonts);
|
||||
});
|
||||
|
||||
test('should return empty array when response data is nullish', async () => {
|
||||
vi.mocked(api.get).mockResolvedValueOnce({ data: null, status: 200 });
|
||||
|
||||
const result = await fetchFontsByIds(['roboto']);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user