From 85e8582bacc1624b665c392ab23b05f36801abe3 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sun, 10 Sep 2023 12:26:21 +0300 Subject: [PATCH 01/22] Show passenger information messages on station and train pages --- public/locales/en/translation.json | 1 + public/locales/fi/translation.json | 1 + .../PassengerInformationMessageAlert.tsx | 54 ++++++++ .../PassengerInformationMessagesDialog.tsx | 51 ++++++++ src/components/TrainInfoContainer.tsx | 25 ++++ src/components/TrainStationTimeline.tsx | 15 +++ src/hooks/usePassengerInformationMessages.ts | 122 ++++++++++++++++++ src/pages/[station].tsx | 28 ++++ 8 files changed, 297 insertions(+) create mode 100644 src/components/PassengerInformationMessageAlert.tsx create mode 100644 src/components/PassengerInformationMessagesDialog.tsx create mode 100644 src/hooks/usePassengerInformationMessages.ts diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 46d9a913..ab3c1a71 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -17,6 +17,7 @@ "track": "Track", "station": "Station", "canceled": "Canceled", + "alerts": "Alerts", "page_not_found": "Page could not be found.", "train_current_composition": "Current composition", "train_composition_change_from_to_station_text": "These train units continue from {{from}} to {{to}}", diff --git a/public/locales/fi/translation.json b/public/locales/fi/translation.json index 78cecee9..16ab04d3 100644 --- a/public/locales/fi/translation.json +++ b/public/locales/fi/translation.json @@ -17,6 +17,7 @@ "track": "Raide", "station": "Asema", "canceled": "Peruttu", + "alerts": "Tiedotteet", "page_not_found": "Etsimääsi sivua ei löytynyt.", "train_current_composition": "Nykyinen kokoonpano", "train_composition_change_from_to_station_text": "Nämä junayksiköt jatkavat asemalta {{from}} asemalle {{to}}", diff --git a/src/components/PassengerInformationMessageAlert.tsx b/src/components/PassengerInformationMessageAlert.tsx new file mode 100644 index 00000000..e3bbd2c3 --- /dev/null +++ b/src/components/PassengerInformationMessageAlert.tsx @@ -0,0 +1,54 @@ +import { Alert, ButtonBase } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +import { PassengerInformationMessage } from '../hooks/usePassengerInformationMessages'; + +type PassengerInformationMessageAlertProps = { + onClick: () => void; + passengerInformationMessages: PassengerInformationMessage[]; +}; + +const PassengerInformationMessageAlert = ({ + onClick, + passengerInformationMessages, +}: PassengerInformationMessageAlertProps) => { + const { i18n } = useTranslation(); + + if (passengerInformationMessages.length === 0) return null; + + const firstMessage = passengerInformationMessages[0]; + + const getTextForCurrentLanguage = (text?: Record) => { + if (!text) { + return ''; + } + if (i18n.resolvedLanguage in text) { + return text[i18n.resolvedLanguage]; + } + return text.fi; + }; + + return ( + + + {getTextForCurrentLanguage( + (firstMessage.video ?? firstMessage.audio)?.text + )} + {passengerInformationMessages.length > 1 && ( + + {passengerInformationMessages.length - 1} + )} + + + ); +}; + +export default PassengerInformationMessageAlert; diff --git a/src/components/PassengerInformationMessagesDialog.tsx b/src/components/PassengerInformationMessagesDialog.tsx new file mode 100644 index 00000000..f95a5c0a --- /dev/null +++ b/src/components/PassengerInformationMessagesDialog.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import { Alert, DialogActions, DialogContent } from '@mui/material'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import { useTranslation } from 'react-i18next'; + +import { PassengerInformationMessage } from '../hooks/usePassengerInformationMessages'; + +type PassengerInformationMessagesDialogProps = { + onClose: () => void; + passengerInformationMessages: PassengerInformationMessage[] | null; +}; + +const PassengerInformationMessagesDialog = ( + props: PassengerInformationMessagesDialogProps +) => { + const { onClose, passengerInformationMessages } = props; + const { i18n } = useTranslation(); + const { t } = useTranslation(); + + const handleClose = () => { + onClose(); + }; + + const getTextForCurrentLanguage = (text?: Record) => { + if (!text) { + return ''; + } + if (i18n.resolvedLanguage in text) { + return text[i18n.resolvedLanguage]; + } + return text.fi; + }; + + return ( + + {t('alerts')} + + {passengerInformationMessages?.map((m) => ( + + {getTextForCurrentLanguage((m.video ?? m.audio)?.text)} + + ))} + + + + ); +}; + +export default PassengerInformationMessagesDialog; diff --git a/src/components/TrainInfoContainer.tsx b/src/components/TrainInfoContainer.tsx index bb13799f..d75ba3ff 100644 --- a/src/components/TrainInfoContainer.tsx +++ b/src/components/TrainInfoContainer.tsx @@ -8,9 +8,13 @@ import { useTrainQuery, Wagon, } from '../graphql/generated/digitraffic'; +import usePassengerInformationMessages, { + getPassengerInformationMessagesByStation, +} from '../hooks/usePassengerInformationMessages'; import { formatEET } from '../utils/date'; import { getTrainScheduledDepartureTime } from '../utils/train'; +import PassengerInformationMessagesDialog from './PassengerInformationMessagesDialog'; import TrainComposition from './TrainComposition'; import TrainStationTimeline from './TrainStationTimeline'; import TrainWagonDetailsDialog from './TrainWagonDetailsDialog'; @@ -22,6 +26,7 @@ type TrainInfoContainerProps = { function TrainInfoContainer({ train }: TrainInfoContainerProps) { const [wagonDialogOpen, setWagonDialogOpen] = useState(false); const [selectedWagon, setSelectedWagon] = useState(null); + const [selectedStation, setSelectedStation] = useState(null); const departureDate = train ? getTrainScheduledDepartureTime(train) : null; const { error, @@ -41,6 +46,16 @@ function TrainInfoContainer({ train }: TrainInfoContainerProps) { } : { skip: true } ); + const { messages: passengerInformationMessages } = + usePassengerInformationMessages({ + skip: !train, + refetchIntervalMs: 10000, + trainNumber: train?.trainNumber, + trainDepartureDate: train?.departureDate, + }); + const stationMessages = getPassengerInformationMessagesByStation( + passengerInformationMessages + ); const realTimeTrain = realTimeData?.train?.[0]; @@ -91,6 +106,8 @@ function TrainInfoContainer({ train }: TrainInfoContainerProps) { train={train} realTimeTrain={realTimeTrain} onWagonClick={handleWagonClick} + onStationAlertClick={(stationCode) => setSelectedStation(stationCode)} + stationMessages={stationMessages} /> {train && ( )} + setSelectedStation(null)} + /> ); } diff --git a/src/components/TrainStationTimeline.tsx b/src/components/TrainStationTimeline.tsx index 88671428..71473b76 100644 --- a/src/components/TrainStationTimeline.tsx +++ b/src/components/TrainStationTimeline.tsx @@ -5,12 +5,14 @@ import RouterLink from 'next/link'; import { useTranslation } from 'react-i18next'; import { TrainDetailsFragment, Wagon } from '../graphql/generated/digitraffic'; +import { PassengerInformationMessage } from '../hooks/usePassengerInformationMessages'; import getTrainCurrentStation from '../utils/getTrainCurrentStation'; import getTrainLatestArrivalRow from '../utils/getTrainLatestArrivalRow'; import getTrainLatestDepartureTimeTableRow from '../utils/getTrainLatestDepartureTimeTableRow'; import getTrainPreviousStation from '../utils/getTrainPreviousStation'; import { getTrainStationName } from '../utils/train'; +import PassengerInformationMessageAlert from './PassengerInformationMessageAlert'; import TimelineRouteStopSeparator from './TimelineRouteStopSeparator'; import TimeTableRowTime from './TimeTableRowTime'; import TrainComposition from './TrainComposition'; @@ -19,12 +21,16 @@ type TrainStationTimelineProps = { train?: TrainDetailsFragment | null; realTimeTrain?: TrainDetailsFragment | null; onWagonClick: (w: Wagon) => void; + onStationAlertClick: (stationCode: string) => void; + stationMessages?: Record; }; const TrainStationTimeline = ({ train, realTimeTrain, onWagonClick, + onStationAlertClick, + stationMessages, }: TrainStationTimelineProps) => { const { t } = useTranslation(); @@ -101,6 +107,15 @@ const TrainStationTimeline = ({ /> )} + {stationMessages && + stationMessages[station.shortCode]?.length > 0 && ( + onStationAlertClick(station.shortCode)} + passengerInformationMessages={ + stationMessages[station.shortCode] + } + /> + )} diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts new file mode 100644 index 00000000..babc242a --- /dev/null +++ b/src/hooks/usePassengerInformationMessages.ts @@ -0,0 +1,122 @@ +import { useEffect, useMemo, useState } from 'react'; + +export type PassengerInformationMessage = { + id: string; + version: number; + creationDateTime: string; + startValidity: string; + endValidity: string; + stations: string[]; + trainNumber?: number; + trainDepartureDate?: string; + audio?: { + text?: PassengerInformationTextContent; + deliveryRules?: DeliveryRules; + }; + video?: { + text?: PassengerInformationTextContent; + deliveryRules?: DeliveryRules; + }; +}; + +type PassengerInformationTextContent = { + fi?: string; + sv?: string; + en?: string; +}; + +type DeliveryRules = { + deliveryType?: + | 'NOW' + | 'DELIVERY_AT' + | 'REPEAT_EVERY' + | 'ON_SCHEDULE' + | 'ON_EVENT'; +}; + +type PassengerInformationMessageQuery = { + skip?: boolean; + stationCode?: string; + trainNumber?: number; + trainDepartureDate?: string; + onlyGeneral?: boolean; + refetchIntervalMs: number; +}; + +export default function usePassengerInformationMessages({ + skip, + stationCode, + trainNumber, + trainDepartureDate, + onlyGeneral, + refetchIntervalMs = 10000, +}: PassengerInformationMessageQuery) { + const [messages, setMessages] = useState(); + const [error, setError] = useState(); + + const params = useMemo(() => { + return new URLSearchParams({ + ...(stationCode && { station: stationCode }), + ...(trainNumber && { train_number: trainNumber?.toString() }), + ...(trainDepartureDate && { train_departure_date: trainDepartureDate }), + ...(onlyGeneral && { only_general: onlyGeneral?.toString() }), + }); + }, [onlyGeneral, stationCode, trainDepartureDate, trainNumber]); + + useEffect(() => { + let interval: NodeJS.Timer | null = null; + if (!skip) { + const fetchData = async () => { + try { + setError(undefined); + const res = await fetch( + `https://rata.digitraffic.fi/api/v1/passenger-information/active?${params}` + ); + const allMessages = + (await res.json()) as PassengerInformationMessage[]; + const relevantMessages = allMessages.filter((m) => { + const passengerInformationContent = m.video ?? m.audio; + return ( + passengerInformationContent?.deliveryRules == null || + passengerInformationContent?.deliveryRules?.deliveryType !== + 'ON_EVENT' + ); + }); + setMessages(relevantMessages); + } catch (error) { + setError(error); + } + }; + interval = setInterval(fetchData, refetchIntervalMs); + fetchData(); + } + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [params, refetchIntervalMs, skip]); + + return { messages, error }; +} + +export function getPassengerInformationMessagesByStation( + passengerInformationMessages: PassengerInformationMessage[] | undefined +) { + return passengerInformationMessages?.reduce((acc, message) => { + // When there are more than one stations, only add this message to the first + // and last station in the array (which are ordered according to the train + // schedule). + const stationsToAdd = + message.stations.length > 1 + ? [message.stations[0], message.stations[message.stations.length - 1]] + : message.stations; + + stationsToAdd.forEach((station) => { + acc[station] = acc[station] || []; + acc[station].push(message); + }); + return acc; + }, {} as Record); +} diff --git a/src/pages/[station].tsx b/src/pages/[station].tsx index 9afeec86..12dcd673 100644 --- a/src/pages/[station].tsx +++ b/src/pages/[station].tsx @@ -11,6 +11,8 @@ import { useTranslation } from 'react-i18next'; import FavoriteStation from '../components/FavoriteStation'; import MapLayout, { VehicleMapContainerPortal } from '../components/MapLayout'; +import PassengerInformationMessageAlert from '../components/PassengerInformationMessageAlert'; +import PassengerInformationMessagesDialog from '../components/PassengerInformationMessagesDialog'; import StationTimeTable from '../components/StationTimeTable'; import SubNavBar from '../components/SubNavBar'; import { gqlClients, vehiclesVar } from '../graphql/client'; @@ -19,6 +21,7 @@ import { useTrainsByStationQuery, } from '../graphql/generated/digitraffic'; import { useRoutesForRailLazyQuery } from '../graphql/generated/digitransit'; +import usePassengerInformationMessages from '../hooks/usePassengerInformationMessages'; import useTrainLiveTracking from '../hooks/useTrainLiveTracking'; import { isDefined } from '../utils/common'; import { formatEET } from '../utils/date'; @@ -40,6 +43,8 @@ const Station: NextPageWithLayout = () => { null ); const [selectedTrainNo, setSelectedTrainNo] = useState(null); + const [stationAlertDialogOpen, setStationAlertDialogOpen] = + useState(); const [executeRouteSearch, { data: routeData }] = useRoutesForRailLazyQuery(); const station = stationName ? trainStations.find( @@ -63,6 +68,13 @@ const Station: NextPageWithLayout = () => { fetchPolicy: 'no-cache', }); useTrainLiveTracking(data?.trainsByStationAndQuantity?.filter(isDefined)); + const { messages: passengerInformationMessages } = + usePassengerInformationMessages({ + skip: stationCode == null, + refetchIntervalMs: 10000, + onlyGeneral: true, + stationCode: stationCode, + }); const handleVehicleIdSelected = useCallback((vehicleId: number) => { setSelectedVehicleId(vehicleId); @@ -183,6 +195,14 @@ const Station: NextPageWithLayout = () => { {t('arrivals')} + + {passengerInformationMessages && ( + setStationAlertDialogOpen(true)} + passengerInformationMessages={passengerInformationMessages} + /> + )} + {stationCode && !loading && data?.trainsByStationAndQuantity && ( { {error && ( {error.message} )} + setStationAlertDialogOpen(false)} + /> ); }; From 50207395852b0600cc9b1ca18a9ff71008809e97 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sun, 10 Sep 2023 14:27:10 +0300 Subject: [PATCH 02/22] Improve passenger information messages with junaan.fi as info source --- src/hooks/usePassengerInformationMessages.ts | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index babc242a..de12df6f 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -82,6 +82,14 @@ export default function usePassengerInformationMessages({ 'ON_EVENT' ); }); + relevantMessages.forEach((m) => { + if (m.audio?.text) { + m.audio.text = getImprovedTextContent(m.audio.text); + } + if (m.video?.text) { + m.video.text = getImprovedTextContent(m.video.text); + } + }); setMessages(relevantMessages); } catch (error) { setError(error); @@ -101,6 +109,33 @@ export default function usePassengerInformationMessages({ return { messages, error }; } +function getImprovedTextContent( + textContent: PassengerInformationTextContent +): PassengerInformationTextContent { + const modifiedContent: PassengerInformationTextContent = {}; + + for (const lng of Object.keys(textContent) as Array< + keyof PassengerInformationTextContent + >) { + const currentText = textContent[lng]; + + if (currentText) { + const idx = currentText.indexOf('junalahdot.fi'); + + if (idx !== -1) { + modifiedContent[lng] = + currentText.slice(0, idx) + 'junaan.fi / ' + currentText.slice(idx); + } else { + modifiedContent[lng] = currentText; + } + } else { + modifiedContent[lng] = currentText; + } + } + + return modifiedContent; +} + export function getPassengerInformationMessagesByStation( passengerInformationMessages: PassengerInformationMessage[] | undefined ) { From 5ddad3a8c7929fc7c585e870dab9530198020753 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:32:18 +0300 Subject: [PATCH 03/22] Follow delivery rules when displaying passenger information messages - refactor: Move logic from usePassengerInformationMessages to separate passengerInformationMessages.ts utils - Implement logic to follow delivery rules stated on the passenger information messages - Add tests for usePassengerInformationMessages hook and passengerInformationMessages utils --- src/components/TrainInfoContainer.tsx | 5 +- .../usePassengerInformationMessages.test.ts | 97 ++++ src/hooks/usePassengerInformationMessages.ts | 102 +--- .../passengerInformationMessages.test.ts | 462 ++++++++++++++++++ src/utils/passengerInformationMessages.ts | 348 +++++++++++++ 5 files changed, 915 insertions(+), 99 deletions(-) create mode 100644 src/hooks/__tests__/usePassengerInformationMessages.test.ts create mode 100644 src/utils/__tests__/passengerInformationMessages.test.ts create mode 100644 src/utils/passengerInformationMessages.ts diff --git a/src/components/TrainInfoContainer.tsx b/src/components/TrainInfoContainer.tsx index d75ba3ff..b54b9cc0 100644 --- a/src/components/TrainInfoContainer.tsx +++ b/src/components/TrainInfoContainer.tsx @@ -8,10 +8,9 @@ import { useTrainQuery, Wagon, } from '../graphql/generated/digitraffic'; -import usePassengerInformationMessages, { - getPassengerInformationMessagesByStation, -} from '../hooks/usePassengerInformationMessages'; +import usePassengerInformationMessages from '../hooks/usePassengerInformationMessages'; import { formatEET } from '../utils/date'; +import { getPassengerInformationMessagesByStation } from '../utils/passengerInformationMessages'; import { getTrainScheduledDepartureTime } from '../utils/train'; import PassengerInformationMessagesDialog from './PassengerInformationMessagesDialog'; diff --git a/src/hooks/__tests__/usePassengerInformationMessages.test.ts b/src/hooks/__tests__/usePassengerInformationMessages.test.ts new file mode 100644 index 00000000..b63b291a --- /dev/null +++ b/src/hooks/__tests__/usePassengerInformationMessages.test.ts @@ -0,0 +1,97 @@ +import { act, renderHook, waitFor } from '@testing-library/react'; +import { parseISO } from 'date-fns'; + +import { PassengerInformationMessage } from '../../utils/passengerInformationMessages'; +import usePassengerInformationMessages from '../usePassengerInformationMessages'; + +const mockPassengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20230727799106000', + version: 7, + creationDateTime: '2023-09-08T04:16:00Z', + startValidity: '2023-09-10T21:00:00Z', + endValidity: '2023-09-15T20:59:00Z', + stations: ['AVP', 'VEH', 'KTÖ'], + video: { + text: { + fi: 'Suomeksi', + sv: 'På svenska', + en: 'In English', + }, + deliveryRules: { + startDateTime: '2023-09-11T00:00:00+03:00', + endDateTime: '2023-09-15T23:59:00+03:00', + startTime: '0:01', + endTime: '4:20', + weekDays: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY'], + deliveryType: 'CONTINUOS_VISUALIZATION', + }, + }, + }, +]; + +describe('usePassengerInformationMessages', () => { + const mockFetchPromise = Promise.resolve({ + json: () => Promise.resolve(mockPassengerInformationMessages), + }); + const refreshCycle = 10000; + + beforeEach(() => { + jest + .spyOn(global, 'fetch') + .mockImplementation(() => mockFetchPromise as any); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.resetAllMocks(); + }); + + it('should return relevant messages immediately before first refresh cycle', async () => { + jest.useFakeTimers().setSystemTime(parseISO('2023-09-15T04:19:55+03:00')); + + const { result } = renderHook(() => + usePassengerInformationMessages({ + skip: false, + refetchIntervalMs: refreshCycle, + }) + ); + + await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(result.current.messages).toBeDefined()); + + expect(result.current.messages!.length).toStrictEqual(1); + + act(() => { + jest.advanceTimersByTime(refreshCycle); + }); + + await waitFor(() => + expect(result.current.messages!.length).toStrictEqual(0) + ); + }); + + it('should update the relevant messages after each refresh cycle', async () => { + jest.useFakeTimers().setSystemTime(parseISO('2023-09-11T00:00:55+03:00')); + + const { result } = renderHook(() => + usePassengerInformationMessages({ + skip: false, + refetchIntervalMs: refreshCycle, + }) + ); + + await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(result.current.messages).toBeDefined()); + + expect(result.current.messages!.length).toStrictEqual(0); + + act(() => { + jest.advanceTimersByTime(refreshCycle); + }); + + await waitFor(() => + expect(result.current.messages!.length).toStrictEqual(1) + ); + }); +}); diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index de12df6f..3fcbdae5 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -1,38 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; -export type PassengerInformationMessage = { - id: string; - version: number; - creationDateTime: string; - startValidity: string; - endValidity: string; - stations: string[]; - trainNumber?: number; - trainDepartureDate?: string; - audio?: { - text?: PassengerInformationTextContent; - deliveryRules?: DeliveryRules; - }; - video?: { - text?: PassengerInformationTextContent; - deliveryRules?: DeliveryRules; - }; -}; - -type PassengerInformationTextContent = { - fi?: string; - sv?: string; - en?: string; -}; - -type DeliveryRules = { - deliveryType?: - | 'NOW' - | 'DELIVERY_AT' - | 'REPEAT_EVERY' - | 'ON_SCHEDULE' - | 'ON_EVENT'; -}; +import { + getPassengerInformationMessagesCurrentlyRelevant, + PassengerInformationMessage, +} from '../utils/passengerInformationMessages'; type PassengerInformationMessageQuery = { skip?: boolean; @@ -74,22 +45,8 @@ export default function usePassengerInformationMessages({ ); const allMessages = (await res.json()) as PassengerInformationMessage[]; - const relevantMessages = allMessages.filter((m) => { - const passengerInformationContent = m.video ?? m.audio; - return ( - passengerInformationContent?.deliveryRules == null || - passengerInformationContent?.deliveryRules?.deliveryType !== - 'ON_EVENT' - ); - }); - relevantMessages.forEach((m) => { - if (m.audio?.text) { - m.audio.text = getImprovedTextContent(m.audio.text); - } - if (m.video?.text) { - m.video.text = getImprovedTextContent(m.video.text); - } - }); + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant(allMessages); setMessages(relevantMessages); } catch (error) { setError(error); @@ -108,50 +65,3 @@ export default function usePassengerInformationMessages({ return { messages, error }; } - -function getImprovedTextContent( - textContent: PassengerInformationTextContent -): PassengerInformationTextContent { - const modifiedContent: PassengerInformationTextContent = {}; - - for (const lng of Object.keys(textContent) as Array< - keyof PassengerInformationTextContent - >) { - const currentText = textContent[lng]; - - if (currentText) { - const idx = currentText.indexOf('junalahdot.fi'); - - if (idx !== -1) { - modifiedContent[lng] = - currentText.slice(0, idx) + 'junaan.fi / ' + currentText.slice(idx); - } else { - modifiedContent[lng] = currentText; - } - } else { - modifiedContent[lng] = currentText; - } - } - - return modifiedContent; -} - -export function getPassengerInformationMessagesByStation( - passengerInformationMessages: PassengerInformationMessage[] | undefined -) { - return passengerInformationMessages?.reduce((acc, message) => { - // When there are more than one stations, only add this message to the first - // and last station in the array (which are ordered according to the train - // schedule). - const stationsToAdd = - message.stations.length > 1 - ? [message.stations[0], message.stations[message.stations.length - 1]] - : message.stations; - - stationsToAdd.forEach((station) => { - acc[station] = acc[station] || []; - acc[station].push(message); - }); - return acc; - }, {} as Record); -} diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts new file mode 100644 index 00000000..2ea7d227 --- /dev/null +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -0,0 +1,462 @@ +import { parseISO } from 'date-fns'; + +import { + TimeTableRowType, + TrainByStationFragment, +} from '../../graphql/generated/digitraffic'; +import { + getPassengerInformationMessagesCurrentlyRelevant, + PassengerInformationMessage, +} from '../passengerInformationMessages'; + +describe('getPassengerInformationMessagesCurrentlyRelevant', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + describe('audio', () => { + describe('deliveryRules type NOW', () => { + describe('message with creationDateTime "2023-09-10T14:37:00Z"', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + audio: { + text: { + fi: 'Huomio! Intercity 963 klo 17.36 Kupittaalle saapuu lähtöraiteelleen noin 5 minuutin kuluttua.', + sv: 'Observera! Intercity 963 till Kuppis kl 17.36 anländer till sitt avgångsspår om cirka 5 minuter.', + en: 'Attention! Intercity 963 to Kupittaa at 17.36 arrives to its departure track in about 5 minutes.', + }, + deliveryRules: { + deliveryType: 'NOW', + repetitions: 0, + repeatEvery: 0, + }, + }, + }, + ]; + + it.each(['2023-09-10T14:37:00Z', '2023-09-10T14:52:00Z'])( + 'should be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + } + ); + + it.each(['2023-09-10T14:36:00Z', '2023-09-10T14:53:00Z'])( + 'should not be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + } + ); + }); + }); + + describe('deliveryRules type DELIVERY_AT', () => { + describe('message with deliveryAt "2023-09-10T14:30:00Z"', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + audio: { + text: { + fi: 'Huomio! Intercity 963 klo 17.36 Kupittaalle saapuu lähtöraiteelleen noin 5 minuutin kuluttua.', + sv: 'Observera! Intercity 963 till Kuppis kl 17.36 anländer till sitt avgångsspår om cirka 5 minuter.', + en: 'Attention! Intercity 963 to Kupittaa at 17.36 arrives to its departure track in about 5 minutes.', + }, + deliveryRules: { + deliveryType: 'DELIVERY_AT', + deliveryAt: '2023-09-10T14:30:00Z', + }, + }, + }, + ]; + + it.each(['2023-09-10T14:30:00Z', '2023-09-10T14:45:00Z'])( + 'should be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + } + ); + + it.each(['2023-09-10T14:29:00Z', '2023-09-10T14:46:00Z'])( + 'should not be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + } + ); + }); + }); + + describe('deliveryRules type REPEAT_EVERY', () => { + describe('message with timespan "2023-09-01T08:20:00+03:00" to "2023-09-03T15:00:00+03:00" on all weekdays', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20230420115013317', + version: 15, + creationDateTime: '2023-09-03T08:07:00Z', + startValidity: '2023-09-01T21:00:00Z', + endValidity: '2023-09-03T20:59:00Z', + stations: ['KV'], + audio: { + text: { + fi: 'Hyvät matkustajat! Rataverkolla tehdään kunnossapitotöitä. 2.-3.9.2023 osa junista Kouvolan ja Pieksämäen, sekä Kouvolan ja Joensuun välillä, on korvattu busseilla. Lisäksi joulukuuhun asti, myös Kouvolan ja Kotkan sataman välillä osa junista on korvattu busseilla. Kouvolassa korvaavat bussit lähtevät asemarakennuksen ja raiteen 1 välistä. Korvatut junavuorot, bussien aikataulut sekä asemakohtaiset lähtöpaikat löydät osoitteesta vr.fi.', + sv: 'Bästa passagerare! Underhållsarbeten pågår på järnvägsnätet. Mellan 2 till 3 september 2023 har en del av tågen mellan Kouvola och Pieksämäki samt Kouvola och Joensuu ersatts av bussar. Dessutom fram till december ersätts några tåg med buss mellan Kouvola och Kotka. I Kouvola avgår ersättningsbussar mellan stationshuset och spår 1. Ersatta tågtider, busstidtabeller och stationsspecifika avgångsplatser finns på vr.fi.', + en: 'Dear passangers! There are changes in train traffic due to maintenance works. Between 2.9.2023 and 3.9.2023, some of the trains between Kouvola and Pieksämäki, and Kouvola and Joensuu, have been replaced by buses. There are also some train replaced by buses until December between Kouvola, and Kotka. In Kouvola, replacement buses leave from between the station building and track 1. Replaced train times, bus schedules and station-specific departure points can be found at vr.fi.', + }, + deliveryRules: { + startDateTime: '2023-09-01T21:00:00Z', + endDateTime: '2023-09-03T20:59:00Z', + startTime: '8:20', + endTime: '15:00', + weekDays: [ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ], + deliveryType: 'REPEAT_EVERY', + repetitions: 1, + repeatEvery: 20, + }, + }, + video: { + text: { + fi: 'Rataverkolla tehdään kunnossapitotöitä. 2.-3.9.2023 osa junista Kouvolan ja Pieksämäen, sekä Kouvolan ja Joensuun välillä, on korvattu busseilla. Lisäksi joulukuuhun asti, myös Kouvolan ja Kotkan sataman välillä osa junista on korvattu busseilla. Kouvolassa korvaavat bussit lähtevät asemarakennuksen ja raiteen 1 välistä. Korvatut junavuorot, bussien aikataulut sekä asemakohtaiset lähtöpaikat löydät osoitteesta vr.fi.', + sv: 'Underhållsarbeten pågår på järnvägsnätet. Mellan 2 till 3 september 2023 har en del av tågen mellan Kouvola och Pieksämäki samt Kouvola och Joensuu ersatts av bussar. Dessutom fram till december ersätts några tåg med buss mellan Kouvola och Kotka. I Kouvola avgår ersättningsbussar mellan stationshuset och spår 1. Ersatta tågtider, busstidtabeller och stationsspecifika avgångsplatser finns på vr.fi.', + en: 'There are changes in train traffic due to maintenance works. Between 2.9.2023 and 3.9.2023, some of the trains between Kouvola and Pieksämäki, and Kouvola and Joensuu, have been replaced by buses. There are also some train replaced by buses until December between Kouvola, and Kotka. In Kouvola, replacement buses leave from between the station building and track 1. Replaced train times, bus schedules and station-specific departure points can be found at vr.fi.', + }, + deliveryRules: { + startDateTime: '2023-09-01T21:00:00Z', + endDateTime: '2023-09-03T20:59:00Z', + startTime: '5:00', + endTime: '15:00', + weekDays: [ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ], + deliveryType: 'CONTINUOS_VISUALIZATION', + }, + }, + }, + ]; + + it.each(['2023-09-02T08:20:00+03:00', '2023-09-03T15:00:00+03:00'])( + 'should be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + } + ); + + it.each(['2023-09-02T08:19:00+03:00', '2023-09-03T15:01:00+03:00'])( + 'should not be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + } + ); + }); + }); + + describe('deliveryRules type ON_SCHEDULE', () => { + describe('train with departureDate 2023-09-02 and message with stations [LUS=15:51, PUN=16:00]', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'MVM20230902154021600', + version: 5, + creationDateTime: '2023-09-02T15:51:00Z', + startValidity: '2023-09-02T15:51:00Z', + endValidity: '2023-09-04T00:00:00Z', + trainNumber: 750, + trainDepartureDate: '2023-09-02', + stations: ['LUS', 'PUN', 'REE', 'KIÄ', 'PKY'], + audio: { + text: { + fi: 'Huomio ilmoitus! Juna H750 on korvattu linja-autolla Savonlinnan ja Parikkalan välillä. Korvaava linja-auto lähtee aseman edustalta. Bussin matka-aika asemien välillä on pidempi kuin junan matka-aika. Info:vr.fi', + sv: 'Observera! Tåg H750 har ersatts av ett buss mellan Nyslott och Parikkala. Bussen avgår från järnvägsstationen. Restiden för bussen mellan stationerna är längre än tågets restid. Info:vr.fi', + en: 'Attention! Train H750 has been replaced by bus between Savonlinna and Parikkala. The bus leaves from the railwaystation. The bus travel time between stations is longer than the train travel time. Info:vr.fi.', + }, + deliveryRules: { + deliveryType: 'ON_SCHEDULE', + repetitions: 1, + repeatEvery: 0, + }, + }, + video: { + text: { + fi: 'Juna H750 on korvattu linja-autolla Savonlinnan ja Parikkalan välillä. Korvaava linja-auto lähtee aseman edustalta. Bussin matka-aika asemien välillä on pidempi kuin junan matka-aika. Restiden för bussen mellan stationerna är längre än tågets restid. Info:vr.fi', + sv: 'Tåg H750 har ersatts av ett buss mellan Nyslott och Parikkala. Bussen avgår från järnvägsstationen. Restiden för bussen mellan stationerna är längre än tågets restid. Info:vr.fi', + en: 'Train H750 has been replaced by bus between Savonlinna and Parikkala. The bus leaves from the railwaystation. The bus travel time between stations is longer than the train travel time. Info:vr.fi.', + }, + }, + }, + ]; + + const train: TrainByStationFragment = { + runningCurrently: true, + trainNumber: 750, + departureDate: '2023-09-02', + version: '1', + trainType: { + name: '', + trainCategory: { + name: '', + }, + }, + operator: { + shortCode: '', + uicCode: 1, + }, + timeTableRows: [ + { + scheduledTime: '2023-09-02T15:51:00Z', + cancelled: false, + station: { + name: '', + shortCode: 'LUS', + }, + trainStopping: true, + type: TimeTableRowType.Arrival, + }, + { + scheduledTime: '2023-09-02T16:00:00Z', + cancelled: false, + station: { + name: '', + shortCode: 'PUN', + }, + trainStopping: true, + type: TimeTableRowType.Arrival, + }, + ], + }; + + it.each([ + '2023-09-02T15:51:00Z', + '2023-09-02T15:52:00Z', + '2023-09-02T16:00:00Z', + '2023-09-02T16:01:00Z', + ])('should be relevant at time %p', (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages, + train + ); + + expect(relevantMessages.length).toBe(1); + }); + + it.each([ + '2023-09-02T15:50:59Z', + '2023-09-02T15:53:00Z', + '2023-09-02T15:59:59Z', + '2023-09-02T16:02:00Z', + ])('should not be relevant at time %p', (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages, + train + ); + + expect(relevantMessages.length).toBe(0); + }); + }); + }); + }); + + describe('video', () => { + describe('deliveryRules type CONTINUOS_VISUALIZATION', () => { + describe('message with timespan "2023-09-11T01:00:00+03:00" to "2023-09-15T04:20:00+03:00" on Mon-Thu', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20230727799106000', + version: 7, + creationDateTime: '2023-09-08T04:16:00Z', + startValidity: '2023-09-10T21:00:00Z', + endValidity: '2023-09-15T20:59:00Z', + stations: ['AVP', 'VEH', 'KTÖ'], + video: { + text: { + fi: 'Suomeksi', + sv: 'På svenska', + en: 'In English', + }, + deliveryRules: { + startDateTime: '2023-09-10T21:00:00Z', + endDateTime: '2023-09-15T20:59:00Z', + startTime: '0:01', + endTime: '4:20', + weekDays: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY'], + deliveryType: 'CONTINUOS_VISUALIZATION', + }, + }, + }, + ]; + + it.each([ + '2023-09-10T21:01:00Z', + '2023-09-12T00:00:00Z', + // 2023-09-14 is Thursday (which is one of the weekDays) and this is one second before Friday + '2023-09-14T23:59:59+03:00', + ])('should be relevant at time %p', (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + }); + + it.each([ + '2023-09-10T21:00:00Z', + '2023-09-15T01:20:01Z', + // 2023-09-15 is Friday which is not one of the weekDays + '2023-09-15T00:00:00+03:00', + ])('should not be relevant at time %p', (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + }); + }); + }); + + describe('video deliveryRules type WHEN', () => { + describe('message with timespan "2023-09-08T21:00:00Z" to "2023-09-10T20:59:00Z" on Mon-Sun from 7:45 to 7:50', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20230504174327890', + version: 10, + creationDateTime: '2023-09-09T05:38:00Z', + startValidity: '2023-09-08T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + video: { + text: { + fi: 'Raide suljettu. Varmista junasi lähtöraide aikataulunäytöiltä tai osoitteesta junalahdot.fi.', + sv: 'Spåret är stängt. Kolla tågens avgångsspår från tidtabellsskärmarna eller på addressen junalahdot.fi.', + en: 'The track is closed. Check the departure tracks from the timetable displays or at junalahdot.fi.', + }, + deliveryRules: { + startDateTime: '2023-09-08T21:00:00Z', + endDateTime: '2023-09-10T20:59:00Z', + startTime: '7:45', + endTime: '7:50', + weekDays: [ + 'MONDAY', + 'TUESDAY', + 'WEDNESDAY', + 'THURSDAY', + 'FRIDAY', + 'SATURDAY', + 'SUNDAY', + ], + deliveryType: 'WHEN', + }, + }, + }, + ]; + + it.each([ + '2023-09-09T07:45:00+03:00', + '2023-09-09T07:47:00+03:00', + '2023-09-09T07:50:00+03:00', + '2023-09-10T07:45:00+03:00', + '2023-09-10T07:47:00+03:00', + '2023-09-10T07:50:00+03:00', + ])('should be relevant at time %p', (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + }); + + it.each([ + '2023-09-09T07:44:59+03:00', + '2023-09-09T07:50:01+03:00', + '2023-09-11T07:47:00+03:00', + ])('should not be relevant at time %p', (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + }); + }); + }); + }); +}); diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts new file mode 100644 index 00000000..d4fbeb62 --- /dev/null +++ b/src/utils/passengerInformationMessages.ts @@ -0,0 +1,348 @@ +import { differenceInMinutes, format, parseISO, startOfDay } from 'date-fns'; +import { utcToZonedTime } from 'date-fns-tz'; +import { orderBy } from 'lodash'; + +import { + TimeTableRowType, + TrainByStationFragment, +} from '../graphql/generated/digitraffic'; +import { getTimeTableRowRealTime } from '../utils/train'; + +export type PassengerInformationMessage = { + id: string; + version: number; + creationDateTime: string; + startValidity: string; + endValidity: string; + stations: string[]; + trainNumber?: number; + trainDepartureDate?: string; + audio?: { + text?: PassengerInformationTextContent; + deliveryRules?: AudioDeliveryRules; + }; + video?: { + text?: PassengerInformationTextContent; + deliveryRules?: VideoDeliveryRules; + }; +}; + +type PassengerInformationTextContent = { + fi?: string; + sv?: string; + en?: string; +}; + +type DeliveryRulesBase = { + /** + * ISO date in format yyyy-MM-ddTHH:mm:ssZ + */ + startDateTime?: string; + weekDays?: Weekday[]; + /** + * Time component in format H:mm + */ + startTime?: string; + endTime?: string; + endDateTime?: string; +}; + +type AudioDeliveryRules = DeliveryRulesBase & { + deliveryType?: + | 'NOW' + | 'DELIVERY_AT' + | 'REPEAT_EVERY' + | 'ON_SCHEDULE' + | 'ON_EVENT'; + deliveryAt?: string; + repeatEvery?: number; + eventType?: 'ARRIVING' | 'DEPARTING'; + repetitions?: number; +}; + +type VideoDeliveryRules = DeliveryRulesBase & { + deliveryType?: 'WHEN' | 'CONTINUOS_VISUALIZATION'; +}; + +type Weekday = + | 'MONDAY' + | 'TUESDAY' + | 'WEDNESDAY' + | 'THURSDAY' + | 'FRIDAY' + | 'SATURDAY' + | 'SUNDAY'; + +export function getPassengerInformationMessagesByStation( + passengerInformationMessages: PassengerInformationMessage[] | undefined +) { + return passengerInformationMessages?.reduce((acc, message) => { + // When there are more than one stations, only add this message to the first + // and last station in the array (which are ordered according to the train + // schedule). + const stationsToAdd = + message.stations.length > 1 + ? [message.stations[0], message.stations[message.stations.length - 1]] + : message.stations; + + stationsToAdd.forEach((station) => { + acc[station] = acc[station] || []; + acc[station].push(message); + }); + return acc; + }, {} as Record); +} + +export function getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages: PassengerInformationMessage[], + train?: TrainByStationFragment +): PassengerInformationMessage[] { + const now = new Date(); + const relevantMessages = passengerInformationMessages.filter((message) => + isMessageRelevant(message, now, train) + ); + relevantMessages.forEach((m) => { + if (m.audio?.text) { + m.audio.text = getImprovedTextContent(m.audio.text); + } + if (m.video?.text) { + m.video.text = getImprovedTextContent(m.video.text); + } + }); + return relevantMessages; +} + +function getImprovedTextContent( + textContent: PassengerInformationTextContent +): PassengerInformationTextContent { + const modifiedContent: PassengerInformationTextContent = {}; + + for (const lng of Object.keys(textContent) as Array< + keyof PassengerInformationTextContent + >) { + const currentText = textContent[lng]; + + if (currentText) { + const idx = currentText.indexOf('junalahdot.fi'); + + if (idx !== -1) { + modifiedContent[lng] = + currentText.slice(0, idx) + 'junaan.fi / ' + currentText.slice(idx); + } else { + modifiedContent[lng] = currentText; + } + } else { + modifiedContent[lng] = currentText; + } + } + + return modifiedContent; +} + +function isMessageRelevant( + message: PassengerInformationMessage, + now: Date, + train?: TrainByStationFragment +): boolean { + if (message.audio) { + return isAudioMessageRelevant(message, now, train); + } + if (message.video) { + return isVideoMessageRelevant(message, now); + } + return false; +} + +function isAudioMessageRelevant( + message: PassengerInformationMessage, + now: Date, + train?: TrainByStationFragment +): boolean { + if (!message.audio) return false; + + const { deliveryRules } = message.audio; + + if (!deliveryRules) return true; + + switch (deliveryRules.deliveryType) { + case 'NOW': { + const diffInMinutes = differenceInMinutes( + now, + parseISO(message.creationDateTime) + ); + return diffInMinutes <= 15 && diffInMinutes >= 0; + } + case 'DELIVERY_AT': { + if (!deliveryRules.deliveryAt) return true; + const diffInMinutes = differenceInMinutes( + now, + parseISO(deliveryRules.deliveryAt) + ); + return diffInMinutes <= 15 && diffInMinutes >= 0; + } + case 'REPEAT_EVERY': + return isWithinTimeSpan(deliveryRules, now); + case 'ON_SCHEDULE': + return isMessageRelevantBasedOnTrainSchedule( + train, + now, + message.stations + ); + case 'ON_EVENT': + // TODO: Not yet supported + return false; + default: + return true; + } +} + +function isVideoMessageRelevant( + message: PassengerInformationMessage, + now: Date +): boolean { + if (!message.video) return false; + + const { deliveryRules } = message.video; + + if (!deliveryRules) return true; + + switch (deliveryRules.deliveryType) { + case 'WHEN': + return isWithinTimeSpanAndHours(deliveryRules, now); + case 'CONTINUOS_VISUALIZATION': + return isWithinTimeSpan(deliveryRules, now); + default: + return true; + } +} + +/** + * Determines whether the messages is relevant based on train schedule and given stations. + * + * @param train The train with time table rows describing scheduled arrival times. + * @param now The current date and time to check whether the station arrival time is within threshold. + * @param stations The stations to check against. + * @returns `true` if the message is determined to be relevant. + */ +function isMessageRelevantBasedOnTrainSchedule( + train: TrainByStationFragment | undefined, + now: Date, + stations: string[] +) { + if (!train) return false; + + // Get train latest arrival row by scheduled times + const latestArrivalRow = orderBy( + train.timeTableRows?.filter( + (r) => + r && + r.type === TimeTableRowType.Arrival && + r.scheduledTime && + parseISO(r.scheduledTime) <= now + ), + (r) => r?.scheduledTime, + 'desc' + )[0]; + + if (!latestArrivalRow) return false; + if (!stations.includes(latestArrivalRow.station.shortCode)) return false; + const latestArrivalRowTime = getTimeTableRowRealTime(latestArrivalRow); + const diffInMinutes = differenceInMinutes(now, latestArrivalRowTime); + return diffInMinutes <= 1 && diffInMinutes >= 0; +} + +/** + * Determines whether the current date and time fall within a specified time span and weekdays. + * + * @param deliveryRules - The rules that define the time span and weekdays. + * @param now - The current date and time to check. + * @returns `true` if the current date and time are within the specified time span and weekdays; otherwise, `false`. + */ +const isWithinTimeSpan = ( + { + startDateTime, + startTime, + endDateTime, + endTime, + weekDays, + }: DeliveryRulesBase, + now: Date +): boolean => { + if (!startDateTime || !endDateTime) return true; + const startDateAdjusted = getDateTime(startDateTime, startTime); + const endDateAdjusted = getDateTime(endDateTime, endTime); + + const isWithinStartAndEnd = + startDateAdjusted <= now && now <= endDateAdjusted; + if (!isWithinStartAndEnd) return false; + + const isWithinWeekdays = + !weekDays || weekDays.includes(getCurrentWeekday(now)); + return isWithinWeekdays; +}; + +/** + * Determines whether the current date and time fall within specified date span + * and the specified time on the specified weekdays. + * + * @param deliveryRules - The rules that define the date span and the time span on given weekdays. + * @param now - The current date and time to check. + * @returns `true` if the current date and time are within the specified rules. + */ +const isWithinTimeSpanAndHours = ( + { + startDateTime, + startTime, + endDateTime, + endTime, + weekDays, + }: DeliveryRulesBase, + now: Date +): boolean => { + if (!startDateTime || !endDateTime) return true; + const startDateAdjusted = parseISO(startDateTime); + const endDateAdjusted = parseISO(endDateTime); + + const isWithinStartAndEnd = + startDateAdjusted <= now && now <= endDateAdjusted; + if (!isWithinStartAndEnd) return false; + + const isWithinWeekdays = + weekDays && weekDays.includes(getCurrentWeekday(now)); + if (!isWithinWeekdays) return false; + + const sameDayStartDateTime = getDateTime(now, startTime); + const sameDayEndDateTime = getDateTime(now, endTime); + return sameDayStartDateTime <= now && now <= sameDayEndDateTime; +}; + +const getCurrentWeekday = (date: Date): Weekday => { + return format(date, 'EEEE').toUpperCase() as Weekday; +}; + +/** + * Converts a date and an optional time string to a new Date object with both date and time components. + * + * The time part represents time in Europe/Helsinki time zone, and if given, + * it overrides the time part of the date parameter. + * + * @param dateTimeISO - The date time in ISO format. + * @param time - The optional time part (format: "H:mm") in Europe/Helsinki (EET) time zone. + * @returns A new Date constructed from the given parameters. + */ +const getDateTime = (dateTimeISO: string | Date, timeISOinEET?: string) => { + let dateISO: Date; + if (dateTimeISO instanceof Date) { + dateISO = dateTimeISO; + } else { + dateISO = parseISO(dateTimeISO); + } + let dateTime = utcToZonedTime(dateISO, 'Europe/Helsinki'); + if (timeISOinEET) { + const [hours, minutes] = timeISOinEET.split(':').map(Number); + dateTime = startOfDay(dateTime); + dateTime.setHours(hours); + dateTime.setMinutes(minutes); + } + return dateTime; +}; From 7fd695211b498f6e0a158bddc8a0156272fc1305 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Fri, 15 Sep 2023 16:59:47 +0300 Subject: [PATCH 04/22] Fix wrong imports --- src/components/PassengerInformationMessageAlert.tsx | 2 +- src/components/PassengerInformationMessagesDialog.tsx | 2 +- src/components/TrainStationTimeline.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/PassengerInformationMessageAlert.tsx b/src/components/PassengerInformationMessageAlert.tsx index e3bbd2c3..e23c6f8d 100644 --- a/src/components/PassengerInformationMessageAlert.tsx +++ b/src/components/PassengerInformationMessageAlert.tsx @@ -1,7 +1,7 @@ import { Alert, ButtonBase } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { PassengerInformationMessage } from '../hooks/usePassengerInformationMessages'; +import { PassengerInformationMessage } from '../utils/passengerInformationMessages'; type PassengerInformationMessageAlertProps = { onClick: () => void; diff --git a/src/components/PassengerInformationMessagesDialog.tsx b/src/components/PassengerInformationMessagesDialog.tsx index f95a5c0a..82eff817 100644 --- a/src/components/PassengerInformationMessagesDialog.tsx +++ b/src/components/PassengerInformationMessagesDialog.tsx @@ -5,7 +5,7 @@ import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; import { useTranslation } from 'react-i18next'; -import { PassengerInformationMessage } from '../hooks/usePassengerInformationMessages'; +import { PassengerInformationMessage } from '../utils/passengerInformationMessages'; type PassengerInformationMessagesDialogProps = { onClose: () => void; diff --git a/src/components/TrainStationTimeline.tsx b/src/components/TrainStationTimeline.tsx index 71473b76..70982c27 100644 --- a/src/components/TrainStationTimeline.tsx +++ b/src/components/TrainStationTimeline.tsx @@ -5,11 +5,11 @@ import RouterLink from 'next/link'; import { useTranslation } from 'react-i18next'; import { TrainDetailsFragment, Wagon } from '../graphql/generated/digitraffic'; -import { PassengerInformationMessage } from '../hooks/usePassengerInformationMessages'; import getTrainCurrentStation from '../utils/getTrainCurrentStation'; import getTrainLatestArrivalRow from '../utils/getTrainLatestArrivalRow'; import getTrainLatestDepartureTimeTableRow from '../utils/getTrainLatestDepartureTimeTableRow'; import getTrainPreviousStation from '../utils/getTrainPreviousStation'; +import { PassengerInformationMessage } from '../utils/passengerInformationMessages'; import { getTrainStationName } from '../utils/train'; import PassengerInformationMessageAlert from './PassengerInformationMessageAlert'; From 1cf1c586bc80f96c07bccb1f48602fbe53dab671 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:33:46 +0300 Subject: [PATCH 05/22] Check message relevance primarily from video and then from audio --- .../passengerInformationMessages.test.ts | 30 ------------------- src/utils/passengerInformationMessages.ts | 8 ++--- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 2ea7d227..6840a155 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -159,29 +159,6 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { repeatEvery: 20, }, }, - video: { - text: { - fi: 'Rataverkolla tehdään kunnossapitotöitä. 2.-3.9.2023 osa junista Kouvolan ja Pieksämäen, sekä Kouvolan ja Joensuun välillä, on korvattu busseilla. Lisäksi joulukuuhun asti, myös Kouvolan ja Kotkan sataman välillä osa junista on korvattu busseilla. Kouvolassa korvaavat bussit lähtevät asemarakennuksen ja raiteen 1 välistä. Korvatut junavuorot, bussien aikataulut sekä asemakohtaiset lähtöpaikat löydät osoitteesta vr.fi.', - sv: 'Underhållsarbeten pågår på järnvägsnätet. Mellan 2 till 3 september 2023 har en del av tågen mellan Kouvola och Pieksämäki samt Kouvola och Joensuu ersatts av bussar. Dessutom fram till december ersätts några tåg med buss mellan Kouvola och Kotka. I Kouvola avgår ersättningsbussar mellan stationshuset och spår 1. Ersatta tågtider, busstidtabeller och stationsspecifika avgångsplatser finns på vr.fi.', - en: 'There are changes in train traffic due to maintenance works. Between 2.9.2023 and 3.9.2023, some of the trains between Kouvola and Pieksämäki, and Kouvola and Joensuu, have been replaced by buses. There are also some train replaced by buses until December between Kouvola, and Kotka. In Kouvola, replacement buses leave from between the station building and track 1. Replaced train times, bus schedules and station-specific departure points can be found at vr.fi.', - }, - deliveryRules: { - startDateTime: '2023-09-01T21:00:00Z', - endDateTime: '2023-09-03T20:59:00Z', - startTime: '5:00', - endTime: '15:00', - weekDays: [ - 'MONDAY', - 'TUESDAY', - 'WEDNESDAY', - 'THURSDAY', - 'FRIDAY', - 'SATURDAY', - 'SUNDAY', - ], - deliveryType: 'CONTINUOS_VISUALIZATION', - }, - }, }, ]; @@ -239,13 +216,6 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { repeatEvery: 0, }, }, - video: { - text: { - fi: 'Juna H750 on korvattu linja-autolla Savonlinnan ja Parikkalan välillä. Korvaava linja-auto lähtee aseman edustalta. Bussin matka-aika asemien välillä on pidempi kuin junan matka-aika. Restiden för bussen mellan stationerna är längre än tågets restid. Info:vr.fi', - sv: 'Tåg H750 har ersatts av ett buss mellan Nyslott och Parikkala. Bussen avgår från järnvägsstationen. Restiden för bussen mellan stationerna är längre än tågets restid. Info:vr.fi', - en: 'Train H750 has been replaced by bus between Savonlinna and Parikkala. The bus leaves from the railwaystation. The bus travel time between stations is longer than the train travel time. Info:vr.fi.', - }, - }, }, ]; diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts index d4fbeb62..3232ab51 100644 --- a/src/utils/passengerInformationMessages.ts +++ b/src/utils/passengerInformationMessages.ts @@ -144,11 +144,11 @@ function isMessageRelevant( now: Date, train?: TrainByStationFragment ): boolean { - if (message.audio) { - return isAudioMessageRelevant(message, now, train); + if (message.video && isVideoMessageRelevant(message, now)) { + return true; } - if (message.video) { - return isVideoMessageRelevant(message, now); + if (message.audio && isAudioMessageRelevant(message, now, train)) { + return true; } return false; } From bc6660afcdbd891010f16c7cbfc12a42378df367 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sat, 16 Sep 2023 09:46:55 +0300 Subject: [PATCH 06/22] Fix time zone handling when handling dates in delivery rules --- src/utils/passengerInformationMessages.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts index 3232ab51..b07d80f6 100644 --- a/src/utils/passengerInformationMessages.ts +++ b/src/utils/passengerInformationMessages.ts @@ -1,5 +1,5 @@ import { differenceInMinutes, format, parseISO, startOfDay } from 'date-fns'; -import { utcToZonedTime } from 'date-fns-tz'; +import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'; import { orderBy } from 'lodash'; import { @@ -277,7 +277,7 @@ const isWithinTimeSpan = ( if (!isWithinStartAndEnd) return false; const isWithinWeekdays = - !weekDays || weekDays.includes(getCurrentWeekday(now)); + !weekDays || weekDays.includes(getCurrentWeekdayInEET(now)); return isWithinWeekdays; }; @@ -308,7 +308,7 @@ const isWithinTimeSpanAndHours = ( if (!isWithinStartAndEnd) return false; const isWithinWeekdays = - weekDays && weekDays.includes(getCurrentWeekday(now)); + weekDays && weekDays.includes(getCurrentWeekdayInEET(now)); if (!isWithinWeekdays) return false; const sameDayStartDateTime = getDateTime(now, startTime); @@ -316,8 +316,9 @@ const isWithinTimeSpanAndHours = ( return sameDayStartDateTime <= now && now <= sameDayEndDateTime; }; -const getCurrentWeekday = (date: Date): Weekday => { - return format(date, 'EEEE').toUpperCase() as Weekday; +const getCurrentWeekdayInEET = (date: Date): Weekday => { + const dateEET = utcToZonedTime(date, 'Europe/Helsinki'); + return format(dateEET, 'EEEE').toUpperCase() as Weekday; }; /** @@ -331,18 +332,19 @@ const getCurrentWeekday = (date: Date): Weekday => { * @returns A new Date constructed from the given parameters. */ const getDateTime = (dateTimeISO: string | Date, timeISOinEET?: string) => { - let dateISO: Date; + let dateTime: Date; if (dateTimeISO instanceof Date) { - dateISO = dateTimeISO; + dateTime = dateTimeISO; } else { - dateISO = parseISO(dateTimeISO); + dateTime = parseISO(dateTimeISO); } - let dateTime = utcToZonedTime(dateISO, 'Europe/Helsinki'); if (timeISOinEET) { + dateTime = utcToZonedTime(dateTime, 'Europe/Helsinki'); const [hours, minutes] = timeISOinEET.split(':').map(Number); dateTime = startOfDay(dateTime); dateTime.setHours(hours); dateTime.setMinutes(minutes); + dateTime = zonedTimeToUtc(dateTime, 'Europe/Helsinki'); } return dateTime; }; From 37e2ad78bab6dfb15702773291c42ab3ad756972 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sat, 16 Sep 2023 10:48:18 +0300 Subject: [PATCH 07/22] Improve PassengerInformationMessagesDialog opening and closing --- .../PassengerInformationMessagesDialog.tsx | 12 ++++++++---- src/components/TrainInfoContainer.tsx | 15 +++++++++++++-- src/pages/[station].tsx | 10 +++------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/components/PassengerInformationMessagesDialog.tsx b/src/components/PassengerInformationMessagesDialog.tsx index 82eff817..0a384082 100644 --- a/src/components/PassengerInformationMessagesDialog.tsx +++ b/src/components/PassengerInformationMessagesDialog.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, DialogActions, DialogContent } from '@mui/material'; import Dialog from '@mui/material/Dialog'; @@ -9,13 +9,17 @@ import { PassengerInformationMessage } from '../utils/passengerInformationMessag type PassengerInformationMessagesDialogProps = { onClose: () => void; - passengerInformationMessages: PassengerInformationMessage[] | null; + open: boolean; + passengerInformationMessages: + | PassengerInformationMessage[] + | null + | undefined; }; const PassengerInformationMessagesDialog = ( props: PassengerInformationMessagesDialogProps ) => { - const { onClose, passengerInformationMessages } = props; + const { onClose, open, passengerInformationMessages } = props; const { i18n } = useTranslation(); const { t } = useTranslation(); @@ -34,7 +38,7 @@ const PassengerInformationMessagesDialog = ( }; return ( - + {t('alerts')} {passengerInformationMessages?.map((m) => ( diff --git a/src/components/TrainInfoContainer.tsx b/src/components/TrainInfoContainer.tsx index b54b9cc0..d80eb8d1 100644 --- a/src/components/TrainInfoContainer.tsx +++ b/src/components/TrainInfoContainer.tsx @@ -26,6 +26,7 @@ function TrainInfoContainer({ train }: TrainInfoContainerProps) { const [wagonDialogOpen, setWagonDialogOpen] = useState(false); const [selectedWagon, setSelectedWagon] = useState(null); const [selectedStation, setSelectedStation] = useState(null); + const [stationAlertDialogOpen, setStationAlertDialogOpen] = useState(false); const departureDate = train ? getTrainScheduledDepartureTime(train) : null; const { error, @@ -67,6 +68,15 @@ function TrainInfoContainer({ train }: TrainInfoContainerProps) { setWagonDialogOpen(true); }; + const handleStationAlertDialogClose = () => { + setStationAlertDialogOpen(false); + }; + + const handleStationAlertClick = (stationCode: string) => { + setSelectedStation(stationCode); + setStationAlertDialogOpen(true); + }; + return ( <> setSelectedStation(stationCode)} + onStationAlertClick={handleStationAlertClick} stationMessages={stationMessages} /> {train && ( @@ -117,12 +127,13 @@ function TrainInfoContainer({ train }: TrainInfoContainerProps) { /> )} setSelectedStation(null)} + onClose={handleStationAlertDialogClose} /> ); diff --git a/src/pages/[station].tsx b/src/pages/[station].tsx index a7fa8ed8..8ee4a929 100644 --- a/src/pages/[station].tsx +++ b/src/pages/[station].tsx @@ -43,8 +43,7 @@ const Station: NextPageWithLayout = () => { null ); const [selectedTrainNo, setSelectedTrainNo] = useState(null); - const [stationAlertDialogOpen, setStationAlertDialogOpen] = - useState(); + const [stationAlertDialogOpen, setStationAlertDialogOpen] = useState(false); const [executeRouteSearch, { data: routeData }] = useRoutesForRailLazyQuery(); const station = stationName ? trainStations.find( @@ -214,11 +213,8 @@ const Station: NextPageWithLayout = () => { {error.message} )} setStationAlertDialogOpen(false)} /> From 418f7595ed9e815d7da55f27323546664a98f225 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sat, 16 Sep 2023 11:12:31 +0300 Subject: [PATCH 08/22] Ignore messages with unsupported deliveryRules type --- .../passengerInformationMessages.test.ts | 90 +++++++++++++++++++ src/utils/passengerInformationMessages.ts | 4 +- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 6840a155..7b18c656 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -15,6 +15,35 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { }); describe('audio', () => { + it('should not return messages with unsupported deliveryRules type', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + audio: { + text: { + fi: 'Huomio!', + sv: 'Observera!', + en: 'Attention!', + }, + deliveryRules: { + deliveryType: 'NO_SUPPORT_FOR_SUCH_TYPE' as any, + }, + }, + }, + ]; + + const relevantMessages = getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + }); + describe('deliveryRules type NOW', () => { describe('message with creationDateTime "2023-09-10T14:37:00Z"', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ @@ -293,9 +322,70 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { }); }); }); + + describe('deliveryRules type ON_EVENT', () => { + it('should not return messages with type ON_EVENT because they are not currently supported', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + audio: { + text: { + fi: 'Huomio!', + sv: 'Observera!', + en: 'Attention!', + }, + deliveryRules: { + deliveryType: 'ON_EVENT', + }, + }, + }, + ]; + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + }); + }); }); describe('video', () => { + it('should not return messages with unsupported deliveryRules type', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + video: { + text: { + fi: 'Huomio!', + sv: 'Observera!', + en: 'Attention!', + }, + deliveryRules: { + deliveryType: 'NO_SUPPORT_FOR_SUCH_TYPE' as any, + }, + }, + }, + ]; + + const relevantMessages = getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + }); + describe('deliveryRules type CONTINUOS_VISUALIZATION', () => { describe('message with timespan "2023-09-11T01:00:00+03:00" to "2023-09-15T04:20:00+03:00" on Mon-Thu', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts index b07d80f6..276e8d46 100644 --- a/src/utils/passengerInformationMessages.ts +++ b/src/utils/passengerInformationMessages.ts @@ -192,7 +192,7 @@ function isAudioMessageRelevant( // TODO: Not yet supported return false; default: - return true; + return false; } } @@ -212,7 +212,7 @@ function isVideoMessageRelevant( case 'CONTINUOS_VISUALIZATION': return isWithinTimeSpan(deliveryRules, now); default: - return true; + return false; } } From 46fcf8fff65534eca761b2b6f7bb4999a4a8e730 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sat, 16 Sep 2023 11:13:10 +0300 Subject: [PATCH 09/22] Add tests for getPassengerInformationMessagesByStation --- .../passengerInformationMessages.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 7b18c656..5ae93156 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -5,10 +5,38 @@ import { TrainByStationFragment, } from '../../graphql/generated/digitraffic'; import { + getPassengerInformationMessagesByStation, getPassengerInformationMessagesCurrentlyRelevant, PassengerInformationMessage, } from '../passengerInformationMessages'; +describe('getPassengerInformationMessagesByStation', () => { + it('should only add the message to first and last station in the stations array when there are multiple stations', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI', 'PSL', 'TKL', 'KE', 'RI'], + }, + ]; + + const messagesByStation = getPassengerInformationMessagesByStation( + passengerInformationMessages + ); + expect(messagesByStation).toBeDefined(); + expect(messagesByStation!['HKI']).toBeDefined(); + expect(messagesByStation!['HKI'].length).toBe(1); + expect(messagesByStation!['PSL']).toBeUndefined(); + expect(messagesByStation!['TKL']).toBeUndefined(); + expect(messagesByStation!['KE']).toBeUndefined(); + expect(messagesByStation!['RI']).toBeDefined(); + expect(messagesByStation!['RI'].length).toBe(1); + }); +}); + describe('getPassengerInformationMessagesCurrentlyRelevant', () => { beforeAll(() => { jest.useFakeTimers(); From e0703d481063012993224d100d804a16bf556323 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sun, 17 Sep 2023 10:41:08 +0300 Subject: [PATCH 10/22] Remove unused imports --- src/components/PassengerInformationMessagesDialog.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/PassengerInformationMessagesDialog.tsx b/src/components/PassengerInformationMessagesDialog.tsx index 0a384082..1bc67fcf 100644 --- a/src/components/PassengerInformationMessagesDialog.tsx +++ b/src/components/PassengerInformationMessagesDialog.tsx @@ -1,5 +1,3 @@ -import React, { useEffect, useState } from 'react'; - import { Alert, DialogActions, DialogContent } from '@mui/material'; import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; From e1776bfb028f3777690656944e89bd73bc8392cc Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sun, 17 Sep 2023 13:19:47 +0300 Subject: [PATCH 11/22] Simplify isWithinTimeSpan logic --- src/utils/passengerInformationMessages.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts index 276e8d46..7a13ae9a 100644 --- a/src/utils/passengerInformationMessages.ts +++ b/src/utils/passengerInformationMessages.ts @@ -276,9 +276,9 @@ const isWithinTimeSpan = ( startDateAdjusted <= now && now <= endDateAdjusted; if (!isWithinStartAndEnd) return false; - const isWithinWeekdays = - !weekDays || weekDays.includes(getCurrentWeekdayInEET(now)); - return isWithinWeekdays; + if (!weekDays) return true; + + return weekDays.includes(getCurrentWeekdayInEET(now)); }; /** From c9999850814a4f299a6ac641e3339a3c56a2cf5d Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sun, 17 Sep 2023 13:22:32 +0300 Subject: [PATCH 12/22] Add more tests for special cases --- .../usePassengerInformationMessages.test.ts | 159 ++++++++++------ src/hooks/usePassengerInformationMessages.ts | 2 +- .../passengerInformationMessages.test.ts | 180 +++++++++++++++++- 3 files changed, 273 insertions(+), 68 deletions(-) diff --git a/src/hooks/__tests__/usePassengerInformationMessages.test.ts b/src/hooks/__tests__/usePassengerInformationMessages.test.ts index b63b291a..01c34289 100644 --- a/src/hooks/__tests__/usePassengerInformationMessages.test.ts +++ b/src/hooks/__tests__/usePassengerInformationMessages.test.ts @@ -34,64 +34,105 @@ describe('usePassengerInformationMessages', () => { const mockFetchPromise = Promise.resolve({ json: () => Promise.resolve(mockPassengerInformationMessages), }); - const refreshCycle = 10000; - beforeEach(() => { - jest - .spyOn(global, 'fetch') - .mockImplementation(() => mockFetchPromise as any); - }); - - afterEach(() => { - jest.useRealTimers(); - jest.resetAllMocks(); - }); - - it('should return relevant messages immediately before first refresh cycle', async () => { - jest.useFakeTimers().setSystemTime(parseISO('2023-09-15T04:19:55+03:00')); - - const { result } = renderHook(() => - usePassengerInformationMessages({ - skip: false, - refetchIntervalMs: refreshCycle, - }) - ); - - await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(result.current.messages).toBeDefined()); - - expect(result.current.messages!.length).toStrictEqual(1); - - act(() => { - jest.advanceTimersByTime(refreshCycle); - }); - - await waitFor(() => - expect(result.current.messages!.length).toStrictEqual(0) - ); - }); - - it('should update the relevant messages after each refresh cycle', async () => { - jest.useFakeTimers().setSystemTime(parseISO('2023-09-11T00:00:55+03:00')); - - const { result } = renderHook(() => - usePassengerInformationMessages({ - skip: false, - refetchIntervalMs: refreshCycle, - }) - ); - - await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(result.current.messages).toBeDefined()); - - expect(result.current.messages!.length).toStrictEqual(0); - - act(() => { - jest.advanceTimersByTime(refreshCycle); - }); - - await waitFor(() => - expect(result.current.messages!.length).toStrictEqual(1) - ); - }); + describe.each([undefined, 5000])( + 'using different refresh cycles', + (refreshCycle) => { + const advanceByTime = refreshCycle ?? 10000; + + beforeEach(() => { + jest + .spyOn(global, 'fetch') + .mockImplementation(() => mockFetchPromise as any); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.resetAllMocks(); + }); + + it('should return relevant messages immediately before first refresh cycle', async () => { + jest + .useFakeTimers() + .setSystemTime(parseISO('2023-09-15T04:19:55+03:00')); + + const { result } = renderHook(() => + usePassengerInformationMessages({ + skip: false, + refetchIntervalMs: refreshCycle, + }) + ); + + await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(result.current.messages).toBeDefined()); + + expect(result.current.messages!.length).toStrictEqual(1); + + act(() => { + jest.advanceTimersByTime(advanceByTime); + }); + + await waitFor(() => + expect(result.current.messages!.length).toStrictEqual(0) + ); + }); + + it('should update the relevant messages after each refresh cycle', async () => { + jest + .useFakeTimers() + .setSystemTime(parseISO('2023-09-11T00:00:55+03:00')); + + const { result } = renderHook(() => + usePassengerInformationMessages({ + skip: false, + refetchIntervalMs: refreshCycle, + }) + ); + + await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(result.current.messages).toBeDefined()); + + expect(result.current.messages!.length).toStrictEqual(0); + + act(() => { + jest.advanceTimersByTime(advanceByTime); + }); + + await waitFor(() => + expect(result.current.messages!.length).toStrictEqual(1) + ); + }); + + it('should return error and previously fetched messages if fetching fails', async () => { + jest + .useFakeTimers() + .setSystemTime(parseISO('2023-09-15T04:19:55+03:00')); + + const { result } = renderHook(() => + usePassengerInformationMessages({ + skip: false, + refetchIntervalMs: refreshCycle, + }) + ); + + await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(result.current.messages).toBeDefined()); + + expect(result.current.messages!.length).toStrictEqual(1); + + jest + .spyOn(global, 'fetch') + .mockImplementation(() => Promise.reject('error')); + + act(() => { + jest.advanceTimersByTime(advanceByTime); + }); + + await waitFor(() => expect(result.current.error).toBeDefined()); + + expect(result.current.messages).toBeDefined(); + expect(result.current.messages!.length).toStrictEqual(1); + }); + } + ); }); diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index 3fcbdae5..fe40189f 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -11,7 +11,7 @@ type PassengerInformationMessageQuery = { trainNumber?: number; trainDepartureDate?: string; onlyGeneral?: boolean; - refetchIntervalMs: number; + refetchIntervalMs?: number; }; export default function usePassengerInformationMessages({ diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 5ae93156..4b7a1593 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -11,6 +11,27 @@ import { } from '../passengerInformationMessages'; describe('getPassengerInformationMessagesByStation', () => { + it('should add the message to the station present in stations array when there is only single station', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['PSL'], + }, + ]; + + const messagesByStation = getPassengerInformationMessagesByStation( + passengerInformationMessages + ); + + expectToBeDefined(messagesByStation); + expect(messagesByStation['PSL']).toBeDefined(); + expect(messagesByStation['PSL'].length).toBe(1); + }); + it('should only add the message to first and last station in the stations array when there are multiple stations', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { @@ -26,14 +47,15 @@ describe('getPassengerInformationMessagesByStation', () => { const messagesByStation = getPassengerInformationMessagesByStation( passengerInformationMessages ); - expect(messagesByStation).toBeDefined(); - expect(messagesByStation!['HKI']).toBeDefined(); - expect(messagesByStation!['HKI'].length).toBe(1); - expect(messagesByStation!['PSL']).toBeUndefined(); - expect(messagesByStation!['TKL']).toBeUndefined(); - expect(messagesByStation!['KE']).toBeUndefined(); - expect(messagesByStation!['RI']).toBeDefined(); - expect(messagesByStation!['RI'].length).toBe(1); + + expectToBeDefined(messagesByStation); + expect(messagesByStation['HKI']).toBeDefined(); + expect(messagesByStation['HKI'].length).toBe(1); + expect(messagesByStation['PSL']).toBeUndefined(); + expect(messagesByStation['TKL']).toBeUndefined(); + expect(messagesByStation['KE']).toBeUndefined(); + expect(messagesByStation['RI']).toBeDefined(); + expect(messagesByStation['RI'].length).toBe(1); }); }); @@ -42,6 +64,55 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { jest.useFakeTimers(); }); + describe('text content modifications', () => { + it('should add junaan.fi text to both audio and video text content if the text contains junalahdot.fi', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + audio: { + text: { + fi: 'Huomio! junalahdot.fi', + sv: 'Observera! junalahdot.fi', + en: 'Attention! vr.fi', + }, + }, + video: { + text: { + fi: 'Huomio! junalahdot.fi', + sv: undefined, + en: 'Attention! junalahdot.fi', + }, + }, + }, + ]; + + const relevantMessages = getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + expect(relevantMessages[0].audio?.text?.fi).toBe( + 'Huomio! junaan.fi / junalahdot.fi' + ); + expect(relevantMessages[0].audio?.text?.sv).toBe( + 'Observera! junaan.fi / junalahdot.fi' + ); + expect(relevantMessages[0].audio?.text?.en).toBe('Attention! vr.fi'); + expect(relevantMessages[0].video?.text?.fi).toBe( + 'Huomio! junaan.fi / junalahdot.fi' + ); + expect(relevantMessages[0].video?.text?.sv).toBeUndefined(); + expect(relevantMessages[0].video?.text?.en).toBe( + 'Attention! junaan.fi / junalahdot.fi' + ); + }); + }); + describe('audio', () => { it('should not return messages with unsupported deliveryRules type', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ @@ -72,6 +143,32 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { expect(relevantMessages.length).toBe(0); }); + it('should return messages that have no deliveryRules', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + audio: { + text: { + fi: 'Huomio!', + sv: 'Observera!', + en: 'Attention!', + }, + }, + }, + ]; + + const relevantMessages = getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + }); + describe('deliveryRules type NOW', () => { describe('message with creationDateTime "2023-09-10T14:37:00Z"', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ @@ -128,6 +225,36 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { }); describe('deliveryRules type DELIVERY_AT', () => { + it('should return messages with no deliveryAt specified', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + audio: { + text: { + fi: 'Huomio!', + sv: 'Observera!', + en: 'Attention!', + }, + deliveryRules: { + deliveryType: 'DELIVERY_AT', + }, + }, + }, + ]; + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + }); + describe('message with deliveryAt "2023-09-10T14:30:00Z"', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { @@ -348,6 +475,17 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { expect(relevantMessages.length).toBe(0); }); + + it('should not return messages if no train is given as parameter', () => { + jest.setSystemTime(parseISO('2023-09-02T15:50:59Z')); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + }); }); }); @@ -414,6 +552,32 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { expect(relevantMessages.length).toBe(0); }); + it('should return messages that have no deliveryRules', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + video: { + text: { + fi: 'Huomio!', + sv: 'Observera!', + en: 'Attention!', + }, + }, + }, + ]; + + const relevantMessages = getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + }); + describe('deliveryRules type CONTINUOS_VISUALIZATION', () => { describe('message with timespan "2023-09-11T01:00:00+03:00" to "2023-09-15T04:20:00+03:00" on Mon-Thu', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ From 2fb456099a3dc1bdbbcdd62434b0f520aad63afd Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sun, 17 Sep 2023 13:40:47 +0300 Subject: [PATCH 13/22] Fix code quality issues --- src/hooks/__tests__/usePassengerInformationMessages.test.ts | 2 +- src/utils/passengerInformationMessages.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/hooks/__tests__/usePassengerInformationMessages.test.ts b/src/hooks/__tests__/usePassengerInformationMessages.test.ts index 01c34289..9a40b129 100644 --- a/src/hooks/__tests__/usePassengerInformationMessages.test.ts +++ b/src/hooks/__tests__/usePassengerInformationMessages.test.ts @@ -122,7 +122,7 @@ describe('usePassengerInformationMessages', () => { jest .spyOn(global, 'fetch') - .mockImplementation(() => Promise.reject('error')); + .mockImplementation(() => Promise.reject(new Error('error'))); act(() => { jest.advanceTimersByTime(advanceByTime); diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts index 7a13ae9a..ade3bd04 100644 --- a/src/utils/passengerInformationMessages.ts +++ b/src/utils/passengerInformationMessages.ts @@ -307,9 +307,7 @@ const isWithinTimeSpanAndHours = ( startDateAdjusted <= now && now <= endDateAdjusted; if (!isWithinStartAndEnd) return false; - const isWithinWeekdays = - weekDays && weekDays.includes(getCurrentWeekdayInEET(now)); - if (!isWithinWeekdays) return false; + if (!weekDays?.includes(getCurrentWeekdayInEET(now))) return false; const sameDayStartDateTime = getDateTime(now, startTime); const sameDayEndDateTime = getDateTime(now, endTime); From 6b803dee8599267ec294feba95f13bbb2f7c8d73 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sun, 17 Sep 2023 14:53:28 +0300 Subject: [PATCH 14/22] Reduce code duplication in passengerInformationMessages.test.ts --- .../passengerInformationMessages.test.ts | 178 ++++-------------- 1 file changed, 40 insertions(+), 138 deletions(-) diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 4b7a1593..1c934dfb 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -64,16 +64,26 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { jest.useFakeTimers(); }); + const passengerInformationMessageBase: PassengerInformationMessage = { + id: 'SHM20220818174358380', + version: 94, + creationDateTime: '2023-09-10T14:37:00Z', + startValidity: '2023-09-09T21:00:00Z', + endValidity: '2023-09-10T20:59:00Z', + stations: ['HKI'], + }; + + const defaultPassengerInformationTextContent = { + fi: 'Huomio!', + sv: 'Observera!', + en: 'Attention!', + }; + describe('text content modifications', () => { it('should add junaan.fi text to both audio and video text content if the text contains junalahdot.fi', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, audio: { text: { fi: 'Huomio! junalahdot.fi', @@ -117,18 +127,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { it('should not return messages with unsupported deliveryRules type', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, audio: { - text: { - fi: 'Huomio!', - sv: 'Observera!', - en: 'Attention!', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { deliveryType: 'NO_SUPPORT_FOR_SUCH_TYPE' as any, }, @@ -146,18 +147,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { it('should return messages that have no deliveryRules', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, audio: { - text: { - fi: 'Huomio!', - sv: 'Observera!', - en: 'Attention!', - }, + text: defaultPassengerInformationTextContent, }, }, ]; @@ -173,22 +165,12 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { describe('message with creationDateTime "2023-09-10T14:37:00Z"', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, + ...passengerInformationMessageBase, creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], audio: { - text: { - fi: 'Huomio! Intercity 963 klo 17.36 Kupittaalle saapuu lähtöraiteelleen noin 5 minuutin kuluttua.', - sv: 'Observera! Intercity 963 till Kuppis kl 17.36 anländer till sitt avgångsspår om cirka 5 minuter.', - en: 'Attention! Intercity 963 to Kupittaa at 17.36 arrives to its departure track in about 5 minutes.', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { deliveryType: 'NOW', - repetitions: 0, - repeatEvery: 0, }, }, }, @@ -228,18 +210,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { it('should return messages with no deliveryAt specified', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, audio: { - text: { - fi: 'Huomio!', - sv: 'Observera!', - en: 'Attention!', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { deliveryType: 'DELIVERY_AT', }, @@ -258,18 +231,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { describe('message with deliveryAt "2023-09-10T14:30:00Z"', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, audio: { - text: { - fi: 'Huomio! Intercity 963 klo 17.36 Kupittaalle saapuu lähtöraiteelleen noin 5 minuutin kuluttua.', - sv: 'Observera! Intercity 963 till Kuppis kl 17.36 anländer till sitt avgångsspår om cirka 5 minuter.', - en: 'Attention! Intercity 963 to Kupittaa at 17.36 arrives to its departure track in about 5 minutes.', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { deliveryType: 'DELIVERY_AT', deliveryAt: '2023-09-10T14:30:00Z', @@ -312,18 +276,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { describe('message with timespan "2023-09-01T08:20:00+03:00" to "2023-09-03T15:00:00+03:00" on all weekdays', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20230420115013317', - version: 15, - creationDateTime: '2023-09-03T08:07:00Z', - startValidity: '2023-09-01T21:00:00Z', - endValidity: '2023-09-03T20:59:00Z', - stations: ['KV'], + ...passengerInformationMessageBase, audio: { - text: { - fi: 'Hyvät matkustajat! Rataverkolla tehdään kunnossapitotöitä. 2.-3.9.2023 osa junista Kouvolan ja Pieksämäen, sekä Kouvolan ja Joensuun välillä, on korvattu busseilla. Lisäksi joulukuuhun asti, myös Kouvolan ja Kotkan sataman välillä osa junista on korvattu busseilla. Kouvolassa korvaavat bussit lähtevät asemarakennuksen ja raiteen 1 välistä. Korvatut junavuorot, bussien aikataulut sekä asemakohtaiset lähtöpaikat löydät osoitteesta vr.fi.', - sv: 'Bästa passagerare! Underhållsarbeten pågår på järnvägsnätet. Mellan 2 till 3 september 2023 har en del av tågen mellan Kouvola och Pieksämäki samt Kouvola och Joensuu ersatts av bussar. Dessutom fram till december ersätts några tåg med buss mellan Kouvola och Kotka. I Kouvola avgår ersättningsbussar mellan stationshuset och spår 1. Ersatta tågtider, busstidtabeller och stationsspecifika avgångsplatser finns på vr.fi.', - en: 'Dear passangers! There are changes in train traffic due to maintenance works. Between 2.9.2023 and 3.9.2023, some of the trains between Kouvola and Pieksämäki, and Kouvola and Joensuu, have been replaced by buses. There are also some train replaced by buses until December between Kouvola, and Kotka. In Kouvola, replacement buses leave from between the station building and track 1. Replaced train times, bus schedules and station-specific departure points can be found at vr.fi.', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { startDateTime: '2023-09-01T21:00:00Z', endDateTime: '2023-09-03T20:59:00Z', @@ -380,20 +335,12 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { describe('train with departureDate 2023-09-02 and message with stations [LUS=15:51, PUN=16:00]', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'MVM20230902154021600', - version: 5, - creationDateTime: '2023-09-02T15:51:00Z', - startValidity: '2023-09-02T15:51:00Z', - endValidity: '2023-09-04T00:00:00Z', + ...passengerInformationMessageBase, trainNumber: 750, trainDepartureDate: '2023-09-02', stations: ['LUS', 'PUN', 'REE', 'KIÄ', 'PKY'], audio: { - text: { - fi: 'Huomio ilmoitus! Juna H750 on korvattu linja-autolla Savonlinnan ja Parikkalan välillä. Korvaava linja-auto lähtee aseman edustalta. Bussin matka-aika asemien välillä on pidempi kuin junan matka-aika. Info:vr.fi', - sv: 'Observera! Tåg H750 har ersatts av ett buss mellan Nyslott och Parikkala. Bussen avgår från järnvägsstationen. Restiden för bussen mellan stationerna är längre än tågets restid. Info:vr.fi', - en: 'Attention! Train H750 has been replaced by bus between Savonlinna and Parikkala. The bus leaves from the railwaystation. The bus travel time between stations is longer than the train travel time. Info:vr.fi.', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { deliveryType: 'ON_SCHEDULE', repetitions: 1, @@ -493,18 +440,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { it('should not return messages with type ON_EVENT because they are not currently supported', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, audio: { - text: { - fi: 'Huomio!', - sv: 'Observera!', - en: 'Attention!', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { deliveryType: 'ON_EVENT', }, @@ -526,18 +464,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { it('should not return messages with unsupported deliveryRules type', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, video: { - text: { - fi: 'Huomio!', - sv: 'Observera!', - en: 'Attention!', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { deliveryType: 'NO_SUPPORT_FOR_SUCH_TYPE' as any, }, @@ -555,18 +484,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { it('should return messages that have no deliveryRules', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20220818174358380', - version: 94, - creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, video: { - text: { - fi: 'Huomio!', - sv: 'Observera!', - en: 'Attention!', - }, + text: defaultPassengerInformationTextContent, }, }, ]; @@ -582,18 +502,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { describe('message with timespan "2023-09-11T01:00:00+03:00" to "2023-09-15T04:20:00+03:00" on Mon-Thu', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20230727799106000', - version: 7, - creationDateTime: '2023-09-08T04:16:00Z', - startValidity: '2023-09-10T21:00:00Z', - endValidity: '2023-09-15T20:59:00Z', - stations: ['AVP', 'VEH', 'KTÖ'], + ...passengerInformationMessageBase, video: { - text: { - fi: 'Suomeksi', - sv: 'På svenska', - en: 'In English', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { startDateTime: '2023-09-10T21:00:00Z', endDateTime: '2023-09-15T20:59:00Z', @@ -644,18 +555,9 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { describe('message with timespan "2023-09-08T21:00:00Z" to "2023-09-10T20:59:00Z" on Mon-Sun from 7:45 to 7:50', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { - id: 'SHM20230504174327890', - version: 10, - creationDateTime: '2023-09-09T05:38:00Z', - startValidity: '2023-09-08T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', - stations: ['HKI'], + ...passengerInformationMessageBase, video: { - text: { - fi: 'Raide suljettu. Varmista junasi lähtöraide aikataulunäytöiltä tai osoitteesta junalahdot.fi.', - sv: 'Spåret är stängt. Kolla tågens avgångsspår från tidtabellsskärmarna eller på addressen junalahdot.fi.', - en: 'The track is closed. Check the departure tracks from the timetable displays or at junalahdot.fi.', - }, + text: defaultPassengerInformationTextContent, deliveryRules: { startDateTime: '2023-09-08T21:00:00Z', endDateTime: '2023-09-10T20:59:00Z', From 1a96ce9ca13dfa37592d54b85edb736746f750a7 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:05:03 +0300 Subject: [PATCH 15/22] Improve usePassengerInformationMessages data fetching --- .../usePassengerInformationMessages.test.ts | 353 +++++++++++------- src/hooks/usePassengerInformationMessages.ts | 43 ++- 2 files changed, 260 insertions(+), 136 deletions(-) diff --git a/src/hooks/__tests__/usePassengerInformationMessages.test.ts b/src/hooks/__tests__/usePassengerInformationMessages.test.ts index 9a40b129..15a9bd10 100644 --- a/src/hooks/__tests__/usePassengerInformationMessages.test.ts +++ b/src/hooks/__tests__/usePassengerInformationMessages.test.ts @@ -4,135 +4,234 @@ import { parseISO } from 'date-fns'; import { PassengerInformationMessage } from '../../utils/passengerInformationMessages'; import usePassengerInformationMessages from '../usePassengerInformationMessages'; -const mockPassengerInformationMessages: PassengerInformationMessage[] = [ - { - id: 'SHM20230727799106000', - version: 7, - creationDateTime: '2023-09-08T04:16:00Z', - startValidity: '2023-09-10T21:00:00Z', - endValidity: '2023-09-15T20:59:00Z', - stations: ['AVP', 'VEH', 'KTÖ'], - video: { - text: { - fi: 'Suomeksi', - sv: 'På svenska', - en: 'In English', - }, - deliveryRules: { - startDateTime: '2023-09-11T00:00:00+03:00', - endDateTime: '2023-09-15T23:59:00+03:00', - startTime: '0:01', - endTime: '4:20', - weekDays: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY'], - deliveryType: 'CONTINUOS_VISUALIZATION', - }, - }, - }, -]; +jest.mock('../../utils/passengerInformationMessages', () => ({ + getPassengerInformationMessagesCurrentlyRelevant: ( + msgs: PassengerInformationMessage[] + ) => msgs, +})); describe('usePassengerInformationMessages', () => { - const mockFetchPromise = Promise.resolve({ - json: () => Promise.resolve(mockPassengerInformationMessages), + const baseUrl = 'https://rata.digitraffic.fi/api/v1/passenger-information'; + const defaultRefetchIntervalMs = 10000; + + const getMockFetchResponse = ( + mockResponse: Partial[] + ) => + ({ + json: async () => mockResponse, + ok: true, + } as Response); + + afterEach(() => { + jest.useRealTimers(); + jest.resetAllMocks(); }); - describe.each([undefined, 5000])( - 'using different refresh cycles', - (refreshCycle) => { - const advanceByTime = refreshCycle ?? 10000; - - beforeEach(() => { - jest - .spyOn(global, 'fetch') - .mockImplementation(() => mockFetchPromise as any); - }); - - afterEach(() => { - jest.useRealTimers(); - jest.resetAllMocks(); - }); - - it('should return relevant messages immediately before first refresh cycle', async () => { - jest - .useFakeTimers() - .setSystemTime(parseISO('2023-09-15T04:19:55+03:00')); - - const { result } = renderHook(() => - usePassengerInformationMessages({ - skip: false, - refetchIntervalMs: refreshCycle, - }) - ); - - await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(result.current.messages).toBeDefined()); - - expect(result.current.messages!.length).toStrictEqual(1); - - act(() => { - jest.advanceTimersByTime(advanceByTime); - }); - - await waitFor(() => - expect(result.current.messages!.length).toStrictEqual(0) - ); - }); - - it('should update the relevant messages after each refresh cycle', async () => { - jest - .useFakeTimers() - .setSystemTime(parseISO('2023-09-11T00:00:55+03:00')); - - const { result } = renderHook(() => - usePassengerInformationMessages({ - skip: false, - refetchIntervalMs: refreshCycle, - }) - ); - - await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(result.current.messages).toBeDefined()); - - expect(result.current.messages!.length).toStrictEqual(0); - - act(() => { - jest.advanceTimersByTime(advanceByTime); - }); - - await waitFor(() => - expect(result.current.messages!.length).toStrictEqual(1) - ); - }); - - it('should return error and previously fetched messages if fetching fails', async () => { - jest - .useFakeTimers() - .setSystemTime(parseISO('2023-09-15T04:19:55+03:00')); - - const { result } = renderHook(() => - usePassengerInformationMessages({ - skip: false, - refetchIntervalMs: refreshCycle, - }) - ); - - await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1)); - await waitFor(() => expect(result.current.messages).toBeDefined()); - - expect(result.current.messages!.length).toStrictEqual(1); - - jest - .spyOn(global, 'fetch') - .mockImplementation(() => Promise.reject(new Error('error'))); - - act(() => { - jest.advanceTimersByTime(advanceByTime); - }); - - await waitFor(() => expect(result.current.error).toBeDefined()); - - expect(result.current.messages).toBeDefined(); - expect(result.current.messages!.length).toStrictEqual(1); - }); - } - ); + it('should construct the correct URL and call fetch after each refetch interval', async () => { + jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00+03:00')); + + const queryParameters = { + stationCode: 'HKI', + trainNumber: 123, + trainDepartureDate: '2023-09-20', + onlyGeneral: true, + }; + + const fetchMock = jest + .spyOn(window, 'fetch') + .mockResolvedValue(getMockFetchResponse([])); + + renderHook(() => usePassengerInformationMessages(queryParameters)); + + await waitFor(() => { + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + `${baseUrl}/active?station=HKI&train_number=123&train_departure_date=2023-09-20&only_general=true` + ); + }); + + act(() => { + jest.advanceTimersByTime(defaultRefetchIntervalMs); + }); + + await waitFor(() => { + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + `${baseUrl}/updated-after/2023-09-12T00:00:00+03:00?station=HKI&train_number=123&train_departure_date=2023-09-20&only_general=true` + ); + }); + + act(() => { + jest.advanceTimersByTime(defaultRefetchIntervalMs); + }); + + await waitFor(() => { + expect(fetchMock).toHaveBeenNthCalledWith( + 3, + `${baseUrl}/updated-after/2023-09-12T00:00:10+03:00?station=HKI&train_number=123&train_departure_date=2023-09-20&only_general=true` + ); + }); + }); + + it('should fetch and return messages', async () => { + const mockResponse = [{ id: '1', text: 'Message 1' }]; + jest + .spyOn(window, 'fetch') + .mockResolvedValueOnce(getMockFetchResponse(mockResponse)); + + const { result } = renderHook(() => + usePassengerInformationMessages({ skip: false }) + ); + + await waitFor(() => { + expect(result.current.messages).toEqual(mockResponse); + expect(result.current.error).toBeUndefined(); + }); + }); + + it('should fetch again based on the interval', async () => { + const refetchIntervalMs = 5000; + const mockResponse1 = [{ id: '1', text: 'Message 1' }]; + const mockResponse2 = [{ id: '2', text: 'Message 2' }]; + const mockResponse3 = [{ id: '2', text: 'Message 2 updated' }]; + jest + .spyOn(window, 'fetch') + .mockResolvedValueOnce(getMockFetchResponse(mockResponse1)) + .mockResolvedValueOnce(getMockFetchResponse(mockResponse2)) + .mockResolvedValueOnce(getMockFetchResponse(mockResponse3)); + jest.useFakeTimers(); + + const { result } = renderHook(() => + usePassengerInformationMessages({ skip: false, refetchIntervalMs }) + ); + + await waitFor(() => { + expect(result.current.messages).toEqual(mockResponse1); + expect(result.current.error).toBeUndefined(); + }); + + act(() => { + jest.advanceTimersByTime(refetchIntervalMs); + }); + + await waitFor(() => { + expect(result.current.messages?.length).toBe(2); + expect(result.current.messages).toEqual([ + ...mockResponse2, + ...mockResponse1, + ]); + expect(result.current.error).toBeUndefined(); + }); + + act(() => { + jest.advanceTimersByTime(refetchIntervalMs); + }); + + await waitFor(() => { + expect(result.current.messages?.length).toBe(2); + expect(result.current.messages).toEqual([ + ...mockResponse3, + ...mockResponse1, + ]); + expect(result.current.error).toBeUndefined(); + }); + }); + + it('should handle fetch error and return it', async () => { + jest.spyOn(window, 'fetch').mockRejectedValueOnce(new Error('Fetch error')); + + const { result } = renderHook(() => + usePassengerInformationMessages({ skip: false }) + ); + + await waitFor(() => { + expect(result.current.messages).toBeUndefined(); + expect(result.current.error).toBeDefined(); + }); + }); + + it('should handle non-ok fetch response and return it', async () => { + jest.spyOn(window, 'fetch').mockResolvedValueOnce({ + ok: false, + } as Response); + + const { result } = renderHook(() => + usePassengerInformationMessages({ skip: false }) + ); + + await waitFor(() => { + expect(result.current.messages).toBeUndefined(); + expect(result.current.error).toBeDefined(); + }); + }); + + it('should not fetch when skip is true', () => { + jest.spyOn(window, 'fetch').mockImplementation(() => { + throw new Error('Fetch should not be called'); + }); + + const { result } = renderHook(() => + usePassengerInformationMessages({ skip: true }) + ); + + expect(result.current.messages).toBeUndefined(); + expect(result.current.error).toBeUndefined(); + }); + + it('should clear messages and use api path /active on first fetch when props change', async () => { + jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00+03:00')); + const mockResponse1 = [{ id: '1', text: 'Message 1 HKI' }]; + const mockResponse2 = [{ id: '1', text: 'Message 1 PSL' }]; + const fetchSpy = jest + .spyOn(window, 'fetch') + .mockResolvedValueOnce(getMockFetchResponse(mockResponse1)) + .mockResolvedValueOnce(getMockFetchResponse(mockResponse2)) + .mockResolvedValueOnce(getMockFetchResponse(mockResponse2)) + .mockResolvedValueOnce(getMockFetchResponse(mockResponse1)); + + const { result, rerender } = renderHook( + (stationCode: string) => + usePassengerInformationMessages({ skip: false, stationCode }), + { initialProps: 'HKI' } + ); + + await waitFor(() => { + expect(result.current.messages).toEqual(mockResponse1); + expect(fetchSpy).toHaveBeenNthCalledWith( + 1, + `${baseUrl}/active?station=HKI` + ); + }); + + rerender('PSL'); + + await waitFor(() => { + expect(result.current.messages).toEqual(mockResponse2); + expect(fetchSpy).toHaveBeenNthCalledWith( + 2, + `${baseUrl}/active?station=PSL` + ); + }); + + act(() => { + jest.advanceTimersByTime(defaultRefetchIntervalMs); + }); + + await waitFor(() => { + expect(fetchSpy).toHaveBeenNthCalledWith( + 3, + `${baseUrl}/updated-after/2023-09-12T00:00:00+03:00?station=PSL` + ); + }); + + rerender('HKI'); + + await waitFor(() => { + expect(result.current.messages).toEqual(mockResponse1); + expect(fetchSpy).toHaveBeenNthCalledWith( + 4, + `${baseUrl}/active?station=HKI` + ); + }); + }); }); diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index fe40189f..ad49c490 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -1,4 +1,7 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { formatISO } from 'date-fns'; +import { unionBy } from 'lodash'; import { getPassengerInformationMessagesCurrentlyRelevant, @@ -14,6 +17,8 @@ type PassengerInformationMessageQuery = { refetchIntervalMs?: number; }; +const apiBaseUrl = 'https://rata.digitraffic.fi/api/v1/passenger-information'; + export default function usePassengerInformationMessages({ skip, stationCode, @@ -23,6 +28,7 @@ export default function usePassengerInformationMessages({ refetchIntervalMs = 10000, }: PassengerInformationMessageQuery) { const [messages, setMessages] = useState(); + const lastFetchTimeRef = useRef(); const [error, setError] = useState(); const params = useMemo(() => { @@ -39,15 +45,28 @@ export default function usePassengerInformationMessages({ if (!skip) { const fetchData = async () => { try { + let url: string; + if (!lastFetchTimeRef.current) { + url = `${apiBaseUrl}/active?${params}`; + } else { + url = `${apiBaseUrl}/updated-after/${formatISO( + lastFetchTimeRef.current + )}?${params}`; + } + setError(undefined); - const res = await fetch( - `https://rata.digitraffic.fi/api/v1/passenger-information/active?${params}` - ); - const allMessages = + lastFetchTimeRef.current = new Date(); + + const res = await fetch(url); + + if (!res.ok) { + throw new Error(`Failed to fetch data (status ${res.status})`); + } + + const latestMessages = (await res.json()) as PassengerInformationMessage[]; - const relevantMessages = - getPassengerInformationMessagesCurrentlyRelevant(allMessages); - setMessages(relevantMessages); + + setMessages((msgs) => unionBy(latestMessages, msgs, 'id')); } catch (error) { setError(error); } @@ -57,11 +76,17 @@ export default function usePassengerInformationMessages({ } return () => { + setMessages(undefined); + lastFetchTimeRef.current = undefined; if (interval) { clearInterval(interval); } }; }, [params, refetchIntervalMs, skip]); - return { messages, error }; + const relevantMessages = messages + ? getPassengerInformationMessagesCurrentlyRelevant(messages) + : undefined; + + return { messages: relevantMessages, error }; } From 5350052bbfb0540e5f248f0627cf6cc76ced840e Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:33:33 +0300 Subject: [PATCH 16/22] Add check for overall passenger message validity --- .../passengerInformationMessages.test.ts | 51 ++++++++++++++++++- src/utils/passengerInformationMessages.ts | 16 +++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 1c934dfb..111f7ff6 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -64,12 +64,18 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { jest.useFakeTimers(); }); + beforeEach(() => { + // Default system time within validity of passengerInformationMessageBase + // that can be overwritten by individual tests + jest.setSystemTime(parseISO('2023-09-09T00:00:00Z')); + }); + const passengerInformationMessageBase: PassengerInformationMessage = { id: 'SHM20220818174358380', version: 94, creationDateTime: '2023-09-10T14:37:00Z', - startValidity: '2023-09-09T21:00:00Z', - endValidity: '2023-09-10T20:59:00Z', + startValidity: '2023-09-01T00:00:00Z', + endValidity: '2023-09-14T21:00:00Z', stations: ['HKI'], }; @@ -123,6 +129,47 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { }); }); + describe('overall message validity', () => { + const passengerInformationMessages: PassengerInformationMessage[] = [ + { + ...passengerInformationMessageBase, + startValidity: '2023-09-01T00:00:00Z', + endValidity: '2023-09-14T21:00:00Z', + audio: { + text: defaultPassengerInformationTextContent, + }, + }, + ]; + + it.each(['2023-09-01T00:00:00Z', '2023-09-14T21:00:00Z'])( + 'should be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(1); + } + ); + + it.each(['2023-08-31T23:59:59Z', '2023-09-14T21:00:01Z'])( + 'should not be relevant at time %p', + (nowISO: string) => { + jest.setSystemTime(parseISO(nowISO)); + + const relevantMessages = + getPassengerInformationMessagesCurrentlyRelevant( + passengerInformationMessages + ); + + expect(relevantMessages.length).toBe(0); + } + ); + }); + describe('audio', () => { it('should not return messages with unsupported deliveryRules type', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts index ade3bd04..4cf8945b 100644 --- a/src/utils/passengerInformationMessages.ts +++ b/src/utils/passengerInformationMessages.ts @@ -144,6 +144,17 @@ function isMessageRelevant( now: Date, train?: TrainByStationFragment ): boolean { + if ( + !isWithinTimeSpan( + { + startDateTime: message.startValidity, + endDateTime: message.endValidity, + }, + now + ) + ) { + return false; + } if (message.video && isVideoMessageRelevant(message, now)) { return true; } @@ -329,7 +340,10 @@ const getCurrentWeekdayInEET = (date: Date): Weekday => { * @param time - The optional time part (format: "H:mm") in Europe/Helsinki (EET) time zone. * @returns A new Date constructed from the given parameters. */ -const getDateTime = (dateTimeISO: string | Date, timeISOinEET?: string) => { +const getDateTime = ( + dateTimeISO: string | Date, + timeISOinEET?: string +): Date => { let dateTime: Date; if (dateTimeISO instanceof Date) { dateTime = dateTimeISO; From 452318ab270e952e61f490a8834d2ec171142ef0 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Thu, 21 Sep 2023 18:04:07 +0300 Subject: [PATCH 17/22] Fix issues usePassengerInformationMessages tests and code style --- .../usePassengerInformationMessages.test.ts | 10 +-- src/hooks/usePassengerInformationMessages.ts | 62 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/hooks/__tests__/usePassengerInformationMessages.test.ts b/src/hooks/__tests__/usePassengerInformationMessages.test.ts index 15a9bd10..f17384d5 100644 --- a/src/hooks/__tests__/usePassengerInformationMessages.test.ts +++ b/src/hooks/__tests__/usePassengerInformationMessages.test.ts @@ -28,7 +28,7 @@ describe('usePassengerInformationMessages', () => { }); it('should construct the correct URL and call fetch after each refetch interval', async () => { - jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00+03:00')); + jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00Z')); const queryParameters = { stationCode: 'HKI', @@ -57,7 +57,7 @@ describe('usePassengerInformationMessages', () => { await waitFor(() => { expect(fetchMock).toHaveBeenNthCalledWith( 2, - `${baseUrl}/updated-after/2023-09-12T00:00:00+03:00?station=HKI&train_number=123&train_departure_date=2023-09-20&only_general=true` + `${baseUrl}/updated-after/2023-09-12T00:00:00Z?station=HKI&train_number=123&train_departure_date=2023-09-20&only_general=true` ); }); @@ -68,7 +68,7 @@ describe('usePassengerInformationMessages', () => { await waitFor(() => { expect(fetchMock).toHaveBeenNthCalledWith( 3, - `${baseUrl}/updated-after/2023-09-12T00:00:10+03:00?station=HKI&train_number=123&train_departure_date=2023-09-20&only_general=true` + `${baseUrl}/updated-after/2023-09-12T00:00:10Z?station=HKI&train_number=123&train_departure_date=2023-09-20&only_general=true` ); }); }); @@ -179,7 +179,7 @@ describe('usePassengerInformationMessages', () => { }); it('should clear messages and use api path /active on first fetch when props change', async () => { - jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00+03:00')); + jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00Z')); const mockResponse1 = [{ id: '1', text: 'Message 1 HKI' }]; const mockResponse2 = [{ id: '1', text: 'Message 1 PSL' }]; const fetchSpy = jest @@ -220,7 +220,7 @@ describe('usePassengerInformationMessages', () => { await waitFor(() => { expect(fetchSpy).toHaveBeenNthCalledWith( 3, - `${baseUrl}/updated-after/2023-09-12T00:00:00+03:00?station=PSL` + `${baseUrl}/updated-after/2023-09-12T00:00:00Z?station=PSL` ); }); diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index ad49c490..89091aef 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -1,6 +1,5 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { formatISO } from 'date-fns'; import { unionBy } from 'lodash'; import { @@ -40,37 +39,38 @@ export default function usePassengerInformationMessages({ }); }, [onlyGeneral, stationCode, trainDepartureDate, trainNumber]); + const fetchData = useCallback(async () => { + try { + let url: string; + if (!lastFetchTimeRef.current) { + url = `${apiBaseUrl}/active?${params}`; + } else { + url = `${apiBaseUrl}/updated-after/${ + lastFetchTimeRef.current.toISOString().split('.')[0] + 'Z' + }?${params}`; + } + + setError(undefined); + lastFetchTimeRef.current = new Date(); + + const res = await fetch(url); + + if (!res.ok) { + throw new Error(`Failed to fetch data (status ${res.status})`); + } + + const latestMessages = + (await res.json()) as PassengerInformationMessage[]; + + setMessages((msgs) => unionBy(latestMessages, msgs, 'id')); + } catch (error) { + setError(error); + } + }, [params]); + useEffect(() => { let interval: NodeJS.Timer | null = null; if (!skip) { - const fetchData = async () => { - try { - let url: string; - if (!lastFetchTimeRef.current) { - url = `${apiBaseUrl}/active?${params}`; - } else { - url = `${apiBaseUrl}/updated-after/${formatISO( - lastFetchTimeRef.current - )}?${params}`; - } - - setError(undefined); - lastFetchTimeRef.current = new Date(); - - const res = await fetch(url); - - if (!res.ok) { - throw new Error(`Failed to fetch data (status ${res.status})`); - } - - const latestMessages = - (await res.json()) as PassengerInformationMessage[]; - - setMessages((msgs) => unionBy(latestMessages, msgs, 'id')); - } catch (error) { - setError(error); - } - }; interval = setInterval(fetchData, refetchIntervalMs); fetchData(); } @@ -82,7 +82,7 @@ export default function usePassengerInformationMessages({ clearInterval(interval); } }; - }, [params, refetchIntervalMs, skip]); + }, [fetchData, refetchIntervalMs, skip]); const relevantMessages = messages ? getPassengerInformationMessagesCurrentlyRelevant(messages) From b4f85fafa1efc2a7effe75db658c3d8f8558ef76 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Thu, 21 Sep 2023 18:30:09 +0300 Subject: [PATCH 18/22] Change default refetch interval to 20 seconds --- .../__tests__/usePassengerInformationMessages.test.ts | 8 +++++--- src/hooks/usePassengerInformationMessages.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/hooks/__tests__/usePassengerInformationMessages.test.ts b/src/hooks/__tests__/usePassengerInformationMessages.test.ts index f17384d5..bc037140 100644 --- a/src/hooks/__tests__/usePassengerInformationMessages.test.ts +++ b/src/hooks/__tests__/usePassengerInformationMessages.test.ts @@ -12,7 +12,7 @@ jest.mock('../../utils/passengerInformationMessages', () => ({ describe('usePassengerInformationMessages', () => { const baseUrl = 'https://rata.digitraffic.fi/api/v1/passenger-information'; - const defaultRefetchIntervalMs = 10000; + const defaultRefetchIntervalMs = 20000; const getMockFetchResponse = ( mockResponse: Partial[] @@ -29,12 +29,14 @@ describe('usePassengerInformationMessages', () => { it('should construct the correct URL and call fetch after each refetch interval', async () => { jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00Z')); + const refetchIntervalMs = 10000; const queryParameters = { stationCode: 'HKI', trainNumber: 123, trainDepartureDate: '2023-09-20', onlyGeneral: true, + refetchIntervalMs, }; const fetchMock = jest @@ -51,7 +53,7 @@ describe('usePassengerInformationMessages', () => { }); act(() => { - jest.advanceTimersByTime(defaultRefetchIntervalMs); + jest.advanceTimersByTime(refetchIntervalMs); }); await waitFor(() => { @@ -62,7 +64,7 @@ describe('usePassengerInformationMessages', () => { }); act(() => { - jest.advanceTimersByTime(defaultRefetchIntervalMs); + jest.advanceTimersByTime(refetchIntervalMs); }); await waitFor(() => { diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index 89091aef..52389982 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -24,7 +24,7 @@ export default function usePassengerInformationMessages({ trainNumber, trainDepartureDate, onlyGeneral, - refetchIntervalMs = 10000, + refetchIntervalMs = 20000, }: PassengerInformationMessageQuery) { const [messages, setMessages] = useState(); const lastFetchTimeRef = useRef(); From 9abb9160eeac6161a20104202abd0d4ea271503c Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Thu, 21 Sep 2023 18:49:40 +0300 Subject: [PATCH 19/22] Simplify creating search params in usePassengerInformationMessages --- src/hooks/usePassengerInformationMessages.ts | 31 +++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index 52389982..7b50e050 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -18,6 +18,19 @@ type PassengerInformationMessageQuery = { const apiBaseUrl = 'https://rata.digitraffic.fi/api/v1/passenger-information'; +function createSearchParams(params: Partial>) { + const filteredParams: Record = {}; + + for (const key in params) { + const paramValue = params[key]; + if (paramValue != null) { + filteredParams[key] = paramValue; + } + } + + return new URLSearchParams(filteredParams); +} + export default function usePassengerInformationMessages({ skip, stationCode, @@ -30,14 +43,16 @@ export default function usePassengerInformationMessages({ const lastFetchTimeRef = useRef(); const [error, setError] = useState(); - const params = useMemo(() => { - return new URLSearchParams({ - ...(stationCode && { station: stationCode }), - ...(trainNumber && { train_number: trainNumber?.toString() }), - ...(trainDepartureDate && { train_departure_date: trainDepartureDate }), - ...(onlyGeneral && { only_general: onlyGeneral?.toString() }), - }); - }, [onlyGeneral, stationCode, trainDepartureDate, trainNumber]); + const params = useMemo( + () => + createSearchParams({ + station: stationCode, + train_number: trainNumber?.toString(), + train_departure_date: trainDepartureDate, + only_general: onlyGeneral?.toString(), + }), + [onlyGeneral, stationCode, trainDepartureDate, trainNumber] + ); const fetchData = useCallback(async () => { try { From 28efb3a33c9ba6ab685fd05dce14cc906d068d5a Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Thu, 21 Sep 2023 19:49:41 +0300 Subject: [PATCH 20/22] Change default refetch interval to 10s and use 20s in pages --- src/components/TrainInfoContainer.tsx | 2 +- .../__tests__/usePassengerInformationMessages.test.ts | 9 ++++----- src/hooks/usePassengerInformationMessages.ts | 2 +- src/pages/[station].tsx | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/TrainInfoContainer.tsx b/src/components/TrainInfoContainer.tsx index e9a7e334..5e1e84f2 100644 --- a/src/components/TrainInfoContainer.tsx +++ b/src/components/TrainInfoContainer.tsx @@ -45,7 +45,7 @@ function TrainInfoContainer({ train }: TrainInfoContainerProps) { const { messages: passengerInformationMessages } = usePassengerInformationMessages({ skip: !train, - refetchIntervalMs: 10000, + refetchIntervalMs: 20000, trainNumber: train?.trainNumber, trainDepartureDate: train?.departureDate, }); diff --git a/src/hooks/__tests__/usePassengerInformationMessages.test.ts b/src/hooks/__tests__/usePassengerInformationMessages.test.ts index bc037140..01cc748b 100644 --- a/src/hooks/__tests__/usePassengerInformationMessages.test.ts +++ b/src/hooks/__tests__/usePassengerInformationMessages.test.ts @@ -12,7 +12,7 @@ jest.mock('../../utils/passengerInformationMessages', () => ({ describe('usePassengerInformationMessages', () => { const baseUrl = 'https://rata.digitraffic.fi/api/v1/passenger-information'; - const defaultRefetchIntervalMs = 20000; + const defaultRefetchIntervalMs = 10000; const getMockFetchResponse = ( mockResponse: Partial[] @@ -29,14 +29,13 @@ describe('usePassengerInformationMessages', () => { it('should construct the correct URL and call fetch after each refetch interval', async () => { jest.useFakeTimers().setSystemTime(parseISO('2023-09-12T00:00:00Z')); - const refetchIntervalMs = 10000; const queryParameters = { stationCode: 'HKI', trainNumber: 123, trainDepartureDate: '2023-09-20', onlyGeneral: true, - refetchIntervalMs, + defaultRefetchIntervalMs, }; const fetchMock = jest @@ -53,7 +52,7 @@ describe('usePassengerInformationMessages', () => { }); act(() => { - jest.advanceTimersByTime(refetchIntervalMs); + jest.advanceTimersByTime(defaultRefetchIntervalMs); }); await waitFor(() => { @@ -64,7 +63,7 @@ describe('usePassengerInformationMessages', () => { }); act(() => { - jest.advanceTimersByTime(refetchIntervalMs); + jest.advanceTimersByTime(defaultRefetchIntervalMs); }); await waitFor(() => { diff --git a/src/hooks/usePassengerInformationMessages.ts b/src/hooks/usePassengerInformationMessages.ts index 7b50e050..02de6941 100644 --- a/src/hooks/usePassengerInformationMessages.ts +++ b/src/hooks/usePassengerInformationMessages.ts @@ -37,7 +37,7 @@ export default function usePassengerInformationMessages({ trainNumber, trainDepartureDate, onlyGeneral, - refetchIntervalMs = 20000, + refetchIntervalMs = 10000, }: PassengerInformationMessageQuery) { const [messages, setMessages] = useState(); const lastFetchTimeRef = useRef(); diff --git a/src/pages/[station].tsx b/src/pages/[station].tsx index d7d64bd1..1ab33655 100644 --- a/src/pages/[station].tsx +++ b/src/pages/[station].tsx @@ -68,7 +68,7 @@ const Station: NextPageWithLayout = () => { const { messages: passengerInformationMessages } = usePassengerInformationMessages({ skip: stationCode == null, - refetchIntervalMs: 10000, + refetchIntervalMs: 20000, onlyGeneral: true, stationCode: stationCode, }); From 5851713ba9a6da1d2a46a68110043f953ebf4d41 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Fri, 22 Sep 2023 16:24:04 +0300 Subject: [PATCH 21/22] Simplify logic of getting passenger message for current language --- .../PassengerInformationMessageAlert.tsx | 20 +-- .../PassengerInformationMessagesDialog.tsx | 20 ++- .../passengerInformationMessages.test.ts | 126 ++++++++++++------ src/utils/passengerInformationMessages.ts | 59 ++++---- 4 files changed, 125 insertions(+), 100 deletions(-) diff --git a/src/components/PassengerInformationMessageAlert.tsx b/src/components/PassengerInformationMessageAlert.tsx index e23c6f8d..143ad626 100644 --- a/src/components/PassengerInformationMessageAlert.tsx +++ b/src/components/PassengerInformationMessageAlert.tsx @@ -1,7 +1,10 @@ import { Alert, ButtonBase } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { PassengerInformationMessage } from '../utils/passengerInformationMessages'; +import { + getPassengerInformationMessageForLanguage, + PassengerInformationMessage, +} from '../utils/passengerInformationMessages'; type PassengerInformationMessageAlertProps = { onClick: () => void; @@ -18,16 +21,6 @@ const PassengerInformationMessageAlert = ({ const firstMessage = passengerInformationMessages[0]; - const getTextForCurrentLanguage = (text?: Record) => { - if (!text) { - return ''; - } - if (i18n.resolvedLanguage in text) { - return text[i18n.resolvedLanguage]; - } - return text.fi; - }; - return ( - {getTextForCurrentLanguage( - (firstMessage.video ?? firstMessage.audio)?.text + {getPassengerInformationMessageForLanguage( + firstMessage, + i18n.resolvedLanguage )} {passengerInformationMessages.length > 1 && ( + {passengerInformationMessages.length - 1} diff --git a/src/components/PassengerInformationMessagesDialog.tsx b/src/components/PassengerInformationMessagesDialog.tsx index 1bc67fcf..88e6937e 100644 --- a/src/components/PassengerInformationMessagesDialog.tsx +++ b/src/components/PassengerInformationMessagesDialog.tsx @@ -3,7 +3,10 @@ import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; import { useTranslation } from 'react-i18next'; -import { PassengerInformationMessage } from '../utils/passengerInformationMessages'; +import { + getPassengerInformationMessageForLanguage, + PassengerInformationMessage, +} from '../utils/passengerInformationMessages'; type PassengerInformationMessagesDialogProps = { onClose: () => void; @@ -25,23 +28,16 @@ const PassengerInformationMessagesDialog = ( onClose(); }; - const getTextForCurrentLanguage = (text?: Record) => { - if (!text) { - return ''; - } - if (i18n.resolvedLanguage in text) { - return text[i18n.resolvedLanguage]; - } - return text.fi; - }; - return ( {t('alerts')} {passengerInformationMessages?.map((m) => ( - {getTextForCurrentLanguage((m.video ?? m.audio)?.text)} + {getPassengerInformationMessageForLanguage( + m, + i18n.resolvedLanguage + )} ))} diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 111f7ff6..84b43c9d 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -5,11 +5,93 @@ import { TrainByStationFragment, } from '../../graphql/generated/digitraffic'; import { + getPassengerInformationMessageForLanguage, getPassengerInformationMessagesByStation, getPassengerInformationMessagesCurrentlyRelevant, PassengerInformationMessage, } from '../passengerInformationMessages'; +describe('getPassengerInformationMessageForLanguage', () => { + const passengerInformationMessageBase: PassengerInformationMessage = { + id: '', + version: 1, + creationDateTime: '', + startValidity: '', + endValidity: '', + stations: [], + }; + + describe('message with audio only', () => { + const passengerInformationMessage: PassengerInformationMessage = { + ...passengerInformationMessageBase, + audio: { + text: { + fi: 'Huomio! junalahdot.fi', + sv: 'Observera! junalahdot.fi', + en: 'Attention! vr.fi', + }, + }, + }; + + it('should return text for given language and add junaan.fi to text if the text contains junalahdot.fi', () => { + expect( + getPassengerInformationMessageForLanguage( + passengerInformationMessage, + 'fi' + ) + ).toBe('Huomio! junaan.fi / junalahdot.fi'); + + expect( + getPassengerInformationMessageForLanguage( + passengerInformationMessage, + 'sv' + ) + ).toBe('Observera! junaan.fi / junalahdot.fi'); + + expect( + getPassengerInformationMessageForLanguage( + passengerInformationMessage, + 'en' + ) + ).toBe('Attention! vr.fi'); + }); + }); + + describe('message with video only', () => { + const passengerInformationMessage: PassengerInformationMessage = { + ...passengerInformationMessageBase, + video: { + text: { + fi: 'Huomio! junalahdot.fi', + sv: undefined, + en: 'Attention! junalahdot.fi', + }, + }, + }; + + it('should return text for given language and add junaan.fi to text if the text contains junalahdot.fi', () => { + expect( + getPassengerInformationMessageForLanguage( + passengerInformationMessage, + 'fi' + ) + ).toBe('Huomio! junaan.fi / junalahdot.fi'); + expect( + getPassengerInformationMessageForLanguage( + passengerInformationMessage, + 'sv' + ) + ).toBeUndefined(); + expect( + getPassengerInformationMessageForLanguage( + passengerInformationMessage, + 'en' + ) + ).toBe('Attention! junaan.fi / junalahdot.fi'); + }); + }); +}); + describe('getPassengerInformationMessagesByStation', () => { it('should add the message to the station present in stations array when there is only single station', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ @@ -85,50 +167,6 @@ describe('getPassengerInformationMessagesCurrentlyRelevant', () => { en: 'Attention!', }; - describe('text content modifications', () => { - it('should add junaan.fi text to both audio and video text content if the text contains junalahdot.fi', () => { - const passengerInformationMessages: PassengerInformationMessage[] = [ - { - ...passengerInformationMessageBase, - audio: { - text: { - fi: 'Huomio! junalahdot.fi', - sv: 'Observera! junalahdot.fi', - en: 'Attention! vr.fi', - }, - }, - video: { - text: { - fi: 'Huomio! junalahdot.fi', - sv: undefined, - en: 'Attention! junalahdot.fi', - }, - }, - }, - ]; - - const relevantMessages = getPassengerInformationMessagesCurrentlyRelevant( - passengerInformationMessages - ); - - expect(relevantMessages.length).toBe(1); - expect(relevantMessages[0].audio?.text?.fi).toBe( - 'Huomio! junaan.fi / junalahdot.fi' - ); - expect(relevantMessages[0].audio?.text?.sv).toBe( - 'Observera! junaan.fi / junalahdot.fi' - ); - expect(relevantMessages[0].audio?.text?.en).toBe('Attention! vr.fi'); - expect(relevantMessages[0].video?.text?.fi).toBe( - 'Huomio! junaan.fi / junalahdot.fi' - ); - expect(relevantMessages[0].video?.text?.sv).toBeUndefined(); - expect(relevantMessages[0].video?.text?.en).toBe( - 'Attention! junaan.fi / junalahdot.fi' - ); - }); - }); - describe('overall message validity', () => { const passengerInformationMessages: PassengerInformationMessage[] = [ { diff --git a/src/utils/passengerInformationMessages.ts b/src/utils/passengerInformationMessages.ts index 4cf8945b..3f311489 100644 --- a/src/utils/passengerInformationMessages.ts +++ b/src/utils/passengerInformationMessages.ts @@ -73,6 +73,27 @@ type Weekday = | 'SATURDAY' | 'SUNDAY'; +export function getPassengerInformationMessageForLanguage( + message: PassengerInformationMessage, + languageCode: string +) { + const textContent: Record | undefined = ( + message.video ?? message.audio + )?.text; + + if (!textContent) { + return ''; + } + + let text: string; + if (languageCode in textContent) { + text = textContent[languageCode]; + } else { + text = textContent.fi; + } + return getImprovedText(text); +} + export function getPassengerInformationMessagesByStation( passengerInformationMessages: PassengerInformationMessage[] | undefined ) { @@ -101,42 +122,18 @@ export function getPassengerInformationMessagesCurrentlyRelevant( const relevantMessages = passengerInformationMessages.filter((message) => isMessageRelevant(message, now, train) ); - relevantMessages.forEach((m) => { - if (m.audio?.text) { - m.audio.text = getImprovedTextContent(m.audio.text); - } - if (m.video?.text) { - m.video.text = getImprovedTextContent(m.video.text); - } - }); return relevantMessages; } -function getImprovedTextContent( - textContent: PassengerInformationTextContent -): PassengerInformationTextContent { - const modifiedContent: PassengerInformationTextContent = {}; - - for (const lng of Object.keys(textContent) as Array< - keyof PassengerInformationTextContent - >) { - const currentText = textContent[lng]; - - if (currentText) { - const idx = currentText.indexOf('junalahdot.fi'); - - if (idx !== -1) { - modifiedContent[lng] = - currentText.slice(0, idx) + 'junaan.fi / ' + currentText.slice(idx); - } else { - modifiedContent[lng] = currentText; - } - } else { - modifiedContent[lng] = currentText; +function getImprovedText(text: string | undefined): string | undefined { + if (text) { + const idx = text.indexOf('junalahdot.fi'); + + if (idx !== -1) { + return text.slice(0, idx) + 'junaan.fi / ' + text.slice(idx); } } - - return modifiedContent; + return text; } function isMessageRelevant( From 105460aed95ac6d5d4256d53403e0c5f4a0d63a7 Mon Sep 17 00:00:00 2001 From: Vili Ketonen <25618592+viliket@users.noreply.github.com> Date: Sat, 23 Sep 2023 09:15:37 +0300 Subject: [PATCH 22/22] Improve tests for getPassengerInformationMessageForLanguage --- .../passengerInformationMessages.test.ts | 119 ++++++++---------- 1 file changed, 54 insertions(+), 65 deletions(-) diff --git a/src/utils/__tests__/passengerInformationMessages.test.ts b/src/utils/__tests__/passengerInformationMessages.test.ts index 84b43c9d..ef39700e 100644 --- a/src/utils/__tests__/passengerInformationMessages.test.ts +++ b/src/utils/__tests__/passengerInformationMessages.test.ts @@ -21,74 +21,63 @@ describe('getPassengerInformationMessageForLanguage', () => { stations: [], }; - describe('message with audio only', () => { - const passengerInformationMessage: PassengerInformationMessage = { - ...passengerInformationMessageBase, - audio: { - text: { - fi: 'Huomio! junalahdot.fi', - sv: 'Observera! junalahdot.fi', - en: 'Attention! vr.fi', - }, + const videoAudioMessage: PassengerInformationMessage = { + ...passengerInformationMessageBase, + video: { + text: { + en: 'English video text! junalahdot.fi', + fi: 'Finnish video text! junalahdot.fi', }, - }; - - it('should return text for given language and add junaan.fi to text if the text contains junalahdot.fi', () => { - expect( - getPassengerInformationMessageForLanguage( - passengerInformationMessage, - 'fi' - ) - ).toBe('Huomio! junaan.fi / junalahdot.fi'); - - expect( - getPassengerInformationMessageForLanguage( - passengerInformationMessage, - 'sv' - ) - ).toBe('Observera! junaan.fi / junalahdot.fi'); - - expect( - getPassengerInformationMessageForLanguage( - passengerInformationMessage, - 'en' - ) - ).toBe('Attention! vr.fi'); - }); - }); + }, + audio: { + text: { + en: 'English audio text! junalahdot.fi', + fi: 'Finnish audio text! junalahdot.fi', + }, + }, + }; - describe('message with video only', () => { - const passengerInformationMessage: PassengerInformationMessage = { - ...passengerInformationMessageBase, - video: { - text: { - fi: 'Huomio! junalahdot.fi', - sv: undefined, - en: 'Attention! junalahdot.fi', - }, + const audioOnlyMessage: PassengerInformationMessage = { + ...passengerInformationMessageBase, + audio: { + text: { + en: 'English audio text! junalahdot.fi', + sv: 'Swedish audio text!', + fi: 'Finnish audio text! junalahdot.fi', }, - }; - - it('should return text for given language and add junaan.fi to text if the text contains junalahdot.fi', () => { - expect( - getPassengerInformationMessageForLanguage( - passengerInformationMessage, - 'fi' - ) - ).toBe('Huomio! junaan.fi / junalahdot.fi'); - expect( - getPassengerInformationMessageForLanguage( - passengerInformationMessage, - 'sv' - ) - ).toBeUndefined(); - expect( - getPassengerInformationMessageForLanguage( - passengerInformationMessage, - 'en' - ) - ).toBe('Attention! junaan.fi / junalahdot.fi'); - }); + }, + }; + + it('should return the message from video content when available in the specified language', () => { + const result = getPassengerInformationMessageForLanguage( + videoAudioMessage, + 'en' + ); + expect(result).toBe('English video text! junaan.fi / junalahdot.fi'); + }); + + it('should return the message from audio content when video content is not available in the specified language', () => { + const result = getPassengerInformationMessageForLanguage( + audioOnlyMessage, + 'sv' + ); + expect(result).toBe('Swedish audio text!'); + }); + + it('should return the default language message when the specified language is not available', () => { + const result = getPassengerInformationMessageForLanguage( + videoAudioMessage, + 'fr' + ); + expect(result).toBe('Finnish video text! junaan.fi / junalahdot.fi'); + }); + + it('should return an empty string when both video and audio content are undefined', () => { + const result = getPassengerInformationMessageForLanguage( + passengerInformationMessageBase, + 'en' + ); + expect(result).toBe(''); }); });