diff --git a/.eslintrc.js b/.eslintrc.js index 0ae9192ea5..7d3d9cccf2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { 'import/no-extraneous-dependencies': 'off', 'import/no-named-default': 'off', 'import/extensions': 'off', + 'import/prefer-default-export': 'off', // react 'react/button-has-type': 'warn', 'react/destructuring-assignment': 'off', diff --git a/app/action/FutureRoutesActions.js b/app/action/FutureRoutesActions.js index f07a0fc755..a0fcd80552 100644 --- a/app/action/FutureRoutesActions.js +++ b/app/action/FutureRoutesActions.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export function saveFutureRoute(actionContext, futureRoute) { actionContext.dispatch('saveFutureRoute', futureRoute); } diff --git a/app/action/SearchActions.js b/app/action/SearchActions.js index 5923f2aaf4..8a3a69576a 100644 --- a/app/action/SearchActions.js +++ b/app/action/SearchActions.js @@ -1,4 +1,3 @@ -// eslint-disable import/prefer-default-export export function saveSearch(actionContext, endpoint) { actionContext.dispatch('SaveSearch', endpoint); } diff --git a/app/action/SearchSettingsActions.js b/app/action/SearchSettingsActions.js index 6ba81bdae3..12675f7780 100644 --- a/app/action/SearchSettingsActions.js +++ b/app/action/SearchSettingsActions.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export function saveRoutingSettings(actionContext, settings) { actionContext.dispatch('saveRoutingSettings', settings); } diff --git a/app/action/userPreferencesActions.js b/app/action/userPreferencesActions.js index 6aa3530495..58473b2a2d 100644 --- a/app/action/userPreferencesActions.js +++ b/app/action/userPreferencesActions.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - export function setLanguage(actionContext, language) { actionContext.dispatch('SetLanguage', language); } diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js index 621d144ea5..06ea3eef92 100644 --- a/app/component/itinerary/ItineraryPage.js +++ b/app/component/itinerary/ItineraryPage.js @@ -1,75 +1,81 @@ /* eslint-disable react/no-array-index-key */ /* eslint-disable no-nested-ternary */ -import PropTypes from 'prop-types'; -import React, { useEffect, useState, useRef, cloneElement } from 'react'; -import { fetchQuery } from 'react-relay'; -import { FormattedMessage, intlShape } from 'react-intl'; import { matchShape, routerShape } from 'found'; -import isEqual from 'lodash/isEqual'; import isEmpty from 'lodash/isEmpty'; +import isEqual from 'lodash/isEqual'; import polyline from 'polyline-encoded'; +import PropTypes from 'prop-types'; +import React, { cloneElement, useEffect, useRef, useState } from 'react'; +import { FormattedMessage, intlShape } from 'react-intl'; +import { fetchQuery } from 'react-relay'; +import { saveFutureRoute } from '../../action/FutureRoutesActions'; +import { saveSearch } from '../../action/SearchActions'; +import { TransportMode } from '../../constants'; +import { mapLayerShape } from '../../store/MapLayerStore'; +import { + clearLatestNavigatorItinerary, + getDialogState, + getLatestNavigatorItinerary, + setDialogState, + setLatestNavigatorItinerary, +} from '../../store/localStorage'; +import { addAnalyticsEvent } from '../../util/analyticsUtils'; +import { getWeatherData } from '../../util/apiUtils'; +import { isIOS } from '../../util/browser'; +import { boundWithMinimumArea } from '../../util/geo-utils'; +import { + getIntermediatePlaces, + otpToLocation, + parseLatLon, +} from '../../util/otpStrings'; +import { getItineraryPagePath, streetHash } from '../../util/path'; +import { + PLANTYPE, + getPlanParams, + getSettings, + planQueryNeeded, +} from '../../util/planParamUtil'; import { - relayShape, configShape, mapLayerOptionsShape, + relayShape, } from '../../util/shapes'; +import { epochToTime } from '../../util/timeUtils'; +import { getAllNetworksOfType } from '../../util/vehicleRentalUtils'; import DesktopView from '../DesktopView'; +import Loading from '../Loading'; import MobileView from '../MobileView'; import ItineraryPageMap from '../map/ItineraryPageMap'; -import ItineraryListContainer from './ItineraryListContainer'; +import AlternativeItineraryBar from './AlternativeItineraryBar'; +import CustomizeSearch from './CustomizeSearch'; import { spinnerPosition } from './ItineraryList'; +import ItineraryListContainer from './ItineraryListContainer'; import ItineraryPageControls from './ItineraryPageControls'; -import ItineraryTabs from './ItineraryTabs'; -import { getWeatherData } from '../../util/apiUtils'; -import Loading from '../Loading'; -import { getItineraryPagePath, streetHash } from '../../util/path'; -import { boundWithMinimumArea } from '../../util/geo-utils'; -import planConnection from './PlanConnection'; import { - getSelectedItineraryIndex, - reportError, - addFeedbackly, - getTopics, - getBounds, - isEqualItineraries, - settingsLimitRouting, - setCurrentTimeToURL, - updateClient, - stopClient, - getRentalStationsToHideOnMap, addBikeStationMapForRentalVehicleItineraries, + addFeedbackly, checkDayNight, filterItinerariesByFeedId, filterWalk, + getBounds, + getRentalStationsToHideOnMap, + getSelectedItineraryIndex, + getTopics, + isEqualItineraries, mergeBikeTransitPlans, mergeScooterTransitPlan, quitIteration, + reportError, scooterEdges, + setCurrentTimeToURL, + settingsLimitRouting, + stopClient, + updateClient, } from './ItineraryPageUtils'; -import { isIOS } from '../../util/browser'; -import { addAnalyticsEvent } from '../../util/analyticsUtils'; -import { - parseLatLon, - otpToLocation, - getIntermediatePlaces, -} from '../../util/otpStrings'; -import AlternativeItineraryBar from './AlternativeItineraryBar'; -import { - PLANTYPE, - getSettings, - getPlanParams, - planQueryNeeded, -} from '../../util/planParamUtil'; -import { epochToTime } from '../../util/timeUtils'; -import { saveFutureRoute } from '../../action/FutureRoutesActions'; -import { saveSearch } from '../../action/SearchActions'; -import CustomizeSearch from './CustomizeSearch'; -import { getAllNetworksOfType } from '../../util/vehicleRentalUtils'; -import { TransportMode } from '../../constants'; -import { mapLayerShape } from '../../store/MapLayerStore'; +import ItineraryTabs from './ItineraryTabs'; import NaviContainer from './NaviContainer'; import NavigatorIntroModal from './NavigatorIntro/NavigatorIntroModal'; -import { getDialogState, setDialogState } from '../../store/localStorage'; +import planConnection from './PlanConnection'; const MAX_QUERY_COUNT = 4; // number of attempts to collect enough itineraries @@ -607,15 +613,50 @@ export default function ItineraryPage(props, context) { } } - const setNavigation = enable => { + const getCombinedPlanEdges = () => { + return [ + ...(state.earlierEdges || []), + ...(mapHashToPlan()?.edges || []), + ...(state.laterEdges || []), + ]; + }; + + const getItinerarySelection = () => { + const hasNoTransitItineraries = filterWalk(state.plan?.edges).length === 0; + const plan = mapHashToPlan(); + let combinedEdges; + // Remove old itineraries if new query cannot find a route + if (state.error) { + combinedEdges = []; + } else if (streetHashes.includes(hash)) { + combinedEdges = plan?.edges || []; + } else { + combinedEdges = getCombinedPlanEdges(); + if (!hasNoTransitItineraries) { + // don't show plain walking in transit itinerary list + combinedEdges = filterWalk(combinedEdges); + } + } + const selectedIndex = getSelectedItineraryIndex(location, combinedEdges); + + return { plan, combinedEdges, selectedIndex, hasNoTransitItineraries }; + }; + + const setNavigation = isEnabled => { if (mobileRef.current) { - mobileRef.current.setBottomSheet(enable ? 'bottom' : 'middle'); + mobileRef.current.setBottomSheet(isEnabled ? 'bottom' : 'middle'); } - if (!enable) { + if (!isEnabled) { setMapState({ center: undefined, zoom: undefined, bounds: undefined }); navigateMap(); + clearLatestNavigatorItinerary(); + } else { + const { combinedEdges, selectedIndex } = getItinerarySelection(); + if (combinedEdges[selectedIndex]?.node) { + setLatestNavigatorItinerary(combinedEdges[selectedIndex]?.node); + } } - setNaviMode(enable); + setNaviMode(isEnabled); }; // save url-defined location to old searches @@ -704,39 +745,17 @@ export default function ItineraryPage(props, context) { ); } - function getCombinedPlanEdges() { - return [ - ...(state.earlierEdges || []), - ...(mapHashToPlan()?.edges || []), - ...(state.laterEdges || []), - ]; - } - - function getItinerarySelection() { - const hasNoTransitItineraries = filterWalk(state.plan?.edges).length === 0; - const plan = mapHashToPlan(); - let combinedEdges; - // Remove old itineraries if new query cannot find a route - if (state.error) { - combinedEdges = []; - } else if (streetHashes.includes(hash)) { - combinedEdges = plan?.edges || []; - } else { - combinedEdges = getCombinedPlanEdges(); - if (!hasNoTransitItineraries) { - // don't show plain walking in transit itinerary list - combinedEdges = filterWalk(combinedEdges); - } - } - const selectedIndex = getSelectedItineraryIndex(location, combinedEdges); - - return { plan, combinedEdges, selectedIndex, hasNoTransitItineraries }; - } - useEffect(() => { setCurrentTimeToURL(config, match); updateLocalStorage(true); addFeedbackly(context); + + const storedItinerary = getLatestNavigatorItinerary(); + + setNavigation( + storedItinerary?.end && Date.parse(storedItinerary.end) > Date.now(), + ); + return () => { if (showVehicles()) { stopClient(context); @@ -1110,6 +1129,7 @@ export default function ItineraryPage(props, context) { // in mobile, settings drawer hides other content const panelHidden = !desktop && settingsDrawer !== null; let content; // bottom content of itinerary panel + if (panelHidden) { content = null; } else if (loading) { @@ -1120,6 +1140,9 @@ export default function ItineraryPage(props, context) { ); } else if (detailView) { if (naviMode) { + const naviModeItinerary = + getLatestNavigatorItinerary() || combinedEdges[selectedIndex]?.node; + content = ( <> {!isNavigatorIntroDismissed && ( @@ -1130,10 +1153,9 @@ export default function ItineraryPage(props, context) { /> )} diff --git a/app/component/itinerary/NaviContainer.js b/app/component/itinerary/NaviContainer.js index 3357b26bd5..b56d6d6ad6 100644 --- a/app/component/itinerary/NaviContainer.js +++ b/app/component/itinerary/NaviContainer.js @@ -1,121 +1,42 @@ -import React, { useEffect, useState, useRef } from 'react'; import PropTypes from 'prop-types'; -import { graphql, fetchQuery } from 'react-relay'; +import React from 'react'; +import { legTime } from '../../util/legUtils'; import { itineraryShape, relayShape } from '../../util/shapes'; -import NaviCardContainer from './NaviCardContainer'; import NaviBottom from './NaviBottom'; -import { legTime } from '../../util/legUtils'; -import { checkPositioningPermission } from '../../action/PositionActions'; - -const legQuery = graphql` - query NaviContainer_legQuery($id: String!) { - leg(id: $id) { - id - start { - scheduledTime - estimated { - time - } - } - end { - scheduledTime - estimated { - time - } - } - - to { - vehicleRentalStation { - availableVehicles { - total - } - } - } - realtimeState - } - } -`; +import NaviCardContainer from './NaviCardContainer'; +import { useRealtimeLegs } from './hooks/useRealtimeLegs'; function NaviContainer( { itinerary, focusToLeg, relayEnvironment, setNavigation, mapRef }, { getStore }, ) { - const [realTimeLegs, setRealTimeLegs] = useState(itinerary.legs); - const [time, setTime] = useState(Date.now()); - const locationOK = useRef(true); + const { legs } = itinerary; const position = getStore('PositionStore').getLocationState(); - // update view after every 10 seconds - useEffect(() => { - checkPositioningPermission().then(permission => { - locationOK.current = permission.state === 'granted'; - if (locationOK.current) { - mapRef?.enableMapTracking(); - } - setTime(Date.now()); // force refresh - }); - const interval = setInterval(() => { - setTime(Date.now()); - }, 10000); - - return () => clearInterval(interval); - }, []); - - useEffect(() => { - const legQueries = []; - itinerary.legs.forEach(leg => { - if (leg.transitLeg) { - legQueries.push( - fetchQuery( - relayEnvironment, - legQuery, - { id: leg.id }, - { force: true }, - ).toPromise(), - ); - } - }); - if (legQueries.length) { - Promise.all(legQueries).then(responses => { - const legMap = {}; - responses.forEach(data => { - legMap[data.leg.id] = data.leg; - }); - const rtLegs = itinerary.legs.map(l => { - const rtLeg = l.id ? legMap[l.id] : null; - if (rtLeg) { - return { - ...l, - ...rtLeg, - to: { - ...l.to, - vehicleRentalStation: rtLeg.to.vehicleRentalStation, - }, - }; - } - return { ...l }; - }); - setRealTimeLegs(rtLegs); - }); - } - }, [time]); + const { realTimeLegs, time, isPositioningAllowed } = useRealtimeLegs( + legs, + mapRef, + relayEnvironment, + ); // recompute estimated arrival let lastTransitLeg; let arrivalChange = 0; - itinerary.legs.forEach(leg => { + + legs.forEach(leg => { if (leg.transitLeg) { lastTransitLeg = leg; } }); + if (lastTransitLeg) { const rtLeg = realTimeLegs.find(leg => { return leg.id === lastTransitLeg.id; }); arrivalChange = legTime(rtLeg.end) - legTime(lastTransitLeg.end); } - const arrivalTime = - legTime(itinerary.legs[itinerary.legs.length - 1].end) + arrivalChange; + + const arrivalTime = legTime(legs[legs.length - 1].end) + arrivalChange; return ( <> @@ -123,7 +44,7 @@ function NaviContainer( itinerary={itinerary} realTimeLegs={realTimeLegs} focusToLeg={ - mapRef?.state.mapTracking || locationOK.current ? null : focusToLeg + mapRef?.state.mapTracking || isPositioningAllowed ? null : focusToLeg } time={time} position={position} diff --git a/app/component/itinerary/hooks/useRealtimeLegs.js b/app/component/itinerary/hooks/useRealtimeLegs.js new file mode 100644 index 0000000000..d0618bab80 --- /dev/null +++ b/app/component/itinerary/hooks/useRealtimeLegs.js @@ -0,0 +1,82 @@ +import { useCallback, useEffect, useState } from 'react'; +import { fetchQuery } from 'react-relay'; +import { checkPositioningPermission } from '../../../action/PositionActions'; +import { legQuery } from '../queries/LegQuery'; + +const useRealtimeLegs = (initialLegs, mapRef, relayEnvironment) => { + const [isPositioningAllowed, setPositioningAllowed] = useState(false); + const [realTimeLegs, setRealTimeLegs] = useState(initialLegs); + const [time, setTime] = useState(Date.now()); + + const enableMapTracking = useCallback(async () => { + const permission = await checkPositioningPermission(); + const isPermissionGranted = permission.state === 'granted'; + if (isPermissionGranted) { + mapRef?.enableMapTracking(); + } + setPositioningAllowed(isPermissionGranted); + }, [mapRef]); + + const queryAndMapRealtimeLegs = useCallback( + async legs => { + if (!legs.length) { + return {}; + } + + const legQueries = legs + .filter(leg => leg.transitLeg) + .map(leg => + fetchQuery( + relayEnvironment, + legQuery, + { id: leg.id }, + { force: true }, + ).toPromise(), + ); + const responses = await Promise.all(legQueries); + return responses.reduce( + (map, response) => ({ ...map, [response.leg.id]: response.leg }), + {}, + ); + }, + [relayEnvironment], + ); + + const fetchAndSetRealtimeLegs = useCallback(async () => { + const rtLegMap = await queryAndMapRealtimeLegs(initialLegs).catch(err => + // eslint-disable-next-line no-console + console.error('Failed to query and map real time legs', err), + ); + + const rtLegs = initialLegs.map(l => { + const rtLeg = l.id ? rtLegMap[l.id] : null; + if (rtLeg) { + return { + ...l, + ...rtLeg, + to: { + ...l.to, + vehicleRentalStation: rtLeg.to.vehicleRentalStation, + }, + }; + } + return { ...l }; + }); + setRealTimeLegs(rtLegs); + }, [initialLegs, queryAndMapRealtimeLegs]); + + useEffect(() => { + enableMapTracking(); + fetchAndSetRealtimeLegs(); + const interval = setInterval(() => { + fetchAndSetRealtimeLegs(); + setTime(Date.now()); + }, 10000); + + return () => clearInterval(interval); + }, [enableMapTracking, fetchAndSetRealtimeLegs]); + + return { realTimeLegs, time, isPositioningAllowed }; +}; + +export { useRealtimeLegs }; diff --git a/app/component/itinerary/queries/LegQuery.js b/app/component/itinerary/queries/LegQuery.js new file mode 100644 index 0000000000..ba552eafba --- /dev/null +++ b/app/component/itinerary/queries/LegQuery.js @@ -0,0 +1,32 @@ +import { graphql } from 'react-relay'; + +const legQuery = graphql` + query LegQuery($id: String!) { + leg(id: $id) { + id + start { + scheduledTime + estimated { + time + } + } + end { + scheduledTime + estimated { + time + } + } + + to { + vehicleRentalStation { + availableVehicles { + total + } + } + } + realtimeState + } + } +`; + +export { legQuery }; diff --git a/app/store/localStorage.js b/app/store/localStorage.js index b55b91c1f4..de2260acbc 100644 --- a/app/store/localStorage.js +++ b/app/store/localStorage.js @@ -286,3 +286,15 @@ export function setSavedGeolocationPermission(key, value) { [key]: value, }); } + +export const setLatestNavigatorItinerary = value => { + setItem('latestNavigatorItinerary', value); +}; + +export const getLatestNavigatorItinerary = () => { + return getItemAsJson('latestNavigatorItinerary', '{}'); +}; + +export const clearLatestNavigatorItinerary = () => { + setItem('latestNavigatorItinerary', {}); +}; diff --git a/app/util/envUtils.js b/app/util/envUtils.js index 75eaf89b8e..40cc01f540 100644 --- a/app/util/envUtils.js +++ b/app/util/envUtils.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - /** Check if application is running in a dev environment. RUN_ENV is defined in kubernetes-deploy for dev instances. For running dev locally, NODE_ENV is checked * */ export const isDevelopmentEnvironment = config => { return ( diff --git a/app/util/feedScopedIdUtils.js b/app/util/feedScopedIdUtils.js index 23de971504..b83f9eae17 100644 --- a/app/util/feedScopedIdUtils.js +++ b/app/util/feedScopedIdUtils.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - /** * Returns id without the feedId prefix. * diff --git a/app/util/filterUtils.js b/app/util/filterUtils.js index f8b4e71ef9..931da943d1 100644 --- a/app/util/filterUtils.js +++ b/app/util/filterUtils.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - /** * Filters object that contains objects so that only objects that have * a certain key with the correct value defined are returned. diff --git a/app/util/gtfs.js b/app/util/gtfs.js index 539ed4ced2..8796b34062 100644 --- a/app/util/gtfs.js +++ b/app/util/gtfs.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ export const typeToName = { 0: 'tram', 1: 'subway', diff --git a/app/util/gtfsRtParser.js b/app/util/gtfsRtParser.js index 597378fc57..d4b71068eb 100644 --- a/app/util/gtfsRtParser.js +++ b/app/util/gtfsRtParser.js @@ -1,6 +1,6 @@ import ceil from 'lodash/ceil'; import Pbf from 'pbf'; -// eslint-disable-next-line import/prefer-default-export + export const parseFeedMQTT = (feedParser, data, topic) => { const pbf = new Pbf(data); const feed = feedParser(pbf); diff --git a/app/util/patternUtils.js b/app/util/patternUtils.js index db9cc42e06..bc26fd68ff 100644 --- a/app/util/patternUtils.js +++ b/app/util/patternUtils.js @@ -4,7 +4,6 @@ import moment from 'moment'; Return false if pattern doesn't have active dates information available or if current day is not found in active days * */ -// eslint-disable-next-line import/prefer-default-export export const isActiveDate = pattern => { if (!pattern || !pattern.activeDates) { return false; diff --git a/app/util/shapes.js b/app/util/shapes.js index c8d0fd1845..3c6cc3c61b 100644 --- a/app/util/shapes.js +++ b/app/util/shapes.js @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import PropTypes from 'prop-types'; import { PlannerMessageType } from '../constants'; diff --git a/package.json b/package.json index b4a25d8966..a78068e860 100644 --- a/package.json +++ b/package.json @@ -266,7 +266,7 @@ "eslint-plugin-react": "7.33.2", "eslint-plugin-react-hooks": "4.6.0", "favicons-webpack-plugin": "4.2.0", - "fetch-mock": "9.11.0", + "fetch-mock": "^12.0.2", "file-loader": "5.0.2", "git-precommit-checks": "3.0.0", "graphql": "16.8.1", diff --git a/test/unit/component/CustomizeSearchNew.test.js b/test/unit/component/CustomizeSearchNew.test.js deleted file mode 100644 index 62520ac269..0000000000 --- a/test/unit/component/CustomizeSearchNew.test.js +++ /dev/null @@ -1,89 +0,0 @@ -/* import { expect } from 'chai'; -import fetchMock from 'fetch-mock'; -import { after, before, describe, it } from 'mocha'; -import React from 'react'; - -import CustomizeSearch from '../../../app/component/CustomizeSearchNew'; -import VehicleRentalStationNetworkSelector from '../../../app/component/VehicleRentalStationNetworkSelector'; - -import { mockContext, mockChildContextTypes } from '../helpers/mock-context'; -import { mountWithIntl } from '../helpers/mock-intl-enzyme'; -import defaultConfig from '../../../app/configurations/config.default'; -import hslConfig from '../../../app/configurations/config.hsl'; - -const mergedConfig = { - ...defaultConfig, - ...hslConfig, -}; - -describe('', () => { - before(() => { - fetchMock.post('/graphql', { - data: { - route: { - id: 'Um91dGU6SFNMOjI1NTA=', - shortName: '550', - longName: 'Itäkeskus-Westendinasema', - mode: 'BUS', - }, - }, - }); - }); - - after(() => { - fetchMock.restore(); - }); - - it.skip('should show citybike network selector when many networks are available', () => { - const wrapper = mountWithIntl( - {}} />, - { - context: { - ...mockContext, - config: { - ...mergedConfig, - transportModes: { - ...mergedConfig.transportModes, - citybike: { - availableForSelection: true, - }, - }, - }, - location: { - ...mockContext.location, - }, - router: createMemoryMockRouter(), - }, - childContextTypes: { - ...mockChildContextTypes, - }, - }, - ); - expect(wrapper.find(VehicleRentalStationNetworkSelector)).to.have.lengthOf(1); - }); - - it.skip('should hide citybike network selector when citybike routing is disabled', () => { - const wrapper = mountWithIntl( - {}} />, - { - context: { - ...mockContext, - config: { - ...mergedConfig, - transportModes: { - ...mergedConfig.transportModes, - citybike: { - availableForSelection: false, - }, - }, - }, - }, - childContextTypes: { - ...mockChildContextTypes, - }, - }, - ); - expect(wrapper.find(VehicleRentalStationNetworkSelector)).to.have.lengthOf(0); - }); -}); -*/ diff --git a/test/unit/component/map/tile-layer/Stops.test.js b/test/unit/component/map/tile-layer/Stops.test.js index cb9cf60d8f..ed5b18b6d7 100644 --- a/test/unit/component/map/tile-layer/Stops.test.js +++ b/test/unit/component/map/tile-layer/Stops.test.js @@ -2,6 +2,8 @@ import fetchMock from 'fetch-mock'; import Stops from '../../../../../app/component/map/tile-layer/Stops'; describe('Stops', () => { + before(() => fetchMock.mockGlobal()); + after(() => fetchMock.unmockGlobal()); const config = { URL: { STOP_MAP: { default: 'https://localhost/stopmap/' }, @@ -18,24 +20,28 @@ describe('Stops', () => { }; describe('fetchStatusAndDrawStop', () => { - afterEach(() => { - fetchMock.reset(); - }); - it('should make a get to the correct url', () => { - const mock = fetchMock.get(`${config.URL.STOP_MAP.default}3/1/2.pbf`, { - status: 404, - }); + fetchMock.get( + 'end:3/1/2.pbf', + { + status: 200, + }, + { repeat: 1 }, + ); new Stops(tile, config, []).getPromise(); // eslint-disable-line no-new - expect(mock.called()).to.equal(true); + expect(fetchMock.callHistory.called('end:/3/1/2.pbf')).to.equal(true); }); it('should add zoom offset to the z coordinate', () => { - const mock = fetchMock.get(`${config.URL.STOP_MAP.default}4/1/2.pbf`, { - status: 404, - }); + fetchMock.get( + 'end:/4/1/2.pbf', + { + status: 200, + }, + { repeat: 1 }, + ); new Stops({ ...tile, props: { zoomOffset: 1 } }, config, []).getPromise(); // eslint-disable-line no-new - expect(mock.called()).to.equal(true); + expect(fetchMock.callHistory.called('end:/4/1/2.pbf')).to.equal(true); }); }); }); diff --git a/test/unit/store/GeoJsonStore.test.js b/test/unit/store/GeoJsonStore.test.js index 96dec09485..9053f3ed26 100644 --- a/test/unit/store/GeoJsonStore.test.js +++ b/test/unit/store/GeoJsonStore.test.js @@ -9,15 +9,19 @@ import GeoJsonStore, { describe('GeoJsonStore', () => { let store; const dispatcher = () => {}; + before(() => fetchMock.mockGlobal()); beforeEach(() => { store = new GeoJsonStore(dispatcher); }); afterEach(() => { - fetchMock.reset(); + fetchMock.removeRoutes(); + fetchMock.clearHistory(); }); + after(() => fetchMock.unmockGlobal()); + describe('getGeoJsonConfig', () => { it('should return undefined if the url is falsey', async () => { expect(await store.getGeoJsonConfig(undefined)).to.equal(undefined); @@ -48,7 +52,7 @@ describe('GeoJsonStore', () => { const result1 = await store.getGeoJsonConfig(url); const result2 = await store.getGeoJsonConfig(url); - expect(fetchMock.calls().length).to.equal(1); + expect(fetchMock.callHistory.calls().length).to.equal(1); expect(result1).to.equal(result2); }); @@ -79,7 +83,7 @@ describe('GeoJsonStore', () => { const result1 = await store.getGeoJsonData(url, undefined, undefined); const result2 = await store.getGeoJsonData(url, undefined, undefined); - expect(fetchMock.calls().length).to.equal(1); + expect(fetchMock.callHistory.calls().length).to.equal(1); expect(result1).to.deep.equal(result2); }); diff --git a/test/unit/store/MessageStore.test.js b/test/unit/store/MessageStore.test.js index 9d7e1c5f26..c828df1fae 100644 --- a/test/unit/store/MessageStore.test.js +++ b/test/unit/store/MessageStore.test.js @@ -9,9 +9,18 @@ import MessageStore, { describe('MessageStore', () => { describe('getMessages', () => { + before(() => fetchMock.mockGlobal()); + + afterEach(() => { + fetchMock.removeRoutes(); + fetchMock.clearHistory(); + }); + + after(() => fetchMock.unmockGlobal()); + it('should show higher priority first', async () => { const staticMessagesUrl = '/staticMessages'; - const mock = fetchMock.sandbox().mock(staticMessagesUrl, { + fetchMock.get(staticMessagesUrl, { staticMessages: [ { id: '2', @@ -26,7 +35,6 @@ describe('MessageStore', () => { }, ], }); - global.fetch = mock; const store = new MessageStore(); const config = { staticMessages: [ @@ -47,7 +55,7 @@ describe('MessageStore', () => { }; await store.addConfigMessages(config); - expect(mock.called(staticMessagesUrl)).to.equal(true); + expect(fetchMock.callHistory.called(staticMessagesUrl)).to.equal(true); expect(store.getMessages()).to.deep.equal([ { content: { diff --git a/test/unit/util/fetchUtil.test.js b/test/unit/util/fetchUtil.test.js index 78bc0b9334..2a762de93a 100644 --- a/test/unit/util/fetchUtil.test.js +++ b/test/unit/util/fetchUtil.test.js @@ -1,6 +1,6 @@ -import { expect, assert } from 'chai'; -import { describe, it } from 'mocha'; +import { assert, expect } from 'chai'; import fetchMock from 'fetch-mock'; +import { describe, it } from 'mocha'; import { retryFetch } from '../../../app/util/fetchUtils'; // retryFetch retries fetch requests (url, options, retry count, delay) where total number or calls is initial request + retry count @@ -11,106 +11,85 @@ const testUrl = const testJSONResponse = '{"test": 3}'; describe('retryFetch', () => { - /* eslint-disable no-unused-vars */ - it('fetching something that does not exist with 5 retries should give Not Found error and 6 requests in total should be made ', done => { - fetchMock.mock(testUrl, 404); - retryFetch(testUrl, 5, 10) - .then(res => res.json()) - .then( - result => { - assert.fail('Error should have been thrown'); - }, - err => { - expect(err).to.equal(`${testUrl}: Not Found`); - // calls has array of requests made to given URL - const calls = fetchMock.calls( - 'https://dev-api.digitransit.fi/timetables/v1/hsl/routes/routes.json', - ); - expect(calls.length).to.equal(6); - fetchMock.restore(); - done(); - }, - ); + before(() => fetchMock.mockGlobal()); + + afterEach(() => { + fetchMock.removeRoutes(); + fetchMock.clearHistory(); }); - it('fetch with larger fetch timeout should take longer', done => { - let firstEnd; - let firstDuration; - const firstStart = performance.now(); - fetchMock.mock(testUrl, 404); - retryFetch(testUrl, 2, 20) - .then(res => res.json()) - .then( - result => { - assert.fail('Error should have been thrown'); - }, - err => { - firstEnd = performance.now(); - firstDuration = firstEnd - firstStart; - expect(firstDuration).to.be.above(40); - // because test system can be slow, requests should take between 40-200ms (because system can be slow) when retry delay is 20ms and 2 retries - expect(firstDuration).to.be.below(200); - fetchMock.restore(); - }, - ) - .then(() => { - const secondStart = performance.now(); - fetchMock.mock(testUrl, 404); - retryFetch(testUrl, 2, 100) - .then(res => res.json()) - .then( - result => { - assert.fail('Error should have been thrown'); - }, - err => { - const secondEnd = performance.now(); - const secondDuration = secondEnd - secondStart; - expect(secondDuration).to.be.above(200); - // because test system can be slow, requests should take between 200-360ms when retry delay is 100ms and 2 retries - expect(firstDuration).to.be.below(360); - // because of longer delay between requests, the difference between 2 retries with 20ms delay - // and 2 retries with 100ms delay should be 160ms but because performance slightly varies, there is a 60ms threshold for test failure - expect(secondDuration - firstDuration).to.be.above(100); - fetchMock.restore(); - done(); - }, - ); - }); + after(() => fetchMock.unmockGlobal()); + + it('fetching something that does not exist with 5 retries should give Not Found error and 6 requests in total should be made ', async () => { + fetchMock.get(testUrl, 404); + + try { + await retryFetch(testUrl, 5, 10); + } catch (err) { + expect(err).to.equal(`${testUrl}: Not Found`); + } + + const calls = fetchMock.callHistory.calls( + 'https://dev-api.digitransit.fi/timetables/v1/hsl/routes/routes.json', + ); + expect(calls.length).to.equal(6); }); - it('fetch that gives 200 should not be retried', done => { + it('fetch with larger fetch timeout should take longer', async () => { + async function measureFetchDuration(retries, delay) { + const start = performance.now(); + try { + await retryFetch(testUrl, retries, delay); + } catch (err) { + // Expected error due to 404 + } + return performance.now() - start; + } + // because test system can be slow, requests should take between 40-200ms when retry delay is 20ms and 2 retries + const firstDuration = await measureFetchDuration(2, 20); + expect(firstDuration).to.be.above(40); + expect(firstDuration).to.be.below(200); + + // because test system can be slow, requests should take between 200-360ms when retry delay is 100ms and 2 retries + const secondDuration = await measureFetchDuration(2, 100); + expect(secondDuration).to.be.above(200); + expect(secondDuration).to.be.below(360); + + // because of longer delay between requests, the difference between 2 retries with 20ms delay + // and 2 retries with 100ms delay should be 160ms but because performance slightly varies, there is a 100ms threshold for test failure + const expectedDifference = 100; + const allowedVariance = 100; + const durationDifference = secondDuration - firstDuration; + + expect(durationDifference).to.be.within( + expectedDifference - allowedVariance, + expectedDifference + allowedVariance, + ); + }); + + it('fetch that gives 200 should not be retried', async () => { fetchMock.get(testUrl, testJSONResponse); - retryFetch(testUrl, 5, 10) - .then(res => res.json()) - .then( - result => { - // calls has array of requests made to given URL - const calls = fetchMock.calls( - 'https://dev-api.digitransit.fi/timetables/v1/hsl/routes/routes.json', - ); - expect(calls.length).to.equal(1); - fetchMock.restore(); - done(); - }, - err => { - assert.fail('No error should have been thrown'); - }, - ); + try { + await retryFetch(testUrl, 5, 10); + } catch (err) { + assert.fail('No error should have been thrown'); + } + const calls = fetchMock.callHistory.calls( + 'https://dev-api.digitransit.fi/timetables/v1/hsl/routes/routes.json', + ); + expect(calls.length).to.equal(1); }); - it('fetch that gives 200 should have correct result data', done => { + it('fetch that gives 200 should have correct result data', async () => { fetchMock.get(testUrl, testJSONResponse); - retryFetch(testUrl, 5, 10) - .then(res => res.json()) - .then( - result => { - expect(result.test).to.equal(3); - fetchMock.restore(); - done(); - }, - err => { - assert.fail('No error should have been thrown'); - }, - ); + + try { + const res = await retryFetch(testUrl, 5, 10); + const data = await res.json(); + + expect(data).to.have.property('test', 3); + } catch (err) { + assert.fail(`Request failed unexpectedly: ${err.message}`); + } }); }); diff --git a/test/unit/component/Itinerary.test.js b/test/unit/views/ItineraryPage/component/Itinerary.test.js similarity index 97% rename from test/unit/component/Itinerary.test.js rename to test/unit/views/ItineraryPage/component/Itinerary.test.js index 3894419812..eac5a2d45f 100644 --- a/test/unit/component/Itinerary.test.js +++ b/test/unit/views/ItineraryPage/component/Itinerary.test.js @@ -2,17 +2,23 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import React from 'react'; -import { mockContext, mockChildContextTypes } from '../helpers/mock-context'; -import { mountWithIntl, shallowWithIntl } from '../helpers/mock-intl-enzyme'; import { component as Itinerary, ModeLeg, - ViaLeg, RouteLeg, -} from '../../../app/component/itinerary/Itinerary'; -import { AlertSeverityLevelType } from '../../../app/constants'; -import RouteNumberContainer from '../../../app/component/RouteNumberContainer'; -import dcw12 from '../test-data/dcw12'; + ViaLeg, +} from '../../../../../app/component/itinerary/Itinerary'; +import RouteNumberContainer from '../../../../../app/component/RouteNumberContainer'; +import { AlertSeverityLevelType } from '../../../../../app/constants'; +import { + mockChildContextTypes, + mockContext, +} from '../../../helpers/mock-context'; +import { + mountWithIntl, + shallowWithIntl, +} from '../../../helpers/mock-intl-enzyme'; +import dcw12 from '../../../test-data/dcw12'; const defaultProps = { breakpoint: 'large', diff --git a/test/unit/component/ItineraryList.test.js b/test/unit/views/ItineraryPage/component/ItineraryList.test.js similarity index 86% rename from test/unit/component/ItineraryList.test.js rename to test/unit/views/ItineraryPage/component/ItineraryList.test.js index b797a64255..0e20fca31b 100644 --- a/test/unit/component/ItineraryList.test.js +++ b/test/unit/views/ItineraryPage/component/ItineraryList.test.js @@ -1,8 +1,11 @@ import React from 'react'; -import { mockContext, mockChildContextTypes } from '../helpers/mock-context'; -import { mountWithIntl } from '../helpers/mock-intl-enzyme'; -import { Component as ItineraryList } from '../../../app/component/itinerary/ItineraryList'; +import { Component as ItineraryList } from '../../../../../app/component/itinerary/ItineraryList'; +import { + mockChildContextTypes, + mockContext, +} from '../../../helpers/mock-context'; +import { mountWithIntl } from '../../../helpers/mock-intl-enzyme'; const noop = () => {}; diff --git a/test/unit/component/NavigatorIntro.test.js b/test/unit/views/ItineraryPage/component/NavigatorIntro.test.js similarity index 89% rename from test/unit/component/NavigatorIntro.test.js rename to test/unit/views/ItineraryPage/component/NavigatorIntro.test.js index af0eae2305..f6b68b2209 100644 --- a/test/unit/component/NavigatorIntro.test.js +++ b/test/unit/views/ItineraryPage/component/NavigatorIntro.test.js @@ -2,9 +2,12 @@ import { assert } from 'chai'; import { describe, it } from 'mocha'; import React from 'react'; -import NavigatorIntro from '../../../app/component/itinerary/NavigatorIntro/NavigatorIntro'; -import { mockChildContextTypes, mockContext } from '../helpers/mock-context'; -import { mountWithIntl } from '../helpers/mock-intl-enzyme'; +import NavigatorIntro from '../../../../../app/component/itinerary/NavigatorIntro/NavigatorIntro'; +import { + mockChildContextTypes, + mockContext, +} from '../../../helpers/mock-context'; +import { mountWithIntl } from '../../../helpers/mock-intl-enzyme'; const defaultProps = { onClose: () => {}, diff --git a/yarn.lock b/yarn.lock index aa33e6547b..4b4008db46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -283,7 +283,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.23.9, @babel/core@npm:^7.0.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.7.5": +"@babel/core@npm:7.23.9, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.7.5": version: 7.23.9 resolution: "@babel/core@npm:7.23.9" dependencies: @@ -5800,6 +5800,13 @@ __metadata: languageName: node linkType: hard +"@types/glob-to-regexp@npm:^0.4.4": + version: 0.4.4 + resolution: "@types/glob-to-regexp@npm:0.4.4" + checksum: be9c924d664592a16129c825aa392365335ce455c34e1c9d3f6dd8b45371088bb5d4a45bbb576559f2b63d4f8bcf464cbd5baafb08cdf89b71d3b6a79356b747 + languageName: node + linkType: hard + "@types/glob@npm:^7.1.1": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -10095,7 +10102,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.0.0, core-js@npm:^3.29.0, core-js@npm:^3.30.2, core-js@npm:^3.6.1": +"core-js@npm:^3.29.0, core-js@npm:^3.30.2, core-js@npm:^3.6.1": version: 3.35.1 resolution: "core-js@npm:3.35.1" checksum: e246af6b634be3763ffe3ce6ac4601b4dc5b928006fb6c95e5d08ecd82a2413bf36f00ffe178b89c9a8e94000288933a78a9881b2c9498e6cf312b031013b952 @@ -11229,7 +11236,7 @@ __metadata: farce: 0.4.5 fast-xml-parser: 4.3.4 favicons-webpack-plugin: 4.2.0 - fetch-mock: 9.11.0 + fetch-mock: ^12.0.2 file-loader: 5.0.2 fluxible: 1.4.0 fluxible-addons-react: 0.2.16 @@ -13281,26 +13288,16 @@ __metadata: languageName: node linkType: hard -"fetch-mock@npm:9.11.0": - version: 9.11.0 - resolution: "fetch-mock@npm:9.11.0" +"fetch-mock@npm:^12.0.2": + version: 12.0.2 + resolution: "fetch-mock@npm:12.0.2" dependencies: - "@babel/core": ^7.0.0 - "@babel/runtime": ^7.0.0 - core-js: ^3.0.0 - debug: ^4.1.1 - glob-to-regexp: ^0.4.0 - is-subset: ^0.1.1 - lodash.isequal: ^4.5.0 - path-to-regexp: ^2.2.1 - querystring: ^0.2.0 - whatwg-url: ^6.5.0 - peerDependencies: - node-fetch: "*" - peerDependenciesMeta: - node-fetch: - optional: true - checksum: debc4dd83bcda79b0aa71c38d08da6036906cdc49393343eb3426112314a7e57557255664f745d2e3f0b9b2a6e852bd3a564ae3f08332c27e422d3441bb865bd + "@types/glob-to-regexp": ^0.4.4 + dequal: ^2.0.3 + glob-to-regexp: ^0.4.1 + is-subset-of: ^3.1.10 + regexparam: ^3.0.0 + checksum: 6de234c4a3e2e36dd1505a4cfc19138e75b7dd123665a33a85047d9a675cbe88309f995d0ec8a8df32d8d9b028c6607e6e1a275b2a5c04c420b21c22d88568d8 languageName: node linkType: hard @@ -14373,7 +14370,7 @@ __metadata: languageName: node linkType: hard -"glob-to-regexp@npm:^0.4.0": +"glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" checksum: e795f4e8f06d2a15e86f76e4d92751cf8bbfcf0157cea5c2f0f35678a8195a750b34096b1256e436f0cebc1883b5ff0888c47348443e69546a5a87f9e1eb1167 @@ -16500,6 +16497,15 @@ __metadata: languageName: node linkType: hard +"is-subset-of@npm:^3.1.10": + version: 3.1.10 + resolution: "is-subset-of@npm:3.1.10" + dependencies: + typedescriptor: 3.0.2 + checksum: 98773fc775596dcfbb0f444e037e5ad101319a7932b82d85f706e77f6e53fe5b8bf19a3a8bea950269313c81c7f4ddec51fb107eedb9b51323b2d97a8dfb06c3 + languageName: node + linkType: hard + "is-subset@npm:^0.1.1": version: 0.1.1 resolution: "is-subset@npm:0.1.1" @@ -21495,13 +21501,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^2.2.1": - version: 2.4.0 - resolution: "path-to-regexp@npm:2.4.0" - checksum: 581175bf2968e51452f2b8c71f10e75c995693668b4ecf7d0b48962fbe0c56830661ca5dd5fd6d8e2f0cc9a045ce07e89af504ab133e1d21887c2712df85b1f4 - languageName: node - linkType: hard - "path-to-regexp@npm:^6.2.1": version: 6.2.1 resolution: "path-to-regexp@npm:6.2.1" @@ -23156,13 +23155,6 @@ __metadata: languageName: node linkType: hard -"querystring@npm:^0.2.0": - version: 0.2.1 - resolution: "querystring@npm:0.2.1" - checksum: 7b83b45d641e75fd39cd6625ddfd44e7618e741c61e95281b57bbae8fde0afcc12cf851924559e5cc1ef9baa3b1e06e22b164ea1397d65dd94b801f678d9c8ce - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -24162,6 +24154,13 @@ __metadata: languageName: node linkType: hard +"regexparam@npm:^3.0.0": + version: 3.0.0 + resolution: "regexparam@npm:3.0.0" + checksum: c8649af1538ccc12b5c5d250525f61bd370227dce41f4fb908433a9651e18b7be21dd8f8518c322dd9ebd75f7caaaea4921e374c39a469c11d4f9d0c738043e0 + languageName: node + linkType: hard + "regexpu-core@npm:^5.3.1": version: 5.3.2 resolution: "regexpu-core@npm:5.3.2" @@ -27762,6 +27761,13 @@ __metadata: languageName: node linkType: hard +"typedescriptor@npm:3.0.2": + version: 3.0.2 + resolution: "typedescriptor@npm:3.0.2" + checksum: 90e637ece22df0687acae70e152e88dd07ec10d0f4c87de2752bbcb78e420c5f5c86c3fe5e41d9f315a741a1e379e197fc5cb52044f33aafe891be6c63825ef7 + languageName: node + linkType: hard + "ua-parser-js@npm:^0.7.30": version: 0.7.37 resolution: "ua-parser-js@npm:0.7.37" @@ -28993,17 +28999,6 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^6.5.0": - version: 6.5.0 - resolution: "whatwg-url@npm:6.5.0" - dependencies: - lodash.sortby: ^4.7.0 - tr46: ^1.0.1 - webidl-conversions: ^4.0.2 - checksum: a10bd5e29f4382cd19789c2a7bbce25416e606b6fefc241c7fe34a2449de5bc5709c165bd13634eda433942d917ca7386a52841780b82dc37afa8141c31a8ebd - languageName: node - linkType: hard - "whatwg-url@npm:^7.0.0": version: 7.1.0 resolution: "whatwg-url@npm:7.1.0"