Files
doormile_milderapp/test/activity_list_test.dart
2026-08-28 11:13:15 +05:30

327 lines
13 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/constants/narrative.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 became the **brand**, so
// it could show through the two rounded top corners [MilerSheet] used to
// clip, and the sheet painted the page.
//
// ── Why the ground is no longer a fixed colour ──
//
// That arrangement is gone. The curve moved onto the bar, which paints the
// page colour behind its own corners (see [milerBrandSurface]), and
// [MilerSheet] clips at zero radius. A brand-red ground is therefore no
// longer load-bearing — it is only invisible for as long as the sheet
// covers every pixel of the body.
//
// On Activity it did not. The sheet was pushed down 30pt to make room for
// the floating figure card, and what showed in the gap was a full-width
// band of maroon directly under the app bar — bare down both gutters, and
// on a day with nothing finished (no card built at all) a solid red stripe
// across the screen. It read as a second, broken app bar.
//
// So the ground is asserted against the thing that actually matters: it
// must be either the brand — fine, where the sheet covers it whole — or
// the sheet's own colour, which is the only other way a band cannot
// appear. What stays banned is a *third* colour, which is how a tab ends
// up a shade dirtier than the rest. See also the Activity-specific test
// below, which pins the case that broke.
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();
final sheet = firstOf<MilerSheet>();
expect(sheet, isNotNull, reason: 'every tab sits in a MilerSheet');
final paper = sheet!.color ?? ColorConstants.pureSurface;
expect(
firstOf<Scaffold>()?.backgroundColor,
anyOf(ColorConstants.primary, paper),
reason:
'a ground that is neither the brand nor the page shows as a band',
);
// 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(
paper,
anyOf(
ColorConstants.pureSurface,
ColorConstants.daylightSurface,
// Activity was rebuilt against the Narrative brief, which pairs a
// *lighter* canvas with cards that carry a border and a lift. Its
// ground is a rung of that ladder rather than this one — see
// [Narrative], and the note there about not mixing the two recipes.
Narrative.canvas,
),
reason: 'a sheet is a rung of a surface ladder, never an off-white',
);
}
});
testWidgets('Activity shows no brand band under the app bar', (
tester,
) async {
// The regression, pinned at the layer it happened on. Activity is the one
// tab that floats a card over the seam, so it is the one tab whose sheet
// was inset — and an inset sheet over a brand-red ground is a red stripe.
//
// Its ground is the page now, so there is nothing to leak whatever the
// sheet does, and the empty-day case — where the card is not built at all
// and the stripe was full width — is covered by the same fact.
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);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => const MaterialApp(home: ActivityPage()),
),
);
await tester.pump();
final ground = firstOf<Scaffold>()?.backgroundColor;
expect(
ground,
isNot(ColorConstants.primary),
reason: 'the 30pt maroon band under the bar was this colour',
);
expect(
ground,
firstOf<MilerSheet>()?.color ?? ColorConstants.pureSurface,
reason: 'ground and page must match, so no inset can expose a band',
);
});
// ── 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.
}