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`] = ` + + + +
+
+
+ + +
+
+ + W Burnside & SW 18th + +
+
+
+
+
+
+ + Stop ID + + : + 9860 + +
+ + Plan a trip: + + + + + + + + + | + + + + + + +
+
+ +
+
+
+ + 20 + + To + Gresham TC +
+
+
+
+ + + + + +
+
+
+ 52 min +
+
+
+
+
+ +
+
+ + +
+ + +
+ +
+
+ +
+
+
+ + + +`; + +exports[`components > viewers > stop viewer should render countdown times for stop times departing 48+ hours from start of service 1`] = ` + + + +
+
+
+ + +
+
+ + W Burnside & SW 18th + +
+
+
+
+
+
+ + Stop ID + + : + 9860 + +
+ + Plan a trip: + + + + + + + + + | + + + + + + +
+
+ +
+
+
+ + 20 + + To + Gresham TC +
+
+
+
+ + + + + +
+
+
+ 52 min +
+
+
+
+
+ +
+
+ + +
+ + +
+ +
+
+ +
+
+
+ + + +`; + +exports[`components > viewers > stop viewer should render times after midnight with the correct day of week 1`] = ` + + + +
+
+
+ + +
+
+ + W Burnside & SW 18th + +
+
+
+
+
+
+ + Stop ID + + : + 9860 + +
+ + Plan a trip: + + + + + + + + + | + + + + + + +
+
+ +
+
+
+ + 20 + + To + Gresham TC +
+
+
+
+ + + + + +
+
+
+ Thursday +
+
+ 00:51 +
+
+
+
+
+ +
+
+ + +
+ + +
+ +
+
+ +
+
+
+ + + +`; + +exports[`components > viewers > stop viewer should render with OTP transit index data 1`] = ` + + + +
+
+
+ + +
+
+ + W Burnside & SW 8th + +
+
+
+
+
+
+ + Stop ID + + : + 715 + +
+ + Plan a trip: + + + + + + + + + | + + + + + + +
+
+ +
+
+
+ + 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 +
+
+
+
+
+ +
+
+ + +
+ + +
+ +
+
+ +
+
+
+ + + +`; + +exports[`components > viewers > stop viewer should render with TriMet transit index data 1`] = ` + + + +
+
+
+ + +
+
+ + W Burnside & SW 8th + +
+
+
+
+
+
+ + Stop ID + + : + 715 + +
+ + Plan a trip: + + + + + + + + + | + + + + + + +
+
+ +
+
+
+ + 20 + + To + Gresham TC +
+
+
+
+ + + + + +
+
+
+ Monday +
+
+ 17:45 +
+
+
+
+
+ +
+
+ + +
+ + +
+ +
+
+ +
+
+
+ + + +`; + +exports[`components > viewers > stop viewer should render with initial stop id and no stop times 1`] = ` + + + +
+
+
+ + +
+
+ + 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}> -
-
-
{title}
-
+ {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 + : ( +
+
+
+ {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==