From baeb4883b9957a136882f54632d96c76b9872427 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 20 May 2019 15:53:37 +0300 Subject: [PATCH 01/26] Removed the unnecessary prop definition. --- app/component/BicycleRentalStationRowContainer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/component/BicycleRentalStationRowContainer.js b/app/component/BicycleRentalStationRowContainer.js index 6312a130a6..f17b5e944e 100644 --- a/app/component/BicycleRentalStationRowContainer.js +++ b/app/component/BicycleRentalStationRowContainer.js @@ -47,7 +47,6 @@ const BicycleRentalStationRow = ({ distance, station }, { config, intl }) => { icon={isOff ? `${networkIcon}_off` : networkIcon} mode={isOff ? 'citybike_off' : 'citybike'} text={station.stationId} - hasDisruption={false} /> From 05ff013563a302a34d79eb9079ff9de7446f1c91 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Fri, 24 May 2019 12:04:34 +0300 Subject: [PATCH 02/26] Fixed an error with an unknown react component. --- app/component/map/VehicleMarkerContainer.js | 3 ++- .../map/VehicleMarkerContainer.test.js | 20 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/component/map/VehicleMarkerContainer.js b/app/component/map/VehicleMarkerContainer.js index 3f96e5d46d..1d808a43cf 100644 --- a/app/component/map/VehicleMarkerContainer.js +++ b/app/component/map/VehicleMarkerContainer.js @@ -30,7 +30,7 @@ function getVehicleIcon(mode, heading, useSmallIcon = false) { } return { - element: , + element: , className: `vehicle-icon bus ${useSmallIcon ? 'small-map-icon' : ''}`, iconSize: [20, 20], iconAnchor: [10, 10], @@ -142,4 +142,5 @@ export { connectedComponent as default, VehicleMarkerContainer as Component, shouldShowVehicle, + getVehicleIcon, }; diff --git a/test/unit/component/map/VehicleMarkerContainer.test.js b/test/unit/component/map/VehicleMarkerContainer.test.js index 8b38e9a883..6feebe3637 100644 --- a/test/unit/component/map/VehicleMarkerContainer.test.js +++ b/test/unit/component/map/VehicleMarkerContainer.test.js @@ -1,9 +1,11 @@ import React from 'react'; -import { shallowWithIntl } from '../../helpers/mock-intl-enzyme'; +import { shallowWithIntl, mountWithIntl } from '../../helpers/mock-intl-enzyme'; +import IconMarker from '../../../../app/component/map/IconMarker'; import { Component as VehicleMarkerContainer, shouldShowVehicle, + getVehicleIcon, } from '../../../../app/component/map/VehicleMarkerContainer'; const defaultProps = { @@ -186,4 +188,20 @@ describe('', () => { expect(shouldShow).to.equal(true); }); }); + + describe('getVehicleIcon', () => { + it('should use an appropriate icon for the given mode', () => { + const icon = getVehicleIcon('subway', 180); + const wrapper = mountWithIntl(icon.element); + expect(wrapper.prop('img')).to.equal('icon-icon_subway-live'); + expect(icon.className).to.contain('subway'); + }); + + it('should use a bus icon for an unknown mode', () => { + const icon = getVehicleIcon('foobar', 180); + const wrapper = mountWithIntl(icon.element); + expect(wrapper.prop('img')).to.equal('icon-icon_bus-live'); + expect(icon.className).to.contain('bus'); + }); + }); }); From 4b7487c7cd5143013951102b5b3d801dc5c5dc61 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Fri, 24 May 2019 12:08:50 +0300 Subject: [PATCH 03/26] Fixed lint. --- test/unit/component/map/VehicleMarkerContainer.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/unit/component/map/VehicleMarkerContainer.test.js b/test/unit/component/map/VehicleMarkerContainer.test.js index 6feebe3637..bc7c1ee30d 100644 --- a/test/unit/component/map/VehicleMarkerContainer.test.js +++ b/test/unit/component/map/VehicleMarkerContainer.test.js @@ -1,7 +1,6 @@ import React from 'react'; import { shallowWithIntl, mountWithIntl } from '../../helpers/mock-intl-enzyme'; -import IconMarker from '../../../../app/component/map/IconMarker'; import { Component as VehicleMarkerContainer, shouldShowVehicle, From 3bd7f2f1abf9b62c24acfa1d7e70a2899f274fcb Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Fri, 24 May 2019 13:49:12 +0300 Subject: [PATCH 04/26] Added an optional background layer for icons. --- app/component/Icon.js | 45 ++++++++++++++----- app/component/map/PositionMarker.js | 2 +- .../map/non-tile-layer/CityBikeMarker.js | 18 ++++---- .../map/non-tile-layer/StopMarker.js | 2 +- app/component/visual/SVGIcon.js | 41 ----------------- test/unit/component/Icon.test.js | 22 +++++++++ 6 files changed, 68 insertions(+), 62 deletions(-) delete mode 100644 app/component/visual/SVGIcon.js create mode 100644 test/unit/component/Icon.test.js diff --git a/app/component/Icon.js b/app/component/Icon.js index cccb72e40a..51f6f87cbf 100644 --- a/app/component/Icon.js +++ b/app/component/Icon.js @@ -54,27 +54,43 @@ IconBadge.asString = (badgeFill, badgeText) => { `; }; -function Icon(props) { +function Icon({ + backgroundShape, + badgeFill, + badgeText, + className, + color, + height, + id, + img, + omitViewBox, + viewBox, + width, +}) { return ( - + {backgroundShape === 'circle' && ( + + )} + - + ); } Icon.propTypes = { + backgroundShape: PropTypes.oneOf(['circle']), badgeFill: PropTypes.string, badgeText: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), className: PropTypes.string, @@ -88,6 +104,7 @@ Icon.propTypes = { }; Icon.defaultProps = { + backgroundShape: undefined, badgeFill: undefined, badgeText: undefined, className: undefined, @@ -99,19 +116,25 @@ Icon.defaultProps = { width: undefined, }; -Icon.asString = ( +Icon.asString = ({ img, className, id, badgeFill = undefined, badgeText = undefined, -) => ` + backgroundShape = undefined, +}) => ` + ${ + backgroundShape === 'circle' + ? '' + : '' + } ${IconBadge.asString(badgeFill, badgeText)} diff --git a/app/component/map/PositionMarker.js b/app/component/map/PositionMarker.js index 65e80b73d3..37b5d3a7e8 100644 --- a/app/component/map/PositionMarker.js +++ b/app/component/map/PositionMarker.js @@ -18,7 +18,7 @@ if (isBrowser) { const currentLocationIcon = isBrowser ? L.divIcon({ - html: Icon.asString('icon-icon_current-location'), + html: Icon.asString({ img: 'icon-icon_current-location' }), className: 'current-location-marker', iconSize: [40, 40], }) diff --git a/app/component/map/non-tile-layer/CityBikeMarker.js b/app/component/map/non-tile-layer/CityBikeMarker.js index 5df294be34..f79ec2d1aa 100644 --- a/app/component/map/non-tile-layer/CityBikeMarker.js +++ b/app/component/map/non-tile-layer/CityBikeMarker.js @@ -85,17 +85,19 @@ class CityBikeMarker extends React.Component { }) : L.divIcon({ html: showBikeAvailability - ? Icon.asString( - iconName, - 'city-bike-medium-size', - undefined, - getCityBikeAvailabilityIndicatorColor( + ? Icon.asString({ + img: iconName, + className: 'city-bike-medium-size', + badgeFill: getCityBikeAvailabilityIndicatorColor( station.bikesAvailable, config, ), - station.bikesAvailable, - ) - : Icon.asString(iconName, 'city-bike-medium-size'), + badgeText: station.bikesAvailable, + }) + : Icon.asString({ + img: iconName, + className: 'city-bike-medium-size', + }), iconSize: [20, 20], className: 'citybike cursor-pointer', }); diff --git a/app/component/map/non-tile-layer/StopMarker.js b/app/component/map/non-tile-layer/StopMarker.js index d16cc5ad98..f8445af7d4 100644 --- a/app/component/map/non-tile-layer/StopMarker.js +++ b/app/component/map/non-tile-layer/StopMarker.js @@ -42,7 +42,7 @@ class StopMarker extends React.Component { getModeIcon = zoom => { const iconId = `icon-icon_${this.props.mode}`; - const icon = Icon.asString(iconId, 'mode-icon'); + const icon = Icon.asString({ img: iconId, className: 'mode-icon' }); let size; if (zoom <= this.context.config.stopsSmallMaxZoom) { size = this.context.config.stopsIconSize.small; diff --git a/app/component/visual/SVGIcon.js b/app/component/visual/SVGIcon.js deleted file mode 100644 index 4e50eb2895..0000000000 --- a/app/component/visual/SVGIcon.js +++ /dev/null @@ -1,41 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import cx from 'classnames'; - -const SVGIcon = ({ id, color, viewBox, className, img }) => ( - - - -); - -SVGIcon.propTypes = { - id: PropTypes.string, - viewBox: PropTypes.string, - color: PropTypes.string, - className: PropTypes.string, - img: PropTypes.string.isRequired, -}; - -SVGIcon.defaultProps = { - id: 'icon', - viewBox: '0 0 40 40', -}; - -SVGIcon.asString = (img, className, id) => ` - - - -`; - -SVGIcon.displayName = 'SVGIcon'; -SVGIcon.description = 'Shows an icon from the SVG sprite'; -export default SVGIcon; diff --git a/test/unit/component/Icon.test.js b/test/unit/component/Icon.test.js new file mode 100644 index 0000000000..c48791d7e0 --- /dev/null +++ b/test/unit/component/Icon.test.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import Icon from '../../../app/component/Icon'; + +describe('', () => { + const backgroundShape = 'circle'; + const className = 'foo_class'; + const id = 'foo_id'; + const img = 'icon-icon_bus'; + + it('should include a circle as part of the svg render', () => { + const props = { backgroundShape, className, id, img }; + const wrapper = shallowWithIntl(); + expect(wrapper.find('circle')).to.have.lengthOf(1); + }); + + it('should include a circle as part of the svg string representation', () => { + const result = Icon.asString({ backgroundShape, className, id, img }); + expect(result).to.contain(' Date: Fri, 24 May 2019 14:32:18 +0300 Subject: [PATCH 05/26] Do not consider falsy alerts to be valid. --- app/util/alertUtils.js | 6 +++++- test/unit/util/alertUtils.test.js | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/util/alertUtils.js b/app/util/alertUtils.js index 65ad6b7a6e..23bbf55092 100644 --- a/app/util/alertUtils.js +++ b/app/util/alertUtils.js @@ -143,10 +143,14 @@ export const DEFAULT_VALIDITY = 5 * 60; * @param {number} defaultValidity the default validity period length in seconds. */ export const isAlertValid = ( - { validityPeriod } = {}, + alert, referenceUnixTime, defaultValidity = DEFAULT_VALIDITY, ) => { + if (!alert) { + return false; + } + const { validityPeriod } = alert; if (!validityPeriod || !isNumber(referenceUnixTime)) { return true; } diff --git a/test/unit/util/alertUtils.test.js b/test/unit/util/alertUtils.test.js index 01c1f2b2fe..fc1098e949 100644 --- a/test/unit/util/alertUtils.test.js +++ b/test/unit/util/alertUtils.test.js @@ -736,6 +736,10 @@ describe('alertUtils', () => { ), ).to.equal(false); }); + + it('should return false if the alert itself is falsy', () => { + expect(utils.isAlertValid(undefined, 0)).to.equal(false); + }); }); describe('getCancelationsForRoute', () => { From bbf6666996f2354b23b72187eb82f325b48c48dc Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Fri, 24 May 2019 15:41:19 +0300 Subject: [PATCH 06/26] Added support for showing the correct alert severity icon for favourites. --- app/component/FavouritesTabLabel.js | 11 +++-- app/component/FavouritesTabLabelContainer.js | 44 ++++++++++++-------- app/component/IconWithCaution.js | 41 +++++++++++------- test/unit/component/IconWithCaution.test.js | 35 ++++++++++++++++ 4 files changed, 95 insertions(+), 36 deletions(-) create mode 100644 test/unit/component/IconWithCaution.test.js diff --git a/app/component/FavouritesTabLabel.js b/app/component/FavouritesTabLabel.js index da4dba7864..c6519ebf1d 100644 --- a/app/component/FavouritesTabLabel.js +++ b/app/component/FavouritesTabLabel.js @@ -5,15 +5,16 @@ import Icon from './Icon'; import IconWithCaution from './IconWithCaution'; export default function FavouritesTabLabel({ - hasDisruption, + alertSeverityLevel, classes, onClick, }) { return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role, jsx-a11y/anchor-is-valid, jsx-a11y/click-events-have-key-events
  • - {hasDisruption ? ( + {alertSeverityLevel ? ( @@ -26,7 +27,11 @@ export default function FavouritesTabLabel({ } FavouritesTabLabel.propTypes = { - hasDisruption: PropTypes.bool, + alertSeverityLevel: PropTypes.string, classes: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, }; + +FavouritesTabLabel.defaultProps = { + alertSeverityLevel: undefined, +}; diff --git a/app/component/FavouritesTabLabelContainer.js b/app/component/FavouritesTabLabelContainer.js index 4a01014be1..1235d2c739 100644 --- a/app/component/FavouritesTabLabelContainer.js +++ b/app/component/FavouritesTabLabelContainer.js @@ -3,31 +3,36 @@ import React from 'react'; import Relay from 'react-relay/classic'; import connectToStores from 'fluxible-addons-react/connectToStores'; import mapProps from 'recompose/mapProps'; -import some from 'lodash/some'; -import flatten from 'lodash/flatten'; -import RoutesRoute from '../route/RoutesRoute'; + import FavouritesTabLabel from './FavouritesTabLabel'; +import RoutesRoute from '../route/RoutesRoute'; +import { RouteAlertsQuery } from '../util/alertQueries'; +import { getActiveAlertSeverityLevel } from '../util/alertUtils'; import { isBrowser } from '../util/browser'; -const hasDisruption = routes => - some(flatten(routes.map(route => route && route.alerts.length > 0))); - -const alertReducer = mapProps(({ routes, ...rest }) => ({ - hasDisruption: hasDisruption(routes), - ...rest, -})); +const alertReducer = mapProps(({ routes, currentTime, ...rest }) => { + const alertSeverityLevel = getActiveAlertSeverityLevel( + Array.isArray(routes) && + routes + .map(route => (Array.isArray(route.alerts) && route.alerts) || []) + .reduce((a, b) => a.concat(b), []), + currentTime, + ); + return { + alertSeverityLevel, + ...rest, + }; +}); const FavouritesTabLabelRelayConnector = Relay.createContainer( alertReducer(FavouritesTabLabel), { fragments: { routes: () => Relay.QL` - fragment on Route @relay(plural:true) { - alerts { - id - } - } - `, + fragment on Route @relay(plural:true) { + ${RouteAlertsQuery} + } + `, }, }, ); @@ -54,12 +59,17 @@ function FavouritesTabLabelContainer({ routes, ...rest }) { FavouritesTabLabelContainer.propTypes = { routes: PropTypes.array.isRequired, + currentTime: PropTypes.number.isRequired, }; export default connectToStores( FavouritesTabLabelContainer, - ['FavouriteRoutesStore'], + ['FavouriteRoutesStore', 'TimeStore'], context => ({ routes: context.getStore('FavouriteRoutesStore').getRoutes(), + currentTime: context + .getStore('TimeStore') + .getCurrentTime() + .unix(), }), ); diff --git a/app/component/IconWithCaution.js b/app/component/IconWithCaution.js index 60ad1abed4..a7cf87a080 100644 --- a/app/component/IconWithCaution.js +++ b/app/component/IconWithCaution.js @@ -1,23 +1,25 @@ import PropTypes from 'prop-types'; import React from 'react'; import cx from 'classnames'; + import ComponentUsageExample from './ComponentUsageExample'; +import { AlertSeverityLevelType } from '../constants'; -const IconWithCaution = props => ( - - - - -); +const IconWithCaution = ({ alertSeverityLevel, className, id, img }) => { + const isInfoLevel = alertSeverityLevel === AlertSeverityLevelType.Info; + return ( + + + {isInfoLevel && } + + + ); +}; IconWithCaution.description = () => ( @@ -28,9 +30,16 @@ IconWithCaution.description = () => ( IconWithCaution.displayName = 'IconWithCaution'; IconWithCaution.propTypes = { - id: PropTypes.string, + alertSeverityLevel: PropTypes.string, className: PropTypes.string, + id: PropTypes.string, img: PropTypes.string.isRequired, }; +IconWithCaution.defaultProps = { + alertSeverityLevel: undefined, + className: undefined, + id: undefined, +}; + export default IconWithCaution; diff --git a/test/unit/component/IconWithCaution.test.js b/test/unit/component/IconWithCaution.test.js new file mode 100644 index 0000000000..0afae68350 --- /dev/null +++ b/test/unit/component/IconWithCaution.test.js @@ -0,0 +1,35 @@ +import React from 'react'; + +import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import IconWithCaution from '../../../app/component/IconWithCaution'; +import { AlertSeverityLevelType } from '../../../app/constants'; + +describe('', () => { + it('should use the caution icon if there is no severity level', () => { + const props = { + img: 'foobar', + }; + const wrapper = shallowWithIntl(); + expect( + wrapper + .find('use') + .at(1) + .prop('xlinkHref'), + ).to.contain('caution'); + }); + + it('should use the info icon with a circular backgound if the severity level is "INFO"', () => { + const props = { + alertSeverityLevel: AlertSeverityLevelType.Info, + img: 'foobar', + }; + const wrapper = shallowWithIntl(); + expect( + wrapper + .find('use') + .at(1) + .prop('xlinkHref'), + ).to.contain('info'); + expect(wrapper.find('circle')).to.have.lengthOf(1); + }); +}); From efed6458d2854f90176b6aeb0ff8cadfbc1abd39 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Fri, 24 May 2019 15:53:19 +0300 Subject: [PATCH 07/26] Included some testing for the FavouritesTabLabel component. --- .../unit/component/FavouritesTabLabel.test.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/unit/component/FavouritesTabLabel.test.js diff --git a/test/unit/component/FavouritesTabLabel.test.js b/test/unit/component/FavouritesTabLabel.test.js new file mode 100644 index 0000000000..109ba3ea93 --- /dev/null +++ b/test/unit/component/FavouritesTabLabel.test.js @@ -0,0 +1,31 @@ +import React from 'react'; + +import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import { AlertSeverityLevelType } from '../../../app/constants'; +import FavouritesTabLabel from '../../../app/component/FavouritesTabLabel'; +import Icon from '../../../app/component/Icon'; +import IconWithCaution from '../../../app/component/IconWithCaution'; + +describe('', () => { + it('should use the caution icon with an alertSeverityLevel', () => { + const props = { + alertSeverityLevel: AlertSeverityLevelType.Warning, + classes: '', + onClick: () => {}, + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(IconWithCaution)).to.have.lengthOf(1); + expect(wrapper.find(IconWithCaution).prop('alertSeverityLevel')).to.equal( + AlertSeverityLevelType.Warning, + ); + }); + + it('should use the normal icon', () => { + const props = { + classes: '', + onClick: () => {}, + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(Icon)).to.have.lengthOf(1); + }); +}); From 3ccfe5b4dcf0fefd1c8db533c78214cacf74e222 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Fri, 24 May 2019 16:40:36 +0300 Subject: [PATCH 08/26] Added handling for alert severity level. --- app/component/FavouriteRouteListContainer.js | 33 ++++++++----- .../FavouriteRouteListContainer.test.js | 46 ++++++++++++++++++- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/app/component/FavouriteRouteListContainer.js b/app/component/FavouriteRouteListContainer.js index afd6c980e1..59f6133870 100644 --- a/app/component/FavouriteRouteListContainer.js +++ b/app/component/FavouriteRouteListContainer.js @@ -2,9 +2,11 @@ import Relay from 'react-relay/classic'; import connectToStores from 'fluxible-addons-react/connectToStores'; import NextDeparturesList from './NextDeparturesList'; +import { RouteAlertsQuery } from '../util/alertQueries'; +import { getActiveAlertSeverityLevel } from '../util/alertUtils'; import { getDistanceToNearestStop } from '../util/geo-utils'; -export const getNextDepartures = (routes, lat, lon) => { +export const getNextDepartures = (routes, lat, lon, currentTime) => { const nextDepartures = []; const seenDepartures = {}; @@ -13,7 +15,10 @@ export const getNextDepartures = (routes, lat, lon) => { return; } - const hasDisruption = route.alerts.length > 0; + const alertSeverityLevel = getActiveAlertSeverityLevel( + route.alerts, + currentTime, + ); route.patterns.forEach(pattern => { const closest = getDistanceToNearestStop(lat, lon, pattern.stops); @@ -35,9 +40,9 @@ export const getNextDepartures = (routes, lat, lon) => { }) .forEach(stoptime => { nextDepartures.push({ + alertSeverityLevel, distance: closest.distance, stoptime, - hasDisruption, }); }); }); @@ -50,13 +55,21 @@ export const getNextDepartures = (routes, lat, lon) => { const FavouriteRouteListContainer = connectToStores( NextDeparturesList, ['TimeStore'], - (context, { routes, origin }) => ({ - currentTime: context + (context, { routes, origin }) => { + const currentTime = context .getStore('TimeStore') .getCurrentTime() - .unix(), - departures: getNextDepartures(routes, origin.lat, origin.lon), - }), + .unix(); + return { + currentTime, + departures: getNextDepartures( + routes, + origin.lat, + origin.lon, + currentTime, + ), + }; + }, ); // TODO: Add filtering in stoptimesForPatterns for route gtfsId @@ -64,9 +77,7 @@ export default Relay.createContainer(FavouriteRouteListContainer, { fragments: { routes: () => Relay.QL` fragment on Route @relay(plural:true) { - alerts { - id - } + ${RouteAlertsQuery} patterns { headsign stops { diff --git a/test/unit/component/FavouriteRouteListContainer.test.js b/test/unit/component/FavouriteRouteListContainer.test.js index 6bd5d3d755..3694b62315 100644 --- a/test/unit/component/FavouriteRouteListContainer.test.js +++ b/test/unit/component/FavouriteRouteListContainer.test.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getNextDepartures } from '../../../app/component/FavouriteRouteListContainer'; +import { AlertSeverityLevelType } from '../../../app/constants'; describe('', () => { describe('getNextDepartures', () => { @@ -9,8 +10,51 @@ describe('', () => { const routes = [null]; const lat = 60.219235; const lon = 24.81329; - const result = getNextDepartures(routes, lat, lon); + const currentTime = 1558703670; + const result = getNextDepartures(routes, lat, lon, currentTime); expect(result).to.deep.equal([]); }); + + it('should map the active alertSeverityLevel if available', () => { + const lat = 60.219235; + const lon = 24.81329; + const currentTime = 1558703670; + const routes = [ + { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Info, + effectiveStartDate: currentTime, + }, + { + alertSeverityLevel: AlertSeverityLevelType.Warning, + effectiveStartDate: currentTime + 1, // in the future + }, + ], + patterns: [ + { + stops: [ + { + lat: 60, + lon: 25, + stoptimes: [ + { + pattern: { + route: {}, + }, + }, + ], + }, + ], + }, + ], + }, + ]; + + const result = getNextDepartures(routes, lat, lon, currentTime); + expect(result[0].alertSeverityLevel).to.equal( + AlertSeverityLevelType.Info, + ); + }); }); }); From d37230095c46cac23532fb73669834bb32bae5b4 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 10:42:52 +0300 Subject: [PATCH 09/26] Improved alert filtering by route's pattern code. --- app/component/FavouriteRouteListContainer.js | 21 +++--- app/util/alertUtils.js | 31 +++++---- .../FavouriteRouteListContainer.test.js | 69 +++++++++++++++++++ test/unit/util/alertUtils.test.js | 34 +++++++++ 4 files changed, 131 insertions(+), 24 deletions(-) diff --git a/app/component/FavouriteRouteListContainer.js b/app/component/FavouriteRouteListContainer.js index 59f6133870..8c917aad53 100644 --- a/app/component/FavouriteRouteListContainer.js +++ b/app/component/FavouriteRouteListContainer.js @@ -3,7 +3,10 @@ import connectToStores from 'fluxible-addons-react/connectToStores'; import NextDeparturesList from './NextDeparturesList'; import { RouteAlertsQuery } from '../util/alertQueries'; -import { getActiveAlertSeverityLevel } from '../util/alertUtils'; +import { + getActiveAlertSeverityLevel, + patternIdPredicate, +} from '../util/alertUtils'; import { getDistanceToNearestStop } from '../util/geo-utils'; export const getNextDepartures = (routes, lat, lon, currentTime) => { @@ -15,12 +18,14 @@ export const getNextDepartures = (routes, lat, lon, currentTime) => { return; } - const alertSeverityLevel = getActiveAlertSeverityLevel( - route.alerts, - currentTime, - ); - route.patterns.forEach(pattern => { + const patternAlerts = + Array.isArray(route.alerts) && + route.alerts.filter(alert => patternIdPredicate(alert, pattern.code)); + const alertSeverityLevel = getActiveAlertSeverityLevel( + patternAlerts || [], + currentTime, + ); const closest = getDistanceToNearestStop(lat, lon, pattern.stops); closest.stop.stoptimes .filter(stoptime => { @@ -103,10 +108,6 @@ export default Relay.createContainer(FavouriteRouteListContainer, { realtime serviceDay } - pattern { - headsign - route { gtfsId } - } } } } diff --git a/app/util/alertUtils.js b/app/util/alertUtils.js index 23bbf55092..379693913c 100644 --- a/app/util/alertUtils.js +++ b/app/util/alertUtils.js @@ -1,4 +1,5 @@ import find from 'lodash/find'; +import get from 'lodash/get'; import isNumber from 'lodash/isNumber'; import uniqBy from 'lodash/uniqBy'; import PropTypes from 'prop-types'; @@ -9,6 +10,18 @@ import { AlertEffectType, } from '../constants'; +/** + * Checks if the alert is for the given pattern. + * + * @param {*} alert the alert object to check. + * @param {*} patternId the pattern's id, optional. + */ +export const patternIdPredicate = (alert, patternId = undefined) => + patternId + ? (alert && !alert.trip) || + get(alert, 'trip.pattern.code', undefined) === patternId + : true; + /** * Checks if the stop has any alerts. * @@ -31,13 +44,7 @@ export const routeHasServiceAlert = (route, patternId = undefined) => { if (!route || !Array.isArray(route.alerts)) { return false; } - return patternId - ? route.alerts.some( - alert => - !alert.trip || - (alert.trip.pattern && alert.trip.pattern.code === patternId), - ) - : route.alerts.length > 0; + return route.alerts.some(alert => patternIdPredicate(alert, patternId)); }; /** @@ -327,13 +334,9 @@ export const getServiceAlertsForRoute = ( } return getServiceAlerts( { - alerts: patternId - ? route.alerts.filter( - alert => - !alert.trip || - (alert.trip.pattern && alert.trip.pattern.code === patternId), - ) - : route.alerts, + alerts: route.alerts.filter(alert => + patternIdPredicate(alert, patternId), + ), }, route, locale, diff --git a/test/unit/component/FavouriteRouteListContainer.test.js b/test/unit/component/FavouriteRouteListContainer.test.js index 3694b62315..a399d3c01d 100644 --- a/test/unit/component/FavouriteRouteListContainer.test.js +++ b/test/unit/component/FavouriteRouteListContainer.test.js @@ -56,5 +56,74 @@ describe('', () => { AlertSeverityLevelType.Info, ); }); + + it('should map the correct alertSeverityLevel for each pattern', () => { + const lat = 60.219235; + const lon = 24.81329; + const currentTime = 1558703670; + const routes = [ + { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Info, + effectiveStartDate: currentTime, + trip: { + pattern: { + code: 'foo', + }, + }, + }, + { + alertSeverityLevel: AlertSeverityLevelType.Warning, + effectiveStartDate: currentTime, + trip: { + pattern: { + code: 'bar', + }, + }, + }, + ], + patterns: [ + { + code: 'foo', + stops: [ + { + lat: 60, + lon: 25, + stoptimes: [ + { + pattern: { + route: {}, + }, + }, + ], + }, + ], + }, + { + code: 'bar', + stops: [ + { + lat: 60, + lon: 25, + stoptimes: [ + { + pattern: { + route: {}, + }, + }, + ], + }, + ], + }, + ], + }, + ]; + + const result = getNextDepartures(routes, lat, lon, currentTime); + expect(result[0].alertSeverityLevel).to.equal( + AlertSeverityLevelType.Info, + ); + }); }); }); diff --git a/test/unit/util/alertUtils.test.js b/test/unit/util/alertUtils.test.js index fc1098e949..a7627372ac 100644 --- a/test/unit/util/alertUtils.test.js +++ b/test/unit/util/alertUtils.test.js @@ -1152,4 +1152,38 @@ describe('alertUtils', () => { ); }); }); + + describe('patternIdPredicate', () => { + it('should return true if alert exists but patternId does not', () => { + expect(utils.patternIdPredicate({}, undefined)).to.equal(true); + }); + + it('should return false if patternId exists but alert does not', () => { + expect(utils.patternIdPredicate(undefined, 'foobar')).to.equal(false); + }); + + it('should return true if the path alert.trip.pattern.code matches the given patternId', () => { + expect( + utils.patternIdPredicate( + { trip: { pattern: { code: 'foobar' } } }, + 'foobar', + ), + ).to.equal(true); + }); + + it('should return false if the path alert.trip.pattern.code does not match the given patternId', () => { + expect( + utils.patternIdPredicate( + { trip: { pattern: { code: 'foobaz' } } }, + 'foobar', + ), + ).to.equal(false); + }); + + it('should return true if trip information is not available', () => { + expect(utils.patternIdPredicate({ trip: undefined }, 'foobar')).to.equal( + true, + ); + }); + }); }); From d5ba52ad68e25ace4e06fad0a610dad01742e6fc Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 12:11:53 +0300 Subject: [PATCH 10/26] Added support for different alert severity levels. --- app/component/IconWithBigCaution.js | 25 +++++++---- app/component/IconWithIcon.js | 1 - app/component/NextDeparturesList.js | 5 +-- app/component/RouteNumber.js | 9 ++-- .../unit/component/IconWithBigCaution.test.js | 42 +++++++++++++++++++ test/unit/component/RouteNumber.test.js | 21 +++++++++- 6 files changed, 86 insertions(+), 17 deletions(-) create mode 100644 test/unit/component/IconWithBigCaution.test.js diff --git a/app/component/IconWithBigCaution.js b/app/component/IconWithBigCaution.js index a294a716f6..bb3adc29d4 100644 --- a/app/component/IconWithBigCaution.js +++ b/app/component/IconWithBigCaution.js @@ -2,16 +2,21 @@ import PropTypes from 'prop-types'; import React from 'react'; import IconWithIcon from './IconWithIcon'; import ComponentUsageExample from './ComponentUsageExample'; +import { AlertSeverityLevelType } from '../constants'; -const IconWithBigCaution = ({ img, className, color }) => ( - -); +const IconWithBigCaution = ({ alertSeverityLevel, className, color, img }) => { + const iconType = + alertSeverityLevel === AlertSeverityLevelType.Info ? 'info' : 'caution'; + return ( + + ); +}; IconWithBigCaution.displayName = 'IconWithBigCaution'; @@ -24,12 +29,14 @@ IconWithBigCaution.description = () => ( ); IconWithBigCaution.propTypes = { + alertSeverityLevel: PropTypes.string, color: PropTypes.string, className: PropTypes.string, img: PropTypes.string.isRequired, }; IconWithBigCaution.defaultProps = { + alertSeverityLevel: undefined, className: '', }; diff --git a/app/component/IconWithIcon.js b/app/component/IconWithIcon.js index 71b9013c36..d191e78708 100644 --- a/app/component/IconWithIcon.js +++ b/app/component/IconWithIcon.js @@ -93,7 +93,6 @@ IconWithIcon.propTypes = { }; IconWithIcon.contextTypes = { - // eslint-disable-next-line intl: intlShape.isRequired, }; diff --git a/app/component/NextDeparturesList.js b/app/component/NextDeparturesList.js index b93c7c5211..abe76fd28f 100644 --- a/app/component/NextDeparturesList.js +++ b/app/component/NextDeparturesList.js @@ -100,9 +100,9 @@ function NextDeparturesList(props, context) { @@ -124,11 +124,10 @@ function NextDeparturesList(props, context) { NextDeparturesList.propTypes = { departures: PropTypes.array.isRequired, - currentTime: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types + currentTime: PropTypes.number.isRequired, }; NextDeparturesList.contextTypes = { - // eslint-disable-next-line react/no-typos router: routerShape.isRequired, }; diff --git a/app/component/RouteNumber.js b/app/component/RouteNumber.js index 2fa0e7664a..17b6793ffa 100644 --- a/app/component/RouteNumber.js +++ b/app/component/RouteNumber.js @@ -11,7 +11,7 @@ const LONG_ROUTE_NUMBER_LENGTH = 6; function RouteNumber(props, context) { let mode = props.mode.toLowerCase(); - const { color } = props; + const { alertSeverityLevel, color } = props; if (mode === 'bicycle' || mode === 'car') { mode += '-withoutBox'; @@ -37,9 +37,10 @@ function RouteNumber(props, context) { ); } - if (hasDisruption) { + if (hasDisruption || !!alertSeverityLevel) { return ( ( ); RouteNumber.propTypes = { + alertSeverityLevel: PropTypes.string, mode: PropTypes.string.isRequired, color: PropTypes.string, text: PropTypes.node, @@ -206,6 +208,7 @@ RouteNumber.propTypes = { }; RouteNumber.defaultProps = { + alertSeverityLevel: undefined, badgeFill: undefined, badgeText: undefined, className: '', @@ -220,7 +223,7 @@ RouteNumber.defaultProps = { }; RouteNumber.contextTypes = { - intl: intlShape.isRequired, // eslint-disable-line react/no-typos + intl: intlShape.isRequired, }; RouteNumber.displayName = 'RouteNumber'; diff --git a/test/unit/component/IconWithBigCaution.test.js b/test/unit/component/IconWithBigCaution.test.js new file mode 100644 index 0000000000..10a96288c3 --- /dev/null +++ b/test/unit/component/IconWithBigCaution.test.js @@ -0,0 +1,42 @@ +import React from 'react'; + +import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import IconWithBigCaution from '../../../app/component/IconWithBigCaution'; +import IconWithIcon from '../../../app/component/IconWithIcon'; +import { AlertSeverityLevelType } from '../../../app/constants'; + +describe('', () => { + it('should have a caution sub icon by default', () => { + it('should have a caution sub icon when alertSeverityLevel is high enough', () => { + const props = { + img: 'foobar', + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(IconWithIcon).prop('subIcon')).to.equal( + 'icon-icon_caution', + ); + }); + }); + + it('should have a caution sub icon when alertSeverityLevel is high enough', () => { + const props = { + alertSeverityLevel: AlertSeverityLevelType.Warning, + img: 'foobar', + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(IconWithIcon).prop('subIcon')).to.equal( + 'icon-icon_caution', + ); + }); + + it('should have an info sub icon when alertSeverityLevel is "INFO"', () => { + const props = { + alertSeverityLevel: AlertSeverityLevelType.Info, + img: 'foobar', + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(IconWithIcon).prop('subIcon')).to.equal( + 'icon-icon_info', + ); + }); +}); diff --git a/test/unit/component/RouteNumber.test.js b/test/unit/component/RouteNumber.test.js index 4fa084be4a..aff3d9f562 100644 --- a/test/unit/component/RouteNumber.test.js +++ b/test/unit/component/RouteNumber.test.js @@ -1,6 +1,7 @@ import React from 'react'; -import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import { mountWithIntl, shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import { AlertSeverityLevelType } from '../../../app/constants'; import IconWithBigCaution from '../../../app/component/IconWithBigCaution'; import IconWithIcon from '../../../app/component/IconWithIcon'; import RouteNumber from '../../../app/component/RouteNumber'; @@ -50,4 +51,22 @@ describe('', () => { 'icon-icon_scooter', ); }); + + it('should have a caution icon when hasDisruption is true', () => { + const props = { + hasDisruption: true, + mode: 'BUS', + }; + const wrapper = mountWithIntl(); + expect(wrapper.find(IconWithBigCaution)).to.have.lengthOf(1); + }); + + it('should have a caution icon when alertSeverityLevel has been defined', () => { + const props = { + alertSeverityLevel: AlertSeverityLevelType.Info, + mode: 'BUS', + }; + const wrapper = mountWithIntl(); + expect(wrapper.find(IconWithBigCaution)).to.have.lengthOf(1); + }); }); From 8ac9da33004fa3d09627f2c6ca9d20b0b9585170 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 12:17:28 +0300 Subject: [PATCH 11/26] Added support for alert severity level handling. --- app/component/RouteAlertsRow.js | 7 ++++++- test/unit/component/RouteAlertsRow.test.js | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/component/RouteAlertsRow.js b/app/component/RouteAlertsRow.js index 9463821614..969300cb7c 100644 --- a/app/component/RouteAlertsRow.js +++ b/app/component/RouteAlertsRow.js @@ -58,7 +58,12 @@ export default function RouteAlertsRow( return (
    {routeMode ? ( - + ) : (
    diff --git a/test/unit/component/RouteAlertsRow.test.js b/test/unit/component/RouteAlertsRow.test.js index fa33a0c7ff..9cc5c466c0 100644 --- a/test/unit/component/RouteAlertsRow.test.js +++ b/test/unit/component/RouteAlertsRow.test.js @@ -87,4 +87,15 @@ describe('', () => { const wrapper = shallowWithIntl(); expect(wrapper.find('.route-alert-url')).to.have.lengthOf(1); }); + + it('should render a RouteNumber with a specified alertSeverityLevel', () => { + const props = { + routeMode: 'BUS', + severityLevel: AlertSeverityLevelType.Warning, + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(RouteNumber).prop('alertSeverityLevel')).to.equal( + AlertSeverityLevelType.Warning, + ); + }); }); From fb318b4211e32247a19eb62a8808e22c8e9b96a7 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 12:33:23 +0300 Subject: [PATCH 12/26] Added support for sub icon background. --- app/component/IconWithBigCaution.js | 1 + app/component/IconWithIcon.js | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/component/IconWithBigCaution.js b/app/component/IconWithBigCaution.js index bb3adc29d4..ba5f96ee61 100644 --- a/app/component/IconWithBigCaution.js +++ b/app/component/IconWithBigCaution.js @@ -14,6 +14,7 @@ const IconWithBigCaution = ({ alertSeverityLevel, className, color, img }) => { img={img} subIcon={`icon-icon_${iconType}`} subIconClassName={`subicon-${iconType}`} + subIconShape={(iconType === 'info' && 'circle') || undefined} /> ); }; diff --git a/app/component/IconWithIcon.js b/app/component/IconWithIcon.js index d191e78708..f16278aa59 100644 --- a/app/component/IconWithIcon.js +++ b/app/component/IconWithIcon.js @@ -14,14 +14,15 @@ const subIconTemplate = { const IconWithIcon = ( { - id, + badgeFill, + badgeText, className, + color, + id, img, subIcon, subIconClassName, - color, - badgeFill, - badgeText, + subIconShape, }, { intl }, ) => ( @@ -40,7 +41,7 @@ const IconWithIcon = ( style={subIconTemplate} title={intl.formatMessage({ id: 'disruption' })} > - + )} @@ -82,14 +83,15 @@ IconWithIcon.description = () => ( IconWithIcon.displayName = 'IconWithIcon'; IconWithIcon.propTypes = { - id: PropTypes.string, + badgeFill: PropTypes.string, + badgeText: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), className: PropTypes.string, + color: PropTypes.string, + id: PropTypes.string, img: PropTypes.string.isRequired, subIcon: PropTypes.string, subIconClassName: PropTypes.string, - color: PropTypes.string, - badgeFill: PropTypes.string, - badgeText: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + subIconShape: PropTypes.string, }; IconWithIcon.contextTypes = { @@ -97,12 +99,13 @@ IconWithIcon.contextTypes = { }; IconWithIcon.defaultProps = { + badgeFill: undefined, + badgeText: undefined, + className: '', id: '', subIcon: '', - className: '', subIconClassName: '', - badgeFill: undefined, - badgeText: undefined, + subIconShape: undefined, }; export default IconWithIcon; From 3eb3935d121a7b6f7e3a2f4d0f177d87ba932083 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 12:38:53 +0300 Subject: [PATCH 13/26] Styling for info icon. --- sass/base/_base.scss | 69 +++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/sass/base/_base.scss b/sass/base/_base.scss index 7a230eaa26..c2e23461ec 100644 --- a/sass/base/_base.scss +++ b/sass/base/_base.scss @@ -86,20 +86,20 @@ h1, font-size: $font-size-xlarge; padding: 0.4em 0 0.2em; margin: 0; - color:$black; + color: $black; } h2, .h2 { font-family: $font-family; font-weight: $font-weight-bold; font-size: $font-size-large; - color:$black; - text-transform:none; + color: $black; + text-transform: none; } h3, .h3 { - color:$black; + color: $black; font-family: $font-family; font-weight: $font-weight-medium; font-size: $font-size-normal; @@ -108,7 +108,7 @@ h3, text-transform: none; } h4, -.h4 { +.h4 { font-family: $font-family; font-weight: $font-weight-bold; font-size: $font-size-xsmall; @@ -133,7 +133,7 @@ h1 a { margin: 0 !important; color: $gray; } -.sub-header-h5 { +.sub-header-h5 { font-family: $font-family; font-weight: $font-weight-book; font-size: $font-size-xsmall; @@ -151,14 +151,14 @@ h1 a { } .transparent { - background:rgba(0,0,0, $transparency) !important; + background: rgba(0, 0, 0, $transparency) !important; color: $transparent-color !important; } div.spinner-loader { position: absolute; left: 50%; - margin: $spinner-size*0.25 0 0 $spinner-size*-0.5; + margin: $spinner-size * 0.25 0 0 $spinner-size * -0.5; width: $spinner-size; height: $spinner-size; background-image: $spinner-image; @@ -167,13 +167,15 @@ div.spinner-loader { } @keyframes spin { - 100% { transform:rotate(360deg); } + 100% { + transform: rotate(360deg); + } } -hr { - clear:both; +hr { + clear: both; } -body select { +body select { background-image: url("data:image/svg+xml;utf8,"); background-position: right 10px top 50%; margin-bottom: 0; @@ -194,29 +196,48 @@ body select { padding: 0 10px; } .color-code { - display: inline-block; - padding-bottom: 20px; + display: inline-block; + padding-bottom: 20px; } -.color-palette { - margin-bottom:-20px; +.color-palette { + margin-bottom: -20px; } .sub-header { - font-weight: bold; - font-size: 20px; - text-transform: uppercase; - padding: 10px 0; + font-weight: bold; + font-size: 20px; + text-transform: uppercase; + padding: 10px 0; } .leaflet-container a { color: currentColor; } -.subicon-caution { +.subicon-caution, +.subicon-info { .icon { - fill: #dc0451; - color: white; display: block; overflow: visible; - width: 13px; + } +} + +.subicon-caution { + .icon { + color: white; + fill: $disruption-color; height: 13px; + width: 13px; + } +} + +.subicon-info { + .icon { + fill: $gray; + height: 11px; + width: 11px; + } + + .icon-circle { + stroke: white; + stroke-width: 12; } } From 6eab7e6a445accbda1719dfc615aac3955f141e0 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 12:59:52 +0300 Subject: [PATCH 14/26] Fill the container. --- app/component/route.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/component/route.scss b/app/component/route.scss index 8fd2e27bd5..ae3951d982 100644 --- a/app/component/route.scss +++ b/app/component/route.scss @@ -872,6 +872,7 @@ div.route-tabs { } .route-alert-contents { + flex-grow: 1; vertical-align: bottom; .route-alert-top-row { From b4c1bdf2adaf8a71d6731906a15c82b52b986bc8 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 14:08:10 +0300 Subject: [PATCH 15/26] Removed hasDisruption, added alertSeverityLevel. --- app/component/Departure.js | 7 ++++--- app/component/RouteNumberContainer.js | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/component/Departure.js b/app/component/Departure.js index 752ebd0672..899a00bfe5 100644 --- a/app/component/Departure.js +++ b/app/component/Departure.js @@ -17,11 +17,11 @@ import { } from './ExampleData'; function Departure({ + alertSeverityLevel, canceled, className, currentTime, departure, - hasDisruption, isArrival, isLastStop, showPlatformCode, @@ -47,8 +47,8 @@ function Departure({ /> )} @@ -127,9 +127,9 @@ Departure.description = () => ( Departure.displayName = 'Departure'; Departure.propTypes = { + alertSeverityLevel: PropTypes.string, canceled: PropTypes.bool, className: PropTypes.string, - hasDisruption: PropTypes.bool, currentTime: PropTypes.number.isRequired, departure: PropTypes.shape({ headsign: PropTypes.string, @@ -155,6 +155,7 @@ Departure.propTypes = { }; Departure.defaultProps = { + alertSeverityLevel: undefined, showPlatformCode: false, }; diff --git a/app/component/RouteNumberContainer.js b/app/component/RouteNumberContainer.js index 9647b8d3ab..1be7a996b3 100644 --- a/app/component/RouteNumberContainer.js +++ b/app/component/RouteNumberContainer.js @@ -16,15 +16,15 @@ const getText = (route, config) => { }; const RouteNumberContainer = ( - { className, route, isCallAgency, ...props }, + { alertSeverityLevel, className, route, isCallAgency, ...props }, { config }, ) => route && ( Date: Mon, 27 May 2019 14:15:14 +0300 Subject: [PATCH 16/26] Include alert severity level handling. --- app/component/DepartureListContainer.js | 53 ++++++++++++++----------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/app/component/DepartureListContainer.js b/app/component/DepartureListContainer.js index df1836e0bb..a893205fa8 100644 --- a/app/component/DepartureListContainer.js +++ b/app/component/DepartureListContainer.js @@ -1,20 +1,20 @@ +import cx from 'classnames'; +import get from 'lodash/get'; +import moment from 'moment'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Relay from 'react-relay/classic'; -import filter from 'lodash/filter'; -import moment from 'moment'; import { Link } from 'react-router'; -import cx from 'classnames'; + import Departure from './Departure'; +import { RouteAlertsQuery } from '../util/alertQueries'; +import { + getActiveAlertSeverityLevel, + patternIdPredicate, +} from '../util/alertUtils'; import { isBrowser } from '../util/browser'; import { PREFIX_ROUTES } from '../util/path'; -const hasActiveDisruption = (t, alerts) => - filter( - alerts, - alert => alert.effectiveStartDate < t && t < alert.effectiveEndDate, - ).length > 0; - const asDepartures = stoptimes => !stoptimes ? [] @@ -39,14 +39,18 @@ const asDepartures = stoptimes => : stoptime.scheduledDeparture); const stoptimeTime = isArrival ? arrivalTime : departureTime; + const { pattern } = stoptime.trip; return { + alerts: get(pattern, 'route.alerts', []).filter(alert => + patternIdPredicate(alert, get(pattern, 'code', undefined)), + ), canceled, isArrival, isLastStop, stoptime: stoptimeTime, stop: stoptime.stop, realtime: stoptime.realtime, - pattern: stoptime.trip.pattern, + pattern, headsign: stoptime.stopHeadsign, trip: stoptime.trip, pickupType: stoptime.pickupType, @@ -80,7 +84,8 @@ class DepartureListContainer extends Component { render() { const departureObjs = []; - const { currentTime } = this.props; + const { currentTime, limit, isTerminal, stoptimes } = this.props; + let currentDate = moment .unix(currentTime) .startOf('day') @@ -91,10 +96,10 @@ class DepartureListContainer extends Component { .startOf('day') .unix(); - const departures = asDepartures(this.props.stoptimes) - .filter(departure => !(this.props.isTerminal && departure.isArrival)) + const departures = asDepartures(stoptimes) + .filter(departure => !(isTerminal && departure.isArrival)) .filter(departure => currentTime < departure.stoptime) - .slice(0, this.props.limit); + .slice(0, limit); departures.forEach(departure => { if (departure.stoptime >= tomorrow) { @@ -117,18 +122,21 @@ class DepartureListContainer extends Component { const id = `${departure.pattern.code}:${departure.stoptime}`; - const classes = { - disruption: hasActiveDisruption(departure.stoptime, departure.alerts), - }; - + const alertSeverityLevel = getActiveAlertSeverityLevel( + departure.alerts, + currentTime, + ); const departureObj = ( Date: Mon, 27 May 2019 14:50:01 +0300 Subject: [PATCH 17/26] Included info icons to itinerary summaries and legs. --- app/component/SummaryRow.js | 4 +- app/component/TransitLeg.js | 4 +- app/util/alertUtils.js | 48 +++++++++-------- test/unit/util/alertUtils.test.js | 86 ++++++++++++++++++++++++------- 4 files changed, 97 insertions(+), 45 deletions(-) diff --git a/app/component/SummaryRow.js b/app/component/SummaryRow.js index be63a233de..2aff2b89a5 100644 --- a/app/component/SummaryRow.js +++ b/app/component/SummaryRow.js @@ -9,7 +9,7 @@ import LocalTime from './LocalTime'; import RelativeDuration from './RelativeDuration'; import RouteNumber from './RouteNumber'; import RouteNumberContainer from './RouteNumberContainer'; -import { legHasActiveAlert } from '../util/alertUtils'; +import { getActiveLegAlertSeverityLevel } from '../util/alertUtils'; import { displayDistance } from '../util/geo-utils'; import { containsBiking, @@ -74,11 +74,11 @@ export const RouteLeg = ({ leg, large, intl }) => { } else { routeNumber = ( ); } diff --git a/app/component/TransitLeg.js b/app/component/TransitLeg.js index afe3b1dfeb..02ec2612cf 100644 --- a/app/component/TransitLeg.js +++ b/app/component/TransitLeg.js @@ -15,7 +15,7 @@ import PlatformNumber from './PlatformNumber'; import ItineraryCircleLine from './ItineraryCircleLine'; import { PREFIX_ROUTES } from '../util/path'; import { - legHasActiveAlert, + getActiveLegAlertSeverityLevel, legHasCancelation, tripHasCancelationForStop, } from '../util/alertUtils'; @@ -180,9 +180,9 @@ class TransitLeg extends React.Component { {originalTime}
    { } return getMaximumAlertSeverityLevel( alerts - .map(getServiceAlertMetadata) + .map( + alert => + alert.validityPeriod ? { ...alert } : getServiceAlertMetadata(alert), + ) .filter(alert => isAlertValid(alert, referenceUnixTime)), ); }; @@ -514,29 +517,30 @@ export const isAlertActive = ( * * @param {*} leg the itinerary leg to check. */ -export const legHasActiveAlert = leg => { +export const getActiveLegAlertSeverityLevel = leg => { if (!leg) { - return false; + return undefined; } - return ( - legHasCancelation(leg) || - isAlertActive( - [], - [ - ...getServiceAlertsForRoute( - leg.route, - leg.trip && leg.trip.pattern && leg.trip.pattern.code, - ), - ...getServiceAlertsForStop(leg.from && leg.from.stop), - ...getServiceAlertsForStop(leg.to && leg.to.stop), - ...(Array.isArray(leg.intermediatePlaces) - ? leg.intermediatePlaces - .map(place => getServiceAlertsForStop(place.stop)) - .reduce((a, b) => a.concat(b), []) - : []), - ], - leg.startTime / 1000, // this field is in ms format - ) + if (legHasCancelation(leg)) { + return AlertSeverityLevelType.Warning; + } + + const serviceAlerts = [ + ...getServiceAlertsForRoute( + leg.route, + leg.trip && leg.trip.pattern && leg.trip.pattern.code, + ), + ...getServiceAlertsForStop(leg.from && leg.from.stop), + ...getServiceAlertsForStop(leg.to && leg.to.stop), + ...(Array.isArray(leg.intermediatePlaces) + ? leg.intermediatePlaces + .map(place => getServiceAlertsForStop(place.stop)) + .reduce((a, b) => a.concat(b), []) + : []), + ]; + return getActiveAlertSeverityLevel( + serviceAlerts, + leg.startTime / 1000, // this field is in ms format ); }; diff --git a/test/unit/util/alertUtils.test.js b/test/unit/util/alertUtils.test.js index a7627372ac..43df3f7fae 100644 --- a/test/unit/util/alertUtils.test.js +++ b/test/unit/util/alertUtils.test.js @@ -968,19 +968,23 @@ describe('alertUtils', () => { }); }); - describe('legHasActiveAlert', () => { - it('should return false if the leg is falsy', () => { - expect(utils.legHasActiveAlert(undefined)).to.equal(false); + describe('getActiveLegAlertSeverityLevel', () => { + it('should return undefined if the leg is falsy', () => { + expect(utils.getActiveLegAlertSeverityLevel(undefined)).to.equal( + undefined, + ); }); - it('should return true if the leg is canceled', () => { + it('should return "WARNING" if the leg is canceled', () => { const leg = { realtimeState: RealtimeStateType.Canceled, }; - expect(utils.legHasActiveAlert(leg)).to.equal(true); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should return true if there is an active route alert', () => { + it('should return "WARNING" if there is an active route alert', () => { const alertEffectiveStartDate = 1553754595; const leg = { route: { @@ -994,10 +998,12 @@ describe('alertUtils', () => { }, startTime: (alertEffectiveStartDate + 1) * 1000, // * 1000 due to ms format }; - expect(utils.legHasActiveAlert(leg)).to.equal(true); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should return false if there is an inactive route alert', () => { + it('should return undefined if there is an inactive route alert', () => { const alertEffectiveEndDate = 1553778000; const leg = { route: { @@ -1011,10 +1017,10 @@ describe('alertUtils', () => { }, startTime: (alertEffectiveEndDate + 1) * 1000, // * 1000 due to ms format }; - expect(utils.legHasActiveAlert(leg)).to.equal(false); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal(undefined); }); - it('should return true if there is an active trip alert', () => { + it('should return "WARNING" if there is an active trip alert', () => { const leg = { route: { alerts: [ @@ -1037,10 +1043,12 @@ describe('alertUtils', () => { }, }, }; - expect(utils.legHasActiveAlert(leg)).to.equal(true); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should return false if there is an active trip alert for another trip', () => { + it('should return undefined if there is an active trip alert for another trip', () => { const leg = { route: { alerts: [ @@ -1063,10 +1071,10 @@ describe('alertUtils', () => { }, }, }; - expect(utils.legHasActiveAlert(leg)).to.equal(false); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal(undefined); }); - it('should return true if there is an active stop alert at the "from" stop', () => { + it('should return "WARNING" if there is an active stop alert at the "from" stop', () => { const leg = { from: { stop: { @@ -1081,10 +1089,12 @@ describe('alertUtils', () => { }, startTime: 1553769600000, }; - expect(utils.legHasActiveAlert(leg)).to.equal(true); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should return true if there is an active stop alert at the "to" stop', () => { + it('should return "WARNING" if there is an active stop alert at the "to" stop', () => { const leg = { to: { stop: { @@ -1099,10 +1109,12 @@ describe('alertUtils', () => { }, startTime: 1553769600000, }; - expect(utils.legHasActiveAlert(leg)).to.equal(true); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should return true if there is an active stop alert at an intermediate stop', () => { + it('should return "WARNING" if there is an active stop alert at an intermediate stop', () => { const leg = { intermediatePlaces: [ { @@ -1119,7 +1131,27 @@ describe('alertUtils', () => { ], startTime: 1553769600000, }; - expect(utils.legHasActiveAlert(leg)).to.equal(true); + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal( + AlertSeverityLevelType.Warning, + ); + }); + + it('should return the given alertSeverityLevel', () => { + const leg = { + route: { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Info, + effectiveEndDate: 1553778000, + effectiveStartDate: 1553754595, + }, + ], + }, + startTime: 1553769600000, + }; + expect(utils.getActiveLegAlertSeverityLevel(leg)).to.equal( + AlertSeverityLevelType.Info, + ); }); }); @@ -1151,6 +1183,22 @@ describe('alertUtils', () => { AlertSeverityLevelType.Info, ); }); + + it('should also work for mapped service alerts', () => { + const currentTime = 1000; + const alerts = [ + { + severityLevel: AlertSeverityLevelType.Info, + validityPeriod: { + endTime: currentTime + 100, + startTime: currentTime - 100, + }, + }, + ]; + expect(utils.getActiveAlertSeverityLevel(alerts, currentTime)).to.equal( + AlertSeverityLevelType.Info, + ); + }); }); describe('patternIdPredicate', () => { From 2806984343bca64db20d974e48f38bbc4acc2e21 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 15:12:15 +0300 Subject: [PATCH 18/26] Included some more unit tests. --- app/component/FavouritesTabLabelContainer.js | 8 +-- test/unit/FavouritesTabLabelContainer.test.js | 62 +++++++++++++++++++ test/unit/component/IconWithIcon.test.js | 22 +++++++ 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 test/unit/FavouritesTabLabelContainer.test.js create mode 100644 test/unit/component/IconWithIcon.test.js diff --git a/app/component/FavouritesTabLabelContainer.js b/app/component/FavouritesTabLabelContainer.js index 1235d2c739..a06acfb990 100644 --- a/app/component/FavouritesTabLabelContainer.js +++ b/app/component/FavouritesTabLabelContainer.js @@ -10,7 +10,7 @@ import { RouteAlertsQuery } from '../util/alertQueries'; import { getActiveAlertSeverityLevel } from '../util/alertUtils'; import { isBrowser } from '../util/browser'; -const alertReducer = mapProps(({ routes, currentTime, ...rest }) => { +export const alertSeverityLevelMapper = ({ routes, currentTime, ...rest }) => { const alertSeverityLevel = getActiveAlertSeverityLevel( Array.isArray(routes) && routes @@ -22,17 +22,17 @@ const alertReducer = mapProps(({ routes, currentTime, ...rest }) => { alertSeverityLevel, ...rest, }; -}); +}; const FavouritesTabLabelRelayConnector = Relay.createContainer( - alertReducer(FavouritesTabLabel), + mapProps(alertSeverityLevelMapper)(FavouritesTabLabel), { fragments: { routes: () => Relay.QL` fragment on Route @relay(plural:true) { ${RouteAlertsQuery} } - `, + `, }, }, ); diff --git a/test/unit/FavouritesTabLabelContainer.test.js b/test/unit/FavouritesTabLabelContainer.test.js new file mode 100644 index 0000000000..933a2826ff --- /dev/null +++ b/test/unit/FavouritesTabLabelContainer.test.js @@ -0,0 +1,62 @@ +import { alertSeverityLevelMapper } from '../../app/component/FavouritesTabLabelContainer'; +import { AlertSeverityLevelType } from '../../app/constants'; + +describe('', () => { + describe('alertSeverityLevelMapper', () => { + it('should not fail if some route alerts are missing', () => { + const currentTime = 1000; + const props = { + currentTime, + routes: [ + { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Warning, + effectiveEndDate: currentTime + 1, + effectiveStartDate: currentTime, + }, + ], + }, + { + alerts: null, + }, + ], + }; + const result = alertSeverityLevelMapper({ ...props }); + expect(result.alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); + }); + + it('should map the highest active alert severity level', () => { + const currentTime = 1000; + const props = { + currentTime, + routes: [ + { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Warning, + effectiveEndDate: currentTime + 1, + effectiveStartDate: currentTime, + }, + ], + }, + { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Info, + effectiveEndDate: currentTime + 1, + effectiveStartDate: currentTime, + }, + ], + }, + ], + }; + const result = alertSeverityLevelMapper({ ...props }); + expect(result.alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); + }); + }); +}); diff --git a/test/unit/component/IconWithIcon.test.js b/test/unit/component/IconWithIcon.test.js new file mode 100644 index 0000000000..d06b1c85e1 --- /dev/null +++ b/test/unit/component/IconWithIcon.test.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import Icon from '../../../app/component/Icon'; +import IconWithIcon from '../../../app/component/IconWithIcon'; + +describe('', () => { + it('should apply the given sub icon shape', () => { + const props = { + img: 'img', + subIcon: 'sub-img', + subIconShape: 'circle', + }; + const wrapper = shallowWithIntl(); + expect( + wrapper + .find(Icon) + .at(1) + .prop('backgroundShape'), + ).to.equal('circle'); + }); +}); From 844b1de28f05b765dc6555f1482452df2377d44e Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 15:15:34 +0300 Subject: [PATCH 19/26] Updated the unit tests. --- test/unit/component/TransitLeg.test.js | 30 +++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/unit/component/TransitLeg.test.js b/test/unit/component/TransitLeg.test.js index 06f23b2d64..2fe3998282 100644 --- a/test/unit/component/TransitLeg.test.js +++ b/test/unit/component/TransitLeg.test.js @@ -357,7 +357,7 @@ describe('', () => { expect(wrapper.find(IntermediateLeg).prop('isCanceled')).to.equal(true); }); - it('should apply hasDisruption due to a route alert', () => { + it('should apply alertSeverityLevel due to a route alert', () => { const props = { ...defaultProps, leg: { @@ -391,10 +391,12 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { itinerary: {} }, focusFunction: () => {} }, }); - expect(wrapper.find(RouteNumber).props().hasDisruption).to.equal(true); + expect(wrapper.find(RouteNumber).props().alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should apply hasDisruption due to a trip alert', () => { + it('should apply alertSeverityLevel due to a trip alert', () => { const props = { ...defaultProps, leg: { @@ -433,10 +435,12 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { itinerary: {} }, focusFunction: () => {} }, }); - expect(wrapper.find(RouteNumber).props().hasDisruption).to.equal(true); + expect(wrapper.find(RouteNumber).props().alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should apply hasDisruption due to a stop alert at the "from" stop', () => { + it('should apply alertSeverityLevel due to a stop alert at the "from" stop', () => { const props = { ...defaultProps, leg: { @@ -471,10 +475,12 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { itinerary: {} }, focusFunction: () => {} }, }); - expect(wrapper.find(RouteNumber).props().hasDisruption).to.equal(true); + expect(wrapper.find(RouteNumber).props().alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should apply hasDisruption due to a stop alert at the "to" stop', () => { + it('should apply alertSeverityLevel due to a stop alert at the "to" stop', () => { const props = { ...defaultProps, leg: { @@ -509,10 +515,12 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { itinerary: {} }, focusFunction: () => {} }, }); - expect(wrapper.find(RouteNumber).props().hasDisruption).to.equal(true); + expect(wrapper.find(RouteNumber).props().alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); }); - it('should apply hasDisruption due to a stop alert at an intermediate stop', () => { + it('should apply alertSeverityLevel due to a stop alert at an intermediate stop', () => { const props = { ...defaultProps, leg: { @@ -553,7 +561,9 @@ describe('', () => { const wrapper = shallowWithIntl(, { context: { config: { itinerary: {} }, focusFunction: () => {} }, }); - expect(wrapper.find(RouteNumber).props().hasDisruption).to.equal(true); + expect(wrapper.find(RouteNumber).props().alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); }); it('should show a disclaimer with relevant information for an unknown ticket', () => { From 59eb03aeb06e1e5f7b31ae390e5e53bcd72ab6a6 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 15:17:59 +0300 Subject: [PATCH 20/26] Updated the unit tests. --- test/unit/component/SummaryRow.test.js | 42 +++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/unit/component/SummaryRow.test.js b/test/unit/component/SummaryRow.test.js index e786dd6586..4761021928 100644 --- a/test/unit/component/SummaryRow.test.js +++ b/test/unit/component/SummaryRow.test.js @@ -350,9 +350,9 @@ describe('', () => { context: { ...mockContext }, childContextTypes: { ...mockChildContextTypes }, }); - expect(wrapper.find(RouteNumberContainer).props().hasDisruption).to.equal( - false, - ); + expect( + wrapper.find(RouteNumberContainer).props().alertSeverityLevel, + ).to.equal(undefined); }); it('should not indicate that there is a disruption if the alert is not in effect', () => { @@ -383,9 +383,9 @@ describe('', () => { context: { ...mockContext }, childContextTypes: { ...mockChildContextTypes }, }); - expect(wrapper.find(RouteNumberContainer).props().hasDisruption).to.equal( - false, - ); + expect( + wrapper.find(RouteNumberContainer).props().alertSeverityLevel, + ).to.equal(undefined); }); it('should indicate that there is a disruption due to a trip alert', () => { @@ -425,9 +425,9 @@ describe('', () => { context: { ...mockContext }, childContextTypes: { ...mockChildContextTypes }, }); - expect(wrapper.find(RouteNumberContainer).props().hasDisruption).to.equal( - true, - ); + expect( + wrapper.find(RouteNumberContainer).props().alertSeverityLevel, + ).to.equal(AlertSeverityLevelType.Warning); }); it('should indicate that there is a disruption due to a route alert', () => { @@ -458,9 +458,9 @@ describe('', () => { context: { ...mockContext }, childContextTypes: { ...mockChildContextTypes }, }); - expect(wrapper.find(RouteNumberContainer).props().hasDisruption).to.equal( - true, - ); + expect( + wrapper.find(RouteNumberContainer).props().alertSeverityLevel, + ).to.equal(AlertSeverityLevelType.Warning); }); it('should indicate that there is a disruption due to a stop alert at the "from" stop', () => { @@ -495,9 +495,9 @@ describe('', () => { context: { ...mockContext }, childContextTypes: { ...mockChildContextTypes }, }); - expect(wrapper.find(RouteNumberContainer).props().hasDisruption).to.equal( - true, - ); + expect( + wrapper.find(RouteNumberContainer).props().alertSeverityLevel, + ).to.equal(AlertSeverityLevelType.Warning); }); it('should indicate that there is a disruption due to a stop alert at the "to" stop', () => { @@ -532,9 +532,9 @@ describe('', () => { context: { ...mockContext }, childContextTypes: { ...mockChildContextTypes }, }); - expect(wrapper.find(RouteNumberContainer).props().hasDisruption).to.equal( - true, - ); + expect( + wrapper.find(RouteNumberContainer).props().alertSeverityLevel, + ).to.equal(AlertSeverityLevelType.Warning); }); it('should indicate that there is a disruption due to a stop alert at an intermediate stop', () => { @@ -579,8 +579,8 @@ describe('', () => { context: { ...mockContext }, childContextTypes: { ...mockChildContextTypes }, }); - expect(wrapper.find(RouteNumberContainer).props().hasDisruption).to.equal( - true, - ); + expect( + wrapper.find(RouteNumberContainer).props().alertSeverityLevel, + ).to.equal(AlertSeverityLevelType.Warning); }); }); From d7c5b3cf562eea06131b1ade8e23cd38d2dcf13c Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Mon, 27 May 2019 15:58:57 +0300 Subject: [PATCH 21/26] Added unit tests for the departure list container component. --- app/component/DepartureListContainer.js | 8 +- .../component/DepartureListContainer.test.js | 89 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 test/unit/component/DepartureListContainer.test.js diff --git a/app/component/DepartureListContainer.js b/app/component/DepartureListContainer.js index a893205fa8..b73f21ce1c 100644 --- a/app/component/DepartureListContainer.js +++ b/app/component/DepartureListContainer.js @@ -171,7 +171,7 @@ class DepartureListContainer extends Component { } } -export default Relay.createContainer(DepartureListContainer, { +const containerComponent = Relay.createContainer(DepartureListContainer, { fragments: { stoptimes: () => Relay.QL` fragment on Stoptime @relay(plural:true) { @@ -214,3 +214,9 @@ export default Relay.createContainer(DepartureListContainer, { `, }, }); + +export { + containerComponent as default, + DepartureListContainer as Component, + asDepartures, +}; diff --git a/test/unit/component/DepartureListContainer.test.js b/test/unit/component/DepartureListContainer.test.js new file mode 100644 index 0000000000..7656f2e92a --- /dev/null +++ b/test/unit/component/DepartureListContainer.test.js @@ -0,0 +1,89 @@ +import React from 'react'; + +import Departure from '../../../app/component/Departure'; +import { + asDepartures, + Component as DepartureListContainer, +} from '../../../app/component/DepartureListContainer'; +import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; +import { AlertSeverityLevelType } from '../../../app/constants'; + +describe('', () => { + it("should include the alerts' severity levels", () => { + // const props = { + // currentTime: 1000, + // rowClasses: '', + // stoptimes: [ + // { + // trip: { + // pattern: { + // code: 'foo', + // route: { + // alerts: [ + // { + // alertSeverityLevel: AlertSeverityLevelType.Warning, + // trip: { + // pattern: { + // code: 'foo', + // }, + // }, + // }, + // { + // alertSeverityLevel: AlertSeverityLevelType.Severe, + // trip: { + // pattern: { + // code: 'bar', + // }, + // }, + // }, + // ], + // }, + // }, + // }, + // }, + // ], + // }; + // const wrapper = shallowWithIntl(); + // expect(wrapper.find(Departure)).to.have.lengthOf(1); + // expect(wrapper.debug()).to.equal(undefined); + }); + + describe('asDepartures', () => { + it("should map the alerts' severity levels", () => { + const stoptimes = [ + { + trip: { + pattern: { + code: 'foo', + route: { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Warning, + trip: { + pattern: { + code: 'foo', + }, + }, + }, + { + alertSeverityLevel: AlertSeverityLevelType.Severe, + trip: { + pattern: { + code: 'bar', + }, + }, + }, + ], + }, + }, + }, + }, + ]; + const departures = asDepartures(stoptimes); + expect(departures[0].alerts).to.have.lengthOf(1); + expect(departures[0].alerts[0].alertSeverityLevel).to.equal( + AlertSeverityLevelType.Warning, + ); + }); + }); +}); From 7e62285c2772eef49d028a5f0c50fce14087482e Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Tue, 28 May 2019 08:07:45 +0300 Subject: [PATCH 22/26] Updated the unit test. --- .../component/DepartureListContainer.test.js | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/test/unit/component/DepartureListContainer.test.js b/test/unit/component/DepartureListContainer.test.js index 7656f2e92a..7fefd5ab43 100644 --- a/test/unit/component/DepartureListContainer.test.js +++ b/test/unit/component/DepartureListContainer.test.js @@ -10,42 +10,49 @@ import { AlertSeverityLevelType } from '../../../app/constants'; describe('', () => { it("should include the alerts' severity levels", () => { - // const props = { - // currentTime: 1000, - // rowClasses: '', - // stoptimes: [ - // { - // trip: { - // pattern: { - // code: 'foo', - // route: { - // alerts: [ - // { - // alertSeverityLevel: AlertSeverityLevelType.Warning, - // trip: { - // pattern: { - // code: 'foo', - // }, - // }, - // }, - // { - // alertSeverityLevel: AlertSeverityLevelType.Severe, - // trip: { - // pattern: { - // code: 'bar', - // }, - // }, - // }, - // ], - // }, - // }, - // }, - // }, - // ], - // }; - // const wrapper = shallowWithIntl(); - // expect(wrapper.find(Departure)).to.have.lengthOf(1); - // expect(wrapper.debug()).to.equal(undefined); + const props = { + currentTime: 1000, + rowClasses: '', + stoptimes: [ + { + realtimeArrival: 1050, + realtimeDeparture: 1100, + scheduledArrival: 1050, + scheduledDeparture: 1100, + serviceDay: 0, + trip: { + pattern: { + code: 'foo', + route: { + alerts: [ + { + alertSeverityLevel: AlertSeverityLevelType.Warning, + trip: { + pattern: { + code: 'foo', + }, + }, + }, + { + alertSeverityLevel: AlertSeverityLevelType.Severe, + trip: { + pattern: { + code: 'foo', + }, + }, + }, + ], + mode: 'BUS', + }, + }, + }, + }, + ], + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(Departure).prop('alertSeverityLevel')).to.equal( + AlertSeverityLevelType.Severe, + ); }); describe('asDepartures', () => { From 4655b6728d386c24b161f8c37c5f18e8a911ede8 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Tue, 28 May 2019 08:49:54 +0300 Subject: [PATCH 23/26] Mark cancelations with a severity level automatically. --- app/component/AlertList.js | 23 ++++++++++++---------- test/unit/component/AlertList.test.js | 28 +++++++++++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/app/component/AlertList.js b/app/component/AlertList.js index 3cb42d6b8e..80037dc486 100644 --- a/app/component/AlertList.js +++ b/app/component/AlertList.js @@ -44,8 +44,6 @@ const AlertList = ({ showExpired, serviceAlerts, }) => { - const currentTimeUnix = currentTime.unix ? currentTime.unix() : currentTime; - const getRoute = alert => alert.route || {}; const getMode = alert => getRoute(alert).mode; const getShortName = alert => getRoute(alert).shortName; @@ -55,14 +53,19 @@ const AlertList = ({ const uniqueAlerts = uniqBy( [ - ...(Array.isArray(cancelations) ? cancelations : []), + ...(Array.isArray(cancelations) + ? cancelations.map(cancelation => ({ + ...cancelation, + severityLevel: AlertSeverityLevelType.Warning, + })) + : []), ...(Array.isArray(serviceAlerts) ? serviceAlerts : []), ], getUniqueId, ) .map(alert => ({ ...alert, - expired: !isAlertValid(alert, currentTimeUnix), + expired: !isAlertValid(alert, currentTime), })) .filter(alert => (showExpired ? true : !alert.expired)); @@ -112,7 +115,7 @@ const AlertList = ({ ) => ( ({ - currentTime: context.getStore('TimeStore').getCurrentTime(), + currentTime: context + .getStore('TimeStore') + .getCurrentTime() + .unix(), }), ); diff --git a/test/unit/component/AlertList.test.js b/test/unit/component/AlertList.test.js index 2568a79c94..f5f945f918 100644 --- a/test/unit/component/AlertList.test.js +++ b/test/unit/component/AlertList.test.js @@ -1,11 +1,9 @@ -import { expect } from 'chai'; -import { describe, it } from 'mocha'; -import moment from 'moment'; import React from 'react'; import { shallowWithIntl } from '../helpers/mock-intl-enzyme'; import { Component as AlertList } from '../../../app/component/AlertList'; import RouteAlertsRow from '../../../app/component/RouteAlertsRow'; +import { AlertSeverityLevelType } from '../../../app/constants'; describe('', () => { it('should show a "no alerts" message', () => { @@ -95,7 +93,7 @@ describe('', () => { it('should indicate that an alert has not expired', () => { const props = { - currentTime: moment.unix(1547464413), + currentTime: 1547464413, cancelations: [ { header: 'foo', @@ -116,7 +114,7 @@ describe('', () => { it('should indicate that an alert has expired', () => { const props = { - currentTime: moment.unix(1547465412), + currentTime: 1547465412, cancelations: [ { header: 'foo', @@ -137,7 +135,7 @@ describe('', () => { it('should omit expired alerts', () => { const props = { - currentTime: moment.unix(1547465412), + currentTime: 1547465412, cancelations: [ { header: 'foo', @@ -249,4 +247,22 @@ describe('', () => { .props().routeLine, ).to.equal('10, 11'); }); + + it('should mark cancelations with severity level "WARNING"', () => { + const props = { + currentTime: 1000, + cancelations: [ + { + validityPeriod: { + startTime: 900, + endTime: 1100, + }, + }, + ], + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(RouteAlertsRow).prop('severityLevel')).to.equal( + AlertSeverityLevelType.Warning, + ); + }); }); From 8732c69ac57cb2d6bb9cb2a722d0edff7ab9566a Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Tue, 28 May 2019 09:07:39 +0300 Subject: [PATCH 24/26] Improved alert handling robustness. --- app/util/alertUtils.js | 5 +++-- test/unit/util/alertUtils.test.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/util/alertUtils.js b/app/util/alertUtils.js index 05f7826cce..1761064221 100644 --- a/app/util/alertUtils.js +++ b/app/util/alertUtils.js @@ -288,8 +288,7 @@ export const getServiceAlertUrl = (alert, locale = 'en') => * * @param {*} alert the Service Alert to map. */ -export const getServiceAlertMetadata = (alert = {}) => ({ - hash: alert.alertHash, +const getServiceAlertMetadata = (alert = {}) => ({ severityLevel: alert.alertSeverityLevel, validityPeriod: { startTime: alert.effectiveStartDate, @@ -306,6 +305,7 @@ const getServiceAlerts = ( ? alerts.map(alert => ({ ...getServiceAlertMetadata(alert), description: getServiceAlertDescription(alert, locale), + hash: alert.alertHash, header: getServiceAlertHeader(alert, locale), route: { color, @@ -443,6 +443,7 @@ export const getActiveAlertSeverityLevel = (alerts, referenceUnixTime) => { } return getMaximumAlertSeverityLevel( alerts + .filter(alert => !!alert) .map( alert => alert.validityPeriod ? { ...alert } : getServiceAlertMetadata(alert), diff --git a/test/unit/util/alertUtils.test.js b/test/unit/util/alertUtils.test.js index 43df3f7fae..39fde56647 100644 --- a/test/unit/util/alertUtils.test.js +++ b/test/unit/util/alertUtils.test.js @@ -1199,6 +1199,23 @@ describe('alertUtils', () => { AlertSeverityLevelType.Info, ); }); + + it('should ignore falsy alerts', () => { + const currentTime = 1000; + const alerts = [ + undefined, + { + severityLevel: AlertSeverityLevelType.Info, + validityPeriod: { + endTime: currentTime + 100, + startTime: currentTime - 100, + }, + }, + ]; + expect(utils.getActiveAlertSeverityLevel(alerts, currentTime)).to.equal( + AlertSeverityLevelType.Info, + ); + }); }); describe('patternIdPredicate', () => { From 3c1416b402a8ed2b4bf3583456327312c32e7a65 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Tue, 28 May 2019 11:21:39 +0300 Subject: [PATCH 25/26] Do not mark future cancelations as expired. --- app/component/AlertList.js | 17 +++-- app/util/alertUtils.js | 6 +- test/unit/component/AlertList.test.js | 99 ++++++++++++++++++++++----- test/unit/util/alertUtils.test.js | 27 +++++++- 4 files changed, 120 insertions(+), 29 deletions(-) diff --git a/app/component/AlertList.js b/app/component/AlertList.js index 80037dc486..f9ef3f5186 100644 --- a/app/component/AlertList.js +++ b/app/component/AlertList.js @@ -57,17 +57,20 @@ const AlertList = ({ ? cancelations.map(cancelation => ({ ...cancelation, severityLevel: AlertSeverityLevelType.Warning, + expired: !isAlertValid(cancelation, currentTime, { + isFutureValid: true, + }), + })) + : []), + ...(Array.isArray(serviceAlerts) + ? serviceAlerts.map(alert => ({ + ...alert, + expired: !isAlertValid(alert, currentTime), })) : []), - ...(Array.isArray(serviceAlerts) ? serviceAlerts : []), ], getUniqueId, - ) - .map(alert => ({ - ...alert, - expired: !isAlertValid(alert, currentTime), - })) - .filter(alert => (showExpired ? true : !alert.expired)); + ).filter(alert => (showExpired ? true : !alert.expired)); if (uniqueAlerts.length === 0) { return ( diff --git a/app/util/alertUtils.js b/app/util/alertUtils.js index 1761064221..e6101f811e 100644 --- a/app/util/alertUtils.js +++ b/app/util/alertUtils.js @@ -152,7 +152,7 @@ export const DEFAULT_VALIDITY = 5 * 60; export const isAlertValid = ( alert, referenceUnixTime, - defaultValidity = DEFAULT_VALIDITY, + { defaultValidity = DEFAULT_VALIDITY, isFutureValid = false } = {}, ) => { if (!alert) { return false; @@ -165,6 +165,9 @@ export const isAlertValid = ( if (!startTime && !endTime) { return true; } + if (isFutureValid && referenceUnixTime < startTime) { + return true; + } return ( startTime <= referenceUnixTime && @@ -190,6 +193,7 @@ export const cancelationHasExpired = ( }, }, referenceUnixTime, + { isFutureValid: true }, ); /** diff --git a/test/unit/component/AlertList.test.js b/test/unit/component/AlertList.test.js index f5f945f918..c4c6b487cb 100644 --- a/test/unit/component/AlertList.test.js +++ b/test/unit/component/AlertList.test.js @@ -91,9 +91,9 @@ describe('', () => { ).to.equal('fourth'); }); - it('should indicate that an alert has not expired', () => { + it('should indicate that a cancelation has expired', () => { const props = { - currentTime: 1547464413, + currentTime: 100, cancelations: [ { header: 'foo', @@ -102,20 +102,20 @@ describe('', () => { shortName: '63', }, validityPeriod: { - startTime: 1547464413, - endTime: 1547464415, + startTime: 1, + endTime: 99, }, }, ], }; - const wrapper = shallowWithIntl(); - expect(wrapper.find(RouteAlertsRow).prop('expired')).to.equal(false); + const wrapper = shallowWithIntl(); + expect(wrapper.find(RouteAlertsRow).prop('expired')).to.equal(true); }); - it('should indicate that an alert has expired', () => { + it('should indicate that a service alert has expired', () => { const props = { - currentTime: 1547465412, - cancelations: [ + currentTime: 100, + serviceAlerts: [ { header: 'foo', route: { @@ -123,8 +123,8 @@ describe('', () => { shortName: '63', }, validityPeriod: { - startTime: 1547464412, - endTime: 1547464415, + startTime: 1, + endTime: 99, }, }, ], @@ -133,19 +133,80 @@ describe('', () => { expect(wrapper.find(RouteAlertsRow).prop('expired')).to.equal(true); }); - it('should omit expired alerts', () => { + it('should not display past cancelations or service alerts', () => { const props = { - currentTime: 1547465412, + currentTime: 100, cancelations: [ { - header: 'foo', - route: { - mode: 'BUS', - shortName: '63', + validityPeriod: { + startTime: 1, + endTime: 99, + }, + }, + ], + serviceAlerts: [ + { + validityPeriod: { + startTime: 1, + endTime: 99, + }, + }, + ], + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find('.stop-no-alerts-container')).to.have.lengthOf(1); + }); + + it('should display current cancelations and service alerts', () => { + const props = { + currentTime: 100, + cancelations: [ + { + header: 'cancelation', + validityPeriod: { + startTime: 100, + endTime: 100, }, + }, + ], + serviceAlerts: [ + { + header: 'servicealert', + validityPeriod: { + startTime: 100, + endTime: 100, + }, + }, + ], + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(RouteAlertsRow)).to.have.lengthOf(2); + }); + + it('should display future cancelations', () => { + const props = { + currentTime: 100, + cancelations: [ + { + validityPeriod: { + startTime: 101, + endTime: 200, + }, + }, + ], + }; + const wrapper = shallowWithIntl(); + expect(wrapper.find(RouteAlertsRow)).to.have.lengthOf(1); + }); + + it('should not display future service alerts', () => { + const props = { + currentTime: 100, + serviceAlerts: [ + { validityPeriod: { - startTime: 1547464412, - endTime: 1547464415, + startTime: 101, + endTime: 200, }, }, ], diff --git a/test/unit/util/alertUtils.test.js b/test/unit/util/alertUtils.test.js index 39fde56647..8820da616e 100644 --- a/test/unit/util/alertUtils.test.js +++ b/test/unit/util/alertUtils.test.js @@ -695,13 +695,17 @@ describe('alertUtils', () => { it('should mark a current alert within DEFAULT_VALIDITY period as valid', () => { expect( - utils.isAlertValid({ validityPeriod: { startTime: 1000 } }, 1100, 200), + utils.isAlertValid({ validityPeriod: { startTime: 1000 } }, 1100, { + defaultValidity: 200, + }), ).to.equal(true); }); it('should mark an alert after the DEFAULT_VALIDITY period as invalid', () => { expect( - utils.isAlertValid({ validityPeriod: { startTime: 1000 } }, 1300, 200), + utils.isAlertValid({ validityPeriod: { startTime: 1000 } }, 1300, { + defaultValidity: 200, + }), ).to.equal(false); }); @@ -740,6 +744,16 @@ describe('alertUtils', () => { it('should return false if the alert itself is falsy', () => { expect(utils.isAlertValid(undefined, 0)).to.equal(false); }); + + it('should return true if the alert is in the future when configured', () => { + expect( + utils.isAlertValid( + { validityPeriod: { startTime: 100, endTime: 100 } }, + 99, + { isFutureValid: true }, + ), + ).to.equal(true); + }); }); describe('getCancelationsForRoute', () => { @@ -931,6 +945,15 @@ describe('alertUtils', () => { }; expect(utils.cancelationHasExpired(cancelation, 15)).to.equal(false); }); + + it('should return false for a future cancelation', () => { + const cancelation = { + scheduledArrival: 10, + scheduledDeparture: 10, + serviceDay: 0, + }; + expect(utils.cancelationHasExpired(cancelation, 5)).to.equal(false); + }); }); describe('getCancelationsForStop', () => { From 4c538269ca03b4d1fc76fab31d4445f9c37136a3 Mon Sep 17 00:00:00 2001 From: Joona Olkkola Date: Wed, 29 May 2019 13:06:55 +0300 Subject: [PATCH 26/26] Fixed a copy-paste error in the test description. --- test/unit/component/IconWithBigCaution.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/component/IconWithBigCaution.test.js b/test/unit/component/IconWithBigCaution.test.js index 10a96288c3..c224b73cf3 100644 --- a/test/unit/component/IconWithBigCaution.test.js +++ b/test/unit/component/IconWithBigCaution.test.js @@ -7,7 +7,7 @@ import { AlertSeverityLevelType } from '../../../app/constants'; describe('', () => { it('should have a caution sub icon by default', () => { - it('should have a caution sub icon when alertSeverityLevel is high enough', () => { + it('should have a caution sub icon when alertSeverityLevel is not defined', () => { const props = { img: 'foobar', };