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"