Files
doormile_milderapp/test/activity_list_test.dart
Thiru-tenext d7348e253f 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>
2026-08-22 05:40:35 +05:30

248 lines
9.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
}