import { QueryClientProvider } from "@tanstack/react-query"; import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from "@testing-library/user-event"; import { IngredientAutocompleter, STORAGE_KEY_LANGUAGE_FILTER, STORAGE_KEY_NUTRISCORE_MAX, STORAGE_KEY_VEGAN, STORAGE_KEY_VEGETARIAN } from '@/components/Nutrition/widgets/IngredientAutocompleter'; import type { Mock } from 'vitest'; import { searchIngredient } from "@/components/Nutrition/api/ingredient"; import { TEST_INGREDIENT_1, TEST_INGREDIENT_2, TEST_INGREDIENT_4 } from "@/tests/ingredientTestdata"; import { testQueryClient } from "@/tests/queryClient"; vi.mock("@/components/Nutrition/api/ingredient"); const mockCallback = vi.fn(); function renderAutocompleter() { render( ); } describe("Test the IngredientAutocompleter component", () => { // Arrange beforeEach(() => { localStorage.clear(); testQueryClient.clear(); (searchIngredient as Mock).mockImplementation(() => Promise.resolve([TEST_INGREDIENT_1, TEST_INGREDIENT_2])); }); test('renders correct results', async () => { // Arrange const user = userEvent.setup(); // Act renderAutocompleter(); const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); await user.click(autocomplete); await user.type(input, 'Bag'); await waitFor(() => expect(searchIngredient).toHaveBeenCalled()); expect(await screen.findByText('0% fat Greek style yogurt')).toBeInTheDocument(); expect(screen.getByText('1001 Nacht Haferbrei')).toBeInTheDocument(); }); test('callback was correctly called', async () => { // Arrange const user = userEvent.setup(); // Act renderAutocompleter(); const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); await user.click(autocomplete); await user.type(input, 'Cru'); // Wait for the option to actually be in the dropdown — not just for // the search call. Otherwise ArrowDown+Enter fires before React has // rendered the resolved results. await screen.findByText('0% fat Greek style yogurt'); // Select the first result await user.click(input); await user.keyboard('{ArrowDown}{Enter}'); // Assert expect(mockCallback).toHaveBeenLastCalledWith(TEST_INGREDIENT_1); }); test('filters are shown after clicking the toggle icon', async () => { const user = userEvent.setup(); renderAutocompleter(); expect(screen.queryByText('nutrition.filterVegan')).not.toBeInTheDocument(); expect(screen.queryByLabelText('language')).not.toBeInTheDocument(); await user.click(screen.getByLabelText('Toggle filters')); const popover = screen.getByRole('presentation'); expect(within(popover).getByLabelText('language')).toBeInTheDocument(); expect(screen.getByText('nutrition.filterVegan')).toBeInTheDocument(); }); test('local storage settings are saved', async () => { // Arrange const user = userEvent.setup(); const setItemSpy = vi.spyOn(window.localStorage, 'setItem'); renderAutocompleter(); // Act await user.click(screen.getByLabelText('Toggle filters')); // Assert const veganFilter = screen.getByLabelText('nutrition.filterVegan'); await user.click(veganFilter); expect(setItemSpy).toHaveBeenCalledWith(STORAGE_KEY_VEGAN, 'true'); const vegetarianFilter = screen.getByLabelText('nutrition.filterVegetarian'); await user.click(vegetarianFilter); expect(setItemSpy).toHaveBeenCalledWith(STORAGE_KEY_VEGETARIAN, 'true'); await user.click(screen.getByLabelText('language')); await user.click(screen.getByRole('option', { name: 'nutrition.languageFilterAll' })); expect(setItemSpy).toHaveBeenCalledWith(STORAGE_KEY_LANGUAGE_FILTER, 'all'); setItemSpy.mockRestore(); }); test('shows vegan chip only when ingredient is vegan, not both vegan and vegetarian', async () => { // Arrange const user = userEvent.setup(); (searchIngredient as Mock).mockImplementation(() => Promise.resolve([TEST_INGREDIENT_2]) ); // Act renderAutocompleter(); const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); await user.click(autocomplete); await user.type(input, 'Haferbrei'); // Assert - vegan ingredient should show "Vegan" but not "Vegetarian" expect(await screen.findByText('nutrition.filterVegan')).toBeInTheDocument(); expect(screen.queryByText('nutrition.filterVegetarian')).not.toBeInTheDocument(); }); test('shows vegetarian chip when ingredient is only vegetarian', async () => { // Arrange const user = userEvent.setup(); (searchIngredient as Mock).mockImplementation(() => Promise.resolve([TEST_INGREDIENT_1]) ); // Act renderAutocompleter(); const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); await user.click(autocomplete); await user.type(input, 'yogurt'); // Assert - vegetarian-only ingredient should show "Vegetarian" but not "Vegan" expect(await screen.findByText('nutrition.filterVegetarian')).toBeInTheDocument(); expect(screen.queryByText('nutrition.filterVegan')).not.toBeInTheDocument(); }); test('shows no dietary chips when ingredient has no dietary info', async () => { // Arrange const user = userEvent.setup(); (searchIngredient as Mock).mockImplementation(() => Promise.resolve([TEST_INGREDIENT_4]) ); // Act renderAutocompleter(); const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); await user.click(autocomplete); await user.type(input, 'Cacao'); await waitFor(() => expect(searchIngredient).toHaveBeenCalled()); // Assert - no dietary info, no chips expect(screen.queryByText('nutrition.filterVegan')).not.toBeInTheDocument(); expect(screen.queryByText('nutrition.filterVegetarian')).not.toBeInTheDocument(); }); test('local storage settings are loaded', async () => { // Arrange const user = userEvent.setup(); localStorage.setItem(STORAGE_KEY_VEGAN, 'true'); localStorage.setItem(STORAGE_KEY_VEGETARIAN, 'true'); localStorage.setItem(STORAGE_KEY_LANGUAGE_FILTER, 'all'); renderAutocompleter(); // Act await user.click(screen.getByLabelText('Toggle filters')); // Assert expect(screen.getByLabelText('nutrition.filterVegan')).toBeChecked(); expect(screen.getByLabelText('nutrition.filterVegetarian')).toBeChecked(); expect(screen.getByText('nutrition.languageFilterAll')).toBeInTheDocument(); localStorage.clear(); }); test('nutriscore slider defaults to Off and is always visible in the popover', async () => { // Arrange const user = userEvent.setup(); renderAutocompleter(); // Act await user.click(screen.getByLabelText('Toggle filters')); // Assert — slider rendered with Off position (index 0) on first load const slider = screen.getByRole('slider', { name: 'nutrition.filterNutriscore' }); expect(slider).toBeInTheDocument(); expect(slider).toHaveAttribute('aria-valuetext', 'nutrition.filterNutriscoreNoFilter'); expect(slider).toHaveAttribute('aria-valuenow', '0'); }); test('moving the slider persists the selected grade to local storage', async () => { // Arrange const user = userEvent.setup(); const setItemSpy = vi.spyOn(window.localStorage, 'setItem'); renderAutocompleter(); // Act await user.click(screen.getByLabelText('Toggle filters')); const slider = screen.getByRole('slider', { name: 'nutrition.filterNutriscore' }); slider.focus(); await user.keyboard('{ArrowRight}'); // Assert — index 1 -> 'a' expect(setItemSpy).toHaveBeenCalledWith(STORAGE_KEY_NUTRISCORE_MAX, 'a'); setItemSpy.mockRestore(); }); test('moving the slider back to Off removes the stored grade', async () => { // Arrange const user = userEvent.setup(); localStorage.setItem(STORAGE_KEY_NUTRISCORE_MAX, 'a'); const removeItemSpy = vi.spyOn(window.localStorage, 'removeItem'); renderAutocompleter(); // Act await user.click(screen.getByLabelText('Toggle filters')); const slider = screen.getByRole('slider', { name: 'nutrition.filterNutriscore' }); slider.focus(); await user.keyboard('{ArrowLeft}'); // Assert expect(removeItemSpy).toHaveBeenCalledWith(STORAGE_KEY_NUTRISCORE_MAX); removeItemSpy.mockRestore(); }); test('nutriscore filter state is loaded from local storage', async () => { // Arrange const user = userEvent.setup(); localStorage.setItem(STORAGE_KEY_NUTRISCORE_MAX, 'b'); renderAutocompleter(); // Act await user.click(screen.getByLabelText('Toggle filters')); // Assert expect(screen.getByRole('slider', { name: 'nutrition.filterNutriscore' })).toHaveAttribute('aria-valuetext', 'nutrition.filterNutriscoreOrBetter'); }); test('searchIngredient is called with nutriscoreMax when a grade is selected', async () => { // Arrange const user = userEvent.setup(); localStorage.setItem(STORAGE_KEY_NUTRISCORE_MAX, 'b'); renderAutocompleter(); const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); // Act await user.click(autocomplete); await user.type(input, 'Yog'); // Assert await waitFor(() => expect(searchIngredient).toHaveBeenCalledWith( 'Yog', expect.objectContaining({ nutriscoreMax: 'b' }), )); }); test('nutriscoreMax is omitted when the slider is at Off', async () => { // Arrange const user = userEvent.setup(); renderAutocompleter(); const autocomplete = screen.getByTestId('autocomplete'); const input = within(autocomplete).getByRole('combobox'); // Act await user.click(autocomplete); await user.type(input, 'Yog'); // Assert await waitFor(() => expect(searchIngredient).toHaveBeenCalledWith( 'Yog', expect.objectContaining({ nutriscoreMax: undefined }), )); }); });