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>
248 lines
9.9 KiB
Dart
248 lines
9.9 KiB
Dart
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/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 ──
|
||
///
|
||
/// Every finished stop was a filled, rounded card with 12pt of air under it,
|
||
/// and inside each one a *second*, white panel holding the three figures. So the
|
||
/// page came down the screen as grey slabs with white slabs punched into them —
|
||
/// the card-inside-card shape, repeated once per stop — and on top of that every
|
||
/// row opened with a 40pt green disc that was identical on all of them.
|
||
///
|
||
/// Finished stops are all the same kind of thing, arriving in one order, read by
|
||
/// scanning down. That is a list, and a list is separated by rules.
|
||
void main() {
|
||
String day(DateTime t) =>
|
||
'${t.year}-${t.month.toString().padLeft(2, '0')}-'
|
||
'${t.day.toString().padLeft(2, '0')}';
|
||
|
||
Map<String, dynamic> finished(
|
||
String id,
|
||
String name,
|
||
String address, {
|
||
required String type,
|
||
double cash = 0,
|
||
int minsAgo = 30,
|
||
}) {
|
||
final now = DateTime.now();
|
||
final at = now.subtract(Duration(minutes: minsAgo));
|
||
return {
|
||
'orderid': id,
|
||
'type': type,
|
||
'pickupcustomer': name,
|
||
'pickupaddress': address,
|
||
'orderstatus': 'picked',
|
||
'completedday': day(now),
|
||
'completedat': at.toIso8601String(),
|
||
'startedat': at.subtract(const Duration(minutes: 14)).toIso8601String(),
|
||
if (cash > 0) 'collectionamt': '$cash',
|
||
'proof': {if (cash > 0) 'amountcollected': cash},
|
||
};
|
||
}
|
||
|
||
Future<void> pumpActivity(WidgetTester tester) async {
|
||
SharedPreferences.setMockInitialValues({'userId': 1});
|
||
await debugSeedStore(
|
||
'completed_bookings',
|
||
jsonEncode([
|
||
finished(
|
||
'DM240817',
|
||
'Sundar Textiles',
|
||
'78, Cross Cut Road, Gandhipuram, Coimbatore',
|
||
type: 'combined',
|
||
cash: 2450,
|
||
minsAgo: 22,
|
||
),
|
||
finished(
|
||
'DM240816',
|
||
'Green Leaf Grocers',
|
||
'29, Kovaipudur Main Road, Coimbatore',
|
||
type: 'delivery',
|
||
minsAgo: 68,
|
||
),
|
||
]),
|
||
);
|
||
|
||
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
|
||
tester.view.devicePixelRatio = 3.0;
|
||
addTearDown(tester.view.reset);
|
||
|
||
await tester.pumpWidget(
|
||
ScreenUtilInit(
|
||
designSize: const Size(390, 844),
|
||
builder: (_, _) => const MaterialApp(home: ActivityPage()),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||
}
|
||
|
||
testWidgets('a finished stop is not a card', (tester) async {
|
||
await pumpActivity(tester);
|
||
expect(find.text('Sundar Textiles'), findsOneWidget);
|
||
|
||
// `cardSurface` was the row fill and `pureSurface` the fill of the figure
|
||
// panel nested inside it. Neither may come back: on a white page a filled
|
||
// row is a box round a list item, and a filled panel inside a filled row is
|
||
// the shape this page was rebuilt to remove.
|
||
final fills = tester
|
||
.widgetList<Container>(find.byType(Container))
|
||
.map((c) => c.decoration)
|
||
.whereType<BoxDecoration>()
|
||
.map((d) => d.color)
|
||
.toList();
|
||
|
||
expect(fills, isNot(contains(ColorConstants.cardSurface)));
|
||
});
|
||
|
||
// ── 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 subject of the row.
|
||
expect(find.text('Sundar Textiles'), findsOneWidget);
|
||
|
||
// `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);
|
||
|
||
// `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);
|
||
|
||
// ── 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', (
|
||
tester,
|
||
) async {
|
||
// 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.
|
||
//
|
||
// 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 T) found.add(w);
|
||
e.visitChildren(visit);
|
||
}
|
||
|
||
WidgetsBinding.instance.rootElement!.visitChildren(visit);
|
||
return found.isEmpty ? null : found.first;
|
||
}
|
||
|
||
SharedPreferences.setMockInitialValues({'userId': 1});
|
||
tester.view.physicalSize = const Size(390 * 3, 844 * 3);
|
||
tester.view.devicePixelRatio = 3.0;
|
||
addTearDown(tester.view.reset);
|
||
|
||
for (final page in <Widget>[const ActivityPage(), const ProfilePage()]) {
|
||
await tester.pumpWidget(
|
||
ScreenUtilInit(
|
||
designSize: const Size(390, 844),
|
||
builder: (_, _) => MaterialApp(home: page),
|
||
),
|
||
);
|
||
await tester.pump();
|
||
|
||
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.
|
||
}
|