Miler rider app: surface system, visible design language, backend lifecycle
Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
265
test/deliveries_queue_test.dart
Normal file
265
test/deliveries_queue_test.dart
Normal file
@@ -0,0 +1,265 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/data/stop_area.dart';
|
||||
import 'package:miler/views/Dashboard/home/homepage_banner.dart';
|
||||
import 'package:miler/views/Dashboard/pickups/pickups.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE WORK QUEUE, NOT A LIST OF CARDS
|
||||
///
|
||||
/// Deliveries answers one question — *where do I go now, and what do I do
|
||||
/// there* — and the whole screen is ranked around it: NOW is the command
|
||||
/// object, UP NEXT is a glance, LATER recedes. That ranking has to be legible
|
||||
/// **before** the labels are read, or the labels are the only thing carrying
|
||||
/// it.
|
||||
///
|
||||
/// These pin the invariants that ranking depends on, plus the two geometry
|
||||
/// rules the bottom of this screen has broken twice.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
_realAddresses();
|
||||
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
||||
|
||||
Map<String, dynamic> drop(String id, String who, {String? address}) => {
|
||||
'orderid': id,
|
||||
'type': 'delivery',
|
||||
'orderstatus': 'outfordelivery',
|
||||
'pickupcustomer': who,
|
||||
'sourcename': 'DailyGrubs RS Puram Kitchen',
|
||||
'kitchenname': 'DailyGrubs RS Puram Kitchen',
|
||||
'pickupaddress': '9 Mettupalayam Road, RS Puram, Coimbatore 641002',
|
||||
'dropaddress': address ?? '12, SNS Colony, Peelamedu, Coimbatore 641004',
|
||||
'pickupcontactno': '9787698259',
|
||||
};
|
||||
|
||||
group('the collapsed queue names a place, not a street', () {
|
||||
test('an area, never a truncated address', () {
|
||||
// The screen's own anti-pattern, named in the brief: twenty rows of
|
||||
// `SEQTEST Gandhipur…` answer nothing a blank line would not have.
|
||||
final area = areaOf(drop('1', 'Joe Mathew'));
|
||||
expect(area, isNot(contains('641004')));
|
||||
expect(area, isNot(contains('12,')));
|
||||
expect(area.length, lessThan(24));
|
||||
expect(RegExp(r'[0-9]').hasMatch(area), isFalse);
|
||||
});
|
||||
|
||||
test('a delivery names where the bag is GOING', () {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
// Once he is carrying it, the kitchen is supporting information. The
|
||||
// destination is the question.
|
||||
// `Peelamedu`, not `SNS Colony` — the locality, not the sub-locality.
|
||||
// See the live-data group above for why the rule prefers a one-word name.
|
||||
expect(areaOf(drop('1', 'Joe Mathew'), preferDrop: true), 'Peelamedu');
|
||||
expect(
|
||||
areaOf(drop('1', 'Joe Mathew'), preferDrop: false),
|
||||
'Mettupalayam Road'.contains('9') ? isNotEmpty : 'RS Puram',
|
||||
);
|
||||
});
|
||||
|
||||
test('a stop with nothing usable yields nothing, never a placeholder', () {
|
||||
expect(areaOf(const {'orderid': '1'}), '');
|
||||
});
|
||||
|
||||
test('one implementation, shared with Home', () {
|
||||
// Two copies of this rule got the same subtle case wrong in the same
|
||||
// way: `12 SNS Colony` returned as an area because it is not *entirely*
|
||||
// digits. One function is one place to find that.
|
||||
expect(
|
||||
areaOf({'dropaddress': '4/2 Cross Cut Road, Gandhipuram, 641012'}),
|
||||
'Gandhipuram',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('the bottom slot is one slot', () {
|
||||
/// `maxHeight` is expressed in `.h`, like every dimension in this app, so
|
||||
/// these run inside a pumped `ScreenUtilInit` rather than as pure tests.
|
||||
Future<void> ready(WidgetTester tester) 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: (_, _) => const MaterialApp(home: SizedBox.shrink()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('the reservation follows whichever surface is showing', (
|
||||
tester,
|
||||
) async {
|
||||
await ready(tester);
|
||||
const inset = 34.0;
|
||||
|
||||
// Live stop: the banner is up, and the floor clears the banner.
|
||||
expect(
|
||||
bottomReservationFor(inset: inset, live: true, unreleased: true),
|
||||
inset + ActivePickupBanner.maxHeight,
|
||||
);
|
||||
// A live stop suppresses the start bar, so the banner wins even when
|
||||
// there is unreleased work — the two never stack.
|
||||
expect(
|
||||
bottomReservationFor(inset: inset, live: true, unreleased: false),
|
||||
inset + ActivePickupBanner.maxHeight,
|
||||
);
|
||||
// Nothing running, load in hand: the start bar.
|
||||
expect(
|
||||
bottomReservationFor(inset: inset, live: false, unreleased: true),
|
||||
inset + StartRoundBar.maxHeight,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the device inset is always counted', (tester) async {
|
||||
await ready(tester);
|
||||
// The regression: the live branch was a flat `80.h` that forgot the
|
||||
// inset, so on a gesture-navigation phone the last row of the run sat
|
||||
// behind the banner *and* behind the navigation.
|
||||
for (final live in const [true, false]) {
|
||||
final withInset = bottomReservationFor(
|
||||
inset: 34,
|
||||
live: live,
|
||||
unreleased: true,
|
||||
);
|
||||
final without = bottomReservationFor(
|
||||
inset: 0,
|
||||
live: live,
|
||||
unreleased: true,
|
||||
);
|
||||
expect(withInset - without, 34);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('an idle queue reserves almost nothing', (tester) async {
|
||||
await ready(tester);
|
||||
expect(
|
||||
bottomReservationFor(inset: 0, live: false, unreleased: false),
|
||||
lessThan(StartRoundBar.maxHeight),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('both surfaces declare a real height', (tester) async {
|
||||
await ready(tester);
|
||||
// Measured, not guessed. Each is the only thing that knows how much of
|
||||
// the list it covers.
|
||||
expect(ActivePickupBanner.maxHeight, greaterThan(48));
|
||||
expect(StartRoundBar.maxHeight, greaterThan(48));
|
||||
});
|
||||
});
|
||||
|
||||
group('the command surface', () {
|
||||
Future<void> pumpBar(
|
||||
WidgetTester tester, {
|
||||
double width = 390,
|
||||
double textScale = 1.0,
|
||||
int count = 5,
|
||||
}) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
tester.view.physicalSize = Size(width * 3, 844 * 3);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => MaterialApp(
|
||||
builder: (context, child) => MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).copyWith(textScaler: TextScaler.linear(textScale)),
|
||||
child: child!,
|
||||
),
|
||||
home: Scaffold(
|
||||
backgroundColor: ColorConstants.daylightSurface,
|
||||
body: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: StartRoundBar(
|
||||
count: count,
|
||||
busy: false,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('it says what is in hand and what the press does', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpBar(tester);
|
||||
// Setting off is reversible and operational, so it is a press — unlike
|
||||
// Picked and Delivered, which are assertions about custody.
|
||||
expect(find.textContaining('Start'), findsOneWidget);
|
||||
});
|
||||
|
||||
for (final (width, scale) in const [
|
||||
(390.0, 1.0),
|
||||
(390.0, 1.3),
|
||||
(390.0, 2.0),
|
||||
(375.0, 2.0),
|
||||
(320.0, 1.0),
|
||||
(320.0, 2.0),
|
||||
]) {
|
||||
testWidgets('${width.toInt()}px @ ${scale}x lays out clean', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpBar(tester, width: width, textScale: scale, count: 12);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('it never wraps itself in a second SafeArea', (tester) async {
|
||||
await pumpBar(tester);
|
||||
// `BottomPage.bottomInset` is the one owner of the device inset on this
|
||||
// screen. A `SafeArea` here would count it twice and float the bar a
|
||||
// navigation bar's height off the bottom of the screen.
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(StartRoundBar),
|
||||
matching: find.byType(SafeArea),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Real addresses from the live tenant, added after seeing them render on a
|
||||
/// device. The rule that only skipped digits printed the landmark —
|
||||
/// `Coimbatore Gandhipuram Mofussil Bus Stand` — on every row of the run.
|
||||
void _realAddresses() {
|
||||
group('the area rule against live data', () {
|
||||
const cases = <String, String>{
|
||||
'Coimbatore Gandhipuram Mofussil Bus Stand, Sathyamurthi Road, '
|
||||
'Gandhipuram, Ward 54, Central Zone, Coimbatore, '
|
||||
'Coimbatore North, Coimbatore, Tamil Nadu, 641001, India':
|
||||
'Gandhipuram',
|
||||
'12, SNS Colony, Peelamedu, Coimbatore 641004': 'Peelamedu',
|
||||
'4 Cross, RS Puram, Coimbatore 641002': 'RS Puram',
|
||||
'19, Thadagam Road, Venkitapuram, Coimbatore': 'Venkitapuram',
|
||||
};
|
||||
|
||||
cases.forEach((address, expected) {
|
||||
test('"${address.split(',').first}…" → $expected', () {
|
||||
expect(areaOf({'dropaddress': address}), expected);
|
||||
});
|
||||
});
|
||||
|
||||
test('a landmark never wins over the locality behind it', () {
|
||||
final area = areaOf(const {
|
||||
'dropaddress':
|
||||
'Coimbatore Gandhipuram Mofussil Bus Stand, '
|
||||
'Sathyamurthi Road, Gandhipuram, Coimbatore',
|
||||
});
|
||||
expect(area.split(' ').length, 1, reason: 'an area is a name');
|
||||
expect(area, isNot(contains('Bus Stand')));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user