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:
231
test/service_card_test.dart
Normal file
231
test/service_card_test.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
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/views/Dashboard/home/trip.dart';
|
||||
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/Dashboard/home/trip_card.dart';
|
||||
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
||||
|
||||
/// P2 — what a stop card says on a service tenant.
|
||||
///
|
||||
/// The three facts under test are the three a rider reads before he moves: what
|
||||
/// kind of work this is, where the order has got to, and which bag out of ten in
|
||||
/// his box belongs at this door.
|
||||
void main() {
|
||||
tearDown(() => ServiceProfile.setActive(ServiceProfile.parcel));
|
||||
|
||||
group('the work-type chip', () {
|
||||
test('Doormile still names each stop kind separately', () {
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
expect(StopKind.pickup.chipLabel, 'PICKUP');
|
||||
expect(StopKind.delivery.chipLabel, 'DELIVERY');
|
||||
expect(StopKind.combined.chipLabel, 'P&D');
|
||||
expect(StopKind.pickup.chipColor, ColorConstants.pickupChip);
|
||||
});
|
||||
|
||||
test('a milk run wears one mark, and it is not the line’s name', () {
|
||||
// ── This assertion changed with product direction ──
|
||||
//
|
||||
// It used to require the word MILK. That is the app's internal name for
|
||||
// a *service profile* — the thing that decides which capabilities exist
|
||||
// and whose records belong to whom — and it was being printed on the
|
||||
// rider's cards and on his Activity record as `Type: MILK`. He did not
|
||||
// choose it, cannot change it and gains nothing from reading it.
|
||||
//
|
||||
// What has NOT changed is the property the mark exists for: on this line
|
||||
// every stop wears the *same* mark, because the rider handles a kitchen
|
||||
// crate and a subscriber's doorstep identically and a chip that flipped
|
||||
// between them would imply a difference in handling that does not exist.
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
for (final kind in StopKind.values) {
|
||||
expect(kind.chipLabel, 'DELIVERY', reason: '$kind');
|
||||
expect(kind.badgeLabel, 'DELIVERY', reason: '$kind');
|
||||
expect(kind.chipColor, ColorConstants.serviceChip, reason: '$kind');
|
||||
}
|
||||
// Sentence case where the type is stated as a label rather than stamped.
|
||||
expect(StopKind.delivery.titleLabel, 'Delivery');
|
||||
|
||||
// And the internal name reaches no screen at all.
|
||||
for (final kind in StopKind.values) {
|
||||
expect(kind.chipLabel.toUpperCase(), isNot(contains('MILK')));
|
||||
expect(kind.titleLabel.toUpperCase(), isNot(contains('MILK')));
|
||||
}
|
||||
});
|
||||
|
||||
test('the stop keeps its real kind underneath the one label', () {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
// P3's collection gate depends on this still being true: a kitchen stop
|
||||
// is a pickup and a subscriber drop is a delivery, whatever the chip says.
|
||||
expect(stopKindOf({'type': 'delivery'}), StopKind.delivery);
|
||||
expect(stopKindOf({'type': 'pickup'}), StopKind.pickup);
|
||||
});
|
||||
});
|
||||
|
||||
group('the status tag', () {
|
||||
test('a parcel route names a pending stop as a decision it awaits', () {
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
expect(StopState.pending.label, 'Awaiting your decision');
|
||||
expect(StopState.done.label, 'Completed');
|
||||
});
|
||||
|
||||
test('a meal rider is told the state, not asked for a decision', () {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
// He accepted the whole assignment at once — there is no per-stop
|
||||
// decision left to await.
|
||||
expect(StopState.pending.label, 'Pending');
|
||||
expect(StopState.active.label, 'Out for delivery');
|
||||
expect(StopState.done.label, 'Delivered');
|
||||
expect(StopState.skipped.label, 'Skipped');
|
||||
});
|
||||
});
|
||||
|
||||
group('source and bag label', () {
|
||||
test('read the spellings the backend might send', () {
|
||||
expect(stopSourceName({'kitchenname': 'Kitchen 1'}), 'Kitchen 1');
|
||||
expect(stopSourceName({'SourceName': ' Anna Nagar '}), 'Anna Nagar');
|
||||
expect(stopSourceId({'kitchenid': 'K-77'}), 'K-77');
|
||||
expect(stopBagLabel({'baglabel': 'DG-1042'}), 'DG-1042');
|
||||
expect(stopBagLabel({'labelcode': 'DG-9'}), 'DG-9');
|
||||
});
|
||||
|
||||
test(
|
||||
'absent on a parcel stop rather than empty-stringed onto the card',
|
||||
() {
|
||||
expect(stopSourceName({'orderid': '7'}), '');
|
||||
expect(stopBagLabel({'orderid': '7'}), '');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('StopSourceLine', () {
|
||||
Future<void> pump(WidgetTester tester, String source, String bag) async {
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: StopSourceLine(source: source, bagLabel: bag),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('takes no space at all on a stop with neither', (tester) async {
|
||||
await pump(tester, '', '');
|
||||
// A Doormile card must be exactly what it was — not what it was plus an
|
||||
// empty row's padding.
|
||||
final size = tester.getSize(find.byType(StopSourceLine));
|
||||
expect(size.height, 0);
|
||||
});
|
||||
|
||||
testWidgets('shows both when the stop carries both', (tester) async {
|
||||
await pump(tester, 'Kitchen 1', 'DG-1042');
|
||||
expect(find.text('Kitchen 1'), findsOneWidget);
|
||||
expect(find.text('Bag DG-1042'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows the bag alone when there is no source', (tester) async {
|
||||
await pump(tester, '', 'DG-1042');
|
||||
expect(find.text('Bag DG-1042'), findsOneWidget);
|
||||
expect(
|
||||
tester.getSize(find.byType(StopSourceLine)).height,
|
||||
greaterThan(0),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('the stop row on a service tenant', () {
|
||||
/// [withSource] mirrors the payload difference between the two tenants: a
|
||||
/// parcel booking has no kitchen and no bag label to send, which is why a
|
||||
/// Doormile card is unchanged by P2 — the line is driven by the data, not
|
||||
/// by who the rider works for.
|
||||
Map<String, dynamic> stop({bool withSource = true}) => {
|
||||
'orderid': '1',
|
||||
'step': 1,
|
||||
'customername': 'Priya Sundaram',
|
||||
'pickupaddress': '2B Lakshmi Flats, 4th Main Road, Anna Nagar East',
|
||||
'status': 'ASSIGNED',
|
||||
'type': 'delivery',
|
||||
'quantity': 1,
|
||||
if (withSource) 'kitchenname': 'Kitchen 1',
|
||||
if (withSource) 'baglabel': 'DG-1042',
|
||||
'pickuplat': 13.08,
|
||||
'pickuplon': 80.21,
|
||||
};
|
||||
|
||||
Future<void> pumpRoute(
|
||||
WidgetTester tester, {
|
||||
bool withSource = true,
|
||||
}) async {
|
||||
tester.view.physicalSize = const Size(1080, 1800);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
final trip = Trip.fromStops(
|
||||
id: 't1',
|
||||
stops: [stop(withSource: withSource)],
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => MaterialApp(
|
||||
home: Scaffold(
|
||||
body: SingleChildScrollView(
|
||||
child: TripCard(trip: trip).buildStops(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// ── The row's marks moved up to the place ──
|
||||
//
|
||||
// A card carried a work-type chip and a state chip of its own, and the
|
||||
// truncation test above was about the two of them sharing one line. Neither
|
||||
// is on a timeline row: the type is progressive disclosure in the detail
|
||||
// sheet, and the *state* belongs to the place rather than to each order
|
||||
// under it — five orders off one counter are all on the same rung, so five
|
||||
// copies of the same chip said nothing five times.
|
||||
//
|
||||
// What survives is the half that was always the contract: the kitchen is
|
||||
// named once, and the bag the order travels in is on the row.
|
||||
|
||||
testWidgets('the kitchen is named once and the bag rides on the row', (
|
||||
tester,
|
||||
) async {
|
||||
ServiceProfile.setActive(ServiceProfile.meals);
|
||||
await pumpRoute(tester);
|
||||
|
||||
// Named once, by the place — not again on every order under it.
|
||||
expect(find.text('Kitchen 1'), findsOneWidget);
|
||||
// The label the kitchen printed, straight from [BagManifest]. The `Bag `
|
||||
// prefix belonged to the card's chip and went with it, which stopped a
|
||||
// printed `DG-1042` reading as `Bag DG-1042`.
|
||||
expect(find.text('DG-1042'), findsOneWidget);
|
||||
// The count the rider checks the shelf against.
|
||||
expect(find.text('1 bag'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a parcel route keeps a bare pending row', (tester) async {
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
await pumpRoute(tester, withSource: false);
|
||||
// The row underneath carries the decision, so a chip would be a caption
|
||||
// on a control.
|
||||
expect(find.text('Awaiting your decision'), findsNothing);
|
||||
// And nothing from the meal run leaks onto a parcel route: no counter to
|
||||
// group under, so the stop is its own node and no bag arithmetic applies.
|
||||
expect(find.text('Kitchen 1'), findsNothing);
|
||||
expect(find.text('MILK'), findsNothing);
|
||||
expect(find.text('1 order · 1 bag'), findsNothing);
|
||||
expect(find.text('Priya Sundaram'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user