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>
232 lines
9.4 KiB
Dart
232 lines
9.4 KiB
Dart
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);
|
||
});
|
||
});
|
||
}
|