diff --git a/src/components/FlightFlagger/FlightFlagger.tsx b/src/components/FlightFlagger/FlightFlagger.tsx index 2bdec27..34287a1 100644 --- a/src/components/FlightFlagger/FlightFlagger.tsx +++ b/src/components/FlightFlagger/FlightFlagger.tsx @@ -1,5 +1,5 @@ import React, {useState} from "react"; -import {Country, FlightFlaggerFilters} from "./FlightFlaggerFilters"; +import {Country, FlightFlaggerFilters, FormState} from "./FlightFlaggerFilters"; import { FlightFlaggerResults } from "./FlightFlaggerResults"; import { SearchFilterPayload } from "./FlightFlaggerFilters"; import {CircularProgress} from "@mui/material"; @@ -11,10 +11,11 @@ export interface IFlightFlagger { ageGroups: string[], submitCallback: (payload:SearchFilterPayload) => void, flights: FlightArrival[], - isLoading: boolean + isLoading: boolean, + maybeInitialFilterFormState?: FormState } -const FlightFlagger = ({nationalities, ageGroups, submitCallback, flights, isLoading}: IFlightFlagger) => { +const FlightFlagger = ({nationalities, ageGroups, submitCallback, flights, isLoading, maybeInitialFilterFormState}: IFlightFlagger) => { const [showHighlightOnly, setShowHighlightOnly] = useState(false); @@ -25,13 +26,15 @@ const FlightFlagger = ({nationalities, ageGroups, submitCallback, flights, isLoa const onChangeInput = (searchTerm: string) => {} return <> - {}} - showAllCallback={toggleHighlightDisplay} /> + submitCallback={submitCallback} + clearFiltersCallback={()=> {}} + showAllCallback={toggleHighlightDisplay} + maybeInitialState={maybeInitialFilterFormState} + /> { isLoading?
: } } diff --git a/src/components/FlightFlagger/FlightFlaggerFilters.tsx b/src/components/FlightFlagger/FlightFlaggerFilters.tsx index 2958e9b..f158eff 100644 --- a/src/components/FlightFlagger/FlightFlaggerFilters.tsx +++ b/src/components/FlightFlagger/FlightFlaggerFilters.tsx @@ -1,39 +1,36 @@ -import React, {useState} from "react"; +import React, {useState} from "react" import { - Grid, - Typography, + Autocomplete, + Button, + Checkbox, + Chip, + Collapse, + FormControl, + FormControlLabel, FormGroup, FormLabel, - RadioGroup, - Radio, - FormControl, - Collapse, + Grid, + InputAdornment, InputLabel, + Link, OutlinedInput, - InputAdornment, - FormControlLabel, - Autocomplete, - Checkbox, - TextField, - Button, Paper, - IconButton, - Chip, - Link -} from "@mui/material"; -import SearchIcon from '@mui/icons-material/Search'; -import {ArrowRight} from "@mui/icons-material"; -import CustomHighlightIcon from "./icon-highlight-pax.svg"; - -export type AutocompleteOption = { - title: string -} + Radio, + RadioGroup, + TextField, + Typography +} from "@mui/material" +import SearchIcon from '@mui/icons-material/Search' +import CustomHighlightIcon from "./icon-highlight-pax.svg" -type FormState = { +export type FormState = { showTransitPaxNumber: boolean, showNumberOfVisaNationals: boolean, requireAllSelected: boolean, - flightNumber: string + flightNumber: string, + selectedNationalities: Country[], + selectedAgeGroups: string[], + showFilters: boolean, } export type Country = { @@ -44,10 +41,10 @@ export type Country = { export type SearchFilterPayload = { showTransitPaxNumber: boolean, showNumberOfVisaNationals: boolean, - selectedAgeGroups: string[], - selectedNationalities: Country[], - flightNumber: string, requireAllSelected: boolean, + flightNumber: string, + selectedNationalities: Country[], + selectedAgeGroups: string[], } export interface IFlightFlaggerFilters { @@ -57,17 +54,33 @@ export interface IFlightFlaggerFilters { showAllCallback: (event: React.ChangeEvent) => void, onChangeInput: (searchTerm: string) => void, clearFiltersCallback: (payload: SearchFilterPayload) => void, - initialState?: { - showTransitPaxNumber: boolean, - showNumberOfVisaNationals: boolean, - requireAllSelected: boolean, - flightNumber: string, - selectedNationalities: Country[], - selectedAgeGroups: AutocompleteOption[], - showFilters: boolean, + maybeInitialState?: FormState +} + +function getInitialState(initialState?: FormState) { + return { + showTransitPaxNumber: initialState?.showTransitPaxNumber || false, + showNumberOfVisaNationals: initialState?.showNumberOfVisaNationals || false, + requireAllSelected: initialState?.requireAllSelected || false, + flightNumber: initialState?.flightNumber || '', + selectedNationalities: initialState?.selectedNationalities || [], + selectedAgeGroups: initialState?.selectedAgeGroups || [], + showFilters: initialState?.showFilters || false, } } +function emptyFilterFormState(flightNumber: string) { + return { + showTransitPaxNumber: false, + showNumberOfVisaNationals: false, + requireAllSelected: false, + flightNumber: flightNumber, + selectedNationalities: [], + selectedAgeGroups: [], + } +} + + export const FlightFlaggerFilters = ({ nationalities, ageGroups, @@ -75,133 +88,98 @@ export const FlightFlaggerFilters = ({ showAllCallback, onChangeInput, clearFiltersCallback, - initialState, + maybeInitialState, }: IFlightFlaggerFilters) => { const HighlightIcon = () => { - return + return } - const nationalitiesOptions = nationalities.map((nationality) => { - return nationality - }); - const ageOptions = ageGroups.map((ageGroup) => { - return {title: ageGroup} - }); - - const [searchFlags, setSearchFlags] = useState({ - showTransitPaxNumber: initialState?.showTransitPaxNumber || false, - showNumberOfVisaNationals: initialState?.showNumberOfVisaNationals || false, - requireAllSelected: initialState?.requireAllSelected || false, - flightNumber: initialState?.flightNumber || '', - }); - const [selectedNationalities, setSelectedNationalities] = useState(initialState?.selectedNationalities || []); - const [selectedAgeGroups, setSelectedAgeGroups] = useState(initialState?.selectedAgeGroups || []); - const [showFilters, setShowFilters] = useState(initialState?.showFilters || false); + const initialFormState = getInitialState(maybeInitialState) + const [currentFormState, setCurrentFormState] = useState(initialFormState) + const [appliedSearchFlags, setAppliedSearchFlags] = useState(initialFormState) - const isTouched = () => { - return (selectedNationalities.length !== 0) || (selectedAgeGroups.length !== 0) || searchFlags.showTransitPaxNumber || searchFlags.showNumberOfVisaNationals + const formIsTouched = (initialForm, currentForm) => { + return currentForm.selectedNationalities.length !== initialForm.selectedNationalities.length || + currentForm.selectedAgeGroups.length !== initialForm.selectedAgeGroups.length || + currentForm.showTransitPaxNumber != initialForm.showTransitPaxNumber || + currentForm.showNumberOfVisaNationals != initialForm.showNumberOfVisaNationals || + currentForm.requireAllSelected != initialForm.requireAllSelected } - const submit = () => { - const ageGroupPayload: string[] = selectedAgeGroups.map((ageGroup: AutocompleteOption) => ageGroup.title) - const nationalityPayload: Country[] = selectedNationalities.map((nationality: Country) => nationality) - submitCallback({ - ...searchFlags, - selectedNationalities: nationalityPayload, - selectedAgeGroups: ageGroupPayload, - }); + const someCriteriaSelected = (form: FormState) => { + return form.selectedNationalities.length > 0 || + form.selectedAgeGroups.length > 0 || + form.showTransitPaxNumber || + form.showNumberOfVisaNationals } - const handleApply = () => { - submit() - setShowFilters(false) - } + const handleApply = (form: FormState) => + () => { + const formToSubmit = {...form, showFilters: false} + + submitCallback(formToSubmit) + + setCurrentFormState(formToSubmit) + setAppliedSearchFlags(formToSubmit) + } const handleCheckboxChange = (event: React.ChangeEvent) => { - const name: string = event.target.name - setSearchFlags({ - ...searchFlags, - [name]: event.target.checked + setCurrentFormState({ + ...currentFormState, + [event.target.name]: event.target.checked }) } const handleTextInputChange = (event: React.ChangeEvent) => { - const name: string = event.target.name - setSearchFlags({ - ...searchFlags, - [name]: event.target.value + setCurrentFormState({ + ...currentFormState, + [event.target.name]: event.target.value }) onChangeInput(event.target.value) } const handleInputSubmit = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { - submit() + submitCallback(currentFormState) } } const toggleFilters = () => { - setShowFilters(!showFilters) + setCurrentFormState({...currentFormState, showFilters: !currentFormState.showFilters}) } - - const buildFilterString = () => { - const paxFlters = [] - if (selectedNationalities.length) { - paxFlters.push(`nationality: ${selectedNationalities.map(n => `${n.name} (${n.code})`).join(', ')}`) - } - if (selectedAgeGroups.length) { - paxFlters.push(`age: ${selectedAgeGroups.map(n => n.title).join(', ')}`) - } - if (searchFlags.showTransitPaxNumber) { - paxFlters.push('show transit pax') - } - if (searchFlags.showNumberOfVisaNationals) { - paxFlters.push('show number of visa nationals') - } - if (searchFlags.requireAllSelected) { - paxFlters.push('only highlight flights with all selected info') - } - return `${paxFlters.join(', ')}`; + const buildFilterString = (flags: FormState) => { + return [ + flags.selectedNationalities.length ? `nationality: ${flags.selectedNationalities.map(n => `${n.name} (${n.code})`).join(', ')}` : null, + flags.selectedAgeGroups.length ? `age: ${flags.selectedAgeGroups.join(', ')}` : null, + flags.showTransitPaxNumber ? 'show transit pax' : null, + flags.showNumberOfVisaNationals ? 'show number of visa nationals' : null, + flags.requireAllSelected ? 'only highlight flights with all selected info' : null + ] + .filter(v => v !== null) + .join(', ') } - const getFilterCount = () => { - let total = 0 - total += selectedNationalities.length - total += selectedAgeGroups.length - if (searchFlags.showTransitPaxNumber) { - total++ - } - if (searchFlags.showNumberOfVisaNationals) { - total++ - } - if (searchFlags.requireAllSelected) { - total++ - } - return total; + const getFilterCount = (form: FormState) => { + return form.selectedNationalities.length + + form.selectedAgeGroups.length + + (form.showTransitPaxNumber ? 1 : 0) + + (form.showNumberOfVisaNationals ? 1 : 0) + + (form.requireAllSelected ? 1 : 0) } const clearHighlights = () => { - const resetFilterPayload = { - ...searchFlags, - showTransitPaxNumber: false, - showNumberOfVisaNationals: false, - requireAllSelected: false, - } - setSearchFlags(resetFilterPayload) - setSelectedNationalities([]); - setSelectedAgeGroups([]); + const emptyState = emptyFilterFormState(currentFormState.flightNumber) - clearFiltersCallback({ - ...resetFilterPayload, - selectedNationalities: [], - selectedAgeGroups: [], - }); + setCurrentFormState({...emptyState, showFilters: false}) + setAppliedSearchFlags({...emptyState, showFilters: false}) + + clearFiltersCallback(emptyState) } return <> - Enter flight details @@ -213,7 +191,7 @@ export const FlightFlaggerFilters = ({ name="flightNumber" onChange={handleTextInputChange} onKeyDown={handleInputSubmit} - value={searchFlags.flightNumber} + value={currentFormState.flightNumber} placeholder="Enter flight, origin or country" startAdornment={ @@ -231,10 +209,10 @@ export const FlightFlaggerFilters = ({ Highlight flights - + {someCriteriaSelected(appliedSearchFlags) && Show flights: - } + } label="All flights"/> } label="Highlighted flights only"/> - + } - + @@ -285,13 +264,13 @@ export const FlightFlaggerFilters = ({ multiple id="nationalities" options={nationalities} - getOptionLabel={(option) => `${option.name} (${option.code})`} - value={selectedNationalities} + getOptionLabel={option => `${option.name} (${option.code})`} + value={currentFormState.selectedNationalities} defaultValue={[]} filterSelectedOptions isOptionEqualToValue={(option, value) => option.code === value.code} - onChange={(event, newValue) => { - setSelectedNationalities(newValue); + onChange={(event, newValue: Country[]) => { + setCurrentFormState({...currentFormState, selectedNationalities: newValue}) }} renderInput={(params) => ( option.title} - value={selectedAgeGroups} + options={ageGroups} + value={currentFormState.selectedAgeGroups} defaultValue={[]} filterSelectedOptions - isOptionEqualToValue={(option, value) => option.title === value.title} - onChange={(event, newValue) => { - setSelectedAgeGroups(newValue); + onChange={(event, newValue: string[]) => { + setCurrentFormState({...currentFormState, selectedAgeGroups: newValue}) }} renderInput={(params) => ( + } label="Show number of visa nationals" /> @@ -339,8 +319,8 @@ export const FlightFlaggerFilters = ({ control={ } @@ -350,17 +330,29 @@ export const FlightFlaggerFilters = ({ - - + + - {isTouched() && - Pax info highlighted - - {buildFilterString()} - clearHighlights()}>Clear - all highlights + {someCriteriaSelected(appliedSearchFlags) && + Pax info highlighted - + {buildFilterString(appliedSearchFlags)} - clearHighlights()}> + Clear all highlights + } diff --git a/src/components/FlightFlagger/__tests__/FlightFlagger.test.tsx b/src/components/FlightFlagger/__tests__/FlightFlagger.test.tsx index ca49e2d..56f935d 100644 --- a/src/components/FlightFlagger/__tests__/FlightFlagger.test.tsx +++ b/src/components/FlightFlagger/__tests__/FlightFlagger.test.tsx @@ -4,9 +4,7 @@ import {screen} from "@testing-library/dom"; import {fireEvent} from "@testing-library/react"; import FlightFlagger from "../FlightFlagger"; import ExampleFlights from "../ExampleFlights"; -import {waitFor, within} from "@testing-library/react"; import '@testing-library/jest-dom' -import {SearchFilterPayload} from "../FlightFlaggerFilters"; const nationalities = [ {name: 'Great Britain', code: 'GBR'}, @@ -17,43 +15,36 @@ const ageGroups = ['0-9', '10-24', '24+']; test("displays all flight results", async () => { - render( console.log(payload)} />); + render( { + }}/>); const tableRows = await screen.getByTestId('flight-flagger-results-table').querySelectorAll('tbody tr'); expect(tableRows).toHaveLength(ExampleFlights.length) }) test("hides and shows non-highlighted flights correctly", async () => { - render( console.log(payload)} />); - - fireEvent.click(screen.getByTestId('show-highlighted-only')); - - let tableRows = await screen.getByTestId('flight-flagger-results-table').querySelectorAll('tbody tr'); - expect(tableRows).toHaveLength(1) - - fireEvent.click(screen.getByTestId('show-all-flights')); - - tableRows = await screen.getByTestId('flight-flagger-results-table').querySelectorAll('tbody tr'); - expect(tableRows).toHaveLength(ExampleFlights.length) -}) - -test("accepts an initial state", async () => { - render( console.log(payload)} />); + render( { + }} + maybeInitialFilterFormState={{ + showTransitPaxNumber: false, + showNumberOfVisaNationals: true, + requireAllSelected: true, + flightNumber: '', + selectedNationalities: [], + selectedAgeGroups: [], + showFilters: true, + }} + />); fireEvent.click(screen.getByTestId('show-highlighted-only')); @@ -67,13 +58,14 @@ test("accepts an initial state", async () => { }) test("displays the circular spinner and hides results when loading prop is true", async () => { - - render( console.log(payload)} />); + + render( { + }}/>); const table = await screen.queryByTestId('flight-flagger-results-table') const loadingSpinner = await screen.queryByTestId('flight-flagger-loading-spinner') @@ -81,175 +73,6 @@ test("displays the circular spinner and hides results when loading prop is true" expect(loadingSpinner).toBeTruthy() }) -test("hides and shows the search filters", async () => { - render( console.log(payload) } />); - - let filters = await screen.queryByTestId('flight-flagger-filters') - await waitFor(() => { - expect(filters).toHaveStyle(`height: 0px`) - }); - - await fireEvent.click(screen.getByTestId('show-filters')); - filters = await screen.queryByTestId('flight-flagger-filters') - await waitFor(() => { - expect(filters).toHaveStyle(`height: auto`) - }); - - fireEvent.click(screen.getByTestId('show-filters')); - filters = await screen.queryByTestId('flight-flagger-filters') - await waitFor(() => { - expect(filters).toHaveStyle(`height: 0px`) - }); -}) - -test("calls the submitCallback with the correct filters", async () => { - - const callBack = jest.fn(); - - const expectedPayload = { - selectedNationalities: [{ - code: 'GBR', - name: 'Great Britain' - }], - selectedAgeGroups: ['0-9'], - showTransitPaxNumber: false, - showNumberOfVisaNationals: true, - requireAllSelected: true, - flightNumber: 'BA1234' - } - - render(); - - await fireEvent.click(screen.getByTestId('show-filters')); - const filters = await screen.queryByTestId('flight-flagger-filters') - await waitFor(() => { - expect(filters).toHaveStyle(`height: auto`) - }); - - const flightNumber = screen.getByLabelText('Enter flight details') - fireEvent.change(flightNumber, {target: {value: 'BA1234'}}) - - const nationalitiesAutocomplete = screen.getByTestId('nationalities-autocomplete'); - const nationalitiesInput = within(nationalitiesAutocomplete).getByRole('combobox') - nationalitiesAutocomplete.focus() - - fireEvent.change(nationalitiesInput, { target: { value: 'G' } }) - fireEvent.keyDown(nationalitiesAutocomplete, { key: 'ArrowDown' }) - fireEvent.keyDown(nationalitiesAutocomplete, { key: 'Enter' }) - - const ageAutocomplete = screen.getByTestId('age-autocomplete'); - const ageInput = within(ageAutocomplete).getByRole('combobox') - ageAutocomplete.focus() - - fireEvent.change(ageInput, { target: { value: 'G' } }) - fireEvent.keyDown(ageAutocomplete, { key: 'ArrowDown' }) - fireEvent.keyDown(ageAutocomplete, { key: 'Enter' }) - - await fireEvent.click(screen.getByTestId('show-visa-nationals-check')); - await fireEvent.click(screen.getByTestId('require-all-selected-check')); - - fireEvent.click(screen.getByTestId('flight-flagger-filter-submit')); - expect(callBack).toHaveBeenCalledWith(expectedPayload) -}) - -test("calls the submitCallback when the user hits enter on the flight number input", async () => { - - const callBack = jest.fn(); - - const expectedPayload = { - selectedNationalities: [], - selectedAgeGroups: [], - showTransitPaxNumber: false, - showNumberOfVisaNationals: false, - requireAllSelected: false, - flightNumber: 'BA1234' - } - - render(); - - await fireEvent.click(screen.getByTestId('show-filters')); - const filters = await screen.queryByTestId('flight-flagger-filters') - await waitFor(() => { - expect(filters).toHaveStyle(`height: auto`) - }); - - const flightNumber = screen.getByLabelText('Enter flight details') - fireEvent.change(flightNumber, {target: {value: 'BA1234'}}) - fireEvent.keyDown(flightNumber, { key: 'Enter' }) - - expect(callBack).toHaveBeenCalledWith(expectedPayload) -}) - -test("calls the submitCallback when the user clears selected filters", async () => { - - const callBack = jest.fn(); - - const expectedPayload = { - selectedNationalities: [], - selectedAgeGroups: [], - showTransitPaxNumber: false, - showNumberOfVisaNationals: false, - requireAllSelected: false, - flightNumber: 'BA1234' - } - - render(); - - await fireEvent.click(screen.getByTestId('show-filters')); - const filters = await screen.queryByTestId('flight-flagger-filters') - await waitFor(() => { - expect(filters).toHaveStyle(`height: auto`) - }); - - const flightNumber = screen.getByLabelText('Enter flight details') - fireEvent.change(flightNumber, {target: {value: 'BA1234'}}) - fireEvent.keyDown(flightNumber, { key: 'Enter' }) - - const nationalitiesAutocomplete = screen.getByTestId('nationalities-autocomplete'); - const nationalitiesInput = within(nationalitiesAutocomplete).getByRole('combobox') - nationalitiesAutocomplete.focus() - - fireEvent.change(nationalitiesInput, { target: { value: 'G' } }) - fireEvent.keyDown(nationalitiesAutocomplete, { key: 'ArrowDown' }) - fireEvent.keyDown(nationalitiesAutocomplete, { key: 'Enter' }) - - const ageAutocomplete = screen.getByTestId('age-autocomplete'); - const ageInput = within(ageAutocomplete).getByRole('combobox') - ageAutocomplete.focus() - - fireEvent.change(ageInput, { target: { value: 'G' } }) - fireEvent.keyDown(ageAutocomplete, { key: 'ArrowDown' }) - fireEvent.keyDown(ageAutocomplete, { key: 'Enter' }) - - await fireEvent.click(screen.getByTestId('show-visa-nationals-check')); - await fireEvent.click(screen.getByTestId('require-all-selected-check')); - - await fireEvent.click(screen.getByTestId('flight-flagger-clear-filters')); - - await fireEvent.click(screen.getByTestId('flight-flagger-filter-submit')); - expect(callBack).toHaveBeenCalledWith(expectedPayload) -}) - test("renders the mobile view on small devices", async () => { window.matchMedia = jest.fn().mockImplementation(query => ({ matches: query !== '(max-width: 400px)', @@ -258,13 +81,14 @@ test("renders the mobile view on small devices", async () => { addListener: jest.fn(), removeListener: jest.fn() })); - - render( console.log(payload)} />); + + render( { + }}/>); const desktopResults = await screen.queryByTestId('flight-flagger-desktop-results') const mobileResults = await screen.queryByTestId('flight-flagger-mobile-results') diff --git a/src/components/FlightFlagger/__tests__/FlightFlaggerFilters.test.tsx b/src/components/FlightFlagger/__tests__/FlightFlaggerFilters.test.tsx new file mode 100644 index 0000000..02e6435 --- /dev/null +++ b/src/components/FlightFlagger/__tests__/FlightFlaggerFilters.test.tsx @@ -0,0 +1,258 @@ +import React from "react" +import {render} from "../../TestProviderRenderer" +import {screen} from "@testing-library/dom" +import {fireEvent, waitFor, within} from "@testing-library/react" +import '@testing-library/jest-dom' +import {FlightFlaggerFilters} from "../FlightFlaggerFilters" + +const nationalities = [ + {name: 'Great Britain', code: 'GBR'}, + {name: 'France', code: 'FRA'}, + {name: 'Spain', code: 'SPA'} +] +const ageGroups = ['0-9', '10-24', '24+'] + +const initialFormStateWithSomeFilter = { + showTransitPaxNumber: false, + showNumberOfVisaNationals: true, + requireAllSelected: false, + flightNumber: '', + selectedNationalities: [], + selectedAgeGroups: [], + showFilters: false, +} + +test("does not display option to hide or show flights before any criteria are applied", async () => { + render( {}} + showAllCallback={() => {}} + onChangeInput={() => {}} + clearFiltersCallback={() => {}} + maybeInitialState={undefined} + />) + + const highlightedOnlyButton = screen.queryByTestId('show-highlighted-only') + + expect(highlightedOnlyButton).toBeNull() +}) + +test("apply button is only enabled when there are changes to apply", async () => { + const callBack = jest.fn() + + render( {}} + onChangeInput={() => {}} + clearFiltersCallback={() => {}} + maybeInitialState={{...initialFormStateWithSomeFilter, showFilters: true}} + />) + + const applyButton = screen.queryByTestId('flight-flagger-filter-submit') + + expect(applyButton).toBeDisabled() + + const nationalitiesAutocomplete = screen.getByTestId('nationalities-autocomplete') + const nationalitiesInput = within(nationalitiesAutocomplete).getByRole('combobox') + fireEvent.change(nationalitiesInput, {target: {value: 'G'}}) + fireEvent.keyDown(nationalitiesAutocomplete, {key: 'ArrowDown'}) + fireEvent.keyDown(nationalitiesAutocomplete, {key: 'Enter'}) + + expect(applyButton).not.toBeDisabled() + + fireEvent.click(screen.getByTestId('flight-flagger-filter-submit')) + + expect(callBack).toHaveBeenCalled() + + expect(applyButton).toBeDisabled() +}) + +test("cancel button removes any un-applied changes to the form", async () => { + render( {}} + showAllCallback={() => {}} + onChangeInput={() => {}} + clearFiltersCallback={() => {}} + />) + + fireEvent.click(screen.getByTestId('show-filters')) + + const showVisaNationals = screen.getByLabelText('show visa nationals') + fireEvent.click(showVisaNationals) + expect(showVisaNationals).toBeChecked() + + await fireEvent.click(screen.getByTestId('flight-flagger-filter-cancel')) + + fireEvent.click(screen.getByTestId('show-filters')) + + expect(showVisaNationals).not.toHaveAttribute('checked') + + const applyButtonAfterApply = screen.queryByTestId('flight-flagger-filter-submit') + expect(applyButtonAfterApply).toBeDisabled() +}) + +test("does display option to hide or show flights when some criteria are applied", async () => { + render( {}} + showAllCallback={() => {}} + onChangeInput={() => {}} + clearFiltersCallback={() => {}} + maybeInitialState={initialFormStateWithSomeFilter} + />) + + const highlightedOnlyButton = screen.queryByTestId('show-highlighted-only') + + expect(highlightedOnlyButton).not.toBeNull() +}) + +test("hides and shows the search filters", async () => { + render( {}} + showAllCallback={() => {}} + onChangeInput={() => {}} + clearFiltersCallback={() => {}} + />) + + let filters = await screen.queryByTestId('flight-flagger-filters') + await waitFor(() => { + expect(filters).toHaveStyle(`height: 0px`) + }) + + await fireEvent.click(screen.getByTestId('show-filters')) + filters = await screen.queryByTestId('flight-flagger-filters') + await waitFor(() => { + expect(filters).toHaveStyle(`height: auto`) + }) + + fireEvent.click(screen.getByTestId('show-filters')) + filters = await screen.queryByTestId('flight-flagger-filters') + await waitFor(() => { + expect(filters).toHaveStyle(`height: 0px`) + }) +}) + +test("calls the submitCallback with the correct filters", async () => { + const callBack = jest.fn() + + const expectedPayload = { + selectedNationalities: [{ + code: 'GBR', + name: 'Great Britain' + }], + selectedAgeGroups: ['0-9'], + showTransitPaxNumber: false, + showFilters: false, + showNumberOfVisaNationals: true, + requireAllSelected: true, + flightNumber: 'BA1234' + } + + render( {}} + onChangeInput={() => {}} + clearFiltersCallback={() => {}} + />) + + await fireEvent.click(screen.getByTestId('show-filters')) + const filters = await screen.queryByTestId('flight-flagger-filters') + await waitFor(() => { + expect(filters).toHaveStyle(`height: auto`) + }) + + const flightNumber = screen.getByLabelText('Enter flight details') + fireEvent.change(flightNumber, {target: {value: 'BA1234'}}) + + const nationalitiesAutocomplete = screen.getByTestId('nationalities-autocomplete') + const nationalitiesInput = within(nationalitiesAutocomplete).getByRole('combobox') + nationalitiesAutocomplete.focus() + + fireEvent.change(nationalitiesInput, {target: {value: 'G'}}) + fireEvent.keyDown(nationalitiesAutocomplete, {key: 'ArrowDown'}) + fireEvent.keyDown(nationalitiesAutocomplete, {key: 'Enter'}) + + const ageAutocomplete = screen.getByTestId('age-autocomplete') + const ageInput = within(ageAutocomplete).getByRole('combobox') + ageAutocomplete.focus() + + fireEvent.change(ageInput, {target: {value: 'G'}}) + fireEvent.keyDown(ageAutocomplete, {key: 'ArrowDown'}) + fireEvent.keyDown(ageAutocomplete, {key: 'Enter'}) + + await fireEvent.click(screen.getByTestId('show-visa-nationals-check')) + await fireEvent.click(screen.getByTestId('require-all-selected-check')) + + fireEvent.click(screen.getByTestId('flight-flagger-filter-submit')) + expect(callBack).toHaveBeenCalledWith(expectedPayload) +}) + +test("calls the submitCallback when the user hits enter on the flight number input", async () => { + + const callBack = jest.fn() + + render( {}} + onChangeInput={() => {}} + clearFiltersCallback={() => {}} + maybeInitialState={initialFormStateWithSomeFilter} + />) + + const flightNumber = screen.getByLabelText('Enter flight details') + fireEvent.change(flightNumber, {target: {value: 'BA1234'}}) + fireEvent.keyDown(flightNumber, {key: 'Enter'}) + + expect(callBack).toHaveBeenCalledWith({...initialFormStateWithSomeFilter, flightNumber: 'BA1234'}) +}) + +test("calls the submitCallback when the user clears selected filters", async () => { + + const callBack = jest.fn() + + const expectedPayload = { + selectedNationalities: [], + selectedAgeGroups: [], + showTransitPaxNumber: false, + showNumberOfVisaNationals: false, + requireAllSelected: false, + flightNumber: 'BA1234' + } + + render( {}} + showAllCallback={() => {}} + onChangeInput={() => {}} + clearFiltersCallback={callBack} + maybeInitialState={{ + showTransitPaxNumber: true, + showNumberOfVisaNationals: true, + requireAllSelected: true, + flightNumber: '', + selectedNationalities: nationalities, + selectedAgeGroups: ageGroups, + showFilters: false, + }} + />) + + const flightNumber = screen.getByLabelText('Enter flight details') + fireEvent.change(flightNumber, {target: {value: 'BA1234'}}) + fireEvent.keyDown(flightNumber, {key: 'Enter'}) + + await fireEvent.click(screen.getByTestId('flight-flagger-clear-filters')) + expect(callBack).toHaveBeenCalledWith(expectedPayload) +}) diff --git a/src/components/FlightFlagger/index.ts b/src/components/FlightFlagger/index.ts index e400f01..539e95e 100644 --- a/src/components/FlightFlagger/index.ts +++ b/src/components/FlightFlagger/index.ts @@ -2,7 +2,7 @@ export { default } from "./FlightFlagger"; export type {IFlightFlagger} from './FlightFlagger' export {FlightFlaggerFilters} from "./FlightFlaggerFilters"; -export type {IFlightFlaggerFilters, AutocompleteOption} from './FlightFlaggerFilters'; +export type {IFlightFlaggerFilters} from './FlightFlaggerFilters'; export {FlightFlaggerResults} from './FlightFlaggerResults'; export type {IFlightFlaggerResults} from './FlightFlaggerResults'; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index d946acb..31c02ad 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { Box, Button, AppBar, Typography, Grid, Menu, MenuItem, Link } from "@mui/material"; +import {AppBar, Box, Button, Grid, Link, Menu, MenuItem, Typography} from "@mui/material"; import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; import LogoutIcon from '@mui/icons-material/Logout'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; -import { Crest } from './Crest'; -import { DynamicIcon, IconNames } from './DynamicIcon'; +import {Crest} from './Crest'; +import {DynamicIcon, IconNames} from './DynamicIcon'; import PortSelector from "./PortSelector"; export type MenuItem = { @@ -21,7 +21,7 @@ export interface IHeader { rightMenuItems?: MenuItem[], portMenuItems: MenuItem[], initialSelectedPortMenuItem: string, - routingFunction: (route:string) => void, + routingFunction: (route: string) => void, logoutLink: () => void, } @@ -30,7 +30,16 @@ const linkStyles = { color: '#000', } -const Header = ({userRoles, adminMenuItems, rightMenuItems, leftMenuItems, portMenuItems, initialSelectedPortMenuItem, routingFunction, logoutLink}: IHeader) => { +const Header = ({ + userRoles, + adminMenuItems, + rightMenuItems, + leftMenuItems, + portMenuItems, + initialSelectedPortMenuItem, + routingFunction, + logoutLink + }: IHeader) => { const [anchorEl, setAnchorEl] = React.useState(null); const [selectedPortOption, setSelectedPortOption] = React.useState(initialSelectedPortMenuItem); const open = Boolean(anchorEl); @@ -53,23 +62,28 @@ const Header = ({userRoles, adminMenuItems, rightMenuItems, leftMenuItems, portM padding: 2, '& svg': { width: '35px' - } - }}> - - Border Force - Dynamic Response Tool + } + }}> + + Border + Force + Dynamic Response Tool - Contact: drtpoiseteam@homeoffice.gov.uk + Contact: drtpoiseteam@homeoffice.gov.uk - { hasAdminMenuRoles && + {hasAdminMenuRoles && + @@ -130,47 +145,48 @@ const Header = ({userRoles, adminMenuItems, rightMenuItems, leftMenuItems, portM ) }) } - { hasAdminMenuRoles && - - } + {hasAdminMenuRoles && + + + } { rightMenuItems && rightMenuItems.map((menuItem) => { - + return ( - + {menuItem.label} + ) }) } - + - ) diff --git a/src/components/index.ts b/src/components/index.ts index 3f2e5e4..22dd7ba 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,7 +1,7 @@ export { default as FlightFlagger } from "./FlightFlagger"; export { FlightFlaggerFilters, FlightHighlightChip } from "./FlightFlagger"; -export type { IFlightFlagger, IFlightFlaggerFilters, IFlightHighlightChip, AutocompleteOption } from "./FlightFlagger"; +export type { IFlightFlagger, IFlightFlaggerFilters, IFlightHighlightChip } from "./FlightFlagger"; export { StatusTag } from "./StatusTags"; export type { IStatusTag } from "./StatusTags";