162 lines
5.9 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|