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:
252
test/kitchen_group_test.dart
Normal file
252
test/kitchen_group_test.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip_card.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE KITCHEN IS THE CARD; ITS ORDERS ARE THE CONTENTS
|
||||
///
|
||||
/// Home's pickup model is one place per card, disclosed to reveal the orders
|
||||
/// under it, ticked in batches and advanced with one gesture:
|
||||
///
|
||||
/// ```
|
||||
/// Vidhya Kitchen · 4 bags · 2 to accept ˅
|
||||
/// ◉ Joe Gandhipuram Bag 1
|
||||
/// ◉ Priya RS Puram Bag 2
|
||||
/// ```
|
||||
///
|
||||
/// ── The regression this file exists for ──
|
||||
///
|
||||
/// Two functions answered "what is this place called" and read different keys:
|
||||
/// `MilkRun.sourceNameOf` took `sourcename` / `kitchenname`, `stopSourceName`
|
||||
/// took those **and** the CamelCase `SourceName` / `KitchenName` the payload
|
||||
/// sometimes carries. `trip_card.dart` uses both in one expression, so a
|
||||
/// booking with only the capitalised key was:
|
||||
///
|
||||
/// • **not flat** — `stopSourceName` found a counter, so the group became a
|
||||
/// foldable place with its orders underneath; and
|
||||
/// • **titled "pickup"** — `sourceNameOf` found none, so `navigationLabel`
|
||||
/// fell through to its leg description.
|
||||
///
|
||||
/// On a device that is a group headed by a backend word, which the rider taps
|
||||
/// and which opens a detail sheet instead of the list. Fixed at the domain
|
||||
/// layer: one reader, four keys, `stopSourceName` delegating to it.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
void main() {
|
||||
tearDown(() {
|
||||
Get.reset();
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
});
|
||||
|
||||
/// An order as the queue hands it over. [kitchenKey] is the variable under
|
||||
/// test: every spelling the payload uses must group and name identically.
|
||||
Map<String, dynamic> order(
|
||||
String id,
|
||||
String who,
|
||||
String drop, {
|
||||
String kitchenKey = 'KitchenName',
|
||||
String status = 'new',
|
||||
}) => {
|
||||
'orderid': id,
|
||||
'pickupid': int.parse(id),
|
||||
'step': int.parse(id) - 1000,
|
||||
kitchenKey: 'Vidhya Kitchen',
|
||||
'sourceid': 'K1',
|
||||
'pickupcustomer': who,
|
||||
'dropaddress': drop,
|
||||
'pickupaddress': '9 Kitchen Street, Coimbatore',
|
||||
'orderstatus': status,
|
||||
'type': 'delivery',
|
||||
'pickuplat': 11.01,
|
||||
'pickuplon': 76.95,
|
||||
};
|
||||
|
||||
Future<void> pump(
|
||||
WidgetTester tester,
|
||||
List<Map<String, dynamic>> stops, {
|
||||
Set<String> accepted = const {},
|
||||
Set<String> selected = const {},
|
||||
Set<String> collected = const {},
|
||||
double width = 390,
|
||||
double scale = 1.0,
|
||||
}) async {
|
||||
ServiceProfile.setActive(ServiceProfile.milkMan);
|
||||
tester.view.physicalSize = Size(width * 3, 1400 * 3);
|
||||
tester.view.devicePixelRatio = 3;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
minTextAdapt: true,
|
||||
builder: (_, _) => MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: MediaQueryData(textScaler: TextScaler.linear(scale)),
|
||||
child: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: TripCard(
|
||||
trip: Trip.fromStops(id: 'T1', stops: stops),
|
||||
acceptedIds: accepted,
|
||||
rejectedIds: const {},
|
||||
collectedIds: collected,
|
||||
selectedIds: selected,
|
||||
onToggleSelect: (_) {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
group('the place is named from the domain, not the screen', () {
|
||||
for (final key in const [
|
||||
'sourcename',
|
||||
'SourceName',
|
||||
'kitchenname',
|
||||
'KitchenName',
|
||||
]) {
|
||||
testWidgets('$key names the card and groups its orders', (tester) async {
|
||||
await pump(tester, [
|
||||
order('1001', 'Joe', 'Gandhipuram, Coimbatore', kitchenKey: key),
|
||||
order('1002', 'Priya', 'RS Puram, Coimbatore', kitchenKey: key),
|
||||
]);
|
||||
|
||||
expect(find.text('Vidhya Kitchen'), findsOneWidget);
|
||||
// The leg description must never surface as a place's name.
|
||||
expect(find.text('pickup'), findsNothing);
|
||||
// Grouped, so the orders are disclosed under the header rather than the
|
||||
// header being the stop.
|
||||
expect(find.text('Joe'), findsOneWidget);
|
||||
expect(find.text('Priya'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('a payload naming no counter still renders its one stop', (
|
||||
tester,
|
||||
) async {
|
||||
final o = order('1001', 'Joe', 'Gandhipuram, Coimbatore');
|
||||
o.remove('KitchenName');
|
||||
o.remove('sourceid');
|
||||
await pump(tester, [o]);
|
||||
|
||||
// Nothing to group under, so the header IS the stop — named for the
|
||||
// customer, never for the leg.
|
||||
expect(find.text('Joe'), findsWidgets);
|
||||
expect(find.text('pickup'), findsNothing);
|
||||
});
|
||||
});
|
||||
|
||||
group('the row carries what the rider decides on', () {
|
||||
testWidgets('the drop area, not the booking reference', (tester) async {
|
||||
await pump(tester, [order('1001', 'Joe', 'Gandhipuram, Coimbatore')]);
|
||||
|
||||
// "Are Joe and Priya the same trip out of the door, or opposite ends of
|
||||
// the city?" is the question this list answers. A reference answers the
|
||||
// one the office asks, once, when something has gone wrong — it lives on
|
||||
// the detail sheet and the Activity record.
|
||||
expect(find.text('Gandhipuram'), findsOneWidget);
|
||||
expect(find.textContaining('1001'), findsNothing);
|
||||
expect(find.text('Bag 1'), findsOneWidget);
|
||||
});
|
||||
|
||||
// Updated when `areaOf` was corrected against real Coimbatore addresses
|
||||
// on a device: it used to take the first part with no digits in it, which
|
||||
// is the *street*. The row's job is to say which neighbourhood a stop is
|
||||
// in, so the rule now prefers the shortest digit-free part — the locality
|
||||
// — and this expectation follows the rule rather than the other way round.
|
||||
testWidgets('a door number is skipped for the area behind it', (
|
||||
tester,
|
||||
) async {
|
||||
await pump(tester, [
|
||||
order('1001', 'Joe', '12, Cross Cut Road, Gandhipuram'),
|
||||
]);
|
||||
expect(find.text('Gandhipuram'), findsOneWidget);
|
||||
expect(find.textContaining('12,'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('no drop means no line, never a blank one', (tester) async {
|
||||
final o = order('1001', 'Joe', '');
|
||||
o.remove('dropaddress');
|
||||
o.remove('pickupaddress');
|
||||
await pump(tester, [o]);
|
||||
|
||||
expect(find.text('Joe'), findsWidgets);
|
||||
expect(find.text(''), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('one order is one bag, counted off the orders', (tester) async {
|
||||
await pump(tester, [
|
||||
order('1001', 'Joe', 'Gandhipuram, Coimbatore'),
|
||||
order('1002', 'Priya', 'RS Puram, Coimbatore'),
|
||||
order('1003', 'Arun', 'Peelamedu, Coimbatore'),
|
||||
]);
|
||||
// The header states the load once, in the unit the rider carries; the
|
||||
// check that orders and bags agree is the manifest itself, one named
|
||||
// bag per order. `N orders · N bags` was the same number twice and it
|
||||
// cost the header its `to accept` clause on a real route.
|
||||
expect(find.textContaining('3 bags'), findsWidgets);
|
||||
for (final bag in const ['Bag 1', 'Bag 2', 'Bag 3']) {
|
||||
expect(find.text(bag), findsOneWidget);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('a partly-accepted kitchen says so', () {
|
||||
testWidgets('2 of 4 taken is visible on the header', (tester) async {
|
||||
await pump(
|
||||
tester,
|
||||
[
|
||||
order('1001', 'Joe', 'Gandhipuram, Coimbatore'),
|
||||
order('1002', 'Priya', 'RS Puram, Coimbatore'),
|
||||
order('1003', 'Arun', 'Peelamedu, Coimbatore'),
|
||||
order('1004', 'Kumar', 'Saibaba Colony, Coimbatore'),
|
||||
],
|
||||
accepted: const {'1002', '1004'},
|
||||
);
|
||||
|
||||
// The rider is mid-decision at one counter; the count of what is still
|
||||
// undecided is the whole content of that state.
|
||||
expect(find.textContaining('4 bags'), findsWidgets);
|
||||
expect(find.textContaining('to accept'), findsWidgets);
|
||||
});
|
||||
});
|
||||
|
||||
group('it survives a narrow phone and a large font', () {
|
||||
for (final (width, scale) in const [
|
||||
(390.0, 1.0),
|
||||
(375.0, 1.0),
|
||||
(320.0, 1.0),
|
||||
(390.0, 2.0),
|
||||
(375.0, 2.0),
|
||||
(320.0, 2.0),
|
||||
]) {
|
||||
testWidgets('${width.toInt()}px at ${scale}x', (tester) async {
|
||||
await pump(
|
||||
tester,
|
||||
[
|
||||
order(
|
||||
'1001',
|
||||
'Ramachandran Venkataraman',
|
||||
'Saibaba Colony, Coimbatore',
|
||||
),
|
||||
order('1002', 'Priya', 'RS Puram, Coimbatore'),
|
||||
],
|
||||
accepted: const {'1002'},
|
||||
selected: const {'1001'},
|
||||
width: width,
|
||||
scale: scale,
|
||||
);
|
||||
// A `RenderFlex` overflow throws, and on a doorstep the stripe bar
|
||||
// reads as the app having crashed.
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user