import { useAddMealItemQuery, useEditMealItemQuery, useSearchIngredientQuery } from "@/components/Nutrition/queries"; import { MealItemForm } from "@/components/Nutrition/widgets/forms/MealItemForm"; import { SEARCH_DEBOUNCE_MS } from "@/components/Nutrition/widgets/IngredientAutocompleter"; import { searchIngredient } from "@/components/Nutrition/api/ingredient"; import { TEST_INGREDIENT_1, TEST_INGREDIENT_2 } from "@/tests/ingredientTestdata"; import { TEST_MEAL_ITEM_1 } from "@/tests/nutritionTestdata"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act, render, screen, within } from "@testing-library/react"; import userEvent, { UserEvent } from "@testing-library/user-event"; import type { Mock } from 'vitest'; const DEBOUNCE_WAIT_MS = SEARCH_DEBOUNCE_MS + 100; vi.mock('@/components/Nutrition/queries'); vi.mock("@/components/Nutrition/api/ingredient"); // Note: this helper still relies on a real-time sleep rather than waitFor. // The MUI Autocomplete flow with a prefilled value has subtle ordering // requirements between the typed search, the dropdown render, and ArrowDown+ // Enter — replacing the sleep with waitFor on searchIngredient changes the // outcome of "An existing entry should be updated" (display value differs). // Worth revisiting if/when the test expectations are modernized. async function fillInEntry(user: UserEvent) { const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); await user.click(autocomplete); await user.type(input, 'Bagu'); // Wait for debounce await act(async () => { await new Promise((r) => setTimeout(r, DEBOUNCE_WAIT_MS)); }); // Select the first result await user.click(input); await user.keyboard('{ArrowDown}{Enter}'); const amountInput = screen.getByLabelText('amount'); await user.clear(amountInput); await user.type(amountInput, '120'); const submitButton = screen.getByRole('button', { name: 'submit' }); await user.click(submitButton); } describe('Test the MealItemForm component', () => { const queryClient = new QueryClient(); let mutateAddMock = vi.fn(); let mutateEditMock = vi.fn(); let closeFnMock = vi.fn(); beforeEach(() => { mutateAddMock = vi.fn(); mutateEditMock = vi.fn(); closeFnMock = vi.fn(); (useEditMealItemQuery as Mock).mockImplementation(() => ({ mutate: mutateEditMock })); (useAddMealItemQuery as Mock).mockImplementation(() => ({ mutate: mutateAddMock })); (searchIngredient as Mock).mockImplementation(() => Promise.resolve([TEST_INGREDIENT_1, TEST_INGREDIENT_2])); (useSearchIngredientQuery as Mock).mockImplementation(() => searchIngredient); }); test('A new entry should be added', async () => { // Arrange const user = userEvent.setup(); // Act render( ); await fillInEntry(user); // Assert expect(screen.getByDisplayValue('0% fat Greek style yogurt')).toBeInTheDocument(); expect(screen.getByDisplayValue('120')).toBeInTheDocument(); expect(mutateEditMock).not.toHaveBeenCalled(); expect(closeFnMock).toHaveBeenCalled(); expect(mutateAddMock).toHaveBeenCalledWith( expect.objectContaining({ mealId: 'bbbbbbbb-0000-0000-0000-000000000123', amount: 120, ingredientId: 101, weightUnitId: null, }) ); }); test('An existing entry should be updated', async () => { // Arrange const user = userEvent.setup(); // Act render( ); await fillInEntry(user); // Assert expect(screen.getByDisplayValue('1001 Nacht Haferbrei')).toBeInTheDocument(); expect(screen.getByDisplayValue('120')).toBeInTheDocument(); expect(mutateAddMock).not.toHaveBeenCalled(); expect(closeFnMock).toHaveBeenCalled(); expect(mutateEditMock).toHaveBeenCalledWith( expect.objectContaining({ id: 'cccccccc-0000-0000-0000-000000000042', mealId: 'bbbbbbbb-0000-0000-0000-000000001001', amount: 120, order: 3, ingredientId: 101, weightUnitId: null, }) ); }); });