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>
191 lines
7.5 KiB
Dart
191 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:miler/views/Dashboard/pickups/stop_verify.dart';
|
|
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
|
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
|
|
|
/// ── The form is the screen; the boxes were not ──
|
|
///
|
|
/// Every group on a verification leg used to be a filled, rounded, 16pt-padded
|
|
/// panel — the stop summary, the parcel checklist, the recipient picker, the
|
|
/// OTP, the photo — four to six of them stacked, each holding white controls.
|
|
/// That put a *container* rank and a *control* rank on the page at the same
|
|
/// weight, when only one of the two is something the rider touches at a door.
|
|
///
|
|
/// They are named and ruled off instead. These tests pin the parts of that
|
|
/// which a later change could quietly undo.
|
|
void main() {
|
|
Future<void> pump(
|
|
WidgetTester tester,
|
|
Map<String, dynamic> stop, {
|
|
double width = 390,
|
|
}) async {
|
|
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(home: StopVerificationPage(pickup: stop)),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Map<String, dynamic> stopOf({
|
|
required String type,
|
|
int deliveryQty = 0,
|
|
int pickupQty = 0,
|
|
}) => <String, dynamic>{
|
|
'orderid': 'T-1',
|
|
'type': type,
|
|
'pickupcustomer': 'Priya Sharma',
|
|
'pickupaddress': '45, Lake View Apartments, Race Course, Coimbatore',
|
|
if (deliveryQty > 0) 'deliveryqty': '$deliveryQty',
|
|
if (pickupQty > 0) 'pickupqty': '$pickupQty',
|
|
'Quantity': '${pickupQty > 0 ? pickupQty : deliveryQty}',
|
|
};
|
|
|
|
/// The width of a chip, measured on the box that actually draws it.
|
|
double chipWidth(WidgetTester tester, String label) => tester
|
|
.getSize(
|
|
find
|
|
.ancestor(
|
|
of: find.text(label),
|
|
matching: find.byType(AnimatedContainer),
|
|
)
|
|
.first,
|
|
)
|
|
.width;
|
|
|
|
testWidgets('a leg step draws no filled panels', (tester) async {
|
|
await pump(tester, stopOf(type: 'combined', deliveryQty: 1, pickupQty: 2));
|
|
|
|
// `cardSurface` is the panel fill. It is still right for the review step —
|
|
// one grey block per leg, on the screen the rider signs — but on a step he
|
|
// is filling in, every one of them was a box around work rather than the
|
|
// work itself.
|
|
final fills = tester
|
|
.widgetList<Container>(
|
|
find.descendant(
|
|
of: find.byType(SingleChildScrollView),
|
|
matching: find.byType(Container),
|
|
),
|
|
)
|
|
.map((c) => c.decoration)
|
|
.whereType<BoxDecoration>()
|
|
.map((d) => d.color);
|
|
|
|
expect(fills, isNot(contains(ColorConstants.cardSurface)));
|
|
});
|
|
|
|
testWidgets('a section names itself, and stays under the stop', (
|
|
tester,
|
|
) async {
|
|
await pump(tester, stopOf(type: 'delivery', deliveryQty: 1));
|
|
|
|
// ── This assertion changed with the design, deliberately ──
|
|
//
|
|
// It used to require a grey letterspaced eyebrow, from when the sections
|
|
// were hairline-ruled groups on a white page: with no boundary of their
|
|
// own, a loud title would have been the only thing separating them, and
|
|
// five loud titles is a page of shouting.
|
|
//
|
|
// They are panels now. The boundary does the separating, so the title is
|
|
// free to be a title — the line the rider reads to know what this step
|
|
// asks of him — set in the same ink as the rest of the panel's content
|
|
// rather than in the grey reserved for metadata.
|
|
final label = tester.widget<Text>(find.text('Who received it?'));
|
|
expect(label.style!.color, ColorConstants.slateText);
|
|
expect(label.style!.fontWeight, FontWeight.w700);
|
|
|
|
// What has not changed, and must not: it is still quieter than the stop it
|
|
// belongs to. The customer's name is the subject of the screen; a step
|
|
// inside it is not allowed to outrank it.
|
|
final name = tester.widget<Text>(find.text('Priya Sharma'));
|
|
expect(label.style!.fontSize, lessThan(name.style!.fontSize!));
|
|
});
|
|
|
|
testWidgets('the pickers are an even grid, with no half-width orphan', (
|
|
tester,
|
|
) async {
|
|
await pump(tester, stopOf(type: 'delivery', deliveryQty: 1));
|
|
|
|
// Four recipients: two rows of two, all the same width. Natural-width chips
|
|
// packed themselves three-then-one, which reads as a list that ran out
|
|
// rather than as a set of four equal choices.
|
|
final customer = chipWidth(tester, 'Customer');
|
|
for (final other in const ['Family', 'Security', 'Neighbour']) {
|
|
expect(chipWidth(tester, other), closeTo(customer, 0.5));
|
|
}
|
|
});
|
|
|
|
testWidgets('an odd picker gives its last option the full row', (
|
|
tester,
|
|
) async {
|
|
await pump(tester, stopOf(type: 'pickup', pickupQty: 2));
|
|
|
|
// Three conditions. The third at half width on a line of its own is an
|
|
// orphan; at full width it is a second row.
|
|
expect(
|
|
chipWidth(tester, 'Damaged'),
|
|
greaterThan(chipWidth(tester, 'Sealed & intact') * 1.8),
|
|
);
|
|
});
|
|
|
|
testWidgets('a blocked step gets a neutral bar, not a faded coloured one', (
|
|
tester,
|
|
) async {
|
|
await pump(tester, stopOf(type: 'combined', deliveryQty: 1, pickupQty: 2));
|
|
|
|
// Nothing has been filled in, so the step is blocked.
|
|
expect(find.text('1 parcel still to hand over'), findsOneWidget);
|
|
|
|
// No accent button at all — a saturated pill at 45% still reads as a
|
|
// button, so the rider taps it and nothing happens.
|
|
expect(find.byType(MilerButton), findsNothing);
|
|
expect(find.byIcon(LucideIcons.lock), findsOneWidget);
|
|
|
|
// And the label stays readable: grey type on a neutral fill, not white
|
|
// dropped to 45% over a wash of itself.
|
|
// Named for where it goes, not for which step it is: the rider is on the
|
|
// hand-over leg, so the button announces the collection that follows it.
|
|
final label = tester.widget<Text>(find.text('Continue to collect'));
|
|
expect(label.style!.color, ColorConstants.secondaryText);
|
|
});
|
|
|
|
testWidgets('completing the step turns the bar a solid accent', (
|
|
tester,
|
|
) async {
|
|
await pump(tester, stopOf(type: 'delivery', deliveryQty: 1));
|
|
|
|
await tester.tap(find.text('Parcel 1 of 1'));
|
|
await tester.pumpAndSettle();
|
|
await tester.enterText(find.byType(TextField).first, '482916');
|
|
await tester.pumpAndSettle();
|
|
|
|
// Photo is the last gate and the camera cannot be reached here, so the bar
|
|
// is still blocked — but on the name of what is missing, not the parcels.
|
|
expect(find.text('Take a proof photo'), findsOneWidget);
|
|
|
|
// The parcel row and the OTP now report themselves done, which is the
|
|
// section-scoped tick: it speaks for its own step, never for the screen.
|
|
expect(find.text('Handed over'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('the app bar clears the status bar', (tester) async {
|
|
await pump(tester, stopOf(type: 'delivery', deliveryQty: 1));
|
|
|
|
// The title used to sit hard against the clock, which is where a system
|
|
// default puts it and nowhere near where the rest of the app puts a
|
|
// heading. `toolbarHeight` alone only moves it half as far as it adds.
|
|
final title = tester.getRect(find.text('Verify delivery'));
|
|
expect(title.top, greaterThan(24.0));
|
|
});
|
|
}
|