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:
@@ -3,12 +3,14 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/views/Dashboard/activity/activity_page.dart';
|
||||
import 'package:miler/views/Dashboard/home/homepage.dart';
|
||||
import 'package:miler/views/Dashboard/profile/Profilepage.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/widgets/miler_app_bar.dart';
|
||||
import 'package:miler/data/accepted_store.dart';
|
||||
|
||||
/// ── A day's work is a list, not a stack of cards ──
|
||||
///
|
||||
@@ -50,9 +52,10 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> pumpActivity(WidgetTester tester) async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'userId': 1,
|
||||
'completed_bookings': jsonEncode([
|
||||
SharedPreferences.setMockInitialValues({'userId': 1});
|
||||
await debugSeedStore(
|
||||
'completed_bookings',
|
||||
jsonEncode([
|
||||
finished(
|
||||
'DM240817',
|
||||
'Sundar Textiles',
|
||||
@@ -69,7 +72,7 @@ void main() {
|
||||
minsAgo: 68,
|
||||
),
|
||||
]),
|
||||
});
|
||||
);
|
||||
|
||||
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
@@ -102,64 +105,43 @@ void main() {
|
||||
expect(fills, isNot(contains(ColorConstants.cardSurface)));
|
||||
});
|
||||
|
||||
testWidgets('the figures line up across rows, not just within one', (
|
||||
tester,
|
||||
) async {
|
||||
// ── A history row says who, when and how much, and nothing else ──
|
||||
//
|
||||
// Every finished stop used to carry a two-line address and a verdict
|
||||
// sentence — `Pickup & Delivery · On time · +2.1 km over · ₹2,450` — so a
|
||||
// rider looking for one drop read four to six lines per row to find it.
|
||||
//
|
||||
// The type and the compliance figures did not disappear; they are fields in
|
||||
// the row's own detail panel, which is where a rider goes when he wants them
|
||||
// for ONE stop rather than for all forty.
|
||||
testWidgets('the collapsed row carries no verdict sentence', (tester) async {
|
||||
await pumpActivity(tester);
|
||||
|
||||
// The point of three fixed columns is comparing one stop against the next.
|
||||
// While every row sat in its own padded card that only held *inside* a row;
|
||||
// sharing the page margin is what makes column two of one stop sit directly
|
||||
// above column two of the one below.
|
||||
final captions = tester.widgetList<Text>(find.text('COLLECTED')).toList();
|
||||
expect(captions.length, 2);
|
||||
// The subject of the row.
|
||||
expect(find.text('Sundar Textiles'), findsOneWidget);
|
||||
|
||||
final rects = [
|
||||
for (var i = 0; i < 2; i++) tester.getRect(find.text('COLLECTED').at(i)),
|
||||
];
|
||||
expect(rects[0].left, closeTo(rects[1].left, 0.5));
|
||||
// `On time` on every row separates nothing. Only exceptions get words now.
|
||||
expect(find.text('On time'), findsNothing);
|
||||
expect(find.text('Time not measured'), findsNothing);
|
||||
|
||||
// Column one starts flush with the stop name above it — no card inset.
|
||||
final time = tester.getRect(find.text('TIME').first);
|
||||
final name = tester.getRect(find.text('Sundar Textiles'));
|
||||
expect(time.left, closeTo(name.left, 0.5));
|
||||
});
|
||||
|
||||
testWidgets('the stop type is a dotted word, not a filled tag', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpActivity(tester);
|
||||
|
||||
// Two tinted rectangles under every address — the type and the order id —
|
||||
// were the loudest things on a row whose subject is the figures below them.
|
||||
expect(find.text('Pickup & Delivery'), findsOneWidget);
|
||||
// `P&D` is `chipLabel`, which only ever reached this page through the
|
||||
// filled tag. (`badgeLabel` still appears inside the collapsed detail,
|
||||
// which `AnimatedCrossFade` builds either way — that one is a field value,
|
||||
// not a mark.)
|
||||
// `titleLabel`, the sentence-case type word, was the verdict line's lead
|
||||
// and is gone with it. `badgeLabel` is a field value in the detail panel,
|
||||
// which `AnimatedCrossFade` builds whether or not the row is open.
|
||||
expect(find.text('Pickup & Delivery'), findsNothing);
|
||||
expect(find.text('P&D'), findsNothing);
|
||||
// The type is the record's business, one tap away — see
|
||||
// `test/delivery_details_test.dart`.
|
||||
expect(find.textContaining('PICKUP & DELIVERY'), findsNothing);
|
||||
|
||||
final label = tester.widget<Text>(find.text('Pickup & Delivery'));
|
||||
expect(label.style!.color, ColorConstants.combinedAccent);
|
||||
|
||||
// The reference stays copyable, in grey, without claiming any rank.
|
||||
final id = tester.widget<Text>(find.text('#DM240817'));
|
||||
expect(id.style!.color, ColorConstants.secondaryText);
|
||||
});
|
||||
|
||||
testWidgets('column captions are readable, not disabled-grey', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpActivity(tester);
|
||||
|
||||
// #C7C7CC is a disabled-control colour. It measured about 1.9:1 on the
|
||||
// white page, which made the three words naming the columns the least
|
||||
// readable text on a screen used outdoors.
|
||||
for (final caption in const ['TIME', 'RIDDEN', 'COLLECTED']) {
|
||||
final text = tester.widget<Text>(find.text(caption).first);
|
||||
expect(text.style!.color, ColorConstants.secondaryText);
|
||||
expect(text.style!.color, isNot(ColorConstants.disabledFill));
|
||||
}
|
||||
// ── The reference left the row ──
|
||||
//
|
||||
// It used to head the card, on the argument that an office reads it down
|
||||
// a phone at a rider. That is true and it is still the wrong place for it:
|
||||
// the office asks about *one* stop, and the rider finds that stop by the
|
||||
// customer's name — which he then taps. Printing a database identifier on
|
||||
// every row of a shift log makes him scan past twelve of them to reach the
|
||||
// one name he is looking for. It lives on the record page.
|
||||
expect(find.text('#DM240817'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the four tabs agree on what colour the paper is', (
|
||||
@@ -168,16 +150,22 @@ void main() {
|
||||
// Account was on `surface` (#FCF9F8, a warm off-white) while Home and
|
||||
// Activity were on `pureSurface`. Bookings hides its background under
|
||||
// cards; Account is bare rows, so the whole tab read a shade dirtier.
|
||||
Color? bgOf(Widget page) {
|
||||
final scaffolds = <Scaffold>[];
|
||||
//
|
||||
// The paper moved: the scaffold's own background is the **brand** now, so
|
||||
// it shows through the two rounded top corners of [MilerSheet], and the
|
||||
// sheet paints the page. Both halves are contracts — a tab whose ground is
|
||||
// white draws no corners at all, and a tab whose sheet is off-white is the
|
||||
// original bug one layer down — so both are asserted.
|
||||
T? firstOf<T extends Widget>() {
|
||||
final found = <T>[];
|
||||
void visit(Element e) {
|
||||
final w = e.widget;
|
||||
if (w is Scaffold) scaffolds.add(w);
|
||||
if (w is T) found.add(w);
|
||||
e.visitChildren(visit);
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.rootElement!.visitChildren(visit);
|
||||
return scaffolds.isEmpty ? null : scaffolds.first.backgroundColor;
|
||||
return found.isEmpty ? null : found.first;
|
||||
}
|
||||
|
||||
SharedPreferences.setMockInitialValues({'userId': 1});
|
||||
@@ -193,7 +181,67 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(bgOf(page), ColorConstants.pureSurface);
|
||||
|
||||
expect(
|
||||
firstOf<Scaffold>()?.backgroundColor,
|
||||
ColorConstants.primary,
|
||||
reason: 'the ground behind the sheet corners must be the brand',
|
||||
);
|
||||
final sheet = firstOf<MilerSheet>();
|
||||
expect(sheet, isNotNull, reason: 'every tab sits in a MilerSheet');
|
||||
// What the tabs must agree on is the *ladder*, not one flat paper:
|
||||
// a sheet is either the working white (rows drawn straight on it —
|
||||
// Activity) or the canvas with white panels standing on it (Home,
|
||||
// Account). What stays banned is what this test was written against —
|
||||
// the warm off-white that made one tab read a shade dirtier than the
|
||||
// rest.
|
||||
expect(
|
||||
sheet!.color ?? ColorConstants.pureSurface,
|
||||
anyOf(ColorConstants.pureSurface, ColorConstants.daylightSurface),
|
||||
reason: 'a sheet is a rung of the surface ladder, never an off-white',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ── A row offers one gesture, and it is the whole row ──
|
||||
//
|
||||
// The card carried a footer band: a hairline, ~22pt of gaps, the booking
|
||||
// reference, and a red bold `Details ›` — which had **no `onTap`**. The
|
||||
// GlassCard around it was the gesture and always had been, so the loudest
|
||||
// element on a finished stop was a link that could not be pressed, pointing
|
||||
// at the tap the rider was already making. It cost ~41pt on every row of a
|
||||
// page that exists to be scanned.
|
||||
//
|
||||
// The reference stayed — the office recites it down a phone at him — and
|
||||
// moved onto the right edge of the measurements line, which was empty.
|
||||
testWidgets('a finished row has no second way in', (tester) async {
|
||||
await pumpActivity(tester);
|
||||
|
||||
expect(
|
||||
find.text('Details'),
|
||||
findsNothing,
|
||||
reason: 'a control that cannot be pressed is worse than no control',
|
||||
);
|
||||
// The chevron RETURNED, deliberately, after the Home disclosure pass
|
||||
// taught the difference: the sin was a bold red "Details ›" styled as a
|
||||
// second button; a quiet border-grey chevron on the name line is the
|
||||
// opposite thing — the passive "this goes somewhere" signal every
|
||||
// Account row wears, marking the gesture the whole card owns.
|
||||
expect(
|
||||
find.byIcon(LucideIcons.chevronRight),
|
||||
findsWidgets,
|
||||
reason: 'a tappable row without any affordance is a secret',
|
||||
);
|
||||
|
||||
// The reference is on the record page now, not the row — see above.
|
||||
expect(find.text('#DM240817'), findsNothing);
|
||||
});
|
||||
|
||||
// ── `the reference shares the measurements line` is gone ──
|
||||
//
|
||||
// It pinned the reference and the duration onto one storey, which was the
|
||||
// right fix while the reference was on the row at all. It is not on the row
|
||||
// any more, so the assertion has no subject — and a test kept alive by
|
||||
// rewriting it to check something it was never about is worse than one
|
||||
// deleted with its reasoning recorded.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user