From 546710151e73d3feca79653bcd9b4fd00192129b Mon Sep 17 00:00:00 2001 From: Conor Brady Date: Tue, 1 Oct 2024 11:06:18 -0700 Subject: [PATCH] Revert "Morecast: Empty Cell validation" (#3979) --- .sonarcloud.properties | 2 +- .vscode/settings.json | 2 - .../moreCast2/components/EditInputCell.tsx | 6 +- .../moreCast2/components/ForecastCell.tsx | 1 - .../components/GridComponentRenderer.tsx | 14 +- .../components/InvalidCellToolTip.tsx | 8 +- .../moreCast2/components/MoreCast2Column.tsx | 2 +- .../moreCast2/components/TabbedDataGrid.tsx | 15 +- .../moreCast2/components/ValidatedCell.tsx | 54 ----- .../components/ValidatedForecastCell.tsx | 46 ++++- .../ValidatedGrassCureForecastCell.tsx | 22 -- .../ValidatedWindDirectionForecastCell.tsx | 28 --- .../components/forecastCell.test.tsx | 195 ++++++++---------- .../components/gridComponentRenderer.test.tsx | 102 +++------ .../components/saveForecastButton.test.tsx | 5 +- .../moreCast2/components/testHelper.tsx | 15 -- .../components/validatedForecastCell.test.tsx | 60 ++---- .../validatedGrassCureForecastCell.test.tsx | 56 ----- ...alidatedWindDirectionForecastCell.test.tsx | 56 ----- .../features/moreCast2/saveForecast.test.ts | 129 +++++++----- web/src/features/moreCast2/saveForecasts.ts | 29 +-- .../features/moreCast2/slices/dataSlice.ts | 1 + .../moreCast2/slices/validInputSlice.test.ts | 27 --- .../moreCast2/slices/validInputSlice.ts | 10 +- 24 files changed, 280 insertions(+), 605 deletions(-) delete mode 100644 web/src/features/moreCast2/components/ValidatedCell.tsx delete mode 100644 web/src/features/moreCast2/components/ValidatedGrassCureForecastCell.tsx delete mode 100644 web/src/features/moreCast2/components/ValidatedWindDirectionForecastCell.tsx delete mode 100644 web/src/features/moreCast2/components/testHelper.tsx delete mode 100644 web/src/features/moreCast2/components/validatedGrassCureForecastCell.test.tsx delete mode 100644 web/src/features/moreCast2/components/validatedWindDirectionForecastCell.test.tsx delete mode 100644 web/src/features/moreCast2/slices/validInputSlice.test.ts diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 991759efd..fe2770b85 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -16,7 +16,7 @@ sonar.test.exclusions=*.feature sonar.tests.inclusions=**/*.test.tsx # Exclude duplication in fba tests due to many similar calculation numbers, ignore sample code as it's temporary, ignore sfms entrypoint, ignore util tests, ignore temporary fwi folder -sonar.cpd.exclusions=api/app/tests/fba_calc/*.py, api/app/weather_models/wind_direction_sample.py, web/src/features/moreCast2/util.test.ts, web/src/features/moreCast2/components/gridComponentRenderer.test.tsx, web/src/utils/fwi +sonar.cpd.exclusions=api/app/tests/fba_calc/*.py, api/app/weather_models/wind_direction_sample.py, web/src/features/moreCast2/util.test.ts, web/src/utils/fwi # Encoding of the source code. Default is default system encoding sonar.sourceEncoding=UTF-8 diff --git a/.vscode/settings.json b/.vscode/settings.json index 6174d53ea..844895d45 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -75,7 +75,6 @@ "ffmc", "fireweather", "firezone", - "FWIs", "GDPS", "geoalchemy", "GEOGCS", @@ -86,7 +85,6 @@ "grib", "gribs", "HAINES", - "Hasher", "hourlies", "HRDPS", "idir", diff --git a/web/src/features/moreCast2/components/EditInputCell.tsx b/web/src/features/moreCast2/components/EditInputCell.tsx index 717d6c12e..873680c28 100644 --- a/web/src/features/moreCast2/components/EditInputCell.tsx +++ b/web/src/features/moreCast2/components/EditInputCell.tsx @@ -14,9 +14,7 @@ export const EditInputCell = (props: GridRenderEditCellParams) => { const inputRef = useRef(null) const dispatch: AppDispatch = useDispatch() - useEffect(() => { - dispatch(setInputValid(isEmpty(error))) - }, []) + dispatch(setInputValid(isEmpty(error))) useEffect(() => { if (hasFocus && inputRef.current) { @@ -45,7 +43,7 @@ export const EditInputCell = (props: GridRenderEditCellParams) => { } return ( - + diff --git a/web/src/features/moreCast2/components/GridComponentRenderer.tsx b/web/src/features/moreCast2/components/GridComponentRenderer.tsx index 7d596470a..957397a9a 100644 --- a/web/src/features/moreCast2/components/GridComponentRenderer.tsx +++ b/web/src/features/moreCast2/components/GridComponentRenderer.tsx @@ -22,8 +22,7 @@ import ForecastHeader from 'features/moreCast2/components/ForecastHeader' import { ColumnClickHandlerProps } from 'features/moreCast2/components/TabbedDataGrid' import { cloneDeep, isNumber } from 'lodash' import ForecastCell from 'features/moreCast2/components/ForecastCell' -import ValidatedGrassCureForecastCell from '@/features/moreCast2/components/ValidatedGrassCureForecastCell' -import ValidatedWindDirectionForecastCell from '@/features/moreCast2/components/ValidatedWindDirectionForecastCell' +import ValidatedForecastCell from '@/features/moreCast2/components/ValidatedForecastCell' export const NOT_AVAILABLE = 'N/A' export const NOT_REPORTING = 'N/R' @@ -121,16 +120,7 @@ export class GridComponentRenderer { // The grass curing 'forecast' field is rendered differently if (isGrassField) { return ( - - ) - } else if (field.includes('windDirection')) { - return ( - { +const InvalidCellToolTip = ({ error, children }: InvalidCellToolTipProps) => { return ( void const TabbedDataGrid = ({ fromTo, setFromTo, fetchWeatherIndeterminates }: TabbedDataGridProps) => { - const dispatch: AppDispatch = useDispatch() const selectedStations = useSelector(selectSelectedStations) const loading = useSelector(selectWeatherIndeterminatesLoading) const { roles, isAuthenticated } = useSelector(selectAuthentication) @@ -448,9 +445,8 @@ const TabbedDataGrid = ({ fromTo, setFromTo, fetchWeatherIndeterminates }: Tabbe } const handleSaveClick = async () => { - const rowsToSave: MoreCast2ForecastRow[] = getRowsToSave(visibleRows) - - if (isRequiredInputSet(rowsToSave)) { + if (isForecastValid(visibleRows)) { + const rowsToSave: MoreCast2ForecastRow[] = getRowsToSave(visibleRows) const result = await submitMoreCastForecastRecords(rowsToSave) if (result.success) { setSnackbarMessage(FORECAST_SAVED_MESSAGE) @@ -463,7 +459,6 @@ const TabbedDataGrid = ({ fromTo, setFromTo, fetchWeatherIndeterminates }: Tabbe setSnackbarOpen(true) } } else { - dispatch(setRequiredInputEmpty({ empty: true })) setSnackbarMessage(FORECAST_WARN_MESSAGE) setSnackbarSeverity('warning') setSnackbarOpen(true) diff --git a/web/src/features/moreCast2/components/ValidatedCell.tsx b/web/src/features/moreCast2/components/ValidatedCell.tsx deleted file mode 100644 index ffd6c5a7b..000000000 --- a/web/src/features/moreCast2/components/ValidatedCell.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { theme } from '@/app/theme' -import InvalidCellToolTip from '@/features/moreCast2/components/InvalidCellToolTip' -import { TextField } from '@mui/material' -import { GridRenderCellParams } from '@mui/x-data-grid-pro' -import React from 'react' - -interface ValidatedCellProps { - disabled: boolean - label: string - error: boolean - invalid: string - value: Pick -} - -const ValidatedGrassCureForecastCell = ({ disabled, label, value, invalid, error }: ValidatedCellProps) => { - const testTag = error ? 'validated-forecast-cell-error' : 'validated-forecast-cell' - return ( - - - - ) -} - -export default React.memo(ValidatedGrassCureForecastCell) diff --git a/web/src/features/moreCast2/components/ValidatedForecastCell.tsx b/web/src/features/moreCast2/components/ValidatedForecastCell.tsx index 22e6527b4..e03208281 100644 --- a/web/src/features/moreCast2/components/ValidatedForecastCell.tsx +++ b/web/src/features/moreCast2/components/ValidatedForecastCell.tsx @@ -1,9 +1,8 @@ import React from 'react' +import { TextField } from '@mui/material' import { GridRenderCellParams } from '@mui/x-data-grid-pro' -import { selectMorecastRequiredInputEmpty } from '@/features/moreCast2/slices/validInputSlice' -import { useSelector } from 'react-redux' -import { isNil } from 'lodash' -import ValidatedCell from '@/features/moreCast2/components/ValidatedCell' +import { theme } from 'app/theme' +import InvalidCellToolTip from '@/features/moreCast2/components/InvalidCellToolTip' interface ValidatedForecastCellProps { disabled: boolean @@ -13,10 +12,41 @@ interface ValidatedForecastCellProps { } const ValidatedForecastCell = ({ disabled, label, value, validator }: ValidatedForecastCellProps) => { - const isRequiredInputEmpty = useSelector(selectMorecastRequiredInputEmpty) - const invalid = validator ? validator(value as string) : '' - const error = (isRequiredInputEmpty.empty && (value as string) === '') || isNil(value) || invalid !== '' - return + const error = validator ? validator(value as string) : '' + return ( + + + + ) } export default React.memo(ValidatedForecastCell) diff --git a/web/src/features/moreCast2/components/ValidatedGrassCureForecastCell.tsx b/web/src/features/moreCast2/components/ValidatedGrassCureForecastCell.tsx deleted file mode 100644 index 90368fbcf..000000000 --- a/web/src/features/moreCast2/components/ValidatedGrassCureForecastCell.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import { GridRenderCellParams } from '@mui/x-data-grid-pro' -import ValidatedCell from '@/features/moreCast2/components/ValidatedCell' -import { Box } from '@mui/material' - -interface ValidatedGrassCureForecastCellProps { - disabled: boolean - label: string - value: Pick - validator?: (value: string) => string -} - -const ValidatedGrassCureForecastCell = ({ disabled, label, value, validator }: ValidatedGrassCureForecastCellProps) => { - const error = validator ? validator(value as string) : '' - return ( - - - - ) -} - -export default React.memo(ValidatedGrassCureForecastCell) diff --git a/web/src/features/moreCast2/components/ValidatedWindDirectionForecastCell.tsx b/web/src/features/moreCast2/components/ValidatedWindDirectionForecastCell.tsx deleted file mode 100644 index 273a62497..000000000 --- a/web/src/features/moreCast2/components/ValidatedWindDirectionForecastCell.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Grid } from '@mui/material' -import { GridRenderCellParams } from '@mui/x-data-grid-pro' -import ValidatedCell from '@/features/moreCast2/components/ValidatedCell' - -interface ForecastCellProps { - disabled: boolean - label: string - value: Pick - validator?: (value: string) => string -} - -const ValidatedWindDirectionForecastCell = ({ disabled, label, value, validator }: ForecastCellProps) => { - const error = validator ? validator(value as string) : '' - - return ( - - - - - - ) -} - -export default ValidatedWindDirectionForecastCell diff --git a/web/src/features/moreCast2/components/forecastCell.test.tsx b/web/src/features/moreCast2/components/forecastCell.test.tsx index 0617a4b8f..dd7e2d3c6 100644 --- a/web/src/features/moreCast2/components/forecastCell.test.tsx +++ b/web/src/features/moreCast2/components/forecastCell.test.tsx @@ -2,9 +2,6 @@ import { render } from '@testing-library/react' import ForecastCell from 'features/moreCast2/components/ForecastCell' import { GridRenderCellParams } from '@mui/x-data-grid-pro' import { vi } from 'vitest' -import { initialState } from '@/features/moreCast2/slices/validInputSlice' -import { Provider } from 'react-redux' -import { buildTestStore } from '@/features/moreCast2/components/testHelper' const params: Pick = { row: undefined, @@ -14,15 +11,13 @@ const params: Pick = { describe('ForecastCell', () => { it('should have input disabled when disabled prop is true', () => { const { container } = render( - - - + ) const inputElement = container.querySelector('input') @@ -31,15 +26,13 @@ describe('ForecastCell', () => { }) it('should have input enabled when disabled prop is false', () => { const { container } = render( - - - + ) const inputElement = container.querySelector('input') @@ -48,15 +41,13 @@ describe('ForecastCell', () => { }) it('should show less than icon when showLessThan is true', () => { const { queryByTestId } = render( - - - + ) const element = queryByTestId('forecast-cell-less-than-icon') @@ -67,30 +58,26 @@ describe('ForecastCell', () => { const consoleErrorFn = vi.spyOn(console, 'error').mockImplementation(() => vi.fn()) expect(() => { render( - - - + ) }).toThrow('ForecastCell cannot show both greater than and less than icons at the same time.') consoleErrorFn.mockRestore() }) it('should not show less than icon when showLessThan is false', () => { const { queryByTestId } = render( - - - + ) const element = queryByTestId('forecast-cell-less-than-icon') @@ -98,15 +85,13 @@ describe('ForecastCell', () => { }) it('should show greater than icon when showGreaterThan is true', () => { const { queryByTestId } = render( - - - + ) const element = queryByTestId('forecast-cell-greater-than-icon') @@ -114,15 +99,13 @@ describe('ForecastCell', () => { }) it('should not show less than icon when showLessThan is false', () => { const { queryByTestId } = render( - - - + ) const element = queryByTestId('forecast-cell-greater-than-icon') @@ -130,15 +113,13 @@ describe('ForecastCell', () => { }) it('should not show less than or greater than icons when showLessThan and showGreater than are both false', () => { const { queryByTestId } = render( - - - + ) const greaterThanElement = queryByTestId('forecast-cell-greater-than-icon') @@ -148,15 +129,13 @@ describe('ForecastCell', () => { }) it('should not show a label when none specified', () => { const { container } = render( - - - + ) const inputElement = container.querySelector('label') @@ -164,15 +143,13 @@ describe('ForecastCell', () => { }) it('should show a label when specified', () => { const { container } = render( - - - + ) const inputElement = container.querySelector('label') @@ -180,15 +157,13 @@ describe('ForecastCell', () => { }) it('should display the value when provided', () => { const { container } = render( - - - + ) const inputElement = container.querySelector('input') @@ -201,15 +176,13 @@ describe('ForecastCell', () => { formattedValue: undefined } const { container } = render( - - - + ) const inputElement = container.querySelector('input') diff --git a/web/src/features/moreCast2/components/gridComponentRenderer.test.tsx b/web/src/features/moreCast2/components/gridComponentRenderer.test.tsx index 53d61af3e..f9dcc3b86 100644 --- a/web/src/features/moreCast2/components/gridComponentRenderer.test.tsx +++ b/web/src/features/moreCast2/components/gridComponentRenderer.test.tsx @@ -1,5 +1,3 @@ -import { buildTestStore } from '@/features/moreCast2/components/testHelper' -import { initialState } from '@/features/moreCast2/slices/validInputSlice' import { GridColumnHeaderParams, GridValueSetterParams } from '@mui/x-data-grid-pro' import { GridStateColDef } from '@mui/x-data-grid-pro/internals' import { render } from '@testing-library/react' @@ -12,7 +10,6 @@ import { } from 'features/moreCast2/components/GridComponentRenderer' import { ColumnClickHandlerProps } from 'features/moreCast2/components/TabbedDataGrid' import { DateTime } from 'luxon' -import { Provider } from 'react-redux' import { vi } from 'vitest' describe('GridComponentRenderer', () => { @@ -51,15 +48,13 @@ describe('GridComponentRenderer', () => { const row = { [field]: NaN, [fieldActual]: NaN, forDate: DateTime.now().plus({ days: 2 }) } const formattedValue = gridComponentRenderer.valueGetter({ row: row, value: NaN }, 1, field, 'Forecast') const { getByRole } = render( - - {gridComponentRenderer.renderForecastCellWith( - { - row: row, - formattedValue: formattedValue - }, - field - )} - + gridComponentRenderer.renderForecastCellWith( + { + row: row, + formattedValue: formattedValue + }, + field + ) ) const renderedCell = getByRole('textbox') expect(renderedCell).toBeInTheDocument() @@ -72,15 +67,13 @@ describe('GridComponentRenderer', () => { const row = { [field]: NaN, forDate: DateTime.now().minus({ days: 2 }) } const formattedValue = gridComponentRenderer.valueGetter({ row: row, value: NaN }, 1, field, 'Forecast') const { getByRole } = render( - - {gridComponentRenderer.renderForecastCellWith( - { - row: row, - formattedValue: formattedValue - }, - field - )} - + gridComponentRenderer.renderForecastCellWith( + { + row: row, + formattedValue: formattedValue + }, + field + ) ) const renderedCell = getByRole('textbox') expect(renderedCell).toBeInTheDocument() @@ -94,15 +87,13 @@ describe('GridComponentRenderer', () => { const row = { [field]: NaN, [fieldActual]: 2, forDate: DateTime.now() } const formattedValue = gridComponentRenderer.valueGetter({ row: row, value: NaN }, 1, field, 'Forecast') const { getByRole } = render( - - {gridComponentRenderer.renderForecastCellWith( - { - row: row, - formattedValue: formattedValue - }, - field - )} - + gridComponentRenderer.renderForecastCellWith( + { + row: row, + formattedValue: formattedValue + }, + field + ) ) const renderedCell = getByRole('textbox') expect(renderedCell).toBeInTheDocument() @@ -116,15 +107,13 @@ describe('GridComponentRenderer', () => { const row = { [field]: NaN, [fieldActual]: NaN, forDate: DateTime.now().minus({ days: 2 }) } const formattedValue = gridComponentRenderer.valueGetter({ row: row, value: NaN }, 1, field, 'Actual') const { getByRole } = render( - - {gridComponentRenderer.renderForecastCellWith( - { - row: row, - formattedValue: formattedValue - }, - field - )} - + gridComponentRenderer.renderForecastCellWith( + { + row: row, + formattedValue: formattedValue + }, + field + ) ) const renderedCell = getByRole('textbox') expect(renderedCell).toBeInTheDocument() @@ -144,9 +133,7 @@ describe('GridComponentRenderer', () => { it('should render the forecast cell as editable with no actual', () => { const field = 'tempForecast' const { getByRole } = render( - - {gridComponentRenderer.renderForecastCellWith({ row: { [field]: 1 }, formattedValue: 1 }, field)} - + gridComponentRenderer.renderForecastCellWith({ row: { [field]: 1 }, formattedValue: 1 }, field) ) const renderedCell = getByRole('textbox') expect(renderedCell).toBeInTheDocument() @@ -170,12 +157,7 @@ describe('GridComponentRenderer', () => { const actualField = `tempActual` const { getByRole } = render( - - {gridComponentRenderer.renderForecastCellWith( - { row: { [field]: 1, [actualField]: 2 }, formattedValue: 1 }, - field - )} - + gridComponentRenderer.renderForecastCellWith({ row: { [field]: 1, [actualField]: 2 }, formattedValue: 1 }, field) ) const renderedCell = getByRole('textbox') expect(renderedCell).toBeInTheDocument() @@ -183,30 +165,6 @@ describe('GridComponentRenderer', () => { expect(renderedCell).toBeDisabled() }) - it('should render the wind direction forecast cell', () => { - const field = 'windDirectionForecast' - - const { getByTestId } = render( - - {gridComponentRenderer.renderForecastCellWith({ row: { [field]: 1 }, formattedValue: 1 }, field)} - - ) - const renderedCell = getByTestId('validated-winddir-forecast-cell') - expect(renderedCell).toBeInTheDocument() - }) - - it('should render the grass curing forecast cell', () => { - const field = 'grassCuringForecast' - - const { getByTestId } = render( - - {gridComponentRenderer.renderForecastCellWith({ row: { [field]: 1 }, formattedValue: 1 }, field)} - - ) - const renderedCell = getByTestId('validated-gc-forecast-cell') - expect(renderedCell).toBeInTheDocument() - }) - it('should set the row correctly', () => { const mockValueSetterParams: GridValueSetterParams = { value: 2, diff --git a/web/src/features/moreCast2/components/saveForecastButton.test.tsx b/web/src/features/moreCast2/components/saveForecastButton.test.tsx index 7f22a05f7..20425e12b 100644 --- a/web/src/features/moreCast2/components/saveForecastButton.test.tsx +++ b/web/src/features/moreCast2/components/saveForecastButton.test.tsx @@ -34,10 +34,7 @@ describe('SaveForecastButton', () => { reducer: rootReducer, preloadedState: { morecastInputValid: { - isValid: false, - isRequiredEmpty: { - empty: false - } + isValid: false } } }) diff --git a/web/src/features/moreCast2/components/testHelper.tsx b/web/src/features/moreCast2/components/testHelper.tsx deleted file mode 100644 index 0fdbc1e69..000000000 --- a/web/src/features/moreCast2/components/testHelper.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import morecastInputValidSlice, { ValidInputState } from '@/features/moreCast2/slices/validInputSlice' -import { combineReducers, configureStore } from '@reduxjs/toolkit' - -export const buildTestStore = (initialState: ValidInputState) => { - const rootReducer = combineReducers({ - morecastInputValid: morecastInputValidSlice - }) - const testStore = configureStore({ - reducer: rootReducer, - preloadedState: { - morecastInputValid: initialState - } - }) - return testStore -} diff --git a/web/src/features/moreCast2/components/validatedForecastCell.test.tsx b/web/src/features/moreCast2/components/validatedForecastCell.test.tsx index 63649eaf6..4827a04a7 100644 --- a/web/src/features/moreCast2/components/validatedForecastCell.test.tsx +++ b/web/src/features/moreCast2/components/validatedForecastCell.test.tsx @@ -1,9 +1,8 @@ import { render } from '@testing-library/react' +import ForecastCell from 'features/moreCast2/components/ForecastCell' import { GridRenderCellParams } from '@mui/x-data-grid-pro' +import { vi } from 'vitest' import ValidatedForecastCell from '@/features/moreCast2/components/ValidatedForecastCell' -import { initialState } from '@/features/moreCast2/slices/validInputSlice' -import { Provider } from 'react-redux' -import { buildTestStore } from '@/features/moreCast2/components/testHelper' const params: Pick = { row: undefined, @@ -11,51 +10,28 @@ const params: Pick = { } describe('ValidatedForecastCell', () => { - it('should render a tooltip and be in error state when value is invalid', async () => { - const testStore = buildTestStore(initialState) - - const { queryByText, queryByTestId } = render( - - 'tooltip-error'} - /> - + it('should render a tooltip when value is invalid', async () => { + const { queryByText } = render( + 'tooltip-error'} + /> ) - expect(queryByTestId('validated-forecast-cell-error')).toBeInTheDocument() - expect(queryByTestId('validated-forecast-cell')).not.toBeInTheDocument() expect(queryByText('tooltip-error')).toBeInTheDocument() }) - it('should render in an error state when value is empty and required', async () => { - const testStore = buildTestStore({ ...initialState, isRequiredEmpty: { empty: true } }) - - const params: Pick = { - row: undefined, - formattedValue: '' - } - - const { queryByText, queryByTestId } = render( - - - + it('should not render a tooltip when value is valid', async () => { + const { queryByText } = render( + (Number(v) > Number.MAX_VALUE ? 'tooltip-error' : '')} + /> ) - expect(queryByTestId('validated-forecast-cell-error')).toBeInTheDocument() - expect(queryByTestId('validated-forecast-cell')).not.toBeInTheDocument() - expect(queryByText('tooltip-error')).not.toBeInTheDocument() - }) - it('should not render a tooltip and not be in an error state when value is valid', async () => { - const testStore = buildTestStore(initialState) - const { queryByText, queryByTestId } = render( - - ''} /> - - ) - expect(queryByTestId('validated-forecast-cell')).toBeInTheDocument() - expect(queryByTestId('validated-forecast-cell-error')).not.toBeInTheDocument() expect(queryByText('tooltip-error')).not.toBeInTheDocument() }) }) diff --git a/web/src/features/moreCast2/components/validatedGrassCureForecastCell.test.tsx b/web/src/features/moreCast2/components/validatedGrassCureForecastCell.test.tsx deleted file mode 100644 index 76c74d24b..000000000 --- a/web/src/features/moreCast2/components/validatedGrassCureForecastCell.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { render } from '@testing-library/react' -import { GridRenderCellParams } from '@mui/x-data-grid-pro' -import { initialState } from '@/features/moreCast2/slices/validInputSlice' -import { Provider } from 'react-redux' -import ValidatedGrassCureForecastCell from '@/features/moreCast2/components/ValidatedGrassCureForecastCell' -import { buildTestStore } from '@/features/moreCast2/components/testHelper' - -const params: Pick = { - row: undefined, - formattedValue: '1' -} - -describe('ValidatedGrassCureForecastCell', () => { - it('should render a tooltip when value is invalid', async () => { - const testStore = buildTestStore(initialState) - - const { queryByText } = render( - - 'tooltip-error'} - /> - - ) - expect(queryByText('tooltip-error')).toBeInTheDocument() - }) - - it('should not render a tooltip when value is valid', async () => { - const testStore = buildTestStore(initialState) - const { queryByText } = render( - - ''} - /> - - ) - - expect(queryByText('tooltip-error')).not.toBeInTheDocument() - }) - - it('should not render a tooltip when value has no validator', async () => { - const testStore = buildTestStore(initialState) - const { queryByText } = render( - - - - ) - - expect(queryByText('tooltip-error')).not.toBeInTheDocument() - }) -}) diff --git a/web/src/features/moreCast2/components/validatedWindDirectionForecastCell.test.tsx b/web/src/features/moreCast2/components/validatedWindDirectionForecastCell.test.tsx deleted file mode 100644 index 1f578101b..000000000 --- a/web/src/features/moreCast2/components/validatedWindDirectionForecastCell.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { render } from '@testing-library/react' -import { GridRenderCellParams } from '@mui/x-data-grid-pro' -import { initialState } from '@/features/moreCast2/slices/validInputSlice' -import { Provider } from 'react-redux' -import ValidatedWindDirectionForecastCell from '@/features/moreCast2/components/ValidatedWindDirectionForecastCell' -import { buildTestStore } from '@/features/moreCast2/components/testHelper' - -const params: Pick = { - row: undefined, - formattedValue: '1' -} - -describe('ValidatedWindDirectionForecastCell', () => { - it('should render a tooltip when value is invalid', async () => { - const testStore = buildTestStore(initialState) - - const { queryByText } = render( - - 'tooltip-error'} - /> - - ) - expect(queryByText('tooltip-error')).toBeInTheDocument() - }) - - it('should not render a tooltip when value is valid', async () => { - const testStore = buildTestStore(initialState) - const { queryByText } = render( - - ''} - /> - - ) - - expect(queryByText('tooltip-error')).not.toBeInTheDocument() - }) - - it('should not render a tooltip when value has no validator', async () => { - const testStore = buildTestStore(initialState) - const { queryByText } = render( - - - - ) - - expect(queryByText('tooltip-error')).not.toBeInTheDocument() - }) -}) diff --git a/web/src/features/moreCast2/saveForecast.test.ts b/web/src/features/moreCast2/saveForecast.test.ts index 409fce68c..6f72cca29 100644 --- a/web/src/features/moreCast2/saveForecast.test.ts +++ b/web/src/features/moreCast2/saveForecast.test.ts @@ -1,6 +1,7 @@ import { ModelChoice } from 'api/moreCast2API' -import { MoreCast2ForecastRow, MoreCast2Row } from 'features/moreCast2/interfaces' -import { getRowsToSave, isRequiredInputSet } from 'features/moreCast2/saveForecasts' +import { MoreCast2Row } from 'features/moreCast2/interfaces' +import { getRowsToSave, isForecastValid } from 'features/moreCast2/saveForecasts' +import { validForecastPredicate } from 'features/moreCast2/util' import { DateTime } from 'luxon' const baseRow = { @@ -85,60 +86,66 @@ const buildCompleteForecast = ( id: string, forDate: DateTime, stationCode: number, - stationName: string -): MoreCast2ForecastRow => ({ + stationName: string, + latitude: number, + longitude: number +): MoreCast2Row => ({ id, forDate, stationCode, stationName, + latitude, + longitude, ...baseRow, - precip: { choice: ModelChoice.GDPS, value: 0 }, - rh: { choice: ModelChoice.GDPS, value: 0 }, - temp: { choice: ModelChoice.GDPS, value: 0 }, - windDirection: { choice: ModelChoice.GDPS, value: 0 }, - windSpeed: { choice: ModelChoice.GDPS, value: 0 }, - grassCuring: { choice: ModelChoice.NULL, value: 0 } + precipForecast: { choice: ModelChoice.GDPS, value: 0 }, + rhForecast: { choice: ModelChoice.GDPS, value: 0 }, + tempForecast: { choice: ModelChoice.GDPS, value: 0 }, + windDirectionForecast: { choice: ModelChoice.GDPS, value: 0 }, + windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 }, + grassCuringForecast: { choice: ModelChoice.NULL, value: 0 } }) const buildForecastMissingWindDirection = ( id: string, forDate: DateTime, stationCode: number, - stationName: string -): MoreCast2ForecastRow => ({ + stationName: string, + latitude: number, + longitude: number +): MoreCast2Row => ({ id, forDate, stationCode, stationName, + latitude, + longitude, ...baseRow, - precip: { choice: ModelChoice.GDPS, value: 0 }, - rh: { choice: ModelChoice.GDPS, value: 0 }, - temp: { choice: ModelChoice.GDPS, value: 0 }, - windDirection: { choice: ModelChoice.NULL, value: NaN }, - windSpeed: { choice: ModelChoice.GDPS, value: 0 }, - grassCuring: { choice: ModelChoice.NULL, value: 0 } + precipForecast: { choice: ModelChoice.GDPS, value: 0 }, + rhForecast: { choice: ModelChoice.GDPS, value: 0 }, + tempForecast: { choice: ModelChoice.GDPS, value: 0 }, + windDirectionForecast: { choice: ModelChoice.NULL, value: NaN }, + windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 }, + grassCuringForecast: { choice: ModelChoice.NULL, value: 0 } }) const buildInvalidForecast = ( id: string, forDate: DateTime, stationCode: number, - stationName: string -): MoreCast2ForecastRow => ({ + stationName: string, + latitude: number, + longitude: number +): MoreCast2Row => ({ id, forDate, stationCode, stationName, - ...baseRow, - precip: { choice: ModelChoice.NULL, value: NaN }, - rh: { choice: ModelChoice.NULL, value: NaN }, - temp: { choice: ModelChoice.NULL, value: NaN }, - windDirection: { choice: ModelChoice.NULL, value: NaN }, - windSpeed: { choice: ModelChoice.NULL, value: NaN }, - grassCuring: { choice: ModelChoice.NULL, value: NaN } + latitude, + longitude, + ...baseRow }) -const buildForecastWithActuals = ( +const buildNAForecast = ( id: string, forDate: DateTime, stationCode: number, @@ -152,16 +159,16 @@ const buildForecastWithActuals = ( stationName, latitude, longitude, - ...baseRowWithActuals, - precipForecast: { choice: ModelChoice.GDPS, value: 0 }, - rhForecast: { choice: ModelChoice.GDPS, value: 0 }, - tempForecast: { choice: ModelChoice.GDPS, value: 0 }, - windDirectionForecast: { choice: ModelChoice.GDPS, value: 0 }, - windSpeedForecast: { choice: ModelChoice.GDPS, value: 0 }, - grassCuringForecast: { choice: ModelChoice.NULL, value: 0 } + ...baseRow, + precipForecast: { choice: ModelChoice.NULL, value: NaN }, + rhForecast: { choice: ModelChoice.NULL, value: NaN }, + tempForecast: { choice: ModelChoice.NULL, value: NaN }, + windDirectionForecast: { choice: ModelChoice.NULL, value: NaN }, + windSpeedForecast: { choice: ModelChoice.NULL, value: NaN }, + grassCuringForecast: { choice: ModelChoice.NULL, value: NaN } }) -const buildForecast = ( +const buildForecastWithActuals = ( id: string, forDate: DateTime, stationCode: number, @@ -175,7 +182,7 @@ const buildForecast = ( stationName, latitude, longitude, - ...baseRow, + ...baseRowWithActuals, precipForecast: { choice: ModelChoice.GDPS, value: 0 }, rhForecast: { choice: ModelChoice.GDPS, value: 0 }, tempForecast: { choice: ModelChoice.GDPS, value: 0 }, @@ -188,38 +195,64 @@ describe('saveForecasts', () => { describe('isForecastValid', () => { it('should return true if all forecasts fields are set', () => { expect( - isRequiredInputSet([ - buildCompleteForecast('1', mockForDate, 1, 'one'), - buildCompleteForecast('2', mockForDate, 2, 'two') + isForecastValid([ + buildCompleteForecast('1', mockForDate, 1, 'one', 1, 1), + buildCompleteForecast('2', mockForDate, 2, 'two', 2, 2) ]) ).toBe(true) }) it('should return true if all forecasts fields are set except windDirectionForecast', () => { expect( - isRequiredInputSet([ - buildForecastMissingWindDirection('1', mockForDate, 1, 'one'), - buildForecastMissingWindDirection('2', mockForDate, 2, 'two') + isForecastValid([ + buildForecastMissingWindDirection('1', mockForDate, 1, 'one', 1, 1), + buildForecastMissingWindDirection('2', mockForDate, 2, 'two', 2, 2) ]) ).toBe(true) }) it('should return false if any forecasts have missing forecast fields', () => { expect( - isRequiredInputSet([ - buildCompleteForecast('1', mockForDate, 1, 'one'), - buildInvalidForecast('2', mockForDate, 2, 'two') + isForecastValid([ + buildCompleteForecast('1', mockForDate, 1, 'one', 1, 1), + buildInvalidForecast('2', mockForDate, 2, 'two', 2, 2) ]) ).toBe(false) }) it('should return false if any forecasts have missing forecast fields set other than windDirectionForecast', () => { - expect(isRequiredInputSet([buildInvalidForecast('1', mockForDate, 2, 'one')])).toBe(false) + expect(isForecastValid([buildNAForecast('1', mockForDate, 2, 'one', 1, 1)])).toBe(false) + }) + }) + describe('validForecastPredicate', () => { + it('should return false for a forecast with missing forecast fields', () => { + expect(validForecastPredicate(buildInvalidForecast('1', mockForDate, 1, 'one', 1, 1))).toBe(false) + }) + it('should return false for a forecast with forecasts but N/A values', () => { + expect(validForecastPredicate(buildNAForecast('1', mockForDate, 1, 'one', 1, 1))).toBe(false) }) }) describe('getRowsToSave', () => { + it('should filter out invalid forecasts', () => { + const res = getRowsToSave([ + buildCompleteForecast('1', mockForDate, 1, 'one', 1, 1), + buildInvalidForecast('2', mockForDate, 2, 'two', 2, 2) + ]) + expect(res).toHaveLength(1) + expect(res[0].id).toBe('1') + }) + it('should filter out N/A forecasts', () => { + const res = getRowsToSave([ + buildCompleteForecast('1', mockForDate, 1, 'one', 1, 1), + buildNAForecast('2', mockForDate, 2, 'two', 2, 2) + ]) + expect(res).toHaveLength(1) + expect(res[0].id).toBe('1') + }) it('should filter out rows with actuals', () => { + const forecastWithActual = buildCompleteForecast('2', mockForDate, 2, 'two', 2, 2) + forecastWithActual.precipActual = 1 const res = getRowsToSave([ - buildForecast('1', mockForDate, 1, 'one', 1, 1), + buildCompleteForecast('1', mockForDate, 1, 'one', 1, 1), buildForecastWithActuals('2', mockForDate, 2, 'two', 2, 2) ]) expect(res).toHaveLength(1) diff --git a/web/src/features/moreCast2/saveForecasts.ts b/web/src/features/moreCast2/saveForecasts.ts index 21ec62688..1b8a7fb7f 100644 --- a/web/src/features/moreCast2/saveForecasts.ts +++ b/web/src/features/moreCast2/saveForecasts.ts @@ -1,6 +1,6 @@ import { ModelChoice } from 'api/moreCast2API' -import { MoreCast2ForecastRow, MoreCast2Row, PredictionItem } from 'features/moreCast2/interfaces' -import { isNil } from 'lodash' +import { MoreCast2ForecastRow, MoreCast2Row } from 'features/moreCast2/interfaces' +import { validForecastPredicate } from 'features/moreCast2/util' // Forecast rows contain all NaN values in their 'actual' fields export const isForecastRowPredicate = (row: MoreCast2Row) => @@ -15,9 +15,16 @@ export const getForecastRows = (rows: MoreCast2Row[]): MoreCast2Row[] => { return rows ? rows.filter(isForecastRowPredicate) : [] } +export const isForecastValid = (rows: MoreCast2Row[]) => { + const forecastRows = getForecastRows(rows) + const validForecastRows = forecastRows.filter(validForecastPredicate) + return forecastRows.length === validForecastRows.length +} + export const getRowsToSave = (rows: MoreCast2Row[]): MoreCast2ForecastRow[] => { const forecastRows = getForecastRows(rows) - return forecastRows.map(r => ({ + const rowsToSave = forecastRows.filter(validForecastPredicate) + return rowsToSave.map(r => ({ id: r.id, stationCode: r.stationCode, stationName: r.stationName, @@ -30,19 +37,3 @@ export const getRowsToSave = (rows: MoreCast2Row[]): MoreCast2ForecastRow[] => { grassCuring: r.grassCuringForecast ?? { choice: ModelChoice.NULL, value: NaN } })) } - -export const isRequiredInputSet = (rowsToSave: MoreCast2ForecastRow[]) => { - const isNilPredictionItem = (item?: PredictionItem) => { - return isNil(item) || isNaN(item.value) || item.choice === ModelChoice.NULL - } - const res = rowsToSave.every(row => { - return ( - !isNilPredictionItem(row.precip) && - !isNilPredictionItem(row.rh) && - !isNilPredictionItem(row.temp) && - !isNilPredictionItem(row.windSpeed) - ) - }) - - return res -} diff --git a/web/src/features/moreCast2/slices/dataSlice.ts b/web/src/features/moreCast2/slices/dataSlice.ts index 5e5997da1..7e0258b42 100644 --- a/web/src/features/moreCast2/slices/dataSlice.ts +++ b/web/src/features/moreCast2/slices/dataSlice.ts @@ -634,6 +634,7 @@ export const createEmptyMoreCast2Row = ( fwiCalcActual: NaN, dgrCalcActual: NaN, + // grassCuringActual: NaN, // GDPS model predictions diff --git a/web/src/features/moreCast2/slices/validInputSlice.test.ts b/web/src/features/moreCast2/slices/validInputSlice.test.ts deleted file mode 100644 index 760391603..000000000 --- a/web/src/features/moreCast2/slices/validInputSlice.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import validInputSliceReducer, { - initialState, - setInputValid, - setRequiredInputEmpty -} from 'features/moreCast2/slices/validInputSlice' - -describe('validInputSlice', () => { - it('initial state should be valid', () => { - expect(validInputSliceReducer(initialState, setInputValid(true))).toEqual(initialState) - }) - it('invalid flag should be reflected in state when set', () => { - expect(validInputSliceReducer(initialState, setInputValid(false))).toEqual({ ...initialState, isValid: false }) - }) - it('required input empty flag should be reflected in state when set', () => { - expect(validInputSliceReducer(initialState, setRequiredInputEmpty({ empty: true }))).toEqual({ - ...initialState, - isRequiredEmpty: { empty: true } - }) - }) - it('required input empty flag should be reflected in state when set', () => { - const invalidState = validInputSliceReducer(initialState, setInputValid(false)) - expect(validInputSliceReducer(invalidState, setRequiredInputEmpty({ empty: true }))).toEqual({ - ...invalidState, - isRequiredEmpty: { empty: true } - }) - }) -}) diff --git a/web/src/features/moreCast2/slices/validInputSlice.ts b/web/src/features/moreCast2/slices/validInputSlice.ts index 835b8c530..ed2610dfb 100644 --- a/web/src/features/moreCast2/slices/validInputSlice.ts +++ b/web/src/features/moreCast2/slices/validInputSlice.ts @@ -3,12 +3,10 @@ import { RootState } from 'app/rootReducer' export interface ValidInputState { isValid: boolean - isRequiredEmpty: { empty: boolean } } export const initialState: ValidInputState = { - isValid: true, - isRequiredEmpty: { empty: false } + isValid: true } const morecastInputValidSlice = createSlice({ @@ -17,16 +15,12 @@ const morecastInputValidSlice = createSlice({ reducers: { setInputValid(state: ValidInputState, action: PayloadAction) { state.isValid = action.payload - }, - setRequiredInputEmpty(state: ValidInputState, action: PayloadAction<{ empty: boolean }>) { - state.isRequiredEmpty = action.payload } } }) -export const { setInputValid, setRequiredInputEmpty } = morecastInputValidSlice.actions +export const { setInputValid } = morecastInputValidSlice.actions export default morecastInputValidSlice.reducer export const selectMorecastInputValid = (state: RootState) => state.morecastInputValid.isValid -export const selectMorecastRequiredInputEmpty = (state: RootState) => state.morecastInputValid.isRequiredEmpty