327 lines
13 KiB
Dart
327 lines
13 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/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.
|
||
}
|