From 10f7ea1536c2394404b23d818ac4cf6ed9fd9b07 Mon Sep 17 00:00:00 2001 From: Ryan Kotval Date: Fri, 7 Jun 2024 12:06:45 -0500 Subject: [PATCH] refactor(Schedules): Consolidate schedules react trees into one tree (#2069) * refactor(Schedules): Consolidate schedules react trees into one tree * Added empty support and reworked data importing * Cleaned up code * Working on the tests * Rewrote tests, and working on updating react state properly * Reworked the schedule modal content tests * Fixed tests * Fixed linting * Reworked schedule loader into schedule page * Moved state management fully to react-redux * Fixed hours of operation tests * Updated schedule direction test * Removed console logs * Fixed tests for SchedulePage and the Line Diagram * Fixed ScheduleFinderModalTests * Fixed ScheduleModalContentTests * Removed un-needed import * Fixed schedulefinderformtest * Fixed schedule finder tests, and removed unsupported test * Removed old snapshots * Removed unused functions from the old schedule store * Fixed linting * Consolidated render logic to use new helper * Cleaned up comment * Fixed code covereage * Addressed PR feedback * Addressed linting * Changed schedule smoke test to support flexible directions * Actually fixed the smoke tests * Cleaned up skipped test --- assets/ts/__tests__/test-render-helper.tsx | 70 + assets/ts/jest.config.js | 1 + .../RapidTransitHoursOfOperation.tsx | 5 +- .../schedule/components/ScheduleDirection.tsx | 3 +- .../ts/schedule/components/ScheduleFinder.tsx | 41 +- .../ts/schedule/components/ScheduleLoader.tsx | 337 ----- .../ts/schedule/components/SchedulePage.tsx | 429 +++++++ .../__tests__/HoursOfOperationTest.tsx | 113 +- .../RapidTransitHoursOfOperationTest.tsx | 35 +- .../__tests__/ScheduleDirectionTest.tsx | 516 ++++---- .../__tests__/ScheduleFinderTest.tsx | 157 +-- .../__tests__/ScheduleLoaderTest.tsx | 935 ++------------ .../components/__tests__/SchedulePageTest.tsx | 992 ++++++++++++++ .../ScheduleDirectionTest.tsx.snap | 1144 +++++++++-------- .../__snapshots__/ScheduleFinderTest.tsx.snap | 64 - .../components/line-diagram/LineDiagram.tsx | 32 +- .../__tests__/LineDiagramTest.tsx | 111 +- .../__snapshots__/LineDiagramTest.tsx.snap | 141 -- .../schedule-finder/ScheduleFinderForm.tsx | 20 +- .../schedule-finder/ScheduleFinderModal.tsx | 27 +- .../schedule-finder/ScheduleModalContent.tsx | 69 +- .../__tests__/ScheduleFinderFormTest.tsx | 143 +-- .../__tests__/ScheduleFinderModalTest.tsx | 171 ++- .../__tests__/ScheduleModalContentTest.tsx | 159 +-- .../ScheduleFinderFormTest.tsx.snap | 116 -- .../ScheduleFinderModalTest.tsx.snap | 224 ---- .../ScheduleModalContentTest.tsx.snap | 271 ---- .../UpcomingDepartures.tsx | 66 +- .../__tests__/UpcomingDeparturesTest.tsx | 49 +- assets/ts/schedule/schedule-loader.tsx | 185 +-- assets/ts/schedule/store/ScheduleStore.tsx | 15 - cypress/e2e/smoke.cy.js | 14 +- .../templates/schedule/_empty.html.eex | 9 +- .../templates/schedule/_line.html.eex | 111 +- .../templates/schedule/_line_map.html.eex | 7 - 35 files changed, 3120 insertions(+), 3662 deletions(-) create mode 100644 assets/ts/__tests__/test-render-helper.tsx delete mode 100644 assets/ts/schedule/components/ScheduleLoader.tsx create mode 100644 assets/ts/schedule/components/SchedulePage.tsx create mode 100644 assets/ts/schedule/components/__tests__/SchedulePageTest.tsx delete mode 100644 assets/ts/schedule/components/__tests__/__snapshots__/ScheduleFinderTest.tsx.snap delete mode 100644 assets/ts/schedule/components/line-diagram/__tests__/__snapshots__/LineDiagramTest.tsx.snap delete mode 100644 assets/ts/schedule/components/schedule-finder/__tests__/__snapshots__/ScheduleFinderFormTest.tsx.snap delete mode 100644 assets/ts/schedule/components/schedule-finder/__tests__/__snapshots__/ScheduleFinderModalTest.tsx.snap delete mode 100644 assets/ts/schedule/components/schedule-finder/__tests__/__snapshots__/ScheduleModalContentTest.tsx.snap delete mode 100644 lib/dotcom_web/templates/schedule/_line_map.html.eex diff --git a/assets/ts/__tests__/test-render-helper.tsx b/assets/ts/__tests__/test-render-helper.tsx new file mode 100644 index 0000000000..3cb88a4884 --- /dev/null +++ b/assets/ts/__tests__/test-render-helper.tsx @@ -0,0 +1,70 @@ +import React, { PropsWithChildren } from "react"; +import { + StoreProps, + scheduleStoreReducer +} from "../schedule/store/ScheduleStore"; +import { Provider } from "react-redux"; +import { render } from "@testing-library/react"; +import { Store, createStore } from "redux"; + +// as allows the user to specify other things such as initialState, store. +interface ExtendedRenderOptions { + preloadedState?: Partial; + store?: Store; +} + +const partialToStoreProps = ( + preloadedState: Partial +): StoreProps => { + return { + selectedDirection: + preloadedState.selectedDirection !== undefined + ? preloadedState.selectedDirection + : 0, + selectedOrigin: + preloadedState.selectedOrigin !== undefined + ? preloadedState.selectedOrigin + : "", + modalOpen: !!preloadedState.modalOpen, + modalMode: preloadedState.modalMode ? preloadedState.modalMode : "schedule" + }; +}; + +export const createScheduleStoreFromPartial = ( + partialState: Partial +): Store => { + const { + selectedDirection, + selectedOrigin, + modalOpen, + modalMode + } = partialToStoreProps(partialState); + return createStore(scheduleStoreReducer, { + selectedDirection, + selectedOrigin, + modalOpen, + modalMode + }); +}; + +export function renderWithProviders( + ui: React.ReactElement, + extendedRenderOptions: ExtendedRenderOptions = {} +) { + const { + preloadedState = {}, + // Automatically create a store instance if no store was passed in + store = createScheduleStoreFromPartial(preloadedState), + ...renderOptions + } = extendedRenderOptions; + + const Wrapper = ({ children }: PropsWithChildren) => { + return {children}; + }; + + // Return an object with the store and all of RTL's query functions + return { + store, + ...render(ui, { wrapper: Wrapper, ...renderOptions }) + }; +} diff --git a/assets/ts/jest.config.js b/assets/ts/jest.config.js index 52e4f267ea..e977bd927b 100644 --- a/assets/ts/jest.config.js +++ b/assets/ts/jest.config.js @@ -57,6 +57,7 @@ module.exports = { "./ts-build", "./tnm/__tests__/setupTests.ts", "./tnm/__tests__/helpers", + "./ts/__tests__/test-render-helper.tsx", "./stop/__tests__/helpers.ts" ], moduleNameMapper: { diff --git a/assets/ts/schedule/components/RapidTransitHoursOfOperation.tsx b/assets/ts/schedule/components/RapidTransitHoursOfOperation.tsx index 824771063e..9a223e1a8d 100644 --- a/assets/ts/schedule/components/RapidTransitHoursOfOperation.tsx +++ b/assets/ts/schedule/components/RapidTransitHoursOfOperation.tsx @@ -1,11 +1,11 @@ import { isDate, isSaturday, isSunday, parseISO } from "date-fns"; import { min } from "lodash"; import React, { ReactElement } from "react"; +import { useDispatch } from "react-redux"; import { formatToBostonTime } from "../../helpers/date"; import useHoursOfOperation from "../../hooks/useHoursOfOperation"; import { EnhancedRoute, StopHours, TransitHours } from "../../__v3api"; import { ScheduleNote } from "./__schedule"; -import { storeHandler } from "../store/ScheduleStore"; const trainsEveryHTML = (minuteString: string | undefined): JSX.Element => (
{`Trains depart every ${minuteString}`}
@@ -75,9 +75,10 @@ const RapidTransitHoursOfOperation = ({ date?: Date; }): ReactElement => { const hours = useHoursOfOperation(route.id) as TransitHours | null; + const dispatch = useDispatch(); const openModal = (): void => { - storeHandler({ + dispatch({ type: "OPEN_MODAL", newStoreValues: { modalMode: "origin" diff --git a/assets/ts/schedule/components/ScheduleDirection.tsx b/assets/ts/schedule/components/ScheduleDirection.tsx index bc215bbaed..7e0630891a 100644 --- a/assets/ts/schedule/components/ScheduleDirection.tsx +++ b/assets/ts/schedule/components/ScheduleDirection.tsx @@ -19,7 +19,7 @@ import { isACommuterRailRoute } from "../../models/route"; import LineDiagram from "./line-diagram/LineDiagram"; -import { fromStopTreeData } from "./ScheduleLoader"; +import { fromStopTreeData } from "./SchedulePage"; export interface Props { route: EnhancedRoute; @@ -240,7 +240,6 @@ const ScheduleDirection = ({ alerts={alerts} /> )} - {!staticMapData && mapState.data && ( void; - selectedOrigin: SelectedOrigin; - changeOrigin: (origin: SelectedOrigin) => void; - closeModal: () => void; - modalMode: ModalMode; - modalOpen: boolean; + changeDirection: (direction: DirectionId, dispatch: Dispatch) => void; + changeOrigin: (origin: SelectedOrigin, dispatch: Dispatch) => void; + closeModal: (dispatch: Dispatch) => void; scheduleNote: ScheduleNote | null; } @@ -40,20 +37,20 @@ const ScheduleFinder = ({ stops, routePatternsByDirection, today, - modalMode, - selectedOrigin, changeDirection, changeOrigin, - modalOpen, closeModal, scheduleNote }: Props): ReactElement => { + const dispatch = useDispatch(); + const { modalOpen, selectedOrigin } = useSelector( + (state: StoreProps) => state + ); + const currentDirection = useDirectionChangeEvent(directionId); const openOriginModal = (): void => { - const currentState = getCurrentState(); - const { modalOpen: modalIsOpen } = currentState; - if (!modalIsOpen) { - storeHandler({ + if (!modalOpen) { + dispatch({ type: "OPEN_MODAL", newStoreValues: { modalMode: "origin" @@ -63,10 +60,8 @@ const ScheduleFinder = ({ }; const openScheduleModal = (): void => { - const currentState = getCurrentState(); - const { modalOpen: modalIsOpen } = currentState; - if (selectedOrigin !== undefined && !modalIsOpen) { - storeHandler({ + if (selectedOrigin !== undefined && !modalOpen) { + dispatch({ type: "OPEN_MODAL", newStoreValues: { modalMode: "schedule" @@ -76,7 +71,7 @@ const ScheduleFinder = ({ }; const handleOriginSelectClick = (): void => { - storeHandler({ + dispatch({ type: "OPEN_MODAL", newStoreValues: { modalMode: "origin" @@ -106,9 +101,7 @@ const ScheduleFinder = ({ void; -} - -export const fromStopTreeData = (stopTreeData: StopTreeData): StopTree => ({ - byId: stopTreeData.by_id, - edges: stopTreeData.edges, - startingNodes: stopTreeData.starting_nodes -}); - -export const changeOrigin = (origin: SelectedOrigin): void => { - storeHandler({ - type: "CHANGE_ORIGIN", - newStoreValues: { - selectedOrigin: origin - } - }); - // reopen modal depending on choice: - storeHandler({ - type: "OPEN_MODAL", - newStoreValues: { - modalMode: origin ? "schedule" : "origin" - } - }); -}; - -export const ScheduleLoader = ({ - component, - schedulePageData, - updateURL -}: Props): ReactElement => { - const [query] = useQueryParams({ - // eslint-disable-next-line camelcase - "schedule_finder[direction_id]": StringParam, - "schedule_direction[direction_id]": StringParam, - "schedule_finder[origin]": StringParam - }); - - const changeDirection = (direction: DirectionId): void => { - storeHandler({ - type: "CHANGE_DIRECTION", - newStoreValues: { - selectedDirection: direction, - selectedOrigin: null - } - }); - }; - - const closeModal = (): void => { - storeHandler({ - type: "CLOSE_MODAL", - newStoreValues: {} - }); - // clear parameters from URL when closing the modal: - updateURL(""); - }; - - React.useEffect(() => { - // get initial values from the store: - const currentState = getCurrentState(); - const { selectedDirection, selectedOrigin } = currentState; - let { modalOpen, modalMode } = currentState; - - let newDirection: DirectionId | undefined; - const newOrigin: SelectedOrigin | undefined = - query["schedule_finder[origin]"]; - - // modify the store values in case URL has parameters: - const queryDirectionId = - query["schedule_finder[direction_id]"] || - query["schedule_direction[direction_id]"]; - if (queryDirectionId !== undefined) { - newDirection = queryDirectionId === "0" ? 0 : 1; - } - - if (newDirection !== undefined && newOrigin !== undefined) { - modalMode = "schedule"; - modalOpen = true; - } - - storeHandler({ - type: "INITIALIZE", - newStoreValues: { - selectedDirection: newDirection || selectedDirection, - selectedOrigin: newOrigin || selectedOrigin, - modalMode, - modalOpen - } - }); - // we disable linting in this next line because we DO want to specify an empty array since we want this piece to run only once - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleOriginSelectClick = (): void => { - storeHandler({ - type: "OPEN_MODAL", - newStoreValues: { - modalMode: "origin" - } - }); - }; - - const { - route, - stops, - services, - route_patterns: routePatternsByDirection, - schedule_note: scheduleNote, - today, - stop_tree, - route_stop_lists: routeStopLists, - alerts, - variant: busVariantId - } = schedulePageData; - const stopTree: StopTree | null = stop_tree - ? fromStopTreeData(stop_tree) - : null; - const routeIsSuspended = Object.keys(routePatternsByDirection).length === 0; - - const currentState = getCurrentState(); - if (!!currentState && Object.keys(currentState).length !== 0) { - const { - selectedDirection: currentDirection, - selectedOrigin, - modalOpen, - modalMode - } = currentState; - - // check first if this is a unidirectional route: - let readjustedDirectionId: DirectionId = currentDirection; - if ( - !routeIsSuspended && - !Object.keys(routePatternsByDirection).includes( - currentDirection.toString() - ) - ) { - // This route doesn't have this direction, so pick first existing direction - readjustedDirectionId = parseInt( - Object.keys(routePatternsByDirection)[0], - 10 - ) as DirectionId; - changeDirection(readjustedDirectionId); - updateURL(selectedOrigin, readjustedDirectionId); - } - - const isFerryRoute = routeToModeName(route) === "ferry"; - - if (component === "ADDITIONAL_LINE_INFORMATION") { - const { - teasers, - pdfs, - connections, - fares, - fare_link: fareLink, - holidays, - hours - } = schedulePageData; - - if (routeIsSuspended) { - return ( - <> - - - - ); - } - - return ( - - ); - } - - if (component === "SCHEDULE_NOTE" && scheduleNote) { - const { pdfs, hours } = schedulePageData; - return ( - <> - {isSubwayRoute(route) ? ( - - ) : null} - {modalOpen && ( - - )} - - ); - } - - if (component === "SCHEDULE_FINDER" && !isFerryRoute) { - return ( - - ); - } - - if (component === "SCHEDULE_DIRECTION") { - let mapData: MapData | undefined; - const mapDataEl = document.getElementById("js-map-data"); - if (mapDataEl) { - mapData = JSON.parse(mapDataEl.innerHTML); - } - - let staticMapData: StaticMapData | undefined; - const staticDataEl = document.getElementById("static-map-data"); - if (staticDataEl) { - staticMapData = JSON.parse(staticDataEl.innerHTML); - } - - return isFerryRoute ? ( - <> - -
-

Route Map

- {staticMapData && ( - <> - {`${route.name} - -