Three fixes found by running the app on a real handset against production. 1. An expired token left the app looking signed in and unable to work. MilerApi.onUnauthorized was declared and called on every 401 but never assigned, so the token was dropped and nothing else happened: the profile stayed on disk, logged_out stayed false, and the rider saw his own name over a dashboard whose every call returned 401. He reads that as "no work today". The teardown now lives in endSession() and both ways out of a session — the Log out button and the 401 path — use it. 2. Arrived was written locally even when the rider was not there. updateArrivedStatus answers false for three different things and the caller treated all of them as "the write did not land", which is only true of one. A geofence refusal and a server refusal now stop the rung and hand back the reason; a dead network still advances, as it should. 3. A multi-destination customer pickup collapsed onto one stop. GET /miler/bookings returns a row per destination once collected, all with the same bookingid and reference. Every local store keys on that id, so the accepted store deduped two of three drops away and their consignment ids were unrecoverable. orderid is now the stop key; bookingreference stays the booking's name. Cards show "Stop 2 of 3" and the receiver's own name and number rather than the sender's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
589 lines
22 KiB
Dart
589 lines
22 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'package:miler/controllers/pickups_controller.dart';
|
|
import 'package:miler/data/geofence.dart';
|
|
import 'package:miler/data/miler_api.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// THE PROXIMITY FENCE
|
|
///
|
|
/// This is the control that decides whether "Picked up" means the rider was
|
|
/// standing at the door or merely pressed a button, and it has been wrong in
|
|
/// four directions inside one year: compiled off by default, set to a 10 m
|
|
/// radius no consumer GPS can resolve, failing **open** on any thrown
|
|
/// exception, and crediting the phone's own error to the rider so that a vague
|
|
/// fix bought slack rather than a refusal.
|
|
///
|
|
/// So the tests below are not a spot check of the arithmetic. They pin, in
|
|
/// order:
|
|
///
|
|
/// 1. the **switch** — on by default, because that default has flipped twice;
|
|
/// 2. the **radius** — 100 m, inclusive at the boundary;
|
|
/// 3. the **distance table** the spec asks for, 0 m to 250 m;
|
|
/// 4. every way of **failing to measure**, each of which must refuse;
|
|
/// 5. the one that matters most — **a blocked rung sends no HTTP request.**
|
|
///
|
|
/// (5) is the whole point. A fence that shows a message and posts the status
|
|
/// anyway is worse than no fence: the hub is told the rider was there *and*
|
|
/// the app claims to have checked.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/// A stop in Coimbatore. Any point would do; a real one keeps the numbers
|
|
/// recognisable when a failure prints them.
|
|
const double kTargetLat = 11.0272;
|
|
const double kTargetLng = 76.9905;
|
|
|
|
/// Metres per degree of latitude. Constant enough for a fixture — the tests
|
|
/// assert against what [Geolocator.distanceBetween] actually measures, not
|
|
/// against this, so the approximation cannot make a test pass wrongly.
|
|
const double _metresPerDegreeLat = 111320.0;
|
|
|
|
/// A rider fix [metres] due north of the stop.
|
|
Position riderAt(
|
|
double metres, {
|
|
double accuracy = 8,
|
|
Duration age = Duration.zero,
|
|
}) => Position(
|
|
latitude: kTargetLat + (metres / _metresPerDegreeLat),
|
|
longitude: kTargetLng,
|
|
timestamp: DateTime.now().subtract(age),
|
|
accuracy: accuracy,
|
|
altitude: 0,
|
|
altitudeAccuracy: 0,
|
|
heading: 0,
|
|
headingAccuracy: 0,
|
|
speed: 0,
|
|
speedAccuracy: 0,
|
|
);
|
|
|
|
void useFix(Position p) =>
|
|
Geofence.positionProvider = () async => GeofenceFix(position: p);
|
|
|
|
void useFailure(GeofenceOutcome outcome) =>
|
|
Geofence.positionProvider = () async => GeofenceFix(failure: outcome);
|
|
|
|
void main() {
|
|
tearDown(Geofence.useDevice);
|
|
|
|
// ───────────────────────────────────────────────────────────────────────
|
|
group('the switch and the radius', () {
|
|
test('enforcement is ON by default', () {
|
|
// It was `defaultValue: false`, so every release build shipped with the
|
|
// fence returning true on its third line. If this fails, either the
|
|
// default was reverted — which is the release blocker this file exists to
|
|
// prevent — or the run passed `--dart-define=ENFORCE_GEOFENCE=false`, in
|
|
// which case the failure is correct and is telling you so.
|
|
expect(
|
|
kGeofenceEnforced,
|
|
isTrue,
|
|
reason:
|
|
'Proximity enforcement must be ON by default. Either the default '
|
|
'was reverted, or this run passed ENFORCE_GEOFENCE=false.',
|
|
);
|
|
});
|
|
|
|
test('the radius is 100 metres, and there is only one of them', () {
|
|
// The app once held three: a configured `pickupradius` defaulting to 100,
|
|
// a hardcoded 500 in Home's bulk gate, and 10 in the controller. Which
|
|
// fence a rider met depended on whether he ticked boxes or slid a sheet.
|
|
expect(kGeofenceRadiusMetres, 100);
|
|
});
|
|
|
|
test('one fix may serve a batch, but only briefly', () {
|
|
// [kGeofenceFixReuse] is what makes a twenty-order bulk action cost one
|
|
// GPS settle instead of twenty. It is a performance change to a safety
|
|
// control, so the two bounds that keep it honest are asserted here.
|
|
expect(
|
|
kGeofenceFixReuse,
|
|
lessThanOrEqualTo(kGeofenceFixMaxAge),
|
|
reason:
|
|
'the reuse window must sit inside the staleness rule — a fix too '
|
|
'old to measure with cannot become acceptable by being cached',
|
|
);
|
|
// At walking pace (~1.4 m/s) the window is ~20 m of possible drift
|
|
// against a 100 m fence. Riding away covers the radius inside it, so a
|
|
// rider who genuinely leaves is refused on the next real fix rather than
|
|
// carried to the next door by the cached one.
|
|
expect(
|
|
kGeofenceFixReuse.inSeconds * 1.4,
|
|
lessThan(kGeofenceRadiusMetres / 2),
|
|
reason: 'drift during the window must stay well under the radius',
|
|
);
|
|
});
|
|
|
|
test('resetting the cache is available to sign-out and tests', () {
|
|
// A cached position is a fact about a rider. It must not outlive him on a
|
|
// shared handset.
|
|
Geofence.resetCache();
|
|
Geofence.useDevice();
|
|
});
|
|
|
|
test('a cached fix has a shelf life, and a vague one is rejected', () {
|
|
expect(kGeofenceFixMaxAge, const Duration(seconds: 30));
|
|
expect(
|
|
kGeofenceMaxAccuracyMetres,
|
|
lessThan(kGeofenceRadiusMetres),
|
|
reason:
|
|
'a fix whose error is as large as the fence cannot resolve it, and '
|
|
'must be refused rather than credited',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────────────
|
|
group('the distance table', () {
|
|
// Every case the spec names. Each asserts the *rule* against what
|
|
// Geolocator actually measured, so a fixture that drifts by a metre cannot
|
|
// quietly invert a boundary case.
|
|
for (final metres in const [0.0, 25.0, 50.0, 99.0, 100.0, 101.0, 125.0,
|
|
250.0]) {
|
|
test('${metres.round()} m', () async {
|
|
final pos = riderAt(metres);
|
|
useFix(pos);
|
|
|
|
final measured = Geofence.distanceBetween(
|
|
kTargetLat,
|
|
kTargetLng,
|
|
pos.latitude,
|
|
pos.longitude,
|
|
);
|
|
expect(
|
|
measured,
|
|
closeTo(metres, 1.0),
|
|
reason: 'fixture should sit where it claims',
|
|
);
|
|
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
|
|
final shouldAllow = measured <= kGeofenceRadiusMetres;
|
|
expect(
|
|
d.allowed,
|
|
shouldAllow,
|
|
reason:
|
|
'at ${measured.toStringAsFixed(1)} m against a '
|
|
'${kGeofenceRadiusMetres.round()} m fence',
|
|
);
|
|
expect(
|
|
d.outcome,
|
|
shouldAllow ? GeofenceOutcome.inside : GeofenceOutcome.outside,
|
|
);
|
|
expect(d.distanceMetres, closeTo(measured, 0.001));
|
|
expect(d.accuracyMetres, 8);
|
|
expect(d.timestamp, isNotNull);
|
|
});
|
|
}
|
|
|
|
test('the boundary is inclusive — 100 m is in, a hair over is out', () {
|
|
// Stated as arithmetic rather than as a fixture, because this is the one
|
|
// place a `<` for a `<=` changes a rider's day and no GPS fixture can be
|
|
// placed accurately enough to catch it.
|
|
expect(99.999 <= kGeofenceRadiusMetres, isTrue);
|
|
expect(100.0 <= kGeofenceRadiusMetres, isTrue);
|
|
expect(100.001 <= kGeofenceRadiusMetres, isFalse);
|
|
});
|
|
|
|
test('a refusal tells the rider how far, and in a unit he uses', () async {
|
|
useFix(riderAt(240));
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.reason, contains('240 m'));
|
|
expect(d.reason, contains('100 m'));
|
|
expect(d.reason, contains('arrived'));
|
|
// Never a developer error, a status code or a coordinate pair.
|
|
expect(d.reason, isNot(contains('Exception')));
|
|
expect(d.reason, isNot(contains('null')));
|
|
});
|
|
|
|
test('over a kilometre reads in kilometres', () async {
|
|
useFix(riderAt(4200));
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Delivered',
|
|
);
|
|
expect(d.reason, contains('4.2 km'));
|
|
expect(d.reason, contains('delivered'));
|
|
});
|
|
|
|
test('the verb follows the rung', () async {
|
|
useFix(riderAt(500));
|
|
for (final action in const [
|
|
'Arrived',
|
|
'Picked',
|
|
'Picked up',
|
|
'Delivery arrived',
|
|
'Delivered',
|
|
'Handed over',
|
|
]) {
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: action,
|
|
);
|
|
expect(d.allowed, isFalse, reason: action);
|
|
expect(d.reason, contains(action.toLowerCase()), reason: action);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────────────
|
|
group('every way of failing to measure is a refusal', () {
|
|
// The old implementation allowed on four of these six. That is what made
|
|
// the fence decorative: switching location off opened every rung.
|
|
|
|
test('GPS switched off', () async {
|
|
useFailure(GeofenceOutcome.serviceDisabled);
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.serviceDisabled);
|
|
expect(d.reason, contains('Location is switched off'));
|
|
});
|
|
|
|
test('permission denied', () async {
|
|
useFailure(GeofenceOutcome.permissionDenied);
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Delivered',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.permissionDenied);
|
|
expect(d.reason, contains('Allow location access'));
|
|
});
|
|
|
|
test('permission denied forever points at Settings', () async {
|
|
useFailure(GeofenceOutcome.permissionDeniedForever);
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.permissionDeniedForever);
|
|
expect(d.reason, contains('Settings'));
|
|
});
|
|
|
|
test('a timeout waiting for the fix', () async {
|
|
useFailure(GeofenceOutcome.timeout);
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.timeout);
|
|
});
|
|
|
|
test('a stale fix is not a measurement', () async {
|
|
// On a round, a stale position is reliably the *previous* stop — which is
|
|
// how a rider marks a delivery arrived from the last street.
|
|
useFailure(GeofenceOutcome.staleFix);
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.staleFix);
|
|
});
|
|
|
|
test('a fix too vague to resolve the fence is refused, not credited',
|
|
() async {
|
|
// The inversion this replaces: the old check was
|
|
// `distance - accuracy > radius`, so a ±250 m fix bought 250 m of slack.
|
|
// The worse the fix, the easier it was to pass. Here the rider is 10 m
|
|
// away — comfortably inside — and is still refused, because the phone
|
|
// cannot show that he is.
|
|
useFix(riderAt(10, accuracy: kGeofenceMaxAccuracyMetres + 1));
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.poorAccuracy);
|
|
expect(d.reason, contains('Step into the open'));
|
|
expect(d.accuracyMetres, kGeofenceMaxAccuracyMetres + 1);
|
|
});
|
|
|
|
test('a vague fix cannot pass even when it is standing on the stop',
|
|
() async {
|
|
useFix(riderAt(0, accuracy: 500));
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Delivered',
|
|
);
|
|
expect(d.allowed, isFalse, reason: 'distance 0 must not beat accuracy');
|
|
});
|
|
|
|
test('a fix at the accuracy ceiling is still usable', () async {
|
|
useFix(riderAt(20, accuracy: kGeofenceMaxAccuracyMetres));
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isTrue, reason: 'the ceiling is inclusive');
|
|
});
|
|
|
|
test('the provider throwing is a refusal, not a pass', () async {
|
|
// This is the `catch { return true; // fail-safe }` that used to open
|
|
// every rung in the app on any throw from the location stack.
|
|
Geofence.positionProvider = () async => throw Exception('platform');
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.error);
|
|
expect(d.reason, isNotNull);
|
|
expect(d.reason, isNot(contains('platform')),
|
|
reason: 'the rider must not be shown the exception');
|
|
});
|
|
|
|
test('a rider fix of 0,0 is absent, not the Gulf of Guinea', () async {
|
|
useFix(
|
|
Position(
|
|
latitude: 0,
|
|
longitude: 0,
|
|
timestamp: DateTime.now(),
|
|
accuracy: 5,
|
|
altitude: 0,
|
|
altitudeAccuracy: 0,
|
|
heading: 0,
|
|
headingAccuracy: 0,
|
|
speed: 0,
|
|
speedAccuracy: 0,
|
|
),
|
|
);
|
|
final d = await Geofence.check(
|
|
targetLat: kTargetLat,
|
|
targetLng: kTargetLng,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.noFix);
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────────────
|
|
group('a stop with no coordinates', () {
|
|
// This used to return true — "Missing coordinates. Proceeding with update."
|
|
// A booking with a blank latitude therefore opened every rung on that stop,
|
|
// which is the bypass that makes a fence decorative.
|
|
for (final pair in const [
|
|
(null, null, 'both null'),
|
|
(0.0, 0.0, 'both zero'),
|
|
(kTargetLat, 0.0, 'longitude zero'),
|
|
(0.0, kTargetLng, 'latitude zero'),
|
|
(95.0, kTargetLng, 'latitude out of range'),
|
|
(kTargetLat, 200.0, 'longitude out of range'),
|
|
]) {
|
|
test('is refused — ${pair.$3}', () async {
|
|
useFix(riderAt(0));
|
|
final d = await Geofence.check(
|
|
targetLat: pair.$1,
|
|
targetLng: pair.$2,
|
|
action: 'Arrived',
|
|
);
|
|
expect(d.allowed, isFalse);
|
|
expect(d.outcome, GeofenceOutcome.noTarget);
|
|
// The rider cannot fix the hub's data from a doorstep, so the sentence
|
|
// sends him to the people who can.
|
|
expect(d.reason, contains('office'));
|
|
});
|
|
}
|
|
|
|
test('it does not even ask the phone for a fix', () async {
|
|
// An 8-second GPS settle to answer a question with no target is time
|
|
// taken off a rider's round for nothing.
|
|
var asked = false;
|
|
Geofence.positionProvider = () async {
|
|
asked = true;
|
|
return GeofenceFix(position: riderAt(0));
|
|
};
|
|
await Geofence.check(
|
|
targetLat: null,
|
|
targetLng: null,
|
|
action: 'Arrived',
|
|
);
|
|
expect(asked, isFalse);
|
|
});
|
|
});
|
|
|
|
// ───────────────────────────────────────────────────────────────────────
|
|
group('BLOCKED MEANS NO REQUEST IS SENT', () {
|
|
// The rule the whole control rests on. Every one of these drives the real
|
|
// `PickupsController` rung with an HTTP client that fails the test if it is
|
|
// ever called — so this asserts the *call sites*, not the policy object.
|
|
|
|
late PickupsController controller;
|
|
late List<String> sent;
|
|
|
|
setUp(() {
|
|
SharedPreferences.setMockInitialValues({'userid': 38});
|
|
sent = <String>[];
|
|
MilerApi.client = MockClient((req) async {
|
|
sent.add('${req.method} ${req.url.path}');
|
|
// A realistic `reached` reply, in the shape production actually
|
|
// returns — `success` is the envelope key (`ApiConfig.toLegacyEnvelope`
|
|
// reads it; `status` is ignored), and the arrival stamp sits inside
|
|
// `data`. A bare 200 is deliberately NOT read as a transition (see
|
|
// `MilerLifecycle.reached`), so a thin fixture would fail the positive
|
|
// case below for a reason that has nothing to do with the fence.
|
|
return http.Response(
|
|
'{"success":true,"data":{"bookingid":4821,'
|
|
'"status":"Miler_Assigned","reachedat":"2026-09-16T10:14:02Z"}}',
|
|
200,
|
|
);
|
|
});
|
|
controller = PickupsController();
|
|
});
|
|
|
|
tearDown(() {
|
|
MilerApi.client = http.Client();
|
|
Geofence.useDevice();
|
|
});
|
|
|
|
test('Arrived, 250 m away — no request', () async {
|
|
useFix(riderAt(250));
|
|
final ok = await controller.updateArrivedStatus(
|
|
pickupId: 4821,
|
|
orderHeaderId: 4821,
|
|
pickupLat: '$kTargetLat',
|
|
pickupLng: '$kTargetLng',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(sent, isEmpty, reason: 'the fence refused; nothing may be posted');
|
|
expect(controller.lastBlockedReason, contains('Move within 100 m'));
|
|
});
|
|
|
|
test('Arrived with GPS off — no request', () async {
|
|
useFailure(GeofenceOutcome.serviceDisabled);
|
|
final ok = await controller.updateArrivedStatus(
|
|
pickupId: 4821,
|
|
orderHeaderId: 4821,
|
|
pickupLat: '$kTargetLat',
|
|
pickupLng: '$kTargetLng',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('Arrived with permission denied forever — no request', () async {
|
|
useFailure(GeofenceOutcome.permissionDeniedForever);
|
|
final ok = await controller.updateArrivedStatus(
|
|
pickupId: 4821,
|
|
orderHeaderId: 4821,
|
|
pickupLat: '$kTargetLat',
|
|
pickupLng: '$kTargetLng',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('Arrived on a stop with no pin — no request', () async {
|
|
useFix(riderAt(0));
|
|
final ok = await controller.updateArrivedStatus(
|
|
pickupId: 4821,
|
|
orderHeaderId: 4821,
|
|
pickupLat: '0',
|
|
pickupLng: '0',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(
|
|
sent,
|
|
isEmpty,
|
|
reason:
|
|
'a booking with no coordinates used to be waved through — that is '
|
|
'the bypass that made the fence decorative',
|
|
);
|
|
});
|
|
|
|
test('Delivery arrived, 250 m away — no request', () async {
|
|
useFix(riderAt(250));
|
|
final ok = await controller.updateDeliveryArrived(
|
|
pickupId: 4821,
|
|
dropLat: '$kTargetLat',
|
|
dropLng: '$kTargetLng',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('Delivery arrived with NO drop pin — no request', () async {
|
|
// The conditional that used to wrap this call site —
|
|
// `if (dLat != 0 && dLng != 0)` — skipped the fence silently for exactly
|
|
// the stops where nobody could say afterwards where the rider was.
|
|
useFix(riderAt(0));
|
|
final ok = await controller.updateDeliveryArrived(
|
|
pickupId: 4821,
|
|
dropLat: '0',
|
|
dropLng: '0',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('Delivered, 250 m away — no request', () async {
|
|
useFix(riderAt(250));
|
|
final ok = await controller.updateDeliveredStatus(
|
|
pickupId: 4821,
|
|
consignmentId: '77',
|
|
deliveredToName: 'Anitha R',
|
|
dropLat: '$kTargetLat',
|
|
dropLng: '$kTargetLng',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('Delivered with NO drop pin — no request', () async {
|
|
useFix(riderAt(0));
|
|
final ok = await controller.updateDeliveredStatus(
|
|
pickupId: 4821,
|
|
consignmentId: '77',
|
|
deliveredToName: 'Anitha R',
|
|
dropLat: '0',
|
|
dropLng: '0',
|
|
);
|
|
expect(ok, isFalse);
|
|
expect(sent, isEmpty);
|
|
});
|
|
|
|
test('inside the fence, the request DOES go out', () async {
|
|
// The other half of the contract, and the reason this group is not just
|
|
// an assertion that everything is broken.
|
|
useFix(riderAt(40));
|
|
final ok = await controller.updateArrivedStatus(
|
|
pickupId: 4821,
|
|
orderHeaderId: 4821,
|
|
pickupLat: '$kTargetLat',
|
|
pickupLng: '$kTargetLng',
|
|
);
|
|
expect(ok, isTrue);
|
|
expect(sent, isNotEmpty);
|
|
expect(sent.single, contains('/miler/bookings/4821/reached'));
|
|
expect(controller.lastBlockedReason, isNull);
|
|
});
|
|
});
|
|
}
|