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
133 lines
4.7 KiB
Dart
133 lines
4.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:miler/views/offline/offline_banner.dart';
|
|
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
|
|
|
/// ── Losing signal must not take the job away ──
|
|
///
|
|
/// Connectivity used to stack an opaque full-screen page over the entire app.
|
|
/// A rider at a doorstep in a basement lost the address, the customer, the
|
|
/// parcel count and the OTP field — while holding the parcel. Everything he
|
|
/// needs at the door was already fetched; the network is needed to *report* the
|
|
/// stop, not to work it.
|
|
void main() {
|
|
Future<void> pump(WidgetTester tester, {required bool offline}) async {
|
|
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
|
|
tester.view.devicePixelRatio = 3.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
await tester.pumpWidget(
|
|
ScreenUtilInit(
|
|
designSize: const Size(390, 844),
|
|
builder: (_, _) => MaterialApp(
|
|
home: OfflineScope(
|
|
offline: offline,
|
|
child: Scaffold(
|
|
body: Column(
|
|
children: const [
|
|
Text('45, Lake View Apartments'),
|
|
Text('Priya Sharma'),
|
|
Text('Delivery OTP'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('the job stays on screen while offline', (tester) async {
|
|
await pump(tester, offline: true);
|
|
|
|
// The four things a rider cannot do the stop without.
|
|
expect(find.text('45, Lake View Apartments'), findsOneWidget);
|
|
expect(find.text('Priya Sharma'), findsOneWidget);
|
|
expect(find.text('Delivery OTP'), findsOneWidget);
|
|
expect(find.byType(OfflineBanner), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the strip takes its own row — it never covers content', (
|
|
tester,
|
|
) async {
|
|
await pump(tester, offline: true);
|
|
|
|
// A Column, not a Stack, on purpose: a floating bar over the page would sit
|
|
// on top of the OTP box, which is worse than the blocker it replaced.
|
|
final strip = tester.getRect(find.byType(OfflineBanner));
|
|
final firstContent = tester.getRect(find.text('45, Lake View Apartments'));
|
|
expect(strip.bottom, lessThanOrEqualTo(firstContent.top));
|
|
});
|
|
|
|
testWidgets('it is thin enough to cost the job nothing', (tester) async {
|
|
await pump(tester, offline: true);
|
|
// Roughly one line plus padding. A "banner" that eats 15% of an 844pt
|
|
// screen is a blocker with a nicer name.
|
|
expect(tester.getSize(find.byType(OfflineBanner)).height, lessThan(64));
|
|
});
|
|
|
|
testWidgets('it says what happens next, and promises nothing it cannot do', (
|
|
tester,
|
|
) async {
|
|
await pump(tester, offline: true);
|
|
final text = tester
|
|
.widgetList<Text>(
|
|
find.descendant(
|
|
of: find.byType(OfflineBanner),
|
|
matching: find.byType(Text),
|
|
),
|
|
)
|
|
.first
|
|
.data!;
|
|
expect(text, contains('Offline'));
|
|
|
|
// ── This used to assert the opposite ──
|
|
//
|
|
// It required the word "sync", and the banner duly said "Changes will sync
|
|
// when you're back online". **There is no mutation queue in this app** — no
|
|
// outbox, no replay, no retry loop. A status the rider set with no signal
|
|
// was attempted once, failed, and was never sent again. The banner was
|
|
// promising a mechanism that does not exist, to the one person who would be
|
|
// blamed when the hub had no record of the stop.
|
|
//
|
|
// If a queue is ever built, this assertion is the place to reverse — and
|
|
// the copy may promise a sync again on the day something performs one.
|
|
expect(
|
|
text.toLowerCase(),
|
|
isNot(contains('sync')),
|
|
reason: 'nothing in this app replays a failed mutation',
|
|
);
|
|
expect(
|
|
text.toLowerCase(),
|
|
contains('connection'),
|
|
reason: 'it must still say what he needs, not only that he is offline',
|
|
);
|
|
});
|
|
|
|
testWidgets('online, it occupies no space at all', (tester) async {
|
|
await pump(tester, offline: false);
|
|
expect(find.byType(OfflineBanner), findsNothing);
|
|
expect(
|
|
tester.getRect(find.text('45, Lake View Apartments')).top,
|
|
lessThan(20),
|
|
);
|
|
});
|
|
|
|
testWidgets('the strip carries a readable label on its fill', (tester) async {
|
|
await pump(tester, offline: true);
|
|
final material = tester.widget<Material>(
|
|
find
|
|
.descendant(
|
|
of: find.byType(OfflineBanner),
|
|
matching: find.byType(Material),
|
|
)
|
|
.first,
|
|
);
|
|
// `warning`, which the contrast contract holds to 4.5:1 behind white.
|
|
expect(material.color, ColorConstants.warning);
|
|
});
|
|
}
|