diff --git a/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap
new file mode 100644
index 000000000..9030fa631
--- /dev/null
+++ b/__tests__/components/viewers/__snapshots__/stop-viewer.js.snap
@@ -0,0 +1,4456 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`components > viewers > stop viewer should render countdown times after midnight with no date if it is the previous day 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ W Burnside & SW 18th
+
+
+
+
+
+
+
+
+ Stop ID
+
+ :
+ 9860
+
+
+
+
+
+
+
+ 00:17
+
+
+
+ Plan a trip:
+
+
+
+
+
+
+
+ From here
+
+
+ |
+
+
+
+
+
+
+ To here
+
+
+
+
+
+
+
+
+ 20
+
+ To
+ Gresham TC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auto-refresh arrivals?
+
+
+
+
+
+
+
+`;
+
+exports[`components > viewers > stop viewer should render countdown times for stop times departing 48+ hours from start of service 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ W Burnside & SW 18th
+
+
+
+
+
+
+
+
+ Stop ID
+
+ :
+ 9860
+
+
+
+
+
+
+
+ 00:17
+
+
+
+ Plan a trip:
+
+
+
+
+
+
+
+ From here
+
+
+ |
+
+
+
+
+
+
+ To here
+
+
+
+
+
+
+
+
+ 20
+
+ To
+ Gresham TC
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auto-refresh arrivals?
+
+
+
+
+
+
+
+`;
+
+exports[`components > viewers > stop viewer should render times after midnight with the correct day of week 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ W Burnside & SW 18th
+
+
+
+
+
+
+
+
+ Stop ID
+
+ :
+ 9860
+
+
+
+
+
+
+
+ 00:17
+
+
+
+ Plan a trip:
+
+
+
+
+
+
+
+ From here
+
+
+ |
+
+
+
+
+
+
+ To here
+
+
+
+
+
+
+
+
+ 20
+
+ To
+ Gresham TC
+
+
+
+
+
+
+
+
+
+
+
+
+ Thursday
+
+
+ 00:51
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auto-refresh arrivals?
+
+
+
+
+
+
+
+`;
+
+exports[`components > viewers > stop viewer should render with OTP transit index data 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ W Burnside & SW 8th
+
+
+
+
+
+
+
+
+ Stop ID
+
+ :
+ 715
+
+
+
+
+
+
+
+ 17:50
+
+
+
+ Plan a trip:
+
+
+
+
+
+
+
+ From here
+
+
+ |
+
+
+
+
+
+
+ To here
+
+
+
+
+
+
+
+
+ 20
+
+ To
+ Gresham TC
+
+
+
+
+
+
+
+
+
+
+
+
+ Monday
+
+
+ 18:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 36
+
+ To
+ Tualatin Park & Ride
+
+
+
+
+
+
+
+
+
+
+
+
+ Tuesday
+
+
+ 16:11
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 94
+
+ To
+ Sherwood
+
+
+
+
+
+
+
+
+
+
+
+
+ Tuesday
+
+
+ 14:28
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 94
+
+ To
+ King City
+
+
+
+
+
+
+
+
+
+
+
+
+ Tuesday
+
+
+ 15:22
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auto-refresh arrivals?
+
+
+
+
+
+
+
+`;
+
+exports[`components > viewers > stop viewer should render with TriMet transit index data 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ W Burnside & SW 8th
+
+
+
+
+
+
+
+
+ Stop ID
+
+ :
+ 715
+
+
+
+
+
+
+
+ 17:38
+
+
+
+ Plan a trip:
+
+
+
+
+
+
+
+ From here
+
+
+ |
+
+
+
+
+
+
+ To here
+
+
+
+
+
+
+
+
+ 20
+
+ To
+ Gresham TC
+
+
+
+
+
+
+
+
+
+
+
+
+ Monday
+
+
+ 17:45
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Auto-refresh arrivals?
+
+
+
+
+
+
+
+`;
+
+exports[`components > viewers > stop viewer should render with initial stop id and no stop times 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Back
+
+
+
+
+
+ Loading Stop...
+
+
+
+
+
+
+
+
+`;
diff --git a/__tests__/components/viewers/mock-otp-transit-index-data-stop-9860-48-hr.json b/__tests__/components/viewers/mock-otp-transit-index-data-stop-9860-48-hr.json
new file mode 100644
index 000000000..1b4c7bf03
--- /dev/null
+++ b/__tests__/components/viewers/mock-otp-transit-index-data-stop-9860-48-hr.json
@@ -0,0 +1,64 @@
+{
+ "rsn": "20",
+ "code": "9860",
+ "name": "W Burnside & SW 18th",
+ "agencyName": "TriMet",
+ "url": "http://trimet.org/#tracker/stop/9860",
+ "lon": -122.689717,
+ "amenities": [
+ "Crosswalk near stop",
+ "Curb ramp near stop",
+ "Pavement at back door",
+ "Pavement at front door",
+ "Schedule display",
+ "Sidewalk at stop",
+ "Traffic signal"
+ ],
+ "mode": "BUS",
+ "lat": 45.522919,
+ "type": 3,
+ "id": "TriMet:9860",
+ "desc": "Eastbound stop in Portland (Stop ID 9860)",
+ "routes": [
+ {
+ "agencyName": "TriMet",
+ "sortOrderSet": true,
+ "mode": "BUS",
+ "longName": "Burnside/Stark",
+ "shortName": "20",
+ "type": 3,
+ "id": "TriMet:20",
+ "sortOrder": 2600
+ }
+ ],
+ "stopTimes": [
+ {
+ "pattern": {
+ "id": "TriMet:20:1:04",
+ "desc": "20 to Gresham Transit Center (TriMet:8199) from Beaverton Transit Center (TriMet:9978) express",
+ "headsign": "Gresham TC"
+ },
+ "times": [
+ {
+ "stopId": "TriMet:9860",
+ "stopIndex": 38,
+ "stopCount": 132,
+ "scheduledArrival": 175860,
+ "scheduledDeparture": 175860,
+ "realtimeArrival": 175860,
+ "realtimeDeparture": 175860,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9230375",
+ "blockId": "2041",
+ "headsign": "Gresham TC"
+ }
+ ]
+ }
+ ],
+ "stopTimesLastUpdated": 1565248650040
+}
diff --git a/__tests__/components/viewers/mock-otp-transit-index-data-stop-9860.json b/__tests__/components/viewers/mock-otp-transit-index-data-stop-9860.json
new file mode 100644
index 000000000..ccfe41c18
--- /dev/null
+++ b/__tests__/components/viewers/mock-otp-transit-index-data-stop-9860.json
@@ -0,0 +1,163 @@
+{
+ "rsn": "20",
+ "code": "9860",
+ "name": "W Burnside & SW 18th",
+ "agencyName": "TriMet",
+ "url": "http://trimet.org/#tracker/stop/9860",
+ "lon": -122.689717,
+ "amenities": [
+ "Crosswalk near stop",
+ "Curb ramp near stop",
+ "Pavement at back door",
+ "Pavement at front door",
+ "Schedule display",
+ "Sidewalk at stop",
+ "Traffic signal"
+ ],
+ "mode": "BUS",
+ "lat": 45.522919,
+ "type": 3,
+ "id": "TriMet:9860",
+ "desc": "Eastbound stop in Portland (Stop ID 9860)",
+ "routes": [
+ {
+ "agencyName": "TriMet",
+ "sortOrderSet": true,
+ "mode": "BUS",
+ "longName": "Burnside/Stark",
+ "shortName": "20",
+ "type": 3,
+ "id": "TriMet:20",
+ "sortOrder": 2600
+ }
+ ],
+ "stopTimes": [
+ {
+ "pattern": {
+ "id": "TriMet:20:1:04",
+ "desc": "20 to Gresham Transit Center (TriMet:8199) from Beaverton Transit Center (TriMet:9978) express",
+ "headsign": "Gresham TC"
+ },
+ "times": [
+ {
+ "stopId": "TriMet:9860",
+ "stopIndex": 38,
+ "stopCount": 132,
+ "scheduledArrival": 89460,
+ "scheduledDeparture": 89460,
+ "realtimeArrival": 89460,
+ "realtimeDeparture": 89460,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9230375",
+ "blockId": "2041",
+ "headsign": "Gresham TC"
+ },
+ {
+ "stopId": "TriMet:9860",
+ "stopIndex": 38,
+ "stopCount": 132,
+ "scheduledArrival": 93120,
+ "scheduledDeparture": 93120,
+ "realtimeArrival": 93120,
+ "realtimeDeparture": 93120,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9230376",
+ "blockId": "2043",
+ "headsign": "Gresham TC"
+ },
+ {
+ "stopId": "TriMet:9860",
+ "stopIndex": 38,
+ "stopCount": 132,
+ "scheduledArrival": 96780,
+ "scheduledDeparture": 96780,
+ "realtimeArrival": 96780,
+ "realtimeDeparture": 96780,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9230377",
+ "blockId": "2049",
+ "headsign": "Gresham TC"
+ }
+ ]
+ },
+ {
+ "pattern": {
+ "id": "TriMet:20:1:01",
+ "desc": "20 to Gresham Transit Center (TriMet:2253) from Beaverton Transit Center (TriMet:9978) express",
+ "headsign": "Gresham TC"
+ },
+ "times": [
+ {
+ "stopId": "TriMet:9860",
+ "stopIndex": 38,
+ "stopCount": 132,
+ "scheduledArrival": 13980,
+ "scheduledDeparture": 13980,
+ "realtimeArrival": 13980,
+ "realtimeDeparture": 13980,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565247600,
+ "tripId": "TriMet:9230305",
+ "blockId": "2067",
+ "headsign": "Gresham TC"
+ },
+ {
+ "stopId": "TriMet:9860",
+ "stopIndex": 38,
+ "stopCount": 132,
+ "scheduledArrival": 17580,
+ "scheduledDeparture": 17580,
+ "realtimeArrival": 17580,
+ "realtimeDeparture": 17580,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565247600,
+ "tripId": "TriMet:9230306",
+ "blockId": "2034",
+ "headsign": "Gresham TC"
+ },
+ {
+ "stopId": "TriMet:9860",
+ "stopIndex": 38,
+ "stopCount": 132,
+ "scheduledArrival": 19020,
+ "scheduledDeparture": 19020,
+ "realtimeArrival": 19020,
+ "realtimeDeparture": 19020,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565247600,
+ "tripId": "TriMet:9230307",
+ "blockId": "2069",
+ "headsign": "Gresham TC"
+ }
+ ]
+ }
+ ],
+ "stopTimesLastUpdated": 1565248650040
+}
diff --git a/__tests__/components/viewers/mock-otp-transit-index-data.json b/__tests__/components/viewers/mock-otp-transit-index-data.json
new file mode 100644
index 000000000..404af7776
--- /dev/null
+++ b/__tests__/components/viewers/mock-otp-transit-index-data.json
@@ -0,0 +1,386 @@
+{
+ "id": "TriMet:715",
+ "name": "W Burnside & SW 8th",
+ "lat": 45.522912,
+ "lon": -122.678538,
+ "code": "715",
+ "desc": "Eastbound stop in Portland (Stop ID 715)",
+ "zoneId": "B",
+ "url": "http://trimet.org/#tracker/stop/715",
+ "locationType": 0,
+ "wheelchairBoarding": 0,
+ "vehicleType": -999,
+ "vehicleTypeSet": false,
+ "routes": [{
+ "id": "TriMet:20",
+ "shortName": "20",
+ "longName": "Burnside/Stark",
+ "mode": "BUS",
+ "agencyName": "TriMet",
+ "sortOrder": 2600
+ }, {
+ "id": "TriMet:94",
+ "shortName": "94",
+ "longName": "Pacific Hwy/Sherwood",
+ "mode": "BUS",
+ "agencyName": "TriMet",
+ "sortOrder": 9100
+ }, {
+ "id": "TriMet:36",
+ "shortName": "36",
+ "longName": "South Shore",
+ "mode": "BUS",
+ "agencyName": "TriMet",
+ "sortOrder": 3900
+ }],
+ "stopTimes": [{
+ "pattern": {
+ "id": "TriMet:36:0:04",
+ "desc": "36 to Tualatin Park & Ride (TriMet:7879) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "Tualatin Park & Ride"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 63,
+ "scheduledArrival": 58260,
+ "scheduledDeparture": 58260,
+ "realtimeArrival": 58260,
+ "realtimeDeparture": 58260,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9231858",
+ "blockId": "3668",
+ "headsign": "Tualatin Park & Ride"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 63,
+ "scheduledArrival": 61740,
+ "scheduledDeparture": 61740,
+ "realtimeArrival": 61740,
+ "realtimeDeparture": 61740,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9231860",
+ "blockId": "3670",
+ "headsign": "Tualatin Park & Ride"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 63,
+ "scheduledArrival": 58260,
+ "scheduledDeparture": 58260,
+ "realtimeArrival": 58260,
+ "realtimeDeparture": 58260,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9231858",
+ "blockId": "3668",
+ "headsign": "Tualatin Park & Ride"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:94:0:04",
+ "desc": "94 to SW Railroad & Washington (TriMet:3670) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "Sherwood"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 40,
+ "scheduledArrival": 52080,
+ "scheduledDeparture": 52080,
+ "realtimeArrival": 52080,
+ "realtimeDeparture": 52080,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238187",
+ "blockId": "9468",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 40,
+ "scheduledArrival": 54120,
+ "scheduledDeparture": 54120,
+ "realtimeArrival": 54120,
+ "realtimeDeparture": 54120,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238189",
+ "blockId": "9372",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 40,
+ "scheduledArrival": 56880,
+ "scheduledDeparture": 56880,
+ "realtimeArrival": 56880,
+ "realtimeDeparture": 56880,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238194",
+ "blockId": "9474",
+ "headsign": "Sherwood"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:94:0:02",
+ "desc": "94 to SW Pacific Hwy & Durham (TriMet:8644) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "King City"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 23,
+ "scheduledArrival": 55320,
+ "scheduledDeparture": 55320,
+ "realtimeArrival": 55320,
+ "realtimeDeparture": 55320,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238192",
+ "blockId": "9472",
+ "headsign": "King City"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 23,
+ "scheduledArrival": 55320,
+ "scheduledDeparture": 55320,
+ "realtimeArrival": 55320,
+ "realtimeDeparture": 55320,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9238192",
+ "blockId": "9472",
+ "headsign": "King City"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 23,
+ "scheduledArrival": 55320,
+ "scheduledDeparture": 55320,
+ "realtimeArrival": 55320,
+ "realtimeDeparture": 55320,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565247600,
+ "tripId": "TriMet:9238192",
+ "blockId": "9472",
+ "headsign": "King City"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:94:0:03",
+ "desc": "94 to SW Langer & Sherwood Plaza (TriMet:9188) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "Sherwood"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 34,
+ "scheduledArrival": 54720,
+ "scheduledDeparture": 54720,
+ "realtimeArrival": 54720,
+ "realtimeDeparture": 54720,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238190",
+ "blockId": "9470",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 34,
+ "scheduledArrival": 54720,
+ "scheduledDeparture": 54720,
+ "realtimeArrival": 54720,
+ "realtimeDeparture": 54720,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9238190",
+ "blockId": "9470",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 34,
+ "scheduledArrival": 54720,
+ "scheduledDeparture": 54720,
+ "realtimeArrival": 54720,
+ "realtimeDeparture": 54720,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565247600,
+ "tripId": "TriMet:9238190",
+ "blockId": "9470",
+ "headsign": "Sherwood"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:20:1:04",
+ "desc": "20 to Gresham Transit Center (TriMet:8199) from Beaverton Transit Center (TriMet:9978) express",
+ "headsign": "Gresham TC"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 64859,
+ "scheduledDeparture": 64859,
+ "realtimeArrival": 64859,
+ "realtimeDeparture": 64859,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230358",
+ "blockId": "2045",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 66668,
+ "scheduledDeparture": 66668,
+ "realtimeArrival": 66668,
+ "realtimeDeparture": 66668,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230360",
+ "blockId": "2047",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 67628,
+ "scheduledDeparture": 67628,
+ "realtimeArrival": 67628,
+ "realtimeDeparture": 67628,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230361",
+ "blockId": "2048",
+ "headsign": "Gresham TC"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:20:1:01",
+ "desc": "20 to Gresham Transit Center (TriMet:2253) from Beaverton Transit Center (TriMet:9978) express",
+ "headsign": "Gresham TC"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 65759,
+ "scheduledDeparture": 65759,
+ "realtimeArrival": 65759,
+ "realtimeDeparture": 65759,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230359",
+ "blockId": "2046",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 70028,
+ "scheduledDeparture": 70028,
+ "realtimeArrival": 70028,
+ "realtimeDeparture": 70028,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230363",
+ "blockId": "2036",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 72436,
+ "scheduledDeparture": 72436,
+ "realtimeArrival": 72436,
+ "realtimeDeparture": 72436,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230365",
+ "blockId": "2071",
+ "headsign": "Gresham TC"
+ }]
+ }],
+ "stopTimesLastUpdated": 1565052624406
+}
diff --git a/__tests__/components/viewers/mock-trimet-transit-index-data.json b/__tests__/components/viewers/mock-trimet-transit-index-data.json
new file mode 100644
index 000000000..9b528449c
--- /dev/null
+++ b/__tests__/components/viewers/mock-trimet-transit-index-data.json
@@ -0,0 +1,374 @@
+{
+ "rsn": "20, 36, 94",
+ "code": "715",
+ "name": "W Burnside & SW 8th",
+ "agencyName": "TriMet",
+ "url": "http://trimet.org/#tracker/stop/715",
+ "lon": -122.678538,
+ "amenities": ["Bench near stop", "Crosswalk near stop", "Curb ramp near stop", "Lighting at stop", "Pavement at back door", "Pavement at front door", "Schedule display", "Sidewalk at stop"],
+ "mode": "BUS",
+ "lat": 45.522912,
+ "type": 3,
+ "id": "TriMet:715",
+ "desc": "Eastbound stop in Portland (Stop ID 715)",
+ "routes": [{
+ "agencyName": "TriMet",
+ "sortOrderSet": true,
+ "mode": "BUS",
+ "longName": "Burnside/Stark",
+ "shortName": "20",
+ "type": 3,
+ "id": "TriMet:20",
+ "sortOrder": 2600
+ }],
+ "stopTimes": [{
+ "pattern": {
+ "id": "TriMet:36:0:04",
+ "desc": "36 to Tualatin Park & Ride (TriMet:7879) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "Tualatin Park & Ride"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 63,
+ "scheduledArrival": 58260,
+ "scheduledDeparture": 58260,
+ "realtimeArrival": 58260,
+ "realtimeDeparture": 58260,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9231858",
+ "blockId": "3668",
+ "headsign": "Tualatin Park & Ride"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 63,
+ "scheduledArrival": 61740,
+ "scheduledDeparture": 61740,
+ "realtimeArrival": 61740,
+ "realtimeDeparture": 61740,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9231860",
+ "blockId": "3670",
+ "headsign": "Tualatin Park & Ride"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 63,
+ "scheduledArrival": 58260,
+ "scheduledDeparture": 58260,
+ "realtimeArrival": 58260,
+ "realtimeDeparture": 58260,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9231858",
+ "blockId": "3668",
+ "headsign": "Tualatin Park & Ride"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:94:0:04",
+ "desc": "94 to SW Railroad & Washington (TriMet:3670) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "Sherwood"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 40,
+ "scheduledArrival": 52080,
+ "scheduledDeparture": 52080,
+ "realtimeArrival": 52080,
+ "realtimeDeparture": 52080,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238187",
+ "blockId": "9468",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 40,
+ "scheduledArrival": 54120,
+ "scheduledDeparture": 54120,
+ "realtimeArrival": 54120,
+ "realtimeDeparture": 54120,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238189",
+ "blockId": "9372",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 40,
+ "scheduledArrival": 56880,
+ "scheduledDeparture": 56880,
+ "realtimeArrival": 56880,
+ "realtimeDeparture": 56880,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238194",
+ "blockId": "9474",
+ "headsign": "Sherwood"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:94:0:02",
+ "desc": "94 to SW Pacific Hwy & Durham (TriMet:8644) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "King City"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 23,
+ "scheduledArrival": 55320,
+ "scheduledDeparture": 55320,
+ "realtimeArrival": 55320,
+ "realtimeDeparture": 55320,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238192",
+ "blockId": "9472",
+ "headsign": "King City"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 23,
+ "scheduledArrival": 55320,
+ "scheduledDeparture": 55320,
+ "realtimeArrival": 55320,
+ "realtimeDeparture": 55320,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9238192",
+ "blockId": "9472",
+ "headsign": "King City"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 23,
+ "scheduledArrival": 55320,
+ "scheduledDeparture": 55320,
+ "realtimeArrival": 55320,
+ "realtimeDeparture": 55320,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565247600,
+ "tripId": "TriMet:9238192",
+ "blockId": "9472",
+ "headsign": "King City"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:94:0:03",
+ "desc": "94 to SW Langer & Sherwood Plaza (TriMet:9188) from W Burnside & SW 8th (TriMet:715)",
+ "headsign": "Sherwood"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 34,
+ "scheduledArrival": 54720,
+ "scheduledDeparture": 54720,
+ "realtimeArrival": 54720,
+ "realtimeDeparture": 54720,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565074800,
+ "tripId": "TriMet:9238190",
+ "blockId": "9470",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 34,
+ "scheduledArrival": 54720,
+ "scheduledDeparture": 54720,
+ "realtimeArrival": 54720,
+ "realtimeDeparture": 54720,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565161200,
+ "tripId": "TriMet:9238190",
+ "blockId": "9470",
+ "headsign": "Sherwood"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 0,
+ "stopCount": 34,
+ "scheduledArrival": 54720,
+ "scheduledDeparture": 54720,
+ "realtimeArrival": 54720,
+ "realtimeDeparture": 54720,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": true,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1565247600,
+ "tripId": "TriMet:9238190",
+ "blockId": "9470",
+ "headsign": "Sherwood"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:20:1:04",
+ "desc": "20 to Gresham Transit Center (TriMet:8199) from Beaverton Transit Center (TriMet:9978) express",
+ "headsign": "Gresham TC"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 63959,
+ "scheduledDeparture": 63959,
+ "realtimeArrival": 63959,
+ "realtimeDeparture": 63959,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230357",
+ "blockId": "2067",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 64859,
+ "scheduledDeparture": 64859,
+ "realtimeArrival": 64859,
+ "realtimeDeparture": 64859,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230358",
+ "blockId": "2045",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 66668,
+ "scheduledDeparture": 66668,
+ "realtimeArrival": 66668,
+ "realtimeDeparture": 66668,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230360",
+ "blockId": "2047",
+ "headsign": "Gresham TC"
+ }]
+ }, {
+ "pattern": {
+ "id": "TriMet:20:1:01",
+ "desc": "20 to Gresham Transit Center (TriMet:2253) from Beaverton Transit Center (TriMet:9978) express",
+ "headsign": "Gresham TC"
+ },
+ "times": [{
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 65759,
+ "scheduledDeparture": 65759,
+ "realtimeArrival": 65759,
+ "realtimeDeparture": 65759,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230359",
+ "blockId": "2046",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 70028,
+ "scheduledDeparture": 70028,
+ "realtimeArrival": 70028,
+ "realtimeDeparture": 70028,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230363",
+ "blockId": "2036",
+ "headsign": "Gresham TC"
+ }, {
+ "stopId": "TriMet:715",
+ "stopIndex": 42,
+ "stopCount": 132,
+ "scheduledArrival": 72436,
+ "scheduledDeparture": 72436,
+ "realtimeArrival": 72436,
+ "realtimeDeparture": 72436,
+ "arrivalDelay": 0,
+ "departureDelay": 0,
+ "timepoint": false,
+ "realtime": false,
+ "realtimeState": "SCHEDULED",
+ "serviceDay": 1564988400,
+ "tripId": "TriMet:9230365",
+ "blockId": "2071",
+ "headsign": "Gresham TC"
+ }]
+ }],
+ "stopTimesLastUpdated": 1565051923389
+}
diff --git a/__tests__/components/viewers/stop-viewer.js b/__tests__/components/viewers/stop-viewer.js
new file mode 100644
index 000000000..26cbe46e1
--- /dev/null
+++ b/__tests__/components/viewers/stop-viewer.js
@@ -0,0 +1,107 @@
+import StopViewer from '../../../lib/components/viewers/stop-viewer'
+import {restoreDateNowBehavior, setDefaultTestTime, setTestTime} from '../../test-utils'
+import {getMockInitialState, mockWithProvider} from '../../test-utils/mock-data/store'
+
+describe('components > viewers > stop viewer', () => {
+ afterEach(restoreDateNowBehavior)
+ beforeEach(setDefaultTestTime)
+
+ it('should render with initial stop id and no stop times', () => {
+ const mockState = getMockInitialState()
+ mockState.otp.ui.viewedStop = {
+ stopId: 'TriMet:13170'
+ }
+
+ expect(
+ mockWithProvider(
+ StopViewer,
+ {},
+ mockState
+ ).snapshot()
+ ).toMatchSnapshot()
+ })
+
+ it('should render with OTP transit index data', () => {
+ const mockState = getMockInitialState()
+ const stopId = 'TriMet:715'
+ mockState.otp.ui.viewedStop = { stopId }
+ mockState.otp.transitIndex.stops[stopId] = require('./mock-otp-transit-index-data.json')
+
+ expect(
+ mockWithProvider(
+ StopViewer,
+ {},
+ mockState
+ ).snapshot()
+ ).toMatchSnapshot()
+ })
+
+ it('should render times after midnight with the correct day of week', () => {
+ const mockState = getMockInitialState()
+ const stopId = 'TriMet:9860'
+ mockState.otp.ui.viewedStop = { stopId }
+ mockState.otp.transitIndex.stops[stopId] = require('./mock-otp-transit-index-data-stop-9860.json')
+
+ expect(
+ mockWithProvider(
+ StopViewer,
+ {},
+ mockState
+ ).snapshot()
+ ).toMatchSnapshot()
+ })
+
+ it('should render countdown times after midnight with no date if it is the previous day', () => {
+ // Test time: Wednesday, August 7 at 11:58pm PT
+ // First departure: Thursday, August 8 12:51am PT
+ setTestTime(Date.UTC(2019, 7, 8, 6, 58, 56, 78))
+ const mockState = getMockInitialState()
+ const stopId = 'TriMet:9860'
+ mockState.otp.ui.viewedStop = { stopId }
+ mockState.otp.transitIndex.stops[stopId] = require('./mock-otp-transit-index-data-stop-9860.json')
+
+ expect(
+ mockWithProvider(
+ StopViewer,
+ {},
+ mockState
+ ).snapshot()
+ ).toMatchSnapshot()
+ })
+
+ it('should render countdown times for stop times departing 48+ hours from start of service', () => {
+ // Test time: Thursday, August 8 at 11:58pm PT
+ // First departure: Friday, August 9 12:51am PT
+ // Note: service day for stop time is Wednesday and departure is
+ // 175860 (seconds since midnight).
+ setTestTime(Date.UTC(2019, 7, 9, 6, 58, 56, 78))
+ const mockState = getMockInitialState()
+ const stopId = 'TriMet:9860'
+ mockState.otp.ui.viewedStop = { stopId }
+ mockState.otp.transitIndex.stops[stopId] = require('./mock-otp-transit-index-data-stop-9860-48-hr.json')
+
+ expect(
+ mockWithProvider(
+ StopViewer,
+ {},
+ mockState
+ ).snapshot()
+ ).toMatchSnapshot()
+ })
+
+ it('should render with TriMet transit index data', () => {
+ const mockState = getMockInitialState()
+ mockState.otp.ui.viewedStop = {
+ stopId: 'TriMet:715'
+ }
+ mockState.otp.transitIndex.stops['TriMet:715'] = require('./mock-trimet-transit-index-data.json')
+
+ expect(
+ mockWithProvider(
+ StopViewer,
+ {},
+ mockState
+ ).snapshot()
+ ).toMatchSnapshot()
+ })
+})
diff --git a/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap
new file mode 100644
index 000000000..4bedf264f
--- /dev/null
+++ b/__tests__/reducers/__snapshots__/create-otp-reducer.js.snap
@@ -0,0 +1,118 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib > reducers > create-otp-reducer should be able to create the initial state 1`] = `
+Object {
+ "activeSearchId": 0,
+ "config": Object {
+ "autoPlan": false,
+ "debouncePlanTimeMs": 0,
+ "homeTimezone": "America/Los_Angeles",
+ "language": Object {},
+ "operators": Array [],
+ "realtimeEffectsDisplayThreshold": 120,
+ "routingTypes": Array [],
+ "stopViewer": Object {
+ "numberOfDepartures": 3,
+ "timeRange": 345600,
+ },
+ },
+ "currentQuery": Object {
+ "bikeSpeed": 3.58,
+ "companies": null,
+ "date": "2019-08-04",
+ "departArrive": "NOW",
+ "endTime": "09:00",
+ "from": null,
+ "ignoreRealtimeUpdates": false,
+ "maxBikeDistance": 4828,
+ "maxBikeTime": 20,
+ "maxEScooterDistance": 4828,
+ "maxWalkDistance": 1207,
+ "maxWalkTime": 15,
+ "mode": "WALK,TRAM,BUS,SUBWAY,FERRY,RAIL,GONDOLA",
+ "optimize": "QUICK",
+ "optimizeBike": "SAFE",
+ "routingType": "ITINERARY",
+ "showIntermediateStops": true,
+ "startTime": "07:00",
+ "time": "19:34",
+ "to": null,
+ "walkSpeed": 1.34,
+ "watts": 250,
+ "wheelchair": false,
+ },
+ "location": Object {
+ "currentPosition": Object {
+ "coords": null,
+ "error": null,
+ "fetching": false,
+ },
+ "nearbyStops": Array [],
+ "sessionSearches": Array [],
+ },
+ "overlay": Object {
+ "bikeRental": Object {
+ "stations": Array [],
+ },
+ "carRental": Object {
+ "stations": Array [],
+ },
+ "parkAndRide": Object {
+ "locations": null,
+ },
+ "transit": Object {
+ "stops": Array [],
+ },
+ "transitive": null,
+ "vehicleRental": Object {
+ "stations": Array [],
+ },
+ "zipcar": Object {
+ "locations": Array [],
+ },
+ },
+ "searches": Object {},
+ "tnc": Object {
+ "etaEstimates": Object {},
+ "rideEstimates": Object {},
+ },
+ "transitIndex": Object {
+ "stops": Object {},
+ "trips": Object {},
+ },
+ "ui": Object {
+ "diagramLeg": null,
+ "mobileScreen": 1,
+ "printView": false,
+ },
+ "useRealtime": true,
+ "user": Object {
+ "autoRefreshStopTimes": true,
+ "defaults": Object {
+ "bikeSpeed": 3.58,
+ "companies": null,
+ "endTime": "09:00",
+ "ignoreRealtimeUpdates": false,
+ "maxBikeDistance": 4828,
+ "maxBikeTime": 20,
+ "maxEScooterDistance": 4828,
+ "maxWalkDistance": 1207,
+ "maxWalkTime": 15,
+ "mode": "WALK,TRAM,BUS,SUBWAY,FERRY,RAIL,GONDOLA",
+ "optimize": "QUICK",
+ "optimizeBike": "SAFE",
+ "routingType": "ITINERARY",
+ "showIntermediateStops": true,
+ "startTime": "07:00",
+ "walkSpeed": 1.34,
+ "watts": 250,
+ "wheelchair": false,
+ },
+ "favoriteStops": Array [],
+ "locations": Array [],
+ "recentPlaces": Array [],
+ "recentSearches": Array [],
+ "trackRecent": false,
+ },
+}
+`;
diff --git a/__tests__/reducers/create-otp-reducer.js b/__tests__/reducers/create-otp-reducer.js
new file mode 100644
index 000000000..05fcdf475
--- /dev/null
+++ b/__tests__/reducers/create-otp-reducer.js
@@ -0,0 +1,11 @@
+import {getInitialState} from '../../lib/reducers/create-otp-reducer'
+import {restoreDateNowBehavior, setDefaultTestTime} from '../test-utils'
+
+describe('lib > reducers > create-otp-reducer', () => {
+ afterEach(restoreDateNowBehavior)
+
+ it('should be able to create the initial state', () => {
+ setDefaultTestTime()
+ expect(getInitialState({}, {})).toMatchSnapshot()
+ })
+})
diff --git a/__tests__/test-utils/fixtures/geocoding/pelias/autocomplete-response.json b/__tests__/test-utils/fixtures/geocoding/pelias/autocomplete-response.json
index 05a5ef965..ea9758c84 100644
--- a/__tests__/test-utils/fixtures/geocoding/pelias/autocomplete-response.json
+++ b/__tests__/test-utils/fixtures/geocoding/pelias/autocomplete-response.json
@@ -11,12 +11,6 @@
"Ends"
],
"size": 10,
- "sources": [
- "geonames",
- "openaddresses",
- "openstreetmap",
- "whosonfirst"
- ],
"private": false,
"focus.point.lat": 45.52,
"focus.point.lon": -122.67,
diff --git a/__tests__/test-utils/global-setup.js b/__tests__/test-utils/global-setup.js
new file mode 100644
index 000000000..3e9e80ff3
--- /dev/null
+++ b/__tests__/test-utils/global-setup.js
@@ -0,0 +1,7 @@
+/**
+ * Performs setup of the test environment outside of the browser. This is where
+ * items such as environment variables can be set.
+ */
+module.exports = async () => {
+ process.env.TZ = 'America/Los_Angeles'
+}
diff --git a/__tests__/test-utils/index.js b/__tests__/test-utils/index.js
index ecc0e24f8..4c5466f88 100644
--- a/__tests__/test-utils/index.js
+++ b/__tests__/test-utils/index.js
@@ -1,5 +1,41 @@
+// Sun Aug 04 2019 19:34:56 GMT-0700
+const DEFAULT_TEST_TIME = Date.UTC(2019, 7, 5, 2, 34, 56, 78)
+
export function timeoutPromise (ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms)
})
}
+
+/**
+ * Temporarily change the internal behavior of the Date.now method such that it
+ * returns a time that is based off of the given value in milliseconds after
+ * the epoch. Typically the method Date.UTC(YYYY, MM, DD) can be used to
+ * generate this number.
+ *
+ * Note: this stack overflow page gives more info on why we're using this:
+ * https://stackoverflow.com/a/42787232/915811 (basically, moment.js uses
+ * Date#now internally).
+ */
+export function setTestTime (time) {
+ const date = new Date(time)
+ // Log human-readable date to help out human testers.
+ console.log(`Setting test time to ${date}`)
+ jest.spyOn(Date, 'now').mockImplementation(() => date.valueOf())
+}
+
+/**
+ * Sets the default mock test time for a variety of tests such that various
+ * calculations and feed version statuses resolve to a certain state.
+ */
+export function setDefaultTestTime () {
+ setTestTime(DEFAULT_TEST_TIME)
+}
+
+/**
+ * Restore the standard functionality of Date library. This should be used in
+ * the afterEach clause in test suites that require a mocked date.
+ */
+export function restoreDateNowBehavior () {
+ Date.now.mockRestore && Date.now.mockRestore()
+}
diff --git a/__tests__/test-utils/mock-data/store.js b/__tests__/test-utils/mock-data/store.js
new file mode 100644
index 000000000..c67a5ff19
--- /dev/null
+++ b/__tests__/test-utils/mock-data/store.js
@@ -0,0 +1,56 @@
+import { connectRouter, routerMiddleware } from 'connected-react-router'
+import Enzyme, {mount} from 'enzyme'
+import EnzymeReactAdapter from 'enzyme-adapter-react-15.4'
+import {mountToJson} from 'enzyme-to-json'
+import { createHashHistory } from 'history'
+import clone from 'lodash/cloneDeep'
+import { Provider } from 'react-redux'
+import configureStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+
+import {getInitialState} from '../../../lib/reducers/create-otp-reducer'
+
+Enzyme.configure({ adapter: new EnzymeReactAdapter() })
+
+const history = createHashHistory()
+const storeMiddleWare = [
+ thunk,
+ routerMiddleware(history) // for dispatching history actions
+]
+
+/**
+ * Get the initial stop of the redux reducer for otp-rr
+ */
+export function getMockInitialState () {
+ const mockConfig = {}
+ const mockInitialQuery = {}
+ return clone({
+ otp: getInitialState(mockConfig, mockInitialQuery),
+ router: connectRouter(history)
+ })
+}
+
+/**
+ * Mount a react component within a mock redux store. This mock redux store
+ * accepts actions, but doesn't send any of those results to the reducers.
+ * This function is primarily used for taking snapshots of components and
+ * containers in order to verify that they are rendering expected values.
+ */
+export function mockWithProvider (
+ ConnectedComponent,
+ connectedComponentProps,
+ storeState = getMockInitialState()
+) {
+ const store = configureStore(storeMiddleWare)(storeState)
+ const wrapper = mount(
+
+
+
+ )
+
+ return {
+ snapshot: () => mountToJson(wrapper),
+ store,
+ wrapper
+ }
+}
diff --git a/__tests__/test-utils/setup-env.js b/__tests__/test-utils/setup-env.js
new file mode 100644
index 000000000..0caecf31f
--- /dev/null
+++ b/__tests__/test-utils/setup-env.js
@@ -0,0 +1,8 @@
+/**
+ * This file performs some actions to setup the browser environment used in each
+ * jest test.
+ */
+
+window.localStorage = {
+ getItem: () => null
+}
diff --git a/__tests__/test-utils/setup.js b/__tests__/test-utils/setup.js
deleted file mode 100644
index 58c16d60f..000000000
--- a/__tests__/test-utils/setup.js
+++ /dev/null
@@ -1,3 +0,0 @@
-window.localStorage = {
- getItem: () => null
-}
diff --git a/__tests__/util/__snapshots__/geocoder.js.snap b/__tests__/util/__snapshots__/geocoder.js.snap
index c0fd6bcae..0ec26af09 100644
--- a/__tests__/util/__snapshots__/geocoder.js.snap
+++ b/__tests__/util/__snapshots__/geocoder.js.snap
@@ -195,12 +195,6 @@ Object {
"parser": "addressit",
"private": false,
"size": 10,
- "sources": Array [
- "geonames",
- "openaddresses",
- "openstreetmap",
- "whosonfirst",
- ],
"text": "Mill Ends",
"tokens": Array [
"Mill",
@@ -215,7 +209,6 @@ Object {
},
"isomorphicMapzenSearchQuery": Object {
"api_key": "dummy-mapzen-key",
- "sources": "gn,oa,osm,wof",
"text": "Mill Ends",
},
"type": "FeatureCollection",
diff --git a/__tests__/util/__snapshots__/state.js.snap b/__tests__/util/__snapshots__/state.js.snap
deleted file mode 100644
index bff045999..000000000
--- a/__tests__/util/__snapshots__/state.js.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`util > state getDefaultQuery should parse window hash if available 1`] = `
-Object {
- "date": "2017-02-03",
- "departArrive": "DEPART",
- "from": Object {
- "lat": "12",
- "lon": "34",
- "name": "12.00000, 34.00000",
- },
- "mode": undefined,
- "routingType": "ITINERARY",
- "time": "12:34",
- "to": Object {
- "lat": "34",
- "lon": "12",
- "name": "34.00000, 12.00000",
- },
-}
-`;
diff --git a/__tests__/util/geocoder.js b/__tests__/util/geocoder.js
index c93f5938d..ada6e8601 100644
--- a/__tests__/util/geocoder.js
+++ b/__tests__/util/geocoder.js
@@ -1,6 +1,6 @@
import nock from 'nock'
-import getGeocoder from '../../lib/util/geocoder'
+import getGeocoder, { PeliasGeocoder } from '../../lib/util/geocoder'
function mockResponsePath (geocoder, file) {
return `__tests__/test-utils/fixtures/geocoding/${geocoder}/${file}`
@@ -118,6 +118,71 @@ describe('geocoder', () => {
const result = await getGeocoder(geocoder).getLocationFromGeocodedFeature(mockFeature)
expect(result).toMatchSnapshot()
})
+
+ // geocoder-specific tests
+ if (geocoderType === 'PELIAS') {
+ const mockSources = 'gn,oa,osm,wof'
+
+ // sources should not be sent unless they are explicitly defined in the
+ // query. See https://github.com/ibi-group/trimet-mod-otp/issues/239
+ it('should not send sources in autocomplete by default', () => {
+ // create mock API to check query
+ const mockPeliasAPI = {
+ autocomplete: query => {
+ expect(query.sources).not.toBe(expect.anything())
+ return Promise.resolve()
+ }
+ }
+ const pelias = new PeliasGeocoder(mockPeliasAPI, geocoder)
+ pelias.autocomplete({ text: 'Mill Ends' })
+ })
+
+ // should send sources if they're defined in the config
+ it('should send sources in autocomplete if defined in config', () => {
+ // create mock API to check query
+ const mockPeliasAPI = {
+ autocomplete: query => {
+ expect(query.sources).toBe(mockSources)
+ return Promise.resolve()
+ }
+ }
+ const pelias = new PeliasGeocoder(
+ mockPeliasAPI,
+ { ...geocoder, sources: mockSources }
+ )
+ pelias.autocomplete({ text: 'Mill Ends' })
+ })
+
+ // sources should not be sent unless they are explicitly defined in the
+ // query. See https://github.com/ibi-group/trimet-mod-otp/issues/239
+ it('should not send sources in search by default', () => {
+ // create mock API to check query
+ const mockPeliasAPI = {
+ search: query => {
+ expect(query.sources).not.toBe(expect.anything())
+ return Promise.resolve()
+ }
+ }
+ const pelias = new PeliasGeocoder(mockPeliasAPI, geocoder)
+ pelias.search({ text: 'Mill Ends' })
+ })
+
+ // should send sources if they're defined in the config
+ it('should send sources in search if defined in config', () => {
+ // create mock API to check query
+ const mockPeliasAPI = {
+ search: query => {
+ expect(query.sources).toBe(mockSources)
+ return Promise.resolve()
+ }
+ }
+ const pelias = new PeliasGeocoder(
+ mockPeliasAPI,
+ { ...geocoder, sources: mockSources }
+ )
+ pelias.search({ text: 'Mill Ends' })
+ })
+ }
})
})
})
diff --git a/__tests__/util/state.js b/__tests__/util/state.js
index 954c150bd..5385038f9 100644
--- a/__tests__/util/state.js
+++ b/__tests__/util/state.js
@@ -1,13 +1,8 @@
/* globals describe, expect, it */
-import {getDefaultQuery, queryIsValid} from '../../lib/util/state'
+import {queryIsValid} from '../../lib/util/state'
describe('util > state', () => {
- it('getDefaultQuery should parse window hash if available', () => {
- window.location.hash = '#plan?arriveBy=false&date=2017-02-03&fromPlace=12,34&toPlace=34,12&time=12:34'
- expect(getDefaultQuery()).toMatchSnapshot()
- })
-
describe('queryIsValid', () => {
const fakeFromLocation = {
lat: 12,
diff --git a/lib/actions/api.js b/lib/actions/api.js
index 280f8627d..3498c8461 100644
--- a/lib/actions/api.js
+++ b/lib/actions/api.js
@@ -12,6 +12,7 @@ import { getTripOptionsFromQuery, getUrlParams } from '../util/query'
import queryParams from '../util/query-params'
import { getStopViewerConfig, queryIsValid } from '../util/state'
import { randId } from '../util/storage'
+import { OTP_API_DATE_FORMAT, OTP_API_TIME_FORMAT } from '../util/time'
if (typeof (fetch) === 'undefined') require('isomorphic-fetch')
// Generic API actions
@@ -199,8 +200,8 @@ function getRoutingParams (otpState, ignoreRealtimeUpdates) {
}
// check date/time validity; ignore both if either is invalid
- const dateValid = moment(params.date, 'YYYY-MM-DD').isValid()
- const timeValid = moment(params.time, 'H:mm').isValid()
+ const dateValid = moment(params.date, OTP_API_DATE_FORMAT).isValid()
+ const timeValid = moment(params.time, OTP_API_TIME_FORMAT).isValid()
if (!dateValid || !timeValid) {
delete params.time
@@ -225,8 +226,8 @@ function getRoutingParams (otpState, ignoreRealtimeUpdates) {
// Additional processing specific to PROFILE mode
} else {
// check start and end time validity; ignore both if either is invalid
- const startTimeValid = moment(params.startTime, 'H:mm').isValid()
- const endTimeValid = moment(params.endTime, 'H:mm').isValid()
+ const startTimeValid = moment(params.startTime, OTP_API_TIME_FORMAT).isValid()
+ const endTimeValid = moment(params.endTime, OTP_API_TIME_FORMAT).isValid()
if (!startTimeValid || !endTimeValid) {
delete params.startTimeValid
@@ -476,6 +477,15 @@ export function findStopTimesForStop (params) {
let { stopId, ...otherParams } = params
// If other params not provided, fall back on defaults from stop viewer config.
const queryParams = { ...getStopViewerConfig(getState().otp), ...otherParams }
+ // If no start time is provided, pass in the current time. Note: this is not
+ // a required param by the back end, but if a value is not provided, the
+ // time defaults to the server's time, which can make it difficult to test
+ // scenarios when you may want to use a different date/time for your local
+ // testing environment.
+ if (!queryParams.startTime) {
+ const nowInSeconds = Math.floor((new Date()).getTime() / 1000)
+ queryParams.startTime = nowInSeconds
+ }
dispatch(createQueryAction(
`index/stops/${stopId}/stoptimes?${qs.stringify(queryParams)}`,
findStopTimesForStopResponse,
diff --git a/lib/actions/form.js b/lib/actions/form.js
index be7b58aa2..66860f1a4 100644
--- a/lib/actions/form.js
+++ b/lib/actions/form.js
@@ -11,6 +11,7 @@ import {
} from '../util/query'
import { getItem, randId } from '../util/storage'
import { queryIsValid } from '../util/state'
+import { OTP_API_TIME_FORMAT } from '../util/time'
import { isMobile } from '../util/ui'
import {
MobileScreens,
@@ -35,7 +36,10 @@ export function resetForm () {
} else {
// Get user overrides and apply to default query
const userOverrides = getItem('defaultQuery', {})
- const defaultQuery = Object.assign(getDefaultQuery(), userOverrides)
+ const defaultQuery = Object.assign(
+ getDefaultQuery(otpState.config),
+ userOverrides
+ )
// Filter out non-options (i.e., date, places).
const options = getTripOptionsFromQuery(defaultQuery)
// Default mode is currently WALK,TRANSIT. We need to update this value
@@ -67,7 +71,15 @@ export function parseUrlQueryString (params = getUrlParams()) {
})
const searchId = params.ui_activeSearch || randId()
// Convert strings to numbers/objects and dispatch
- dispatch(setQueryParam(planParamsToQuery(planParams), searchId))
+ dispatch(
+ setQueryParam(
+ planParamsToQuery(
+ planParams,
+ getState().otp.config
+ ),
+ searchId
+ )
+ )
}
}
@@ -80,7 +92,7 @@ export function formChanged (oldQuery, newQuery) {
// If departArrive is set to 'NOW', update the query time to current
if (otpState.currentQuery && otpState.currentQuery.departArrive === 'NOW') {
- dispatch(settingQueryParam({ time: moment().format('HH:mm') }))
+ dispatch(settingQueryParam({ time: moment().format(OTP_API_TIME_FORMAT) }))
}
// Determine if either from/to location has changed
diff --git a/lib/components/app/print-layout.js b/lib/components/app/print-layout.js
index 2205243f0..89b05c12f 100644
--- a/lib/components/app/print-layout.js
+++ b/lib/components/app/print-layout.js
@@ -59,7 +59,7 @@ class PrintLayout extends Component {
}
render () {
- const { itinerary, companies, timeFormat } = this.props
+ const { configCompanies, customIcons, itinerary, timeFormat } = this.props
return (
{/* The header bar, including the Toggle Map and Print buttons */}
@@ -87,7 +87,14 @@ class PrintLayout extends Component {
}
{/* The main itinerary body */}
- {itinerary &&
}
+ {itinerary
+ ?
+ : null
+ }
)
}
@@ -98,7 +105,7 @@ class PrintLayout extends Component {
const mapStateToProps = (state, ownProps) => {
return {
itinerary: getActiveItinerary(state.otp),
- companies: state.otp.currentQuery.companies,
+ configCompanies: state.otp.config.companies,
timeFormat: getTimeFormat(state.otp.config)
}
}
diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js
index f70787102..36bfd1ce5 100644
--- a/lib/components/app/responsive-webapp.js
+++ b/lib/components/app/responsive-webapp.js
@@ -193,7 +193,13 @@ class RouterWrapper extends Component {
/>
{
+ // combine the router props with the other props that get
+ // passed to the exported component. This way it's possible for
+ // the PrintLayout component to receive the custom icons prop.
+ const props = {...this.props, ...routerProps}
+ return
+ }}
/>
{/* For any other route, simply return the web app. */}
- {moment(date).calendar(null, { sameElse: dateFormat }).split(' at')[0]}
+ {
+ moment(date, OTP_API_DATE_FORMAT)
+ .calendar(null, { sameElse: dateFormat })
+ .split(' at')[0]}
{timeStr}
diff --git a/lib/components/form/date-time-selector.js b/lib/components/form/date-time-selector.js
index 59ea3c97e..7abefb43b 100644
--- a/lib/components/form/date-time-selector.js
+++ b/lib/components/form/date-time-selector.js
@@ -6,7 +6,12 @@ import { connect } from 'react-redux'
import moment from 'moment'
import { setQueryParam } from '../../actions/form'
-import { getTimeFormat, getDateFormat } from '../../util/time'
+import {
+ OTP_API_DATE_FORMAT,
+ OTP_API_TIME_FORMAT,
+ getTimeFormat,
+ getDateFormat
+} from '../../util/time'
class DateTimeSelector extends Component {
static propTypes = {
@@ -36,7 +41,7 @@ class DateTimeSelector extends Component {
_onDayOfWeekChange = evt => {
this.props.setQueryParam({
- date: moment().weekday(evt.target.value).format('YYYY-MM-DD')
+ date: moment().weekday(evt.target.value).format(OTP_API_DATE_FORMAT)
})
}
@@ -53,21 +58,24 @@ class DateTimeSelector extends Component {
}
_onBackupTimeChange = (evt) => {
- const time = moment(evt.target.value, this.props.timeFormat).format('HH:mm')
- this.props.setQueryParam({ time })
+ const {setQueryParam, timeFormat} = this.props
+ const time = moment(evt.target.value, timeFormat).format(OTP_API_TIME_FORMAT)
+ setQueryParam({ time })
}
_onBackupDateChange = (evt) => {
- const date = moment(evt.target.value, this.props.dateFormat).format('YYYY-MM-DD')
- this.props.setQueryParam({ date })
+ const {setQueryParam, dateFormat} = this.props
+ const date = moment(evt.target.value, dateFormat).format(OTP_API_DATE_FORMAT)
+ setQueryParam({ date })
}
_setDepartArrive = (type) => {
- this.props.setQueryParam({ departArrive: type })
+ const {setQueryParam} = this.props
+ setQueryParam({ departArrive: type })
if (type === 'NOW') {
- this.props.setQueryParam({
- date: moment().format('YYYY-MM-DD'),
- time: moment().format('HH:mm')
+ setQueryParam({
+ date: moment().format(OTP_API_DATE_FORMAT),
+ time: moment().format(OTP_API_TIME_FORMAT)
})
}
}
@@ -148,7 +156,11 @@ class DateTimeSelector extends Component {
{['NOW', 'DEPART', 'ARRIVE'].map((type, i) => (
-
+
))}
@@ -158,7 +170,7 @@ class DateTimeSelector extends Component {
@@ -167,7 +179,7 @@ class DateTimeSelector extends Component {
diff --git a/lib/components/form/location-field.js b/lib/components/form/location-field.js
index 296006f24..ef5036a7f 100644
--- a/lib/components/form/location-field.js
+++ b/lib/components/form/location-field.js
@@ -19,6 +19,7 @@ import { distanceStringImperial } from '../../util/distance'
import getGeocoder from '../../util/geocoder'
import { formatStoredPlaceName } from '../../util/map'
import { getActiveSearch, getShowUserSettings } from '../../util/state'
+import { isIE } from '../../util/ui'
class LocationField extends Component {
static propTypes = {
@@ -518,10 +519,27 @@ function createOption (icon, title, onSelect, isActive, isLast) {
// style={{ borderBottom: '1px solid lightgrey' }}
key={itemKey++}
active={isActive}>
-
+ {isIE()
+ // In internet explorer 11, some really weird stuff is happening where it
+ // is not possible to click the text of the title, but if you click just
+ // above it, then it works. So, if using IE 11, just return the title text
+ // and avoid all the extra fancy stuff.
+ // See https://github.com/ibi-group/trimet-mod-otp/issues/237
+ ? title
+ : (
+
+ )
+ }
}
diff --git a/lib/components/narrative/line-itin/access-leg-body.js b/lib/components/narrative/line-itin/access-leg-body.js
index 511e3746e..698651796 100644
--- a/lib/components/narrative/line-itin/access-leg-body.js
+++ b/lib/components/narrative/line-itin/access-leg-body.js
@@ -163,7 +163,7 @@ class AccessLegSummary extends Component {
{getLegModeLabel(leg)}
{' '}
- {leg.distance && {distanceString(leg.distance)} }
+ {leg.distance > 0 && {distanceString(leg.distance)} }
{` to ${getPlaceName(leg.to, config.companies)}`}
diff --git a/lib/components/narrative/line-itin/place-row.js b/lib/components/narrative/line-itin/place-row.js
index cd00495e1..09337abf4 100644
--- a/lib/components/narrative/line-itin/place-row.js
+++ b/lib/components/narrative/line-itin/place-row.js
@@ -4,7 +4,7 @@ import { connect } from 'react-redux'
import LocationIcon from '../../icons/location-icon'
import ViewStopButton from '../../viewers/view-stop-button'
import {
- getCompanyForNetwork,
+ getCompaniesLabelFromNetworks,
getModeForPlace,
getPlaceName
} from '../../../util/itinerary'
@@ -187,9 +187,11 @@ class RentedVehicleLeg extends PureComponent {
// resumes there won't be any network info. In that case we simply return
// that the rental is continuing.
if (leg.from.networks) {
- const companies = leg.from.networks.map(n => getCompanyForNetwork(n, configCompanies))
- const companyLabel = companies.map(co => co.label).join('/')
- rentalDescription += ` ${companyLabel}`
+ const companiesLabel = getCompaniesLabelFromNetworks(
+ leg.from.networks,
+ configCompanies
+ )
+ rentalDescription += ` ${companiesLabel}`
// Only show vehicle name for car rentals. For bikes and eScooters, these
// IDs/names tend to be less relevant (or entirely useless) in this context.
if (leg.rentedCar && leg.from.name) {
@@ -200,7 +202,7 @@ class RentedVehicleLeg extends PureComponent {
rentalDescription = 'Continue using rental'
}
- rentalDescription += ` ${modeString}${vehicleName}`
+ rentalDescription += ` ${modeString} ${vehicleName}`
}
// e.g., Pick up REACHNOW rented car XYZNDB OR
// Pick up SPIN eScooter
diff --git a/lib/components/narrative/line-itin/transit-leg-body.js b/lib/components/narrative/line-itin/transit-leg-body.js
index a062739d8..d8eaf524a 100644
--- a/lib/components/narrative/line-itin/transit-leg-body.js
+++ b/lib/components/narrative/line-itin/transit-leg-body.js
@@ -6,7 +6,11 @@ import moment from 'moment'
import ViewTripButton from '../../viewers/view-trip-button'
import { getIcon } from '../../../util/itinerary'
-import { formatDuration, getTimeFormat } from '../../../util/time'
+import {
+ formatDuration,
+ getLongDateFormat,
+ getTimeFormat
+} from '../../../util/time'
// TODO: support multi-route legs for profile routing
@@ -38,7 +42,7 @@ class TransitLegBody extends Component {
}
render () {
- const { customIcons, leg, operator, timeFormat } = this.props
+ const { customIcons, leg, longDateFormat, operator, timeFormat } = this.props
const {
agencyBrandingUrl,
agencyName,
@@ -108,7 +112,13 @@ class TransitLegBody extends Component {
{/* The Alerts body, if visible */}
- {alertsExpanded && }
+ {alertsExpanded &&
+
+ }
{/* The "Ride X Min / X Stops" Row, including IntermediateStops body */}
{leg.intermediateStops && leg.intermediateStops.length > 0 && (
@@ -151,6 +161,7 @@ class TransitLegBody extends Component {
const mapStateToProps = (state, ownProps) => {
return {
+ longDateFormat: getLongDateFormat(state.otp.config),
operator: state.otp.config.operators.find(operator => operator.id === ownProps.leg.agencyId),
timeFormat: getTimeFormat(state.otp.config)
}
@@ -185,20 +196,21 @@ class AlertsBody extends Component {
}
render () {
- const { timeFormat } = this.props
+ const { longDateFormat, timeFormat } = this.props
return (
{this.props.alerts
.sort((a, b) => b.effectiveStartDate - a.effectiveStartDate)
.map((alert, i) => {
const effectiveStartDate = moment(alert.effectiveStartDate)
- let effectiveDateString = 'Effective as of '
const daysAway = moment().diff(effectiveStartDate, 'days')
- // Add time if alert is effective within one day.
- if (Math.abs(daysAway) <= 1) {
- effectiveDateString += `${effectiveStartDate.format(timeFormat)}, `
- }
- effectiveDateString += effectiveStartDate.calendar(null, { sameElse: 'MMMM D, YYYY' }).split(' at')[0]
+ // Add time if alert is effective within one day. Otherwise, use
+ // calendar long date format (e.g., July 31, 2019).
+ const dateTimeFormat = Math.abs(daysAway) <= 1
+ ? `${timeFormat}, ${longDateFormat}`
+ : longDateFormat
+ const dateTimeString = effectiveStartDate.format(dateTimeFormat)
+ const effectiveDateString = `Effective as of ${dateTimeString}`
return (
diff --git a/lib/components/narrative/printable/printable-itinerary.js b/lib/components/narrative/printable/printable-itinerary.js
index 1f644f02f..8aa48a5ac 100644
--- a/lib/components/narrative/printable/printable-itinerary.js
+++ b/lib/components/narrative/printable/printable-itinerary.js
@@ -1,10 +1,18 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import ModeIcon from '../../icons/mode-icon'
import TripDetails from '../trip-details'
+import { distanceString } from '../../../util/distance'
import { formatTime, formatDuration } from '../../../util/time'
-import { getLegModeLabel, getStepDirection, getStepStreetName, getTimeZoneOffset } from '../../../util/itinerary'
+import {
+ getCompaniesLabelFromNetworks,
+ getLegIcon,
+ getLegModeLabel,
+ getPlaceName,
+ getStepDirection,
+ getStepStreetName,
+ getTimeZoneOffset
+} from '../../../util/itinerary'
export default class PrintableItinerary extends Component {
static propTypes = {
@@ -12,7 +20,12 @@ export default class PrintableItinerary extends Component {
}
render () {
- const { itinerary, timeFormat } = this.props
+ const {
+ configCompanies,
+ customIcons,
+ itinerary,
+ timeFormat
+ } = this.props
const timeOptions = {
format: timeFormat,
@@ -31,10 +44,26 @@ export default class PrintableItinerary extends Component {
)}
{itinerary.legs.map((leg, k) => leg.transitLeg
- ?
+ ?
: leg.hailedCar
- ?
- :
+ ?
+ :
)}
@@ -48,7 +77,7 @@ class TransitLeg extends Component {
}
render () {
- const { leg, interlineFollows, timeOptions } = this.props
+ const { customIcons, leg, interlineFollows, timeOptions } = this.props
// Handle case of transit leg interlined w/ previous
if (leg.interlineWithPreviousLeg) {
@@ -56,10 +85,15 @@ class TransitLeg extends Component {
- Continues as {leg.routeShortName} {leg.routeLongName} to {leg.to.name}
+ Continues as{' '}
+ {leg.routeShortName} {leg.routeLongName} {' '}
+ to {leg.to.name}
-
Get off at {leg.to.name} at {formatTime(leg.endTime, timeOptions)}
+
+ Get off at {leg.to.name} {' '}
+ at {formatTime(leg.endTime, timeOptions)}
+
@@ -68,17 +102,25 @@ class TransitLeg extends Component {
return (
-
+
{getLegIcon(leg, customIcons)}
{leg.routeShortName} {leg.routeLongName} to {leg.to.name}
-
Board at {leg.from.name} at {formatTime(leg.startTime, timeOptions)}
- {interlineFollows
- ?
Stay on board at {leg.to.name}
- :
Get off at {leg.to.name} at {formatTime(leg.endTime, timeOptions)}
- }
+
+ Board at {leg.from.name} {' '}
+ at {formatTime(leg.startTime, timeOptions)}
+
+
+ {interlineFollows
+ ? Stay on board at {leg.to.name}
+ :
+ Get off at {leg.to.name} {' '}
+ at {formatTime(leg.endTime, timeOptions)}
+
+ }
+
@@ -92,19 +134,49 @@ class AccessLeg extends Component {
}
render () {
- const { leg } = this.props
+ const { configCompanies, customIcons, leg } = this.props
+
+ // calculate leg mode label in a special way for this component
+ let legModeLabel = getLegModeLabel(leg)
+
+ if (leg.rentedBike) {
+ // FIXME: Special case for TriMet that needs to be refactored to
+ // incorporate actual company.
+ legModeLabel = 'Ride BIKETOWN bike'
+ } else if (leg.rentedCar) {
+ // Add extra information to printview that would otherwise clutter up
+ // other places that use the getLegModeLabel function
+ const companiesLabel = getCompaniesLabelFromNetworks(
+ leg.from.networks,
+ configCompanies
+ )
+ legModeLabel = `Drive ${companiesLabel} ${leg.from.name}`
+ } else if (leg.rentedVehicle) {
+ const companiesLabel = getCompaniesLabelFromNetworks(
+ leg.from.networks,
+ configCompanies
+ )
+ legModeLabel = `Ride ${companiesLabel} eScooter`
+ }
+
return (
-
+
{getLegIcon(leg, customIcons)}
- {getLegModeLabel(leg)} to {leg.to.name}
+ {legModeLabel} {' '}
+ {!leg.hailedCar &&
+ leg.distance > 0 &&
+ {distanceString(leg.distance)} }
+ to {getPlaceName(leg.to, configCompanies)}
{!leg.hailedCar && (
{leg.steps.map((step, k) => {
return (
-
{getStepDirection(step)} on {getStepStreetName(step)}
+
+ {getStepDirection(step)} on {getStepStreetName(step)}
+
)
})}
@@ -121,20 +193,26 @@ class TNCLeg extends Component {
}
render () {
- const { leg } = this.props
+ const { customIcons, leg } = this.props
const { tncData } = leg
if (!tncData) return null
return (
-
+
{getLegIcon(leg, customIcons)}
Take {tncData.displayName} to {leg.to.name}
-
Estimated wait time for pickup: {formatDuration(tncData.estimatedArrival)}
-
Estimated travel time: {formatDuration(leg.duration)} (does not account for traffic)
+
+ Estimated wait time for pickup:{' '}
+ {formatDuration(tncData.estimatedArrival)}
+
+
+ Estimated travel time:{' '}
+ {formatDuration(leg.duration)} (does not account for traffic)
+
diff --git a/lib/components/viewers/stop-viewer.js b/lib/components/viewers/stop-viewer.js
index 90ec9ddba..480b31eee 100644
--- a/lib/components/viewers/stop-viewer.js
+++ b/lib/components/viewers/stop-viewer.js
@@ -14,7 +14,7 @@ import { findStop, findStopTimesForStop } from '../../actions/api'
import { forgetStop, rememberStop, setLocation } from '../../actions/map'
import { routeComparator } from '../../util/itinerary'
import { getShowUserSettings, getStopViewerConfig } from '../../util/state'
-import { formatDuration, formatStopTime, getTimeFormat } from '../../util/time'
+import { formatDuration, formatSecondsAfterMidnight, getTimeFormat, getUserTimezone } from '../../util/time'
class StopViewer extends Component {
state = {}
@@ -136,6 +136,15 @@ class StopViewer extends Component {
const id = `${routeId}-${headsign}`
if (!(id in stopTimesByPattern)) {
const route = stopData.routes.find(r => r.id === routeId)
+ // in some cases, the TriMet transit index will not return all routes
+ // that serve a stop. Perhaps it doesn't return some routes if the
+ // route only performs a drop-off at the stop... not quite sure. So a
+ // check is needed to make sure we don't add data for routes not found
+ // from the routes query.
+ if (!route) {
+ console.warn(`Route with id ${routeId} not found in list of routes! No stop times from this route will be displayed.`)
+ return
+ }
stopTimesByPattern[id] = {
id,
route,
@@ -203,7 +212,9 @@ class StopViewer extends Component {
{' '}
- {moment(stopData.stopTimesLastUpdated).format(timeFormat)}
+ {moment(stopData.stopTimesLastUpdated)
+ .tz(getUserTimezone())
+ .format(timeFormat)}
Plan a trip: {' '}
@@ -387,44 +398,55 @@ class PatternRow extends Component {
}
const ONE_HOUR_IN_SECONDS = 3600
+const ONE_DAY_IN_SECONDS = 86400
/**
- * @param {string} [homeTimezone='America/New_York'] Timezone string
- * @return {number} Current time in home timezone in seconds since midnight
+ * Helper method to generate stop time w/ status icon
+ *
+ * @param {object} stopTime A stopTime object as received from a transit index API
+ * @param {string} [homeTimezone] If configured, the timezone of the area
+ * @param {string} [soonText='Due'] The text to display for departure times
+ * about to depart in a short amount of time
+ * @param {string} timeFormat A valid moment.js formatting string
*/
-function getHomeTime (homeTimezone = 'America/New_York') {
- const now = moment()
- return now.tz(homeTimezone).diff(now.clone().startOf('day'), 'seconds')
-}
-
-// helper method to generate stop time w/ status icon
function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', timeFormat) {
- const now = moment()
- const serviceDay = moment(stopTime.serviceDay * 1000)
- const currentHomeTime = getHomeTime(homeTimezone)
- const differentDay = now.date() !== serviceDay.date()
-
- const userTimeZone = moment.tz.guess()
+ const userTimeZone = getUserTimezone()
const inHomeTimezone = homeTimezone && homeTimezone === userTimeZone
- // Determine whether to show departure as countdown (e.g. "5 min") or as HH:mm time
- const secondsUntilDeparture = stopTime.realtimeDeparture - currentHomeTime
- const departsInFuture = stopTime.realtimeDeparture > currentHomeTime
- // Show the exact time if the departure occurs after midnight and if the
- // departure happens within an hour.
- // FIXME: It's unclear why this was designed to show exact time after midnight.
- // We should determine whether this is the desired behavior.
- const showCountdown = !differentDay &&
- secondsUntilDeparture < ONE_HOUR_IN_SECONDS &&
- departsInFuture
+
+ const now = moment().tz(homeTimezone)
+ const serviceDay = moment(stopTime.serviceDay * 1000).tz(homeTimezone)
+ // Determine if arrival occurs on different day, making sure to account for
+ // any extra days added to the service day if it arrives after midnight. Note:
+ // this can handle the rare (and non-existent?) case where an arrival occurs
+ // 48:00 hours (or more) from the start of the service day.
+ const departureTimeRemainder = stopTime.realtimeDeparture % ONE_DAY_IN_SECONDS
+ const daysAfterServiceDay = (stopTime.realtimeDeparture - departureTimeRemainder) / ONE_DAY_IN_SECONDS
+ const departureDay = serviceDay.add(daysAfterServiceDay, 'day')
+ const vehicleDepartsToday = now.dayOfYear() === departureDay.dayOfYear()
+ // Determine whether to show departure as countdown (e.g. "5 min") or as HH:mm
+ // time.
+ const secondsUntilDeparture = (stopTime.realtimeDeparture + stopTime.serviceDay) - now.unix()
+ // Determine if vehicle arrives after midnight in order to advance the day of
+ // the week when showing arrival time/day.
+ const departsInFuture = secondsUntilDeparture > 0
+ // Show the exact time if the departure happens within an hour.
+ const showCountdown = secondsUntilDeparture < ONE_HOUR_IN_SECONDS && departsInFuture
// Use "soon text" (e.g., Due) if vehicle is approaching.
const countdownString = secondsUntilDeparture < 60
? soonText
: formatDuration(secondsUntilDeparture)
- // Only show timezone (e.g., PDT) if user is not in home time zone (e.g., user
- // in New York, but viewing a trip planner for service based in Los Angeles).
- const tzToDisplay = inHomeTimezone ? null : homeTimezone
- const formattedTime = formatStopTime(stopTime.realtimeDeparture, tzToDisplay, timeFormat)
+ const formattedTime = formatSecondsAfterMidnight(
+ stopTime.realtimeDeparture,
+ // Only show timezone (e.g., PDT) if user is not in home time zone (e.g., user
+ // in New York, but viewing a trip planner for service based in Los Angeles).
+ inHomeTimezone ? timeFormat : `${timeFormat} z`
+ )
+ // We only want to show the day of the week if the arrival is on a
+ // different day and we're not showing the countdown string. This avoids
+ // cases such as when it's Wednesday at 11:55pm and an arrival occurs at
+ // Thursday 12:19am. We don't want the time to read: 'Thursday, 24 minutes'.
+ const showDayOfWeek = !vehicleDepartsToday && !showCountdown
return (
@@ -437,9 +459,9 @@ function getFormattedStopTime (stopTime, homeTimezone, soonText = 'Due', timeFor
style={{ color: '#888', fontSize: '0.8em', marginRight: 2 }} />
}
-
- {differentDay &&
-
{serviceDay.format('dddd')}
+
+ {showDayOfWeek &&
+
{departureDay.format('dddd')}
}
{showCountdown
diff --git a/lib/components/viewers/trip-viewer.js b/lib/components/viewers/trip-viewer.js
index 53f881cda..0bd7290c2 100644
--- a/lib/components/viewers/trip-viewer.js
+++ b/lib/components/viewers/trip-viewer.js
@@ -10,7 +10,7 @@ import { setViewedTrip } from '../../actions/ui'
import { findTrip } from '../../actions/api'
import { setLocation } from '../../actions/map'
-import { formatStopTime } from '../../util/time'
+import { formatSecondsAfterMidnight, getTimeFormat } from '../../util/time'
class TripViewer extends Component {
static propTypes = {
@@ -28,7 +28,13 @@ class TripViewer extends Component {
}
render () {
- const { viewedTrip, tripData, hideBackButton, languageConfig } = this.props
+ const {
+ hideBackButton,
+ languageConfig,
+ timeFormat,
+ tripData,
+ viewedTrip
+ } = this.props
return (
@@ -93,7 +99,7 @@ class TripViewer extends Component {
{/* the departure time */}
- {formatStopTime(tripData.stopTimes[i].scheduledDeparture)}
+ {formatSecondsAfterMidnight(tripData.stopTimes[i].scheduledDeparture, timeFormat)}
{/* the vertical strip map */}
@@ -128,9 +134,10 @@ class TripViewer extends Component {
const mapStateToProps = (state, ownProps) => {
const viewedTrip = state.otp.ui.viewedTrip
return {
+ languageConfig: state.otp.config.language,
+ timeFormat: getTimeFormat(state.otp.config),
tripData: state.otp.transitIndex.trips[viewedTrip.tripId],
- viewedTrip,
- languageConfig: state.otp.config.language
+ viewedTrip
}
}
diff --git a/lib/reducers/create-otp-reducer.js b/lib/reducers/create-otp-reducer.js
index a3e473400..89ed97830 100644
--- a/lib/reducers/create-otp-reducer.js
+++ b/lib/reducers/create-otp-reducer.js
@@ -12,48 +12,10 @@ import {
import { isTransit, getTransitModes } from '../util/itinerary'
import { filterProfileOptions } from '../util/profile'
import { getItem, removeItem, storeItem } from '../util/storage'
+import { getUserTimezone } from '../util/time'
import { MainPanelContent, MobileScreens } from '../actions/ui'
-const defaultConfig = {
- autoPlan: false,
- debouncePlanTimeMs: 0,
- language: {},
- operators: [],
- realtimeEffectsDisplayThreshold: 120,
- routingTypes: [],
- stopViewer: {
- numberOfDepartures: 3, // per pattern
- // This is set to 345600 (four days) so that, for example, if it is Friday and
- // a route does not begin service again until Monday, we are showing its next
- // departure and it is not entirely excluded from display.
- timeRange: 345600 // four days in seconds
- }
-}
-
-// Load user settings from local storage.
-// TODO: Make this work with settings fetched from alternative storage system
-// (e.g., OTP backend middleware containing user profile system).
-// User overrides determine user's default mode/query parameters.
-const userOverrides = getItem('defaultQuery', {})
-// Combine user overrides with default query to get default search settings.
-const defaults = Object.assign(getDefaultQuery(), userOverrides)
-// Whether to auto-refresh stop arrival times in the Stop Viewer.
-const autoRefreshStopTimes = getItem('autoRefreshStopTimes', true)
-// User's home and work locations
-const home = getItem('home')
-const work = getItem('work')
-// Whether recent searches and places should be tracked in local storage.
-const trackRecent = getItem('trackRecent', false)
-// Recent places used in trip plan searches.
-const recentPlaces = getItem('recent', [])
-// List of user's favorite stops.
-const favoriteStops = getItem('favoriteStops', [])
-// Recent trip plan searches (excluding time/date parameters to avoid complexity).
-const recentSearches = getItem('recentSearches', [])
-// Filter valid locations found into locations list.
-const locations = [home, work].filter(p => p)
const MAX_RECENT_STORAGE = 5
-// TODO: parse and merge URL query params w/ default query
// TODO: fire planTrip action if default query is complete/error-free
@@ -93,7 +55,64 @@ function validateInitalState (initialState) {
}
}
-function createOtpReducer (config, initialQuery) {
+/**
+ * Create the initial state of otp-react-redux using user-provided config, any
+ * items in localStorage and a few defaults.
+ */
+export function getInitialState (userDefinedConfig, initialQuery) {
+ const defaultConfig = {
+ autoPlan: false,
+ debouncePlanTimeMs: 0,
+ language: {},
+ operators: [],
+ realtimeEffectsDisplayThreshold: 120,
+ routingTypes: [],
+ stopViewer: {
+ numberOfDepartures: 3, // per pattern
+ // This is set to 345600 (four days) so that, for example, if it is Friday and
+ // a route does not begin service again until Monday, we are showing its next
+ // departure and it is not entirely excluded from display.
+ timeRange: 345600 // four days in seconds
+ }
+ }
+
+ const config = Object.assign(defaultConfig, userDefinedConfig)
+
+ if (!config.homeTimezone) {
+ config.homeTimezone = getUserTimezone()
+ console.warn(
+ `Config value 'homeTimezone' not configured for this webapp!\n
+ This value is recommended in order to properly display stop times for
+ users that are not in the timezone that the transit system is in. The
+ detected user timezone of '${config.homeTimezone}' will be used. Hopefully
+ that is the right one...`
+ )
+ }
+
+ // Load user settings from local storage.
+ // TODO: Make this work with settings fetched from alternative storage system
+ // (e.g., OTP backend middleware containing user profile system).
+ // User overrides determine user's default mode/query parameters.
+ const userOverrides = getItem('defaultQuery', {})
+ // Combine user overrides with default query to get default search settings.
+ const defaults = Object.assign(getDefaultQuery(config), userOverrides)
+ // Whether to auto-refresh stop arrival times in the Stop Viewer.
+ const autoRefreshStopTimes = getItem('autoRefreshStopTimes', true)
+ // User's home and work locations
+ const home = getItem('home')
+ const work = getItem('work')
+ // Whether recent searches and places should be tracked in local storage.
+ const trackRecent = getItem('trackRecent', false)
+ // Recent places used in trip plan searches.
+ const recentPlaces = getItem('recent', [])
+ // List of user's favorite stops.
+ const favoriteStops = getItem('favoriteStops', [])
+ // Recent trip plan searches (excluding time/date parameters to avoid complexity).
+ const recentSearches = getItem('recentSearches', [])
+ // Filter valid locations found into locations list.
+ const locations = [home, work].filter(p => p)
+ // TODO: parse and merge URL query params w/ default query
+
// populate query by merging any provided query params w/ the default params
const currentQuery = Object.assign(defaults, initialQuery)
// Add configurable locations to home and work locations
@@ -117,8 +136,8 @@ function createOtpReducer (config, initialQuery) {
queryModes = ensureSingleAccessMode(queryModes)
}
- const initialState = {
- config: Object.assign(defaultConfig, config),
+ return {
+ config,
currentQuery,
location: {
currentPosition: {
@@ -178,6 +197,10 @@ function createOtpReducer (config, initialQuery) {
diagramLeg: null
}
}
+}
+
+function createOtpReducer (config, initialQuery) {
+ const initialState = getInitialState(config, initialQuery)
// validate the inital state
validateInitalState(initialState)
diff --git a/lib/util/geocoder.js b/lib/util/geocoder.js
index c3aae4001..8ce68c9dc 100644
--- a/lib/util/geocoder.js
+++ b/lib/util/geocoder.js
@@ -20,14 +20,8 @@ class Geocoder {
* address or POI, attempt to find possible matches.
*/
autocomplete (query) {
- const {apiKey, baseUrl, boundary, focusPoint} = this.geocoderConfig
- return this.api.autocomplete({
- apiKey,
- boundary,
- focusPoint,
- url: baseUrl ? `${baseUrl}/autocomplete` : undefined,
- ...query
- }).then(this._rewriteAutocompleteResponse)
+ return this.api.autocomplete(this.getAutocompleteQuery(query))
+ .then(this.rewriteAutocompleteResponse)
}
/**
@@ -50,46 +44,76 @@ class Geocoder {
* GPS coordiante.
*/
reverse (query) {
+ return this.api.reverse(this.getReverseQuery(query))
+ .then(this.rewriteReverseResponse)
+ }
+
+ /**
+ * Perform a search query. A search query is different from autocomplete in
+ * that it is assumed that the text provided is more or less a complete
+ * well-fromatted address.
+ */
+ search (query) {
+ return this.api.search(this.getSearchQuery(query))
+ .then(this.rewriteSearchResponse)
+ }
+
+ /**
+ * Default autocomplete query generator
+ */
+ getAutocompleteQuery (query) {
+ const {apiKey, baseUrl, boundary, focusPoint} = this.geocoderConfig
+ return {
+ apiKey,
+ boundary,
+ focusPoint,
+ url: baseUrl ? `${baseUrl}/autocomplete` : undefined,
+ ...query
+ }
+ }
+
+ /**
+ * Default reverse query generator
+ */
+ getReverseQuery (query) {
const {apiKey, baseUrl} = this.geocoderConfig
- return this.api.reverse({
+ return {
apiKey,
format: true,
url: baseUrl ? `${baseUrl}/reverse` : undefined,
...query
- }).then(this._rewriteReverseResponse)
+ }
}
/**
- * Perform a search query. This query assumes that the text being searched
- * is more-or-less an exact address or POI.
+ * Default search query generator.
*/
- search (query) {
+ getSearchQuery (query) {
const {apiKey, baseUrl, boundary, focusPoint} = this.geocoderConfig
- return this.api.search({
+ return {
apiKey,
boundary,
focusPoint,
- sources: null,
url: baseUrl ? `${baseUrl}/search` : undefined,
format: false, // keep as returned GeoJSON,
...query
- }).then(this._rewriteSearchResponse)
+ }
}
/**
* Default rewriter for autocomplete responses
*/
- _rewriteAutocompleteResponse (response) { return response }
+ rewriteAutocompleteResponse (response) { return response }
/**
* Default rewriter for reverse responses
*/
- _rewriteReverseResponse (response) { return response }
+ rewriteReverseResponse (response) { return response }
/**
* Default rewriter for search responses
*/
- _rewriteSearchResponse (response) { return response }
+ rewriteSearchResponse (response) { return response }
}
/**
@@ -118,7 +142,7 @@ class ArcGISGeocoder extends Geocoder {
* Rewrite an autocomplete response into an application specific data format.
* Also, filter out any results that are collections.
*/
- _rewriteAutocompleteResponse (response) {
+ rewriteAutocompleteResponse (response) {
return {
// remove any autocomplete results that are collections
// (eg multiple Starbucks)
@@ -137,7 +161,7 @@ class ArcGISGeocoder extends Geocoder {
* Rewrite the response into an application-specific data format using the
* first feature returned from the geocoder.
*/
- _rewriteReverseResponse (response) {
+ rewriteReverseResponse (response) {
const { features, query } = response
const { lat, lon } = query
return {
@@ -159,7 +183,7 @@ class NoApiGeocoder extends Geocoder {
* Use coordinate string parser.
*/
autocomplete (query) {
- return this._parseCoordinateString(query.text)
+ return this.parseCoordinateString(query.text)
}
/**
@@ -167,8 +191,8 @@ class NoApiGeocoder extends Geocoder {
*/
reverse (query) {
let { lat, lon } = query.point
- lat = this._roundGPSDecimal(lat)
- lon = this._roundGPSDecimal(lon)
+ lat = this.roundGPSDecimal(lat)
+ lon = this.roundGPSDecimal(lon)
return Promise.resolve({ lat, lon, name: `${lat}, ${lon}` })
}
@@ -176,14 +200,14 @@ class NoApiGeocoder extends Geocoder {
* Use coordinate string parser.
*/
search (query) {
- return this._parseCoordinateString(query.text)
+ return this.parseCoordinateString(query.text)
}
/**
* Attempt to parse the input as a GPS coordinate. If parseable, return a
* feature.
*/
- _parseCoordinateString (string) {
+ parseCoordinateString (string) {
let feature
try {
feature = {
@@ -201,7 +225,7 @@ class NoApiGeocoder extends Geocoder {
return Promise.resolve({ features: [feature] })
}
- _roundGPSDecimal (number) {
+ roundGPSDecimal (number) {
const roundFactor = 100000
return Math.round(number * roundFactor) / roundFactor
}
@@ -211,14 +235,55 @@ class NoApiGeocoder extends Geocoder {
* Geocoder implementation for the Pelias geocoder.
* See https://pelias.io
*
+ * This is exported for testing purposes only.
+ *
* @extends Geocoder
*/
-class PeliasGeocoder extends Geocoder {
+export class PeliasGeocoder extends Geocoder {
+ /**
+ * Generate an autocomplete query specifically for the Pelias API. The
+ * `sources` parameter is a Pelias-specific option.
+ */
+ getAutocompleteQuery (query) {
+ const {apiKey, baseUrl, boundary, focusPoint, sources} = this.geocoderConfig
+ return {
+ apiKey,
+ boundary,
+ focusPoint,
+ // explicitly send over null for sources if provided sources is not truthy
+ // in order to avoid default isomorphic-mapzen-search sources form being
+ // applied
+ sources: sources || null,
+ url: baseUrl ? `${baseUrl}/autocomplete` : undefined,
+ ...query
+ }
+ }
+
+ /**
+ * Generate a search query specifically for the Pelias API. The
+ * `sources` parameter is a Pelias-specific option.
+ */
+ getSearchQuery (query) {
+ const {apiKey, baseUrl, boundary, focusPoint, sources} = this.geocoderConfig
+ return {
+ apiKey,
+ boundary,
+ focusPoint,
+ // explicitly send over null for sources if provided sources is not truthy
+ // in order to avoid default isomorphic-mapzen-search sources form being
+ // applied
+ sources: sources || null,
+ url: baseUrl ? `${baseUrl}/search` : undefined,
+ format: false, // keep as returned GeoJSON,
+ ...query
+ }
+ }
+
/**
* Rewrite the response into an application-specific data format using the
* first feature returned from the geocoder.
*/
- _rewriteReverseResponse (response) {
+ rewriteReverseResponse (response) {
const { 'point.lat': lat, 'point.lon': lon } = response.isomorphicMapzenSearchQuery
return {
lat,
diff --git a/lib/util/itinerary.js b/lib/util/itinerary.js
index 412e1bfae..6241d3add 100644
--- a/lib/util/itinerary.js
+++ b/lib/util/itinerary.js
@@ -379,7 +379,7 @@ export function toSentenceCase (str) {
*/
export function getLegIcon (leg, customIcons) {
// check if a custom function exists for determining the icon for a leg
- if (typeof customIcons.customIconForLeg === 'function') {
+ if (customIcons && typeof customIcons.customIconForLeg === 'function') {
// function exits, get the icon string lookup. It's possible for there to be
// a custom function that only returns a string for when a leg meets the
// criteria of the custom function
@@ -402,7 +402,11 @@ export function getLegIcon (leg, customIcons) {
return getIcon(iconStr, customIcons)
}
-export function getCompanyForNetwork (networkString, companies = []) {
+/**
+ * Get the configured company object for the given network string if the company
+ * has been defined in the provided companies array config.
+ */
+function getCompanyForNetwork (networkString, companies = []) {
const company = companies.find(co => co.id === networkString)
if (!company) {
console.warn(`No company found in config.yml that matches rented vehicle network: ${networkString}`, companies)
@@ -410,6 +414,19 @@ export function getCompanyForNetwork (networkString, companies = []) {
return company
}
+/**
+ * Get a string label to display from a list of vehicle rental networks.
+ *
+ * @param {Array
} networks A list of network ids.
+ * @param {Array} [companies=[]] An optional list of the companies config.
+ * @return {string} A label for use in presentation on a website.
+ */
+export function getCompaniesLabelFromNetworks (networks, companies = []) {
+ return networks.map(network => getCompanyForNetwork(network, companies))
+ .map(co => co.label)
+ .join('/')
+}
+
/**
* Returns mode name by checking the vertex type (VertexType class in OTP) for
* the provided place. NOTE: this is currently only intended for vehicles at
diff --git a/lib/util/query-params.js b/lib/util/query-params.js
index 1991e0e46..dfcfd7487 100644
--- a/lib/util/query-params.js
+++ b/lib/util/query-params.js
@@ -18,7 +18,12 @@ import { getCurrentDate, getCurrentTime } from './time'
* parameter) indicating whether this query parameter is applicable to the query.
* (Applicability is assumed if this function is not provided.)
*
- * default: the default value for this parameter
+ * default: the default value for this parameter. The default can be also be a
+ * function that gets executed when accessing the default value. When the value
+ * is a funciton, it will take an argument of the current config of the otp-rr
+ * store. This is needed when a brand new time-dependent value is desired to be
+ * calculated. It's also helpful for producing tests that have consistent data
+ * output.
*
* itineraryRewrite: an optional function for translating the key and/or value
* for ITINERARY mode only (e.g. 'to' is rewritten as 'toPlace'). Accepts the
@@ -79,13 +84,13 @@ const queryParams = [
{ /* date - the date of travel, in MM-DD-YYYY format */
name: 'date',
routingTypes: [ 'ITINERARY', 'PROFILE' ],
- default: getCurrentDate()
+ default: getCurrentDate
},
{ /* time - the arrival/departure time for an itinerary trip, in HH:mm format */
name: 'time',
routingTypes: [ 'ITINERARY' ],
- default: getCurrentTime()
+ default: getCurrentTime
},
{ /* departArrive - whether this is a depart-at, arrive-by, or leave-now trip */
diff --git a/lib/util/query.js b/lib/util/query.js
index a1ae6b207..5779108c7 100644
--- a/lib/util/query.js
+++ b/lib/util/query.js
@@ -97,6 +97,14 @@ export function getTripOptionsFromQuery (query, keepPlace = false) {
return options
}
+/**
+ * Gets the default query param by executing the default value function with the
+ * provided otp config if the default value is a function.
+ */
+function getDefaultQueryParamValue (param, config) {
+ return typeof param.default === 'function' ? param.default(config) : param.default
+}
+
/**
* Determines whether the specified query differs from the default query, i.e.,
* whether the user has modified any trip options (including mode) from their
@@ -119,7 +127,7 @@ export function isNotDefaultQuery (query, config) {
// Check that the applicability test (if provided) is satisfied
if (typeof paramInfo.applicable === 'function' &&
!paramInfo.applicable(query, config)) return
- if (query[param] !== paramInfo.default) {
+ if (query[param] !== getDefaultQueryParamValue(paramInfo, config)) {
queryIsDifferent = true
}
})
@@ -127,15 +135,27 @@ export function isNotDefaultQuery (query, config) {
return queryIsDifferent
}
-export function getDefaultQuery () {
+/**
+ * Get the default query to OTP based on the given config.
+ *
+ * @param config the config in the otp-rr store.
+ */
+export function getDefaultQuery (config) {
const defaultQuery = { routingType: 'ITINERARY' }
queryParams.filter(qp => 'default' in qp).forEach(qp => {
- defaultQuery[qp.name] = qp.default
+ defaultQuery[qp.name] = getDefaultQueryParamValue(qp, config)
})
return defaultQuery
}
-export function planParamsToQuery (params) {
+/**
+ * Create a otp query based on a the url params.
+ *
+ * @param {Object} params An object representing the parsed querystring of url
+ * params.
+ * @param config the config in the otp-rr store.
+ */
+export function planParamsToQuery (params, config) {
const query = {}
for (var key in params) {
switch (key) {
@@ -153,10 +173,10 @@ export function planParamsToQuery (params) {
: 'NOW'
break
case 'date':
- query.date = params.date || getCurrentDate()
+ query.date = params.date || getCurrentDate(config)
break
case 'time':
- query.time = params.time || getCurrentTime()
+ query.time = params.time || getCurrentTime(config)
break
default:
if (!isNaN(params[key])) query[key] = parseFloat(params[key])
diff --git a/lib/util/state.js b/lib/util/state.js
index a4679c450..8019564e8 100644
--- a/lib/util/state.js
+++ b/lib/util/state.js
@@ -1,9 +1,5 @@
import isEqual from 'lodash.isequal'
-import { coordsToString } from './map'
-import { getUrlParams } from './query'
-import { getCurrentTime, getCurrentDate } from './time'
-
/**
* Get the active search object
* @param {Object} otpState the OTP state object
@@ -70,43 +66,6 @@ export function queryIsValid (otpState) {
// TODO: add date/time validation
}
-/**
- * This parses the current URL params and returns a simple query object
- * containing only the primary query params.
- *
- * TODO: Determine if this needs to/can be replaced with
- * 'util/query.js#getDefaultQuery'
- */
-export function getDefaultQuery () {
- let params = {}
- if (typeof (window) !== 'undefined') {
- params = getUrlParams()
- }
- const from = (params.fromPlace && params.fromPlace.split(',')) || []
- const to = (params.toPlace && params.toPlace.split(',')) || []
- return {
- routingType: 'ITINERARY',
- from: from.length ? {
- name: coordsToString(from) || null,
- lat: from[0] || null,
- lon: from[1] || null
- } : null,
- to: to.length ? {
- name: coordsToString(to) || null,
- lat: to[0] || null,
- lon: to[1] || null
- } : null,
- departArrive: params.arriveBy === 'true'
- ? 'ARRIVE'
- : params.arriveBy === 'false'
- ? 'DEPART'
- : 'NOW',
- date: params.date || getCurrentDate(),
- time: params.time || getCurrentTime(),
- mode: params.mode
- }
-}
-
export function getRealtimeEffects (otpState) {
const search = getActiveSearch(otpState)
diff --git a/lib/util/time.js b/lib/util/time.js
index 4f99b8e47..be7e5964c 100644
--- a/lib/util/time.js
+++ b/lib/util/time.js
@@ -1,7 +1,10 @@
import moment from 'moment'
import 'moment-timezone'
-const TIME_FORMAT_24_HR = 'HH:mm'
+// special constants for making sure the following date format is always sent to
+// OTP regardless of whatever the user has configured as the display format
+export const OTP_API_DATE_FORMAT = 'YYYY-MM-DD'
+export const OTP_API_TIME_FORMAT = 'HH:mm'
/**
* @param {[type]} config the OTP config object found in store
@@ -10,13 +13,13 @@ const TIME_FORMAT_24_HR = 'HH:mm'
export function getTimeFormat (config) {
return (config.dateTime && config.dateTime.timeFormat)
? config.dateTime.timeFormat
- : TIME_FORMAT_24_HR
+ : OTP_API_TIME_FORMAT
}
export function getDateFormat (config) {
return (config.dateTime && config.dateTime.dateFormat)
? config.dateTime.dateFormat
- : 'YYYY-MM-DD'
+ : OTP_API_DATE_FORMAT
}
export function getLongDateFormat (config) {
@@ -47,34 +50,40 @@ export function formatDuration (seconds) {
*/
export function formatTime (ms, options) {
return moment(ms + (options && options.offset ? options.offset : 0))
- .format(options && options.format ? options.format : TIME_FORMAT_24_HR)
+ .format(options && options.format ? options.format : OTP_API_TIME_FORMAT)
}
/**
- * Formats a stop time value for display in narrative
- * @param {[type]} seconds time since midnight in seconds
- * @param {[type]} [timezone=null] optional time zone to include in result
- * @param {string} [timeFormat='h:mm a'] time format
+ * Formats a seconds after midnight value for display in narrative
+ * @param {number} seconds time since midnight in seconds
+ * @param {string} timeFormat A valid moment.js time format
* @return {string} formatted text representation
*/
-export function formatStopTime (seconds, timezone = null, timeFormat = 'h:mm a') {
- const m = timezone ? moment().tz(timezone) : moment()
- const format = timezone ? `${timeFormat} z` : timeFormat
- return m.startOf('day').seconds(seconds).format(format)
+export function formatSecondsAfterMidnight (seconds, timeFormat) {
+ return moment().startOf('day').seconds(seconds).format(timeFormat)
}
/**
* Formats current time for use in OTP query
- * @returns {string} formatted text representation
+ * The conversion to the user's timezone is needed for testing purposes.
*/
export function getCurrentTime () {
- return moment().format(TIME_FORMAT_24_HR)
+ return moment().tz(getUserTimezone()).format(OTP_API_TIME_FORMAT)
}
/**
* Formats current date for use in OTP query
- * @returns {string} formatted text representation
+ * The conversion to the user's timezone is needed for testing purposes.
+ */
+export function getCurrentDate (config) {
+ return moment().tz(getUserTimezone()).format(OTP_API_DATE_FORMAT)
+}
+
+/**
+ * Get the timezone name that is set for the user that is currently looking at
+ * this website. Use a bit of hackery to force a specific timezone if in a
+ * test environment.
*/
-export function getCurrentDate () {
- return moment().format('YYYY-MM-DD')
+export function getUserTimezone () {
+ return process.env.NODE_ENV === 'test' ? process.env.TZ : moment.tz.guess()
}
diff --git a/lib/util/ui.js b/lib/util/ui.js
index bf5be4753..c430cc04e 100644
--- a/lib/util/ui.js
+++ b/lib/util/ui.js
@@ -1,3 +1,5 @@
+import bowser from 'bowser'
+
import { MainPanelContent } from '../actions/ui'
import { summarizeQuery } from './query'
import { getActiveSearch } from './state'
@@ -10,6 +12,13 @@ export function isMobile () {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
}
+/**
+ * Returns true if the user is using a [redacted] browser
+ */
+export function isIE () {
+ return bowser.name === 'Internet Explorer'
+}
+
/**
* Enables scrolling for a specified selector, while disabling scrolling for all
* other targets. This is adapted from https://stackoverflow.com/a/41601290/915811
diff --git a/package.json b/package.json
index 593cafb0c..ab9a43c3d 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,8 @@
"main": "build/index.js",
"scripts": {
"build": "mastarm build --env production",
- "cover": "mastarm test --coverage",
- "jest": "mastarm test",
+ "cover": "mastarm test -e test --coverage",
+ "jest": "mastarm test -e test",
"lint": "mastarm lint lib __tests__ --quiet",
"lint-docs": "documentation lint lib/**/*.js",
"prepublish": "mastarm prepublish --config configurations/prepublish",
@@ -78,11 +78,17 @@
},
"devDependencies": {
"documentation": "^12.0.3",
+ "enzyme": "^3.10.0",
+ "enzyme-adapter-react-15.4": "^1.4.0",
+ "enzyme-to-json": "^3.4.0",
"es6-math": "^1.0.0",
+ "lodash.clonedeep": "^4.5.0",
"mastarm": "^5.1.3",
"nock": "^9.0.9",
"react": "^15.4.2",
+ "react-addons-test-utils": "^15.6.2",
"react-dom": "^15.4.2",
+ "redux-mock-store": "^1.5.3",
"semantic-release": "^15.13.12"
},
"peerDependencies": {
@@ -90,7 +96,8 @@
"react-dom": ">=15.0.0"
},
"jest": {
- "setupTestFrameworkScriptFile": "/__tests__/test-utils/setup.js",
+ "globalSetup": "/__tests__/test-utils/global-setup.js",
+ "setupFilesAfterEnv": ["/__tests__/test-utils/setup-env.js"],
"testURL": "http://localhost/"
}
}
diff --git a/yarn.lock b/yarn.lock
index 0795b5b10..aa5611c7b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1537,7 +1537,30 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^3.2.0"
-airbnb-prop-types@^2.1.0:
+airbnb-js-shims@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-2.2.0.tgz#46e1d9d9516f704ef736de76a3b6d484df9a96d8"
+ integrity sha512-pcSQf1+Kx7/0ibRmxj6rmMYc5V8SHlKu+rkQ80h0bjSLDaIxHg/3PiiFJi4A9mDc01CoBHoc8Fls2G/W0/+s5g==
+ dependencies:
+ array-includes "^3.0.3"
+ array.prototype.flat "^1.2.1"
+ array.prototype.flatmap "^1.2.1"
+ es5-shim "^4.5.13"
+ es6-shim "^0.35.5"
+ function.prototype.name "^1.1.0"
+ globalthis "^1.0.0"
+ object.entries "^1.1.0"
+ object.fromentries "^2.0.0 || ^1.0.0"
+ object.getownpropertydescriptors "^2.0.3"
+ object.values "^1.1.0"
+ promise.allsettled "^1.0.0"
+ promise.prototype.finally "^3.1.0"
+ string.prototype.matchall "^3.0.1"
+ string.prototype.padend "^3.0.0"
+ string.prototype.padstart "^3.0.0"
+ symbol.prototype.description "^1.0.0"
+
+airbnb-prop-types@^2.1.0, airbnb-prop-types@^2.13.2:
version "2.14.0"
resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.14.0.tgz#3d45cb1459f4ce78fdf1240563d1aa2315391168"
integrity sha512-Yb09vUkr3KP9r9NqfRuYtDYZG76wt8mhTUi2Vfzsghk+qkg01/gOc9NU8n63ZcMCLzpAdMEXyKjCHlxV62yN1A==
@@ -1612,7 +1635,7 @@ ansi-regex@^0.2.0, ansi-regex@^0.2.1:
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
integrity sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=
-ansi-regex@^2.0.0:
+ansi-regex@^2.0.0, ansi-regex@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
@@ -1760,6 +1783,11 @@ array-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
+array-filter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
+ integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
+
array-filter@~0.0.0:
version "0.0.1"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
@@ -1821,6 +1849,24 @@ array.prototype.find@^2.1.0:
define-properties "^1.1.3"
es-abstract "^1.13.0"
+array.prototype.flat@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+ integrity sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
+array.prototype.flatmap@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.1.tgz#3103cd4826ef90019c9b0a4839b2535fa6faf4e9"
+ integrity sha512-i18e2APdsiezkcqDyZor78Pbfjfds3S94dG6dgIV2ZASJaUf1N0dz2tGdrmwrmlZuNUgxH+wz6Z0zYVH2c5xzQ==
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.10.0"
+ function-bind "^1.1.1"
+
arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@@ -3536,6 +3582,18 @@ check-error@^1.0.2:
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
+cheerio@^1.0.0-rc.2:
+ version "1.0.0-rc.3"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
+ integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.1"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
chokidar@^1.6.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
@@ -3633,6 +3691,18 @@ cli-boxes@^1.0.0:
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM=
+cli-color@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-1.4.0.tgz#7d10738f48526824f8fe7da51857cb0f572fe01f"
+ integrity sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==
+ dependencies:
+ ansi-regex "^2.1.1"
+ d "1"
+ es5-ext "^0.10.46"
+ es6-iterator "^2.0.3"
+ memoizee "^0.4.14"
+ timers-ext "^0.1.5"
+
cli-columns@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/cli-columns/-/cli-columns-3.1.2.tgz#6732d972979efc2ae444a1f08e08fa139c96a18e"
@@ -3885,7 +3955,7 @@ comma-separated-tokens@^1.0.1:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz#419cd7fb3258b1ed838dc0953167a25e152f5b59"
integrity sha512-Jrx3xsP4pPv4AwJUDWY9wOXGtwPXARej6Xd99h4TUGotmf8APuquKMpK+dnD3UgyxK7OEWaisjZz+3b5jtL6xQ==
-commander@^2.11.0, commander@^2.19.0, commander@~2.20.0:
+commander@^2.11.0, commander@^2.19.0, commander@^2.9.0, commander@~2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
@@ -4350,6 +4420,16 @@ css-select@^2.0.0:
domutils "^1.7.0"
nth-check "^1.0.2"
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
css-selector-tokenizer@^0.5.1:
version "0.5.4"
resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.5.4.tgz#139bafd34a35fd0c1428487049e0699e6f6a2c21"
@@ -4388,7 +4468,7 @@ css-unit-converter@^1.1.1:
resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
-css-what@^2.1.2:
+css-what@2.1, css-what@^2.1.2:
version "2.1.3"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
@@ -4641,6 +4721,14 @@ d3@^3.5.8:
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=
+d@1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+ integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+ dependencies:
+ es5-ext "^0.10.50"
+ type "^1.0.1"
+
damerau-levenshtein@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz#780cf7144eb2e8dbd1c3bb83ae31100ccc31a414"
@@ -4952,6 +5040,11 @@ dir-glob@^3.0.0, dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+ integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=
+
dlv@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
@@ -5064,7 +5157,7 @@ dom-helpers@^3.2.0, dom-helpers@^3.2.1, dom-helpers@^3.4.0:
dependencies:
"@babel/runtime" "^7.1.2"
-dom-serializer@0:
+dom-serializer@0, dom-serializer@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
@@ -5077,7 +5170,7 @@ domain-browser@^1.1.1, domain-browser@^1.2.0:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
-domelementtype@1, domelementtype@^1.3.0:
+domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@@ -5089,7 +5182,22 @@ domexception@^1.0.1:
dependencies:
webidl-conversions "^4.0.2"
-domutils@^1.7.0:
+domhandler@^2.3.0:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
+ integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1, domutils@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
@@ -5223,7 +5331,7 @@ enhanced-resolve@^3.3.0:
object-assign "^4.0.1"
tapable "^0.2.7"
-entities@^1.1.1:
+entities@^1.1.1, entities@~1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
@@ -5249,6 +5357,77 @@ envify@^4.1.0:
esprima "^4.0.0"
through "~2.3.4"
+enzyme-adapter-react-15.4@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-15.4/-/enzyme-adapter-react-15.4-1.4.0.tgz#a50306ecc429fe9a863902b70743dddd085afb72"
+ integrity sha512-FXMAEj9FwBjeAUXA5EIk2VIueRdpDdGXyva1FzJmg2a2U2xtxRJvOGwHR87QPix9+WIjVi3GLrsFVuVK8an/nw==
+ dependencies:
+ enzyme-adapter-react-helper "^1.3.2"
+ enzyme-adapter-utils "^1.10.1"
+ object.assign "^4.1.0"
+ object.values "^1.1.0"
+ prop-types "^15.7.2"
+ react-is "^16.8.6"
+
+enzyme-adapter-react-helper@^1.3.2:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-helper/-/enzyme-adapter-react-helper-1.3.6.tgz#534a723e3bce23093862d16525883274cf34f6de"
+ integrity sha512-UFnpXMeUd6aX8WK6RZ+vGbnjYQLk7/XQ1o/vcOQ2rKyY36l9IegMH2abyiHnsl9brmfrdOGrzOhX+WozQs+Krw==
+ dependencies:
+ airbnb-js-shims "^2.2.0"
+ install-peerdeps "^1.10.2"
+ npm-run "^5.0.1"
+ object.assign "^4.1.0"
+ object.getownpropertydescriptors "^2.0.3"
+ rimraf "^2.6.3"
+ semver "^5.7.0"
+
+enzyme-adapter-utils@^1.10.1:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93"
+ integrity sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA==
+ dependencies:
+ airbnb-prop-types "^2.13.2"
+ function.prototype.name "^1.1.0"
+ object.assign "^4.1.0"
+ object.fromentries "^2.0.0"
+ prop-types "^15.7.2"
+ semver "^5.6.0"
+
+enzyme-to-json@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.0.tgz#2b6330a784a57ba68298e3c0d6cef17ee4fedc0e"
+ integrity sha512-gbu8P8PMAtb+qtKuGVRdZIYxWHC03q1dGS3EKRmUzmTDIracu3o6cQ0d4xI2YWojbelbxjYOsmqM5EgAL0WgIA==
+ dependencies:
+ lodash "^4.17.12"
+
+enzyme@^3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.10.0.tgz#7218e347c4a7746e133f8e964aada4a3523452f6"
+ integrity sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg==
+ dependencies:
+ array.prototype.flat "^1.2.1"
+ cheerio "^1.0.0-rc.2"
+ function.prototype.name "^1.1.0"
+ has "^1.0.3"
+ html-element-map "^1.0.0"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.4"
+ is-number-object "^1.0.3"
+ is-regex "^1.0.4"
+ is-string "^1.0.4"
+ is-subset "^0.1.1"
+ lodash.escape "^4.0.1"
+ lodash.isequal "^4.5.0"
+ object-inspect "^1.6.0"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+ object.values "^1.0.4"
+ raf "^3.4.0"
+ rst-selector-parser "^2.2.3"
+ string.prototype.trim "^1.1.2"
+
err-code@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
@@ -5281,7 +5460,7 @@ errorify@^0.3.1:
resolved "https://registry.yarnpkg.com/errorify/-/errorify-0.3.1.tgz#53e0aaeeb18adc3e55f9f1eb4e2d95929f41b79b"
integrity sha1-U+Cq7rGK3D5V+fHrTi2Vkp9Bt5s=
-es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.5.1, es-abstract@^1.7.0:
+es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.4.3, es-abstract@^1.5.1, es-abstract@^1.7.0, es-abstract@^1.9.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
@@ -5302,6 +5481,29 @@ es-to-primitive@^1.2.0:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
+es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
+ version "0.10.50"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778"
+ integrity sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==
+ dependencies:
+ es6-iterator "~2.0.3"
+ es6-symbol "~3.1.1"
+ next-tick "^1.0.0"
+
+es5-shim@^4.5.13:
+ version "4.5.13"
+ resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.13.tgz#5d88062de049f8969f83783f4a4884395f21d28b"
+ integrity sha512-xi6hh6gsvDE0MaW4Vp1lgNEBpVcCXRWfPXj5egDvtgLz4L9MEvNwYEMdJH+JJinWkwa8c3c3o5HduV7dB/e1Hw==
+
+es6-iterator@^2.0.3, es6-iterator@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+ integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+ dependencies:
+ d "1"
+ es5-ext "^0.10.35"
+ es6-symbol "^3.1.1"
+
es6-math@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es6-math/-/es6-math-1.0.0.tgz#5eac891860c2024b728e7122444df388e1d8177a"
@@ -5324,6 +5526,29 @@ es6-promisify@^6.0.0:
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.0.1.tgz#6edaa45f3bd570ffe08febce66f7116be4b1cdb6"
integrity sha512-J3ZkwbEnnO+fGAKrjVpeUAnZshAdfZvbhQpqfIH9kSAspReRC4nJnu8ewm55b4y9ElyeuhCTzJD0XiH8Tsbhlw==
+es6-shim@^0.35.5:
+ version "0.35.5"
+ resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.5.tgz#46f59dc0a84a1c5029e8ff1166ca0a902077a9ab"
+ integrity sha512-E9kK/bjtCQRpN1K28Xh4BlmP8egvZBGJJ+9GtnzOwt7mdqtrjHFuVGr7QJfdjBIKqrlU5duPf3pCBoDrkjVYFg==
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53"
+ integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==
+ dependencies:
+ d "1"
+ es5-ext "^0.10.46"
+ es6-iterator "^2.0.3"
+ es6-symbol "^3.1.1"
+
escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -5602,6 +5827,14 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+event-emitter@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
events@1.1.1, events@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@@ -6199,12 +6432,12 @@ fsevents@^1.0.0, fsevents@^1.2.7:
nan "^2.12.1"
node-pre-gyp "^0.12.0"
-function-bind@^1.1.1:
+function-bind@^1.0.2, function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-function.prototype.name@^1.1.1:
+function.prototype.name@^1.1.0, function.prototype.name@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.1.tgz#6d252350803085abc2ad423d4fe3be2f9cbda392"
integrity sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q==
@@ -6506,6 +6739,15 @@ globals@^9.18.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
+globalthis@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.0.tgz#c5fb98213a9b4595f59cf3e7074f141b4169daae"
+ integrity sha512-vcCAZTJ3r5Qcu5l8/2oyVdoFwxKgfYnMTR2vwWeux/NAVZK3PwcMaWkdUIn4GJbmKuRK7xcvDsLuK+CKcXyodg==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ object-keys "^1.0.12"
+
globby@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22"
@@ -6790,6 +7032,13 @@ html-comment-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
+html-element-map@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/html-element-map/-/html-element-map-1.1.0.tgz#e5aab9a834caf883b421f8bd9eaedcaac887d63c"
+ integrity sha512-iqiG3dTZmy+uUaTmHarTL+3/A2VW9ox/9uasKEZC+R/wAtUrTcRlXPSaPqsnWPfIu8wqn09jQNwMRqzL54jSYA==
+ dependencies:
+ array-filter "^1.0.0"
+
html-encoding-sniffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
@@ -6807,6 +7056,18 @@ htmlescape@^1.1.0:
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=
+htmlparser2@^3.9.1:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
+ integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
+ dependencies:
+ domelementtype "^1.3.1"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^3.1.1"
+
http-cache-semantics@^3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2"
@@ -7102,6 +7363,20 @@ insert-module-globals@^7.0.0:
undeclared-identifiers "^1.1.2"
xtend "^4.0.0"
+install-peerdeps@^1.10.2:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/install-peerdeps/-/install-peerdeps-1.10.2.tgz#b244563d1ead9229d9520729dbb015122e0382c5"
+ integrity sha512-cnEy9kfGB8SlxyQFGa7+uKk+PvrD/NTKjn5Emg/+kKBIF7AXxFgTWOHwUhQNbZnauuPuVCtZnap6d3ADYis8FA==
+ dependencies:
+ babel-polyfill "^6.26.0"
+ cli-color "^1.2.0"
+ commander "^2.11.0"
+ https-proxy-agent "^2.2.1"
+ promptly "^2.1.0"
+ request "^2.83.0"
+ request-promise-native "^1.0.5"
+ semver "^5.5.0"
+
internal-ip@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-3.0.1.tgz#df5c99876e1d2eb2ea2d74f520e3f669a00ece27"
@@ -7217,6 +7492,11 @@ is-binary-path@^1.0.0:
dependencies:
binary-extensions "^1.0.0"
+is-boolean-object@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+ integrity sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=
+
is-buffer@^1.1.0, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -7419,6 +7699,11 @@ is-npm@^1.0.0:
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ=
+is-number-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+ integrity sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=
+
is-number@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@@ -7484,7 +7769,7 @@ is-primitive@^2.0.0:
resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
-is-promise@^2.1.0:
+is-promise@^2.1, is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
@@ -7530,6 +7815,11 @@ is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0, is-stream@~1.1.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+is-string@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+ integrity sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=
+
is-subset@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
@@ -8697,11 +8987,21 @@ lodash.deburr@^3.0.0:
dependencies:
lodash._root "^3.0.0"
+lodash.escape@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
+ integrity sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=
+
lodash.escaperegexp@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"
integrity sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+ integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
+
lodash.foreach@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
@@ -8909,6 +9209,13 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
+lru-queue@0.1:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
+ integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=
+ dependencies:
+ es5-ext "~0.10.2"
+
macos-release@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
@@ -9249,6 +9556,20 @@ mem@^4.0.0:
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
+memoizee@^0.4.14:
+ version "0.4.14"
+ resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57"
+ integrity sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==
+ dependencies:
+ d "1"
+ es5-ext "^0.10.45"
+ es6-weak-map "^2.0.2"
+ event-emitter "^0.3.5"
+ is-promise "^2.1"
+ lru-queue "0.1"
+ next-tick "1"
+ timers-ext "^0.1.5"
+
memory-fs@^0.4.0, memory-fs@~0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@@ -9563,6 +9884,11 @@ moment-timezone@^0.5.23:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+moo@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
+ integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -9627,6 +9953,17 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+nearley@^2.7.10:
+ version "2.18.0"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.18.0.tgz#a9193612dd6d528a2e47e743b1dc694cfe105223"
+ integrity sha512-/zQOMCeJcioI0xJtd5RpBiWw2WP7wLe6vq8/3Yu0rEwgus/G/+pViX80oA87JdVgjRt2895mZSv2VfZmy4W1uw==
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.4.3"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+ semver "^5.4.1"
+
needle@^2.2.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c"
@@ -9646,6 +9983,11 @@ nerf-dart@^1.0.0:
resolved "https://registry.yarnpkg.com/nerf-dart/-/nerf-dart-1.0.0.tgz#e6dab7febf5ad816ea81cf5c629c5a0ebde72c1a"
integrity sha1-5tq3/r9a2Bbqgc9cYpxaDr3nLBo=
+next-tick@1, next-tick@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+ integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -9916,6 +10258,13 @@ npm-packlist@^1.1.12, npm-packlist@^1.1.6, npm-packlist@^1.4.4:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
+npm-path@^2.0.2, npm-path@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64"
+ integrity sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==
+ dependencies:
+ which "^1.2.10"
+
npm-pick-manifest@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40"
@@ -9953,11 +10302,30 @@ npm-run-path@^2.0.0:
dependencies:
path-key "^2.0.0"
+npm-run@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/npm-run/-/npm-run-5.0.1.tgz#1baea93389b50ae25a32382c8ca322398e50cd16"
+ integrity sha512-s7FyRpHUgaJfzkRgOnevX8rAWWsv1dofY1XS7hliWCF6LSQh+HtDfBvpigPS1krLvXw+Fi17CYMY8mUtblnyWw==
+ dependencies:
+ minimist "^1.2.0"
+ npm-path "^2.0.4"
+ npm-which "^3.0.1"
+ serializerr "^1.0.3"
+
npm-user-validate@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.0.tgz#8ceca0f5cea04d4e93519ef72d0557a75122e951"
integrity sha1-jOyg9c6gTU6TUZ73LQVXp1Ei6VE=
+npm-which@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa"
+ integrity sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=
+ dependencies:
+ commander "^2.9.0"
+ npm-path "^2.0.2"
+ which "^1.2.10"
+
npm@^6.8.0:
version "6.10.2"
resolved "https://registry.yarnpkg.com/npm/-/npm-6.10.2.tgz#62cd56f9bc39e26a5eae411a20236bb0c2026d85"
@@ -10089,7 +10457,7 @@ npm@^6.8.0:
gauge "~2.7.3"
set-blocking "~2.0.0"
-nth-check@^1.0.2:
+nth-check@^1.0.2, nth-check@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
@@ -10135,6 +10503,11 @@ object-hash@^1.3.1:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
+object-inspect@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
+ integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==
+
object-is@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
@@ -10167,7 +10540,7 @@ object.assign@^4.0.4, object.assign@^4.1.0:
has-symbols "^1.0.0"
object-keys "^1.0.11"
-object.entries@^1.1.0:
+object.entries@^1.0.4, object.entries@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519"
integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==
@@ -10177,7 +10550,7 @@ object.entries@^1.1.0:
function-bind "^1.1.1"
has "^1.0.3"
-object.fromentries@^2.0.0:
+object.fromentries@^2.0.0, "object.fromentries@^2.0.0 || ^1.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.0.tgz#49a543d92151f8277b3ac9600f1e930b189d30ab"
integrity sha512-9iLiI6H083uiqUuvzyY6qrlmc/Gz8hLQFOcb/Ri/0xXFkSNS3ctV+CbE6yM2+AnkYfOB3dGjdzC0wrMLIhQICA==
@@ -10210,7 +10583,7 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
-object.values@^1.1.0:
+object.values@^1.0.4, object.values@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
@@ -10669,6 +11042,13 @@ parse5@4.0.0:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
+ dependencies:
+ "@types/node" "*"
+
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -11891,6 +12271,24 @@ promise-retry@^1.1.1:
err-code "^1.0.0"
retry "^0.10.0"
+promise.allsettled@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.1.tgz#afe4bfcc13b26e2263a97a7fbbb19b8ca6eb619c"
+ integrity sha512-3ST7RS7TY3TYLOIe+OACZFvcWVe1osbgz2x07nTb446pa3t4GUZWidMDzQ4zf9jC2l6mRa1/3X81icFYbi+D/g==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.13.0"
+ function-bind "^1.1.1"
+
+promise.prototype.finally@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.0.tgz#66f161b1643636e50e7cf201dc1b84a857f3864e"
+ integrity sha512-7p/K2f6dI+dM8yjRQEGrTQs5hTQixUAdOGpMEA3+pVxpX5oHKRSKAXyLw9Q9HUWDTdwtoo39dSHGQtN90HcEwQ==
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.9.0"
+ function-bind "^1.1.1"
+
promise@^7.1.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
@@ -11898,6 +12296,13 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
+promptly@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/promptly/-/promptly-2.2.0.tgz#2a13fa063688a2a5983b161fff0108a07d26fc74"
+ integrity sha1-KhP6BjaIoqWYOxYf/wEIoH0m/HQ=
+ dependencies:
+ read "^1.0.4"
+
prompts@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.1.0.tgz#bf90bc71f6065d255ea2bdc0fe6520485c1b45db"
@@ -11948,6 +12353,11 @@ proto-list@~1.2.1:
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
+protochain@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/protochain/-/protochain-1.0.5.tgz#991c407e99de264aadf8f81504b5e7faf7bfa260"
+ integrity sha1-mRxAfpneJkqt+PgVBLXn+ve/omA=
+
protocols@^1.1.0, protocols@^1.4.0:
version "1.4.7"
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32"
@@ -12089,6 +12499,26 @@ qw@~1.0.1:
resolved "https://registry.yarnpkg.com/qw/-/qw-1.0.1.tgz#efbfdc740f9ad054304426acb183412cc8b996d4"
integrity sha1-77/cdA+a0FQwRCassYNBLMi5ltQ=
+raf@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+ integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+ integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
randomatic@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
@@ -12144,6 +12574,11 @@ react-addons-shallow-compare@^15.4.1, react-addons-shallow-compare@^15.4.2:
fbjs "^0.8.4"
object-assign "^4.1.0"
+react-addons-test-utils@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156"
+ integrity sha1-wStu/cIkfBDae4dw0YUICnsEcVY=
+
react-bootstrap@^0.30.7:
version "0.30.10"
resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.30.10.tgz#dbba6909595f2af4d91937db0f96ec8c2df2d1a8"
@@ -12469,7 +12904,7 @@ read-pkg@^5.0.0, read-pkg@^5.1.1:
parse-json "^5.0.0"
type-fest "^0.6.0"
-read@1, read@~1.0.1, read@~1.0.7:
+read@1, read@^1.0.4, read@~1.0.1, read@~1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=
@@ -12489,7 +12924,7 @@ read@1, read@~1.0.1, read@~1.0.7:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
-"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.4.0:
+"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
@@ -12628,6 +13063,13 @@ redux-logger@^2.7.4:
dependencies:
deep-diff "0.3.4"
+redux-mock-store@^1.5.3:
+ version "1.5.3"
+ resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d"
+ integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==
+ dependencies:
+ lodash.isplainobject "^4.0.6"
+
redux-thunk@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
@@ -12711,6 +13153,13 @@ regexp-tree@^0.1.6:
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.11.tgz#c9c7f00fcf722e0a56c7390983a7a63dd6c272f3"
integrity sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg==
+regexp.prototype.flags@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c"
+ integrity sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==
+ dependencies:
+ define-properties "^1.1.2"
+
regexpp@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
@@ -12958,7 +13407,7 @@ request-promise-native@^1.0.5:
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
-request@^2.72.0, request@^2.74.0, request@^2.87.0, request@^2.88.0:
+request@^2.72.0, request@^2.74.0, request@^2.83.0, request@^2.87.0, request@^2.88.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
@@ -13166,6 +13615,14 @@ rounded-rect@^0.0.1:
resolved "https://registry.yarnpkg.com/rounded-rect/-/rounded-rect-0.0.1.tgz#47d7f0356c6d893a2f6f057e379686b25190e0ac"
integrity sha1-R9fwNWxtiTovbwV+N5aGslGQ4Kw=
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ integrity sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
rsvp@^4.8.4:
version "4.8.5"
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
@@ -13337,6 +13794,13 @@ send@0.17.1:
range-parser "~1.2.1"
statuses "~1.5.0"
+serializerr@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91"
+ integrity sha1-EtTFqhw/+49tHcXzlaqUVVacP5E=
+ dependencies:
+ protochain "^1.0.5"
+
serve-static@^1.10.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
@@ -13906,6 +14370,44 @@ string-width@^4.0.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^5.2.0"
+string.prototype.matchall@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-3.0.1.tgz#5a9e0b64bcbeb336aa4814820237c2006985646d"
+ integrity sha512-NSiU0ILQr9PQ1SZmM1X327U5LsM+KfDTassJfqN1al1+0iNpKzmQ4BfXOJwRnTEqv8nKJ67mFpqRoPaGWwvy5A==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ regexp.prototype.flags "^1.2.0"
+
+string.prototype.padend@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
+ integrity sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.4.3"
+ function-bind "^1.0.2"
+
+string.prototype.padstart@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.padstart/-/string.prototype.padstart-3.0.0.tgz#5bcfad39f4649bb2d031292e19bcf0b510d4b242"
+ integrity sha1-W8+tOfRkm7LQMSkuGbzwtRDUskI=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.4.3"
+ function-bind "^1.0.2"
+
+string.prototype.trim@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz#75a729b10cfc1be439543dae442129459ce61e3d"
+ integrity sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.13.0"
+ function-bind "^1.1.1"
+
string_decoder@0.10, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -14109,6 +14611,13 @@ symbol-tree@^3.2.2:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+symbol.prototype.description@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/symbol.prototype.description/-/symbol.prototype.description-1.0.0.tgz#6e355660eb1e44ca8ad53a68fdb72ef131ca4b12"
+ integrity sha512-I9mrbZ5M96s7QeJDv95toF1svkUjeBybe8ydhY7foPaBmr0SPJMFupArmMkDrOKTTj0sJVr+nvQNxWLziQ7nDQ==
+ dependencies:
+ has-symbols "^1.0.0"
+
syntax-error@^1.1.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c"
@@ -14271,6 +14780,14 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
+timers-ext@^0.1.5:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6"
+ integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
+ dependencies:
+ es5-ext "~0.10.46"
+ next-tick "1"
+
timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
@@ -14566,6 +15083,11 @@ type-fest@^0.6.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
+type@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/type/-/type-1.0.1.tgz#084c9a17fcc9151a2cdb1459905c2e45e4bb7d61"
+ integrity sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==
+
typedarray@^0.0.6, typedarray@~0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -15391,7 +15913,7 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
-which@1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
+which@1, which@^1.2.10, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==