From 955f03170943af728faedea4e9612c85a1a245ce Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Mon, 16 Mar 2026 11:51:25 -0700 Subject: [PATCH 01/10] Snow warning --- .../sfmsInsights/pages/SFMSInsightsPage.tsx | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx index 14280ec5f..a50f09667 100644 --- a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx +++ b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx @@ -1,20 +1,22 @@ +import { getMostRecentProcessedSnowByDate } from '@/api/snow' +import { AppDispatch } from '@/app/store' +import { theme } from '@/app/theme' import { GeneralHeader } from '@/components/GeneralHeader' +import { StyledFormControl } from '@/components/StyledFormControl' +import ASADatePicker from '@/features/fba/components/ASADatePicker' +import { fetchSFMSBounds, selectEarliestSFMSBounds, selectLatestSFMSBounds } from '@/features/fba/slices/runDatesSlice' import Footer from '@/features/landingPage/components/Footer' +import { RasterType } from '@/features/sfmsInsights/components/map/rasterConfig' import SFMSMap from '@/features/sfmsInsights/components/map/SFMSMap' -import ASADatePicker from '@/features/fba/components/ASADatePicker' import RasterTypeDropdown from '@/features/sfmsInsights/components/RasterTypeDropdown' -import { StyledFormControl } from '@/components/StyledFormControl' import { SFMS_INSIGHTS_NAME } from '@/utils/constants' -import { getMostRecentProcessedSnowByDate } from '@/api/snow' -import { fetchSFMSBounds, selectLatestSFMSBounds, selectEarliestSFMSBounds } from '@/features/fba/slices/runDatesSlice' -import { Box, Checkbox, FormControlLabel, Grid, CircularProgress } from '@mui/material' +import { getDateTimeNowPST } from '@/utils/date' +import WarningAmberIcon from '@mui/icons-material/WarningAmber' +import { Box, Checkbox, CircularProgress, FormControlLabel, Grid, Tooltip } from '@mui/material' +import { isNil, isNull } from 'lodash' import { DateTime } from 'luxon' import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { isNull } from 'lodash' -import { RasterType } from '@/features/sfmsInsights/components/map/rasterConfig' -import { getDateTimeNowPST } from '@/utils/date' -import { AppDispatch } from '@/app/store' export const SFMSInsightsPage = () => { const dispatch = useDispatch() @@ -66,6 +68,17 @@ export const SFMSInsightsPage = () => { fetchLastProcessedSnow(rasterDate) }, [rasterDate]) + const renderSnowWarning = () => { + const tempLastSnow = DateTime.fromISO("2026-03-08").ordinal + if (!isNil(snowDate?.ordinal) && snowDate?.ordinal === tempLastSnow && !isNil(rasterDate?.ordinal) && rasterDate?.ordinal > tempLastSnow) { + return ( + + + + ) + } + } + return ( @@ -113,7 +126,9 @@ export const SFMSInsightsPage = () => { setShowSnow(e.target.checked)} />} label={snowDate ? `Show Latest Snow: ${snowDate.toLocaleString(DateTime.DATE_MED)}` : 'Show Latest Snow'} + sx={{verticalAlign: "unset"}} /> + { renderSnowWarning() } From cf5d9b6f21d72ca4909db0b9a177824bb2498cf5 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Mon, 16 Mar 2026 12:15:54 -0700 Subject: [PATCH 02/10] tests --- .../pages/SFMSInsightsPage.test.tsx | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx index e97323dd8..01c7306cc 100644 --- a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx +++ b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx @@ -493,4 +493,108 @@ describe('SFMSInsightsPage', () => { const dropdown = screen.getByTestId('raster-type-dropdown') expect(dropdown).toHaveAttribute('data-raster-data-available', 'true') }) + + it('should display warning tooltip when rasterDate greater than March 8, 2026 and snowDate is March 8, 2026.', async () => { + const dateTimeNow = DateTime.fromISO('2026-03-10') + ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ + forDate: DateTime.fromISO('2026-03-08'), + processedDate: DateTime.fromISO('2025-03-09'), + snowSource: 'viirs' + }) + ;(getSFMSBounds as Mock).mockResolvedValue({ + sfms_bounds: { + '2026': { + forecast: { + minimum: '2026-01-01', + maximum: '2026-03-10' + } + } + } + }) + + renderWithStore() + await waitForPageLoad() + + const warningIcon = screen.queryByTestId("WarningAmberIcon") + expect(warningIcon).toBeInTheDocument() + }) + + it('should not display warning tooltip when rasterDate equal to March 8, 2026 and snowDate is March 8, 2026.', async () => { + const dateTimeNow = DateTime.fromISO('2026-03-08') + ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ + forDate: DateTime.fromISO('2026-03-08'), + processedDate: DateTime.fromISO('2025-03-09'), + snowSource: 'viirs' + }) + ;(getSFMSBounds as Mock).mockResolvedValue({ + sfms_bounds: { + '2026': { + forecast: { + minimum: '2026-01-01', + maximum: '2026-03-10' + } + } + } + }) + + renderWithStore() + await waitForPageLoad() + + const warningIcon = screen.queryByTestId("WarningAmberIcon") + expect(warningIcon).toBe(null) + }) + + it('should not display warning tooltip when rasterDate greater than March 8, 2026 and snowDate greater than March 8, 2026.', async () => { + const dateTimeNow = DateTime.fromISO('2026-03-09') + ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ + forDate: DateTime.fromISO('2026-03-10'), + processedDate: DateTime.fromISO('2025-03-09'), + snowSource: 'viirs' + }) + ;(getSFMSBounds as Mock).mockResolvedValue({ + sfms_bounds: { + '2026': { + forecast: { + minimum: '2026-01-01', + maximum: '2026-03-10' + } + } + } + }) + + renderWithStore() + await waitForPageLoad() + + const warningIcon = screen.queryByTestId("WarningAmberIcon") + expect(warningIcon).toBe(null) + }) + + it('should not display warning tooltip when rasterDate less than March 8, 2026 and snowDate greater than March 8, 2026.', async () => { + const dateTimeNow = DateTime.fromISO('2026-03-07') + ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ + forDate: DateTime.fromISO('2026-03-10'), + processedDate: DateTime.fromISO('2025-03-09'), + snowSource: 'viirs' + }) + ;(getSFMSBounds as Mock).mockResolvedValue({ + sfms_bounds: { + '2026': { + forecast: { + minimum: '2026-01-01', + maximum: '2026-03-10' + } + } + } + }) + + renderWithStore() + await waitForPageLoad() + + const warningIcon = screen.queryByTestId("WarningAmberIcon") + expect(warningIcon).toBe(null) + }) }) From 5b772615135f1b2439667dec2725938f627d439e Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Mon, 16 Mar 2026 12:17:13 -0700 Subject: [PATCH 03/10] Comment --- web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx index a50f09667..27bad6fbe 100644 --- a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx +++ b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx @@ -69,7 +69,7 @@ export const SFMSInsightsPage = () => { }, [rasterDate]) const renderSnowWarning = () => { - const tempLastSnow = DateTime.fromISO("2026-03-08").ordinal + const tempLastSnow = DateTime.fromISO("2026-03-08").ordinal // Ordinal gets the numerical day of the year in the range of 1-365 if (!isNil(snowDate?.ordinal) && snowDate?.ordinal === tempLastSnow && !isNil(rasterDate?.ordinal) && rasterDate?.ordinal > tempLastSnow) { return ( From dc4991b4d9a0746cc1202a999f33deafe57c2a3c Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Mon, 16 Mar 2026 12:52:44 -0700 Subject: [PATCH 04/10] Remove stuff --- .../pages/SFMSInsightsPage.test.tsx | 52 ++----------------- 1 file changed, 4 insertions(+), 48 deletions(-) diff --git a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx index 01c7306cc..69fbb0f4e 100644 --- a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx +++ b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx @@ -495,23 +495,12 @@ describe('SFMSInsightsPage', () => { }) it('should display warning tooltip when rasterDate greater than March 8, 2026 and snowDate is March 8, 2026.', async () => { - const dateTimeNow = DateTime.fromISO('2026-03-10') - ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getDateTimeNowPST as Mock).mockReturnValue(DateTime.fromISO('2026-03-10')) ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ forDate: DateTime.fromISO('2026-03-08'), processedDate: DateTime.fromISO('2025-03-09'), snowSource: 'viirs' }) - ;(getSFMSBounds as Mock).mockResolvedValue({ - sfms_bounds: { - '2026': { - forecast: { - minimum: '2026-01-01', - maximum: '2026-03-10' - } - } - } - }) renderWithStore() await waitForPageLoad() @@ -521,23 +510,12 @@ describe('SFMSInsightsPage', () => { }) it('should not display warning tooltip when rasterDate equal to March 8, 2026 and snowDate is March 8, 2026.', async () => { - const dateTimeNow = DateTime.fromISO('2026-03-08') - ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getDateTimeNowPST as Mock).mockReturnValue(DateTime.fromISO('2026-03-08')) ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ forDate: DateTime.fromISO('2026-03-08'), processedDate: DateTime.fromISO('2025-03-09'), snowSource: 'viirs' }) - ;(getSFMSBounds as Mock).mockResolvedValue({ - sfms_bounds: { - '2026': { - forecast: { - minimum: '2026-01-01', - maximum: '2026-03-10' - } - } - } - }) renderWithStore() await waitForPageLoad() @@ -547,23 +525,12 @@ describe('SFMSInsightsPage', () => { }) it('should not display warning tooltip when rasterDate greater than March 8, 2026 and snowDate greater than March 8, 2026.', async () => { - const dateTimeNow = DateTime.fromISO('2026-03-09') - ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getDateTimeNowPST as Mock).mockReturnValue(DateTime.fromISO('2026-03-09')) ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ forDate: DateTime.fromISO('2026-03-10'), processedDate: DateTime.fromISO('2025-03-09'), snowSource: 'viirs' }) - ;(getSFMSBounds as Mock).mockResolvedValue({ - sfms_bounds: { - '2026': { - forecast: { - minimum: '2026-01-01', - maximum: '2026-03-10' - } - } - } - }) renderWithStore() await waitForPageLoad() @@ -573,23 +540,12 @@ describe('SFMSInsightsPage', () => { }) it('should not display warning tooltip when rasterDate less than March 8, 2026 and snowDate greater than March 8, 2026.', async () => { - const dateTimeNow = DateTime.fromISO('2026-03-07') - ;(getDateTimeNowPST as Mock).mockReturnValue(dateTimeNow) + ;(getDateTimeNowPST as Mock).mockReturnValue(DateTime.fromISO('2026-03-07')) ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ forDate: DateTime.fromISO('2026-03-10'), processedDate: DateTime.fromISO('2025-03-09'), snowSource: 'viirs' }) - ;(getSFMSBounds as Mock).mockResolvedValue({ - sfms_bounds: { - '2026': { - forecast: { - minimum: '2026-01-01', - maximum: '2026-03-10' - } - } - } - }) renderWithStore() await waitForPageLoad() From a4c008957175eb8a4587a3ebafa1c58a723da845 Mon Sep 17 00:00:00 2001 From: Darren Boss Date: Tue, 17 Mar 2026 14:48:23 -0700 Subject: [PATCH 05/10] Revamp warning --- .../pages/SFMSInsightsPage.test.tsx | 132 +++++++++++++----- .../sfmsInsights/pages/SFMSInsightsPage.tsx | 93 ++++++++++-- 2 files changed, 183 insertions(+), 42 deletions(-) diff --git a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx index 69fbb0f4e..26b97c21c 100644 --- a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx +++ b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.test.tsx @@ -95,6 +95,10 @@ vi.mock('@/features/sfmsInsights/components/RasterTypeDropdown', () => ({ ) })) +// Create spies for local storage +const setItemSpy = vi.spyOn(Storage.prototype, 'setItem') +const getItemSpy = vi.spyOn(Storage.prototype, 'getItem') + describe('SFMSInsightsPage', () => { const dateTimeNow = DateTime.fromISO('2025-11-02') const dateTimeNowPlusTen = dateTimeNow.plus({ days: 10 }) @@ -146,7 +150,8 @@ describe('SFMSInsightsPage', () => { } } - const renderWithStore = (sfmsBounds?: any) => { + const renderWithStore = (hideModal: string, sfmsBounds?: any) => { + getItemSpy.mockReturnValue(hideModal) const store = createTestStore({ authentication: defaultAuthentication, runDates: { @@ -196,10 +201,13 @@ describe('SFMSInsightsPage', () => { } } }) + localStorage.clear() + getItemSpy.mockClear() + setItemSpy.mockClear() }) it('should load rasterDate from SFMS bounds in store', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() // Verify that the rasterDate was set from the sfmsBounds in the store @@ -209,7 +217,7 @@ describe('SFMSInsightsPage', () => { }) it('should set date picker max date based on SFMS bounds', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() // The date picker should be rendered with max date from SFMS bounds (2025-11-02) @@ -218,21 +226,21 @@ describe('SFMSInsightsPage', () => { }) it('should render the snow checkbox', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const checkbox = screen.getByRole('checkbox', { name: /show latest snow/i }) expect(checkbox).toBeInTheDocument() }) it('should have the snow checkbox checked by default', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const checkbox = screen.getByRole('checkbox', { name: /show latest snow/i }) as HTMLInputElement expect(checkbox.checked).toBe(true) }) it('should toggle snow checkbox when clicked', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const checkbox = screen.getByRole('checkbox', { name: /show latest snow/i }) as HTMLInputElement @@ -246,7 +254,7 @@ describe('SFMSInsightsPage', () => { }) it('should pass showSnow prop to SFMSMap when checkbox is checked', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const map = screen.getByTestId('sfms-map') @@ -254,7 +262,7 @@ describe('SFMSInsightsPage', () => { }) it('should pass showSnow=false to SFMSMap when checkbox is unchecked', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const checkbox = screen.getByRole('checkbox', { name: /show latest snow/i }) @@ -267,7 +275,7 @@ describe('SFMSInsightsPage', () => { }) it('should render raster type dropdown next to snow checkbox', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const rasterDropdown = screen.getByTestId('raster-type-dropdown') @@ -278,14 +286,14 @@ describe('SFMSInsightsPage', () => { }) it('should fetch snow data on mount with initial rasterDate', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() expect(getMostRecentProcessedSnowByDate).toHaveBeenCalledWith(DateTime.fromISO('2025-11-02')) }) it('should pass fetched snow date to SFMSMap', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const map = screen.getByTestId('sfms-map') @@ -294,7 +302,7 @@ describe('SFMSInsightsPage', () => { }) it('should display snow date in checkbox label when available', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const checkbox = screen.getByRole('checkbox', { name: /show latest snow: nov 2, 2025/i }) @@ -304,7 +312,7 @@ describe('SFMSInsightsPage', () => { it('should display "Show Latest Snow" without date when no snow data available', async () => { ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue(null) - renderWithStore() + renderWithStore('true') await waitForPageLoad() const checkbox = screen.getByRole('checkbox', { name: 'Show Latest Snow' }) @@ -324,7 +332,7 @@ describe('SFMSInsightsPage', () => { snowSource: 'viirs' }) - renderWithStore() + renderWithStore('true') await waitForPageLoad() // Wait for initial fetch @@ -349,7 +357,7 @@ describe('SFMSInsightsPage', () => { }) it('should set maxDate from latestSFMSBounds.maximum', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const maxDate = screen.getByTestId('historical-max-date') @@ -357,7 +365,7 @@ describe('SFMSInsightsPage', () => { }) it('should set minDate from earliestSFMSBounds.minimum', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const minDate = screen.getByTestId('historical-min-date') @@ -365,7 +373,7 @@ describe('SFMSInsightsPage', () => { }) it('should pass both min and max dates to ASADatePicker', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const minDate = screen.getByTestId('current-year-min-date') @@ -376,7 +384,7 @@ describe('SFMSInsightsPage', () => { }) it('should update min bounds when latestBounds changes', async () => { - renderWithStore({ + renderWithStore('true', { '2025': { forecast: { minimum: '2025-05-01', @@ -394,7 +402,7 @@ describe('SFMSInsightsPage', () => { // Mock getSFMSBounds to return null ;(getSFMSBounds as Mock).mockResolvedValueOnce({ sfms_bounds: null }) - renderWithStore(null) + renderWithStore('true', null) // Wait for fetch to complete await waitFor(() => { @@ -413,7 +421,7 @@ describe('SFMSInsightsPage', () => { }) it('should set rasterDate to today when latestBounds.maximum is empty', async () => { - renderWithStore({ + renderWithStore('true', { '2025': { forecast: { minimum: '2025-05-01', @@ -435,7 +443,7 @@ describe('SFMSInsightsPage', () => { }) it('should not set minDate when earliestBounds.minimum is empty', async () => { - renderWithStore({ + renderWithStore('true', { '2025': { forecast: { minimum: '', @@ -452,7 +460,7 @@ describe('SFMSInsightsPage', () => { }) it('should set rasterDate to today when all years have empty maximum', async () => { - renderWithStore({ + renderWithStore('true', { '2024': { forecast: { minimum: '2024-01-01', @@ -480,14 +488,14 @@ describe('SFMSInsightsPage', () => { }) it('should disable raster dropdown options when no SFMS bounds data available', async () => { - renderWithStore(null) + renderWithStore('true', null) const dropdown = screen.getByTestId('raster-type-dropdown') expect(dropdown).toHaveAttribute('data-raster-data-available', 'false') }) it('should enable raster dropdown options when SFMS bounds data available', async () => { - renderWithStore() + renderWithStore('true') await waitForPageLoad() const dropdown = screen.getByTestId('raster-type-dropdown') @@ -502,10 +510,10 @@ describe('SFMSInsightsPage', () => { snowSource: 'viirs' }) - renderWithStore() + renderWithStore('true') await waitForPageLoad() - const warningIcon = screen.queryByTestId("WarningAmberIcon") + const warningIcon = screen.queryByTestId('WarningAmberIcon') expect(warningIcon).toBeInTheDocument() }) @@ -517,13 +525,13 @@ describe('SFMSInsightsPage', () => { snowSource: 'viirs' }) - renderWithStore() + renderWithStore('true') await waitForPageLoad() - const warningIcon = screen.queryByTestId("WarningAmberIcon") + const warningIcon = screen.queryByTestId('WarningAmberIcon') expect(warningIcon).toBe(null) }) - + it('should not display warning tooltip when rasterDate greater than March 8, 2026 and snowDate greater than March 8, 2026.', async () => { ;(getDateTimeNowPST as Mock).mockReturnValue(DateTime.fromISO('2026-03-09')) ;(getMostRecentProcessedSnowByDate as Mock).mockResolvedValue({ @@ -532,10 +540,10 @@ describe('SFMSInsightsPage', () => { snowSource: 'viirs' }) - renderWithStore() + renderWithStore('true') await waitForPageLoad() - const warningIcon = screen.queryByTestId("WarningAmberIcon") + const warningIcon = screen.queryByTestId('WarningAmberIcon') expect(warningIcon).toBe(null) }) @@ -547,10 +555,68 @@ describe('SFMSInsightsPage', () => { snowSource: 'viirs' }) - renderWithStore() + renderWithStore('true') await waitForPageLoad() - const warningIcon = screen.queryByTestId("WarningAmberIcon") + const warningIcon = screen.queryByTestId('WarningAmberIcon') expect(warningIcon).toBe(null) }) + + it("should display modal warning when user hasn't permanently dismissed", async () => { + renderWithStore('false') + await waitForPageLoad() + screen.debug(undefined, 30000) + const modalHeading = screen.queryByText('Snow Coverage Imagery Warning') + expect(modalHeading).toBeInTheDocument() + }) + + it('should not display modal warning if user has permanently dismissed', async () => { + renderWithStore('true') + await waitForPageLoad() + const modalHeading = screen.queryByText('Snow Coverage Imagery Warning') + expect(modalHeading).not.toBeInTheDocument() + }) + + it('modal dismiss button should dismiss modal', async () => { + renderWithStore('false') + await waitForPageLoad() + + const modalHeading = screen.queryByText('Snow Coverage Imagery Warning') + expect(modalHeading).toBeInTheDocument() + + const dismissButton = screen.getByRole('button', { name: 'Dismiss' }) + expect(dismissButton).toBeInTheDocument() + + fireEvent.click(dismissButton) + + await waitFor(() => { + const modalHeading = screen.queryByText('Snow Coverage Imagery Warning') + expect(modalHeading).not.toBeInTheDocument() + }) + }) + + it("modal don't show again toggle should call localStorage.setItem", async () => { + renderWithStore('false') + await waitForPageLoad() + + const checkbox = screen.getByRole('checkbox', { name: /don't show again/i }) + expect(checkbox).toBeInTheDocument() + expect(checkbox).not.toBeChecked() + + // Toggle checkbox on + fireEvent.click(checkbox) + + await waitFor(() => { + expect(checkbox).toBeChecked() + expect(setItemSpy).toHaveBeenCalledWith('SFMSInsightsAlwaysHideSnowMessage', 'true') + }) + + // Toggle checkbox off + fireEvent.click(checkbox) + + await waitFor(() => { + expect(checkbox).not.toBeChecked() + expect(setItemSpy).toHaveBeenCalledWith('SFMSInsightsAlwaysHideSnowMessage', 'false') + }) + }) }) diff --git a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx index 27bad6fbe..13bb71a48 100644 --- a/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx +++ b/web/src/features/sfmsInsights/pages/SFMSInsightsPage.tsx @@ -12,12 +12,26 @@ import RasterTypeDropdown from '@/features/sfmsInsights/components/RasterTypeDro import { SFMS_INSIGHTS_NAME } from '@/utils/constants' import { getDateTimeNowPST } from '@/utils/date' import WarningAmberIcon from '@mui/icons-material/WarningAmber' -import { Box, Checkbox, CircularProgress, FormControlLabel, Grid, Tooltip } from '@mui/material' +import { + Box, + Button, + Checkbox, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Grid, + IconButton +} from '@mui/material' import { isNil, isNull } from 'lodash' import { DateTime } from 'luxon' import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +const SFMS_INSIGHTS_ALWAYS_HIDE_KEY = 'SFMSInsightsAlwaysHideSnowMessage' +const TEMP_LAST_SNOW = DateTime.fromISO('2026-02-12') export const SFMSInsightsPage = () => { const dispatch = useDispatch() const latestBounds = useSelector(selectLatestSFMSBounds) @@ -33,6 +47,12 @@ export const SFMSInsightsPage = () => { const [rasterType, setRasterType] = useState('fuel') const [showSnow, setShowSnow] = useState(true) + const [showSnowDialog, setShowSnowDialog] = useState(false) + const [showSnowWarningIcon, setShowSnowWarningIcon] = useState(false) + const [alwaysHide, setAlwaysHide] = useState(() => { + const stored = localStorage.getItem(SFMS_INSIGHTS_ALWAYS_HIDE_KEY) + return stored === 'true' + }) useEffect(() => { // Only fetch SFMS bounds if we haven't fetched yet (undefined) and aren't already loading @@ -42,6 +62,26 @@ export const SFMSInsightsPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + useEffect(() => { + // Display warning about stale snow data if user hasn't chosen to always hide the dialog + if (showSnow && !alwaysHide) { + setShowSnowDialog(true) + } + }, [alwaysHide, showSnow]) + + useEffect(() => { + if ( + !isNil(snowDate?.ordinal) && + snowDate?.ordinal === TEMP_LAST_SNOW.ordinal && + !isNil(rasterDate?.ordinal) && + rasterDate?.ordinal > TEMP_LAST_SNOW.ordinal + ) { + setShowSnowWarningIcon(true) + } else { + setShowSnowWarningIcon(false) + } + }, [rasterDate, snowDate]) + useEffect(() => { if (earliestBounds?.minimum) { setMinDate(DateTime.fromISO(earliestBounds.minimum)) @@ -68,13 +108,48 @@ export const SFMSInsightsPage = () => { fetchLastProcessedSnow(rasterDate) }, [rasterDate]) - const renderSnowWarning = () => { - const tempLastSnow = DateTime.fromISO("2026-03-08").ordinal // Ordinal gets the numerical day of the year in the range of 1-365 - if (!isNil(snowDate?.ordinal) && snowDate?.ordinal === tempLastSnow && !isNil(rasterDate?.ordinal) && rasterDate?.ordinal > tempLastSnow) { + const handleAlwaysHide = () => { + localStorage.setItem(SFMS_INSIGHTS_ALWAYS_HIDE_KEY, `${!alwaysHide}`) + setAlwaysHide(!alwaysHide) + } + + const renderSnowWarningIcon = () => { + if (showSnowWarningIcon) { return ( - - - + setShowSnowDialog(true)} sx={{ pl: 0, pt: 0 }}> + + + ) + } + } + + const renderSnowWarningDialog = () => { + if (showSnow && showSnowDialog) { + return ( + + Snow Coverage Imagery Warning + + The VIIRS satellite sensor that supplies snow coverage imagery experienced an anomaly on March 9, 2026 and + updated imagery is currently unavailable. + + + + } + label="Don't show again" + style={{ fontSize: '0.8rem', color: theme.palette.warning.contrastText }} + /> + + + ) } } @@ -126,15 +201,15 @@ export const SFMSInsightsPage = () => { setShowSnow(e.target.checked)} />} label={snowDate ? `Show Latest Snow: ${snowDate.toLocaleString(DateTime.DATE_MED)}` : 'Show Latest Snow'} - sx={{verticalAlign: "unset"}} /> - { renderSnowWarning() } + {renderSnowWarningIcon()} + {renderSnowWarningDialog()}