Files
doormile_milderapp/test/route_flow_test.dart
2026-08-11 13:16:33 +05:30

162 lines
5.9 KiB
Dart

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 = <RouteFlowPhase>[];
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);
});
});
}