From a64244e060640b884aa749f9b2ffee1a0eefd70b Mon Sep 17 00:00:00 2001 From: Alex <1353716+alexstojda@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:59:06 -0400 Subject: [PATCH] tests(leagues-frontend): Update tests for Form and new LocationSelect component --- .../components/LocationSelect/index.test.tsx | 164 ++++++++++++++++++ .../src/components/LocationSelect/index.tsx | 30 ++-- web/app/src/pages/Leagues/Form.test.tsx | 75 +++++++- web/app/src/test/fake/index.ts | 1 + web/app/src/test/fake/location.ts | 15 +- 5 files changed, 259 insertions(+), 26 deletions(-) create mode 100644 web/app/src/components/LocationSelect/index.test.tsx diff --git a/web/app/src/components/LocationSelect/index.test.tsx b/web/app/src/components/LocationSelect/index.test.tsx new file mode 100644 index 0000000..2a32c49 --- /dev/null +++ b/web/app/src/components/LocationSelect/index.test.tsx @@ -0,0 +1,164 @@ +import {act, fireEvent, render, waitFor} from "@testing-library/react"; +import React from "react"; +import {LocationSelect} from "./index"; +import {ChakraProvider} from "@chakra-ui/react"; +import {Api, LocationsApi, pinballMap} from "../../api"; +import {fake} from "../../test"; + +window.matchMedia = window.matchMedia || function () { + return { + matches: false, + addListener: function () { + }, + removeListener: function () { + } + }; +}; +jest.useFakeTimers(); + +jest.mock('../../api') +const mockApi = jest.mocked(Api) +const mockPinballMapApi = jest.mocked(pinballMap.Api) + +beforeEach(() => { + mockApi.prototype.parseError.mockReset() + mockApi.prototype.locationsApi.mockReset() + + mockPinballMapApi.prototype.locationsGet.mockReset() + mockPinballMapApi.prototype.parseError.mockReset() + jest.resetAllMocks() +}) + +describe('LocationSelect', () => { + it('renders without error', async () => { + render( + + + + ) + }); + it('invokes the onError callback if pinball map api call fails', async () => { + mockPinballMapApi.prototype.locationsGet.mockRejectedValue(false) + const mockError: pinballMap.ErrorResponse = { + errors: "test error", + } + mockPinballMapApi.prototype.parseError.mockReturnValue(mockError) + + jest.mocked(LocationsApi).prototype.locationsGet.mockResolvedValue({ + config: {}, + data: { + locations: [fake.location()], + }, + headers: {}, + request: {}, + status: 200, + statusText: "", + }) + mockApi.prototype.locationsApi.mockReturnValue(new LocationsApi()) + + const mockOnError = jest.fn() + + const wrapper = render( + + + + ) + + await act(async () => { + fireEvent.change(wrapper.getByRole('combobox'), {target: {value: 'valid search'}}) + jest.runAllTimers() + }) + + await waitFor(() => { + expect(mockOnError).toBeCalled() + }) + }) + it('invokes the onError callback when pinman api call fails', async () => { + const mockError = fake.errorResponse() + mockApi.prototype.parseError.mockReturnValue(mockError) + + mockPinballMapApi.prototype.locationsGet.mockResolvedValue([ + fake.pinballMapLocation(), + ]) + jest.mocked(LocationsApi).prototype.locationsGet.mockRejectedValue(false) + mockApi.prototype.locationsApi.mockReturnValue(new LocationsApi()) + const mockOnError = jest.fn() + + const wrapper = render( + + + + ) + + await act(async () => { + fireEvent.change(wrapper.getByRole('combobox'), {target: {value: 'te'}}) + jest.advanceTimersByTime(500) + fireEvent.change(wrapper.getByRole('combobox'), {target: {value: 'test l'}}) + jest.runAllTimers() + }) + + await waitFor(() => { + expect(mockOnError).toBeCalled() + }) + }) + it('renders with options when filter provided', async () => { + const mockPinballMapLocation = { + ...fake.pinballMapLocation(), + name: "test location", + id: 456, + } + const mockKnownPinballMapLocation = { + ...fake.pinballMapLocation(), + name: "should not be in results", + id: 123, + } + const mockPinmanLocation = { + ...fake.location(), + name: "test location 2", + pinball_map_id: 123, + } + mockPinballMapApi.prototype.locationsGet.mockResolvedValue([ + mockPinballMapLocation, + mockKnownPinballMapLocation + ]) + jest.mocked(LocationsApi).prototype.locationsGet.mockResolvedValue({ + config: {}, + data: { + locations: [ + mockPinmanLocation, + ] + }, + headers: {}, + request: {}, + status: 200, + statusText: "", + }) + mockApi.prototype.locationsApi.mockReturnValue(new LocationsApi()) + + const wrapper = render( + + + + ) + + jest.spyOn(global, 'setTimeout') + + await act(async () => { + fireEvent.change(wrapper.getByRole('combobox'), {target: {value: 'te'}}) + expect(setTimeout).not.toBeCalled() + }) + + await act(async () => { + fireEvent.change(wrapper.getByRole('combobox'), {target: {value: 'test'}}) + jest.advanceTimersByTime(500) + fireEvent.change(wrapper.getByRole('combobox'), {target: {value: 'test l'}}) + jest.runAllTimers() + }) + + await waitFor(async () => { + expect(await wrapper.findByText(mockPinballMapLocation.name)).toBeInTheDocument() + expect(await wrapper.findByText(mockPinmanLocation.name)).toBeInTheDocument() + expect(wrapper.baseElement).not.toHaveTextContent(mockKnownPinballMapLocation.name) + }) + }); +}); diff --git a/web/app/src/components/LocationSelect/index.tsx b/web/app/src/components/LocationSelect/index.tsx index 79a3dff..14ead76 100644 --- a/web/app/src/components/LocationSelect/index.tsx +++ b/web/app/src/components/LocationSelect/index.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useState} from "react"; import {AxiosError} from "axios"; -import {Api, ErrorResponse, pinballMap} from "../../api"; +import {Api, pinballMap} from "../../api"; import {ActionMeta, Select, SingleValue} from "chakra-react-select"; export type LocationOption = { @@ -19,8 +19,8 @@ const pinmanApi = new Api() const pinballMapApi = new pinballMap.Api() type LocationSelectProps = { - onChange: (value: LocationOption) => void - onError: (error: any) => void + onChange?: (value: LocationOption) => void + onError?: (error: any) => void value?: LocationOption } @@ -78,10 +78,6 @@ export function LocationSelect(props: LocationSelectProps) { } function updatePinballMapLocations(nameFilter: string) { - if (nameFilter.length < 4) { - return - } - pinballMapApi.locationsGet(nameFilter).then((locations) => { setPinmapLocations( locations.map((location): LocationOption => ({ @@ -108,6 +104,7 @@ export function LocationSelect(props: LocationSelectProps) { useEffect(() => { if (locationSelectFilterValue.length < 4) { setLocationSelectIsLoading(false) + return } else { setLocationSelectIsLoading(true) } @@ -126,21 +123,22 @@ export function LocationSelect(props: LocationSelectProps) { }; }, [locationSelectFilterValue]); - function onChange(newValue: SingleValue, _ : ActionMeta) { + function onChange(newValue: SingleValue, _: ActionMeta) { if (props.onChange) { props.onChange(newValue as LocationOption) } } return ( - { + setLocationSelectFilterValue(newValue) + }} + options={locationOptions} + value={props.value} /> ) } diff --git a/web/app/src/pages/Leagues/Form.test.tsx b/web/app/src/pages/Leagues/Form.test.tsx index 9a8b0fe..9b851f5 100644 --- a/web/app/src/pages/Leagues/Form.test.tsx +++ b/web/app/src/pages/Leagues/Form.test.tsx @@ -1,11 +1,12 @@ -import {act, render, RenderResult} from "@testing-library/react"; +import {act, render, RenderResult, waitFor} from "@testing-library/react"; import LeagueForm from "./Form"; import {faker} from "@faker-js/faker"; import {Simulate} from "react-dom/test-utils"; import {randomInt} from "crypto"; import * as helpers from '../../helpers' -import {Api, LeaguesApi} from "../../api"; +import {Api, LeaguesApi, LocationsApi} from "../../api"; import {fake} from "../../test"; +import {LocationOption} from "../../components/LocationSelect"; jest.mock('../../api') const mockApi = jest.mocked(Api) @@ -16,6 +17,22 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockedNavigate, })); +jest.mock('../../components/LocationSelect', () => ({ + __esModule: true, + LocationSelect: (props: { onChange: (o: LocationOption) => void }) => { + props.onChange({ + label: 'test-location', + type: 'pinman', + value: e.target.value, + pinballMapId: '1', + }) + }} + placeholder={'test-location'} + />, +})); + beforeEach(() => { mockedNavigate.mockReset() mockApi.prototype.leaguesApi.mockReset() @@ -51,9 +68,8 @@ describe('LeagueForm', function () { Simulate.click(submitButton) }) - expect(mockedNavigate).toHaveBeenCalledWith('/leagues') + await waitFor(() => expect(mockedNavigate).toBeCalledWith('/leagues')) }) - it('renders error from API', async () => { const mockError = fake.errorResponse() mockApi.prototype.parseError.mockReturnValue(mockError) @@ -71,9 +87,50 @@ describe('LeagueForm', function () { Simulate.click(submitButton) }) - expect(mockApi.prototype.parseError).toBeCalled() - expect(result.getByText(mockError.title)).toBeInTheDocument() + await waitFor(() => { + expect(mockApi.prototype.parseError).toBeCalled() + expect(result.getByText(mockError.title)).toBeInTheDocument() + }) }) + it( + 'creates new location with pinball map location and redirects to /leagues if successful', + async function () { + jest.mocked(LeaguesApi).prototype.leaguesPost.mockResolvedValue({ + config: {}, + data: { + league: fake.league() + }, + headers: {}, + request: {}, + status: 201, + statusText: "", + }) + mockApi.prototype.leaguesApi.mockReturnValue(new LeaguesApi()) + + jest.mocked(LocationsApi).prototype.locationsPost.mockResolvedValue({ + config: {}, + data: { + location: fake.location() + }, + headers: {}, + request: {}, + status: 201, + statusText: "", + }) + mockApi.prototype.locationsApi.mockReturnValue(new LocationsApi()) + + const result = render( + + ) + await typeLeagueInfo(result) + + const submitButton = await result.findByText("Create") + act(() => { + Simulate.click(submitButton) + }) + + await waitFor(() => expect(mockedNavigate).toBeCalledWith('/leagues')) + }) }); @@ -92,9 +149,9 @@ async function typeLeagueInfo(result: RenderResult) { Simulate.change(slugField) }) - const locationField = await result.findByPlaceholderText("location") - locationField.setAttribute("value", faker.random.words(4)) + const locationSelectValue = await result.findByPlaceholderText('test-location') + slugField.setAttribute("value", faker.datatype.uuid()) act(() => { - Simulate.change(locationField) + Simulate.change(locationSelectValue) }) } diff --git a/web/app/src/test/fake/index.ts b/web/app/src/test/fake/index.ts index 1dc7d4a..4d9995a 100644 --- a/web/app/src/test/fake/index.ts +++ b/web/app/src/test/fake/index.ts @@ -1,3 +1,4 @@ export * from "./user"; export * from "./league"; export * from "./errorResponse"; +export * from "./location"; diff --git a/web/app/src/test/fake/location.ts b/web/app/src/test/fake/location.ts index 00f7ccf..c5b7216 100644 --- a/web/app/src/test/fake/location.ts +++ b/web/app/src/test/fake/location.ts @@ -1,4 +1,4 @@ -import {Location} from "../../api"; +import {Location, pinballMap} from "../../api"; import {faker} from "@faker-js/faker"; import * as helpers from "../../helpers"; @@ -15,3 +15,16 @@ export function location(): Location { updated_at: faker.date.recent(1).toISOString() } } + +export function pinballMapLocation(): pinballMap.Location { + return { + id: faker.datatype.number(), + name: faker.hacker.phrase(), + street: faker.address.streetAddress(), + city: faker.address.city(), + state: faker.address.stateAbbr(), + zip: faker.address.zipCode(), + country: faker.address.countryCode(), + num_machines: faker.datatype.number(), + } +}