import 'package:flutter_test/flutter_test.dart'; import 'package:miler/views/Dashboard/pickups/pickups.dart'; /// The route flow on the stop map used to be a comet: a window covering 18% of /// the route, sliding from one end to the other. At any moment the rider saw a /// short white dash somewhere in the middle of his journey, which reads as a /// decoration on the map rather than as a statement about his trip — and on a /// long route the dash never visibly connected the two ends at all. /// /// It is now one continuous stream: white grows from the rider's own position to /// the stop, holds, then drains back down the line to him, forever. The map, the /// ticker and the live route make that untestable as a widget — but the claims /// are claims about numbers, and these are the numbers. void main() { // Representative shares: a ~2.4s grow, a beat, a ~0.95s drain. const grow = 0.66; const hold = 0.07; ({RouteFlowPhase phase, double progress}) at(double t) => routeFlowFrame(t, growShare: grow, holdShare: hold); group('the cycle', () { test('starts at the rider, with nothing drawn ahead of him', () { final f = at(0); expect(f.phase, RouteFlowPhase.grow); // Head at 0 of the route's length: the stream begins exactly at the // rider's own position, not somewhere along the line. expect(f.progress, 0); }); test('grows at a constant rate all the way to the stop', () { // Linear, not eased. The draw-in is eased because it is an entrance; this // is travel, and an eased stream appears to slow down in the middle of the // road for no reason. double head(double t) => at(t).progress; final quarter = head(grow * 0.25); final half = head(grow * 0.5); final threeQuarters = head(grow * 0.75); expect(quarter, closeTo(0.25, 1e-9)); expect(half, closeTo(0.5, 1e-9)); expect(threeQuarters, closeTo(0.75, 1e-9)); // Equal time, equal distance — at every point in the phase. expect(half - quarter, closeTo(threeQuarters - half, 1e-9)); }); test('reaches the destination exactly at the end of the grow', () { expect(at(grow).phase, RouteFlowPhase.grow); expect(at(grow).progress, closeTo(1.0, 1e-9)); }); test('holds at full length before draining', () { final f = at(grow + hold / 2); expect(f.phase, RouteFlowPhase.hold); }); test('drains for the rest of the cycle, and is finished at the end', () { expect(at(grow + hold + 0.001).phase, RouteFlowPhase.fade); final end = at(1); expect(end.phase, RouteFlowPhase.fade); expect(end.progress, closeTo(1.0, 1e-9)); }); test('the phases tile the cycle with no gap and no overlap', () { // A gap is a frame with nothing drawn; an overlap is a frame drawn twice. // Either shows up as a stutter at the seam. RouteFlowPhase? previous; final seen = []; for (var i = 0; i <= 1000; i++) { final phase = at(i / 1000).phase; if (phase != previous) seen.add(phase); previous = phase; } expect(seen, [ RouteFlowPhase.grow, RouteFlowPhase.hold, RouteFlowPhase.fade, ]); }); }); group('the drain', () { test('the line is whole the instant the fade begins', () { // Not already clipped at the far end — which is what made the old loop // appear to restart before it had arrived. for (final x in kRouteFlowStops) { expect(routeFlowFadeAlpha(0, x), closeTo(1.0, 1e-9)); } }); test('nothing is left when the fade ends', () { for (final x in kRouteFlowStops) { expect(routeFlowFadeAlpha(1, x), 0); } }); test('it drains from the destination back towards the rider', () { // At any partial moment the stop end is at most as bright as the rider // end: the front travels backwards down the route, it does not dissolve // the whole line at once. for (var i = 1; i < 10; i++) { final f = i / 10; final atRider = routeFlowFadeAlpha(f, 0); final atStop = routeFlowFadeAlpha(f, 1); expect( atStop, lessThanOrEqualTo(atRider + 1e-9), reason: 'at f=$f the stop end should lead the fade', ); } // And the rider's end is the last thing still lit. expect(routeFlowFadeAlpha(0.6, 1), 0); expect(routeFlowFadeAlpha(0.6, 0), greaterThan(0)); }); test('it is soft, not a wipe', () { // Mid-drain there is a ramp rather than a hard edge: at least one stop // sits strictly between lit and dark, which is the whole difference // between a fade and a shutter. final partial = [ for (final x in kRouteFlowStops) routeFlowFadeAlpha(0.45, x), ]; expect(partial.any((a) => a > 0.01 && a < 0.99), isTrue); }); test('every point only ever gets dimmer', () { for (final x in kRouteFlowStops) { var previous = double.infinity; for (var i = 0; i <= 100; i++) { final a = routeFlowFadeAlpha(i / 100, x); expect(a, lessThanOrEqualTo(previous + 1e-9)); previous = a; } } }); }); group('degenerate shares', () { test('a cycle with no hold still tiles', () { final f = routeFlowFrame(0.9, growShare: 0.8, holdShare: 0); expect(f.phase, RouteFlowPhase.fade); expect(f.progress, closeTo(0.5, 1e-9)); }); test('a cycle with no grow starts already arrived', () { final f = routeFlowFrame(0, growShare: 0, holdShare: 0.1); expect(f.phase, RouteFlowPhase.hold); }); test('t outside 0..1 is clamped rather than extrapolated', () { expect(routeFlowFrame(-1, growShare: grow, holdShare: hold).progress, 0); expect(routeFlowFrame(2, growShare: grow, holdShare: hold).phase, RouteFlowPhase.fade); expect(routeFlowFadeAlpha(-1, 0.5), 1); expect(routeFlowFadeAlpha(9, 0.5), 0); }); }); }