Skip to content

Commit

Permalink
Allow updates of the base forecast date (#203)
Browse files Browse the repository at this point in the history
* Initial addition of base forecast date picker

* Updating the base forecast date updates the displayed data

* Ensured all time scales use the same start and end

* Added new datetime component

* Added loading spinner when changing the base forecast date

* Make the city data charts auto zoom to the in-situ data

* Remove accidental checking of debug text

* Pulled the table header into a new component and sorted styling

* Reset the height of the top black bar

* Updated the date field in the header to use dark mode

* Updates to some failing jest tests

* Added extra UI tests

* Added jest tests for BaseForecastDatetimePicker

* Added missing newline and updated date comparison

* Added some extra tests to get code coverage up

* Added jest tests for loading spinner usage

* Updated package-lock.json after merge conflict
  • Loading branch information
rstrange-scottlogic authored Jul 12, 2024
1 parent d75e8fe commit d7dbf1a
Show file tree
Hide file tree
Showing 24 changed files with 2,462 additions and 750 deletions.
2,407 changes: 1,870 additions & 537 deletions air-quality-ui/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions air-quality-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"test:ci": "jest --ci --json --coverage --testLocationInResults --outputFile=report.json"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/x-date-pickers": "^7.9.0",
"@tanstack/react-query": "^5.40.1",
"@types/echarts": "^4.9.22",
"ag-grid-react": "^31.3.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import '@testing-library/jest-dom'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { DateTime } from 'luxon'

import { BaseForecastDatetimePicker } from './BaseForecastDatetimePicker'

const mockSetBaseForecastDateTime: (val: DateTime) => void = jest.fn()

jest.mock('../../context', () => ({
useForecastContext: jest.fn().mockReturnValue({
forecastBaseDate: DateTime.fromISO('2024-06-01T03:00:00', { zone: 'utc' }),
maxInSituDate: DateTime.fromISO('2024-06-10T09:00:00', { zone: 'utc' }),
setForecastBaseDate: (x: DateTime) => mockSetBaseForecastDateTime(x),
}),
}))

describe('BaseForecastDatetimePicker component', () => {
it('Displays date picker', async () => {
render(<BaseForecastDatetimePicker />)
expect(screen.getByLabelText('Base Forecast Date')).toBeInTheDocument()
})
it('Sets forecast base date on change', async () => {
render(<BaseForecastDatetimePicker />)
const datePicker = screen.getByLabelText('Base Forecast Date')
const updatedDate = DateTime.fromISO('2024-06-03T12:00:00', { zone: 'UTC' })

fireEvent.change(datePicker, { target: { value: '03/06/2024 12:00' } })
await waitFor(() => {
expect(mockSetBaseForecastDateTime).toHaveBeenCalledWith(updatedDate)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ThemeProvider, createTheme } from '@mui/material/styles'
import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'
import { DateTime } from 'luxon'

import { useForecastContext } from '../../context'

export const BaseForecastDatetimePicker = (): JSX.Element => {
const { forecastBaseDate, setForecastBaseDate } = useForecastContext()
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
})

return (
<LocalizationProvider dateAdapter={AdapterLuxon} adapterLocale="en-gb">
<ThemeProvider theme={darkTheme}>
<DateTimePicker
sx={{ '.MuiFormLabel-root': { color: 'white' } }}
label="Base Forecast Date"
disableFuture={true}
skipDisabled={true}
timeSteps={{ minutes: 720 }}
value={forecastBaseDate}
onChange={(newValue) => {
const valueToSet = newValue == null ? DateTime.utc() : newValue
setForecastBaseDate(valueToSet)
}}
/>
</ThemeProvider>
</LocalizationProvider>
)
}
4 changes: 2 additions & 2 deletions air-quality-ui/src/components/header/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
background-color: black;
color: white;
display: flex;
height: 70px;
height: 42px;
padding: 0 32px 0 32px;
justify-content: space-between;
}
.vairify-logo{
height: 30px;
width: auto;
height: 70px;
}
10 changes: 2 additions & 8 deletions air-quality-ui/src/components/header/Header.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'

import { Header } from './Header'

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useMatches: jest.fn().mockReturnValue([]),
}))
jest.mock('./Toolbar', () => ({ Toolbar: () => 'mocked toolbar' }))

describe('Header component', () => {
it('shows application name', () => {
render(<Header />, {
wrapper: BrowserRouter,
})
render(<Header />)
expect(screen.getByTestId('vairify-logo')).toBeInTheDocument()
})
})
2 changes: 1 addition & 1 deletion air-quality-ui/src/components/header/Toolbar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
border-top: solid 1px white;
color: white;
display: flex;
height: 48px;
height: 72px;
padding: 0 32px 0 32px;
justify-content: space-between;
}
10 changes: 6 additions & 4 deletions air-quality-ui/src/components/header/Toolbar.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'

import { Toolbar } from './Toolbar'

Expand All @@ -9,11 +8,14 @@ jest.mock('react-router-dom', () => ({
useMatches: jest.fn().mockReturnValue([]),
}))

jest.mock('./Breadcrumbs', () => ({ Breadcrumbs: () => 'mocked breadcrumbs' }))
jest.mock('./BaseForecastDatetimePicker', () => ({
BaseForecastDatetimePicker: () => 'mocked datepicker',
}))

describe('Toolbar component', () => {
it('renders toolbar', () => {
render(<Toolbar />, {
wrapper: BrowserRouter,
})
render(<Toolbar />)
expect(screen.getByRole('toolbar')).toBeInTheDocument()
})
})
2 changes: 2 additions & 0 deletions air-quality-ui/src/components/header/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BaseForecastDatetimePicker } from './BaseForecastDatetimePicker'
import { Breadcrumbs } from './Breadcrumbs'
import classes from './Toolbar.module.css'

Expand All @@ -9,6 +10,7 @@ export const Toolbar = () => {
className={classes['toolbar-section']}
>
<Breadcrumbs />
<BaseForecastDatetimePicker />
</section>
)
}
11 changes: 7 additions & 4 deletions air-quality-ui/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Outlet } from 'react-router-dom'

import { ForecastContextProvider } from '../../context'
import { Header } from '../header/Header'

export default function Layout() {
return (
<>
<Header />
<main>
<Outlet />
</main>
<ForecastContextProvider>
<Header />
<main>
<Outlet />
</main>
</ForecastContextProvider>
</>
)
}
29 changes: 29 additions & 0 deletions air-quality-ui/src/components/single-city/SingleCity.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@testing-library/jest-dom'
import { useQueries } from '@tanstack/react-query'
import { act, render, screen, waitFor } from '@testing-library/react'
import { DateTime } from 'luxon'

import { SingleCity } from './SingleCity'
import { pollutantTypes } from '../../models'
Expand All @@ -13,9 +14,37 @@ jest.mock('@tanstack/react-query', () => ({
]),
}))

jest.mock('../../context', () => ({
useForecastContext: jest.fn().mockReturnValue({
forecastBaseDate: DateTime.now(),
maxInSituDate: DateTime.now(),
maxForecastDate: DateTime.now(),
}),
}))

jest.mock('echarts-for-react', () => () => <div>Mock Chart</div>)

describe('SingleCityComponent', () => {
it('shows loading spinner when forecast data loading', async () => {
;(useQueries as jest.Mock).mockReturnValue([
{ data: [], isPending: true, isError: false },
{ data: [], isPending: false, isError: false },
])
render(<SingleCity />)
await waitFor(() => {
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
})
})
it('shows loading spinner when measurement data loading', async () => {
;(useQueries as jest.Mock).mockReturnValue([
{ data: [], isPending: false, isError: false },
{ data: [], isPending: true, isError: false },
])
render(<SingleCity />)
await waitFor(() => {
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument()
})
})
it('shows message when loading forecast data errors', async () => {
;(useQueries as jest.Mock).mockReturnValue([
{ data: [], isPending: false, isError: true },
Expand Down
26 changes: 12 additions & 14 deletions air-quality-ui/src/components/single-city/SingleCity.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useQueries } from '@tanstack/react-query'
import { useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useParams } from 'react-router-dom'
import Select, { ActionMeta, MultiValue, OnChangeValue } from 'react-select'

import { AverageComparisonChart } from './AverageComparisonChart'
import classes from './SingleCity.module.css'
import { SiteMeasurementsChart } from './SiteMeasurementsChart'
import { ForecastContext } from '../../context'
import { useForecastContext } from '../../context'
import { PollutantType, pollutantTypes } from '../../models'
import { textToColor } from '../../services/echarts-service'
import { getForecastData } from '../../services/forecast-data-service'
Expand All @@ -24,7 +24,8 @@ const getSiteName = (measurement: MeasurementsResponseDto): string => {
}

export const SingleCity = () => {
const forecastBaseTime = useContext(ForecastContext)
const { forecastBaseDate, maxInSituDate, maxForecastDate } =
useForecastContext()
const { name: locationName = '' } = useParams()
const [
{
Expand All @@ -40,24 +41,21 @@ export const SingleCity = () => {
] = useQueries({
queries: [
{
queryKey: ['forecast', locationName],
queryKey: [forecastBaseDate, locationName],
queryFn: () =>
getForecastData(
forecastBaseTime,
forecastBaseTime.plus({ days: 5 }),
forecastBaseTime,
forecastBaseDate,
maxForecastDate,
forecastBaseDate,
locationName,
),
},
{
queryKey: ['measurements', locationName],
queryKey: ['measurements', locationName, forecastBaseDate],
queryFn: () =>
getMeasurements(
forecastBaseTime,
forecastBaseTime.plus({ days: 5 }),
'city',
[locationName],
),
getMeasurements(forecastBaseDate, maxInSituDate, 'city', [
locationName,
]),
},
],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { render, screen } from '@testing-library/react'

import '@testing-library/jest-dom'
import { DateTime } from 'luxon'

import { SiteMeasurementsChart } from './SiteMeasurementsChart'
import { PollutantType } from '../../models'

jest.mock('echarts-for-react', () => () => <div>Mock Chart</div>)

jest.mock('../../context', () => ({
useForecastContext: jest.fn().mockReturnValue({
forecastBaseDate: DateTime.now(),
maxInSituDate: DateTime.now(),
maxForecastDate: DateTime.now(),
}),
}))

describe('SiteMeasurementChart', () => {
it.each<[PollutantType, string]>([
['no2', 'Nitrogen Dioxide'],
Expand Down
Loading

0 comments on commit d7dbf1a

Please sign in to comment.