MeshiTrack/packages/web/src/services/__tests__/recipes.test.ts

119 lines
3.8 KiB
TypeScript
Raw Normal View History

2026-05-14 14:47:23 +09:00
import { describe, it, expect, vi, beforeEach } from 'vitest';
const { mockGet, mockPost, mockPatch, mockDelete } = vi.hoisted(() => ({
mockGet: vi.fn(),
mockPost: vi.fn(),
mockPatch: vi.fn(),
mockDelete: vi.fn(),
}));
vi.mock('../api-client', () => ({
apiClient: { get: mockGet, post: mockPost, patch: mockPatch, delete: mockDelete },
}));
import {
listRecipes,
getRecipe,
createRecipe,
updateRecipe,
deleteRecipe,
scaleRecipe,
importRecipeFromText,
importRecipeFromUrl,
listRecipesByProduct,
} from '../recipes';
beforeEach(() => vi.clearAllMocks());
describe('recipes service', () => {
it('listRecipes with no query', async () => {
mockGet.mockResolvedValue({ data: [] });
await listRecipes('hh1');
expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes');
});
it('listRecipes builds query string', async () => {
mockGet.mockResolvedValue({ data: [] });
await listRecipes('hh1', {
q: 'pasta',
tags: 'italian',
cuisine: 'Italian',
maxCalories: 500,
isFavorite: true,
cursor: 'cur1',
limit: 10,
});
const url = mockGet.mock.calls[0][0] as string;
expect(url).toContain('q=pasta');
expect(url).toContain('tags=italian');
expect(url).toContain('cuisine=Italian');
expect(url).toContain('maxCalories=500');
expect(url).toContain('isFavorite=true');
expect(url).toContain('cursor=cur1');
expect(url).toContain('limit=10');
});
it('getRecipe calls GET', async () => {
mockGet.mockResolvedValue({ _id: 'r1' });
await getRecipe('hh1', 'r1');
expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes/r1');
});
it('createRecipe calls POST', async () => {
mockPost.mockResolvedValue({ _id: 'r1' });
const data = { name: 'Pasta' };
await createRecipe('hh1', data as never);
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes', data);
});
it('updateRecipe calls PATCH', async () => {
mockPatch.mockResolvedValue({ _id: 'r1' });
await updateRecipe('hh1', 'r1', { name: 'Updated' } as never);
expect(mockPatch).toHaveBeenCalledWith('/households/hh1/recipes/r1', { name: 'Updated' });
});
it('deleteRecipe calls DELETE', async () => {
mockDelete.mockResolvedValue(undefined);
await deleteRecipe('hh1', 'r1');
expect(mockDelete).toHaveBeenCalledWith('/households/hh1/recipes/r1');
});
it('scaleRecipe calls POST', async () => {
mockPost.mockResolvedValue({ _id: 'r1' });
await scaleRecipe('hh1', 'r1', { targetServings: 8 });
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/r1/scale', {
targetServings: 8,
});
});
it('importRecipeFromText calls POST', async () => {
mockPost.mockResolvedValue({ available: false });
await importRecipeFromText('hh1', { text: 'recipe text' } as never);
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/import-text', {
text: 'recipe text',
});
});
it('importRecipeFromUrl calls POST', async () => {
mockPost.mockResolvedValue({ available: false });
await importRecipeFromUrl('hh1', { url: 'http://example.com' } as never);
expect(mockPost).toHaveBeenCalledWith('/households/hh1/recipes/import-url', {
url: 'http://example.com',
});
});
it('listRecipesByProduct with no query', async () => {
mockGet.mockResolvedValue({ data: [] });
await listRecipesByProduct('hh1', 'p1');
expect(mockGet).toHaveBeenCalledWith('/households/hh1/recipes/by-product/p1');
});
it('listRecipesByProduct with query', async () => {
mockGet.mockResolvedValue({ data: [] });
await listRecipesByProduct('hh1', 'p1', { cursor: 'c1', limit: 5 });
const url = mockGet.mock.calls[0][0] as string;
expect(url).toContain('cursor=c1');
expect(url).toContain('limit=5');
});
});