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:
280
test/account_page_test.dart
Normal file
280
test/account_page_test.dart
Normal file
@@ -0,0 +1,280 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/controllers/rewards_controller.dart';
|
||||
import 'package:miler/controllers/summary_controller.dart';
|
||||
import 'package:miler/data/service_profile.dart';
|
||||
import 'package:miler/views/Dashboard/profile/Profilepage.dart';
|
||||
import 'package:miler/views/Dashboard/profile/informations/help_center.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/design_constants.dart';
|
||||
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
||||
import 'package:miler/views/helpers/widgets/settings_ui.dart';
|
||||
|
||||
/// ── The rider's control centre, not a settings screen ──
|
||||
///
|
||||
/// Account opened on four boxes: an outlined card holding the rider's name, and
|
||||
/// three more side by side holding two figures and a constant, each with a
|
||||
/// saturated brand glyph on it. Under them, twelve rows whose icons were drawn
|
||||
/// in the same near-black as the words they labelled, and at the bottom a
|
||||
/// red-bordered notice with Logout tucked under it in the brand colour, at the
|
||||
/// same weight as the row that picks an alert tone.
|
||||
///
|
||||
/// It is one page on one surface now: the identity on the paper, the figures on
|
||||
/// a single quiet fill, the icons demoted under the titles they mark, and the
|
||||
/// one row that ends the session set apart and drawn destructive.
|
||||
///
|
||||
/// `profile_pages_test.dart` holds the bars on every screen this page opens.
|
||||
/// This file holds the page itself.
|
||||
void main() {
|
||||
Future<void> pump(
|
||||
WidgetTester tester, {
|
||||
Map<String, Object> prefs = const {},
|
||||
ServiceProfile profile = ServiceProfile.parcel,
|
||||
int points = 1240,
|
||||
int pickups = 318,
|
||||
String figuresError = '',
|
||||
double height = 1600,
|
||||
}) async {
|
||||
ServiceProfile.setActive(profile);
|
||||
// 0 keeps the page from firing its two network fetches, so the figures
|
||||
// seeded below are the ones under test.
|
||||
SharedPreferences.setMockInitialValues({'userid': 0, ...prefs});
|
||||
|
||||
Get.put(RewardsController())
|
||||
..totalPoints.value = points
|
||||
..isLoading.value = false
|
||||
..error.value = figuresError;
|
||||
Get.put(SummaryController())
|
||||
..total.value = pickups
|
||||
..isLoading.value = false;
|
||||
|
||||
tester.view.physicalSize = Size(390 * 3, height * 3);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => const MaterialApp(home: ProfilePage()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
tearDown(() {
|
||||
Get.reset();
|
||||
ServiceProfile.setActive(ServiceProfile.parcel);
|
||||
});
|
||||
|
||||
const rider = <String, Object>{
|
||||
'user_name': 'Joe Mathew',
|
||||
'user_email': 'joe.mathew@doormile.in',
|
||||
'contactno': '+91 98765 43210',
|
||||
};
|
||||
|
||||
testWidgets('identity leads, and it is not a card', (tester) async {
|
||||
await pump(tester, prefs: rider);
|
||||
|
||||
expect(find.text('Joe Mathew'), findsOneWidget);
|
||||
expect(find.text('+91 98765 43210'), findsOneWidget);
|
||||
|
||||
// The header, the three figures and every section used to be a
|
||||
// `SettingsCard`. Nothing on this page is one now — the only fill above the
|
||||
// fold is the strip holding the figures, and it carries no outline.
|
||||
expect(find.byType(SettingsCard), findsNothing);
|
||||
|
||||
// "View profile" was a second control for the destination the row already
|
||||
// goes to, taking a third line to say so.
|
||||
expect(find.text('View profile'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the line of work is read, never named', (tester) async {
|
||||
await pump(tester, prefs: rider);
|
||||
expect(find.text(ServiceProfile.parcel.label), findsOneWidget);
|
||||
expect(find.text('Bookings'), findsOneWidget);
|
||||
expect(find.text('What a new booking sounds like'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
Get.reset();
|
||||
|
||||
await pump(tester, prefs: rider, profile: ServiceProfile.milkMan);
|
||||
expect(find.text(ServiceProfile.milkMan.label), findsOneWidget);
|
||||
// A milk-run rider does not take bookings, and every stop at a customer's
|
||||
// door is a drop. Both strings follow `ServiceProfile.active`.
|
||||
expect(find.text('Deliveries'), findsOneWidget);
|
||||
expect(find.text('What a new delivery sounds like'), findsOneWidget);
|
||||
expect(find.text('Bookings'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a rider who has entered nothing is told nothing', (
|
||||
tester,
|
||||
) async {
|
||||
await pump(tester);
|
||||
|
||||
// These defaulted to `rider@miler.com` and `Not added` and were rendered as
|
||||
// facts — so a new rider's own account page stated an email he had never
|
||||
// given. A field with nothing behind it is a field the page does not draw.
|
||||
expect(find.textContaining('rider@miler.com'), findsNothing);
|
||||
expect(find.text('Not added'), findsNothing);
|
||||
// The row it belonged to is still there, without its second line.
|
||||
expect(find.text('Edit Profile'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a figure that could not be fetched is a gap, not a zero', (
|
||||
tester,
|
||||
) async {
|
||||
await pump(tester, prefs: rider, figuresError: "Couldn't load your points");
|
||||
|
||||
expect(find.text('—'), findsOneWidget);
|
||||
expect(find.text('1,240'), findsNothing);
|
||||
|
||||
// Compact and actionable: one line and a retry, not a screen the whole tab
|
||||
// has to sit in — everything else on this page is read off the phone.
|
||||
expect(find.text('Could not refresh these figures'), findsOneWidget);
|
||||
expect(find.text('Retry'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ending the session is not a preference', (tester) async {
|
||||
await pump(tester, prefs: rider);
|
||||
|
||||
final logout = tester.widget<Text>(find.text('Logout'));
|
||||
// Brand maroon is navigation and identity; red destroys. Logout was drawn
|
||||
// in the brand colour, which made the row that ends the session the same
|
||||
// colour as the wordmark above it.
|
||||
expect(logout.style!.color, ColorConstants.errorRed);
|
||||
|
||||
// Nothing is being navigated to, so there is no chevron on it — and it sits
|
||||
// below a rule with a screenful of air above, rather than 18pt under the
|
||||
// emergency banner in the shape of a settings group.
|
||||
final row = find.ancestor(
|
||||
of: find.text('Logout'),
|
||||
matching: find.byType(SettingsRow),
|
||||
);
|
||||
expect(tester.widget<SettingsRow>(row).danger, isTrue);
|
||||
|
||||
expect(
|
||||
tester.getRect(find.text('Logout')).top -
|
||||
tester.getRect(find.byType(EmergencyRow)).bottom,
|
||||
greaterThan(30),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the emergency row is a wash, not an outline', (tester) async {
|
||||
await pump(tester, prefs: rider);
|
||||
|
||||
final box = tester.widget<Container>(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(EmergencyRow),
|
||||
matching: find.byType(Container),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
final decoration = box.decoration! as BoxDecoration;
|
||||
expect(decoration.border, isNull, reason: 'no red outline');
|
||||
expect(decoration.color, isNot(ColorConstants.primary));
|
||||
|
||||
// The words are the page's own near-black so they read at a glance; the
|
||||
// red is spent on the two marks that carry the meaning.
|
||||
expect(
|
||||
tester.widget<Text>(find.text('Emergency help')).style!.color,
|
||||
ColorConstants.slateText,
|
||||
);
|
||||
expect(
|
||||
find.text('Calls 112 if you are in immediate danger'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Help & Support draws the same emergency row', (tester) async {
|
||||
SharedPreferences.setMockInitialValues({'userid': 0});
|
||||
tester.view.physicalSize = const Size(390 * 3, 900 * 3);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) => const MaterialApp(home: HelpCenter()),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// It used to be a second, differently-worded copy of the same banner. The
|
||||
// sentence promising a rider what a press will do is not a thing to have
|
||||
// two versions of.
|
||||
expect(find.byType(EmergencyRow), findsOneWidget);
|
||||
expect(find.text('Emergency help'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('every row is a target a thumb can hit', (tester) async {
|
||||
await pump(tester, prefs: rider);
|
||||
|
||||
for (final title in const [
|
||||
'Earnings',
|
||||
'Edit Profile',
|
||||
'Saved Address',
|
||||
'Alert Sound',
|
||||
'Notifications',
|
||||
'Help & Support',
|
||||
'FAQ',
|
||||
'Terms & Conditions',
|
||||
'Logout',
|
||||
]) {
|
||||
final row = find.ancestor(
|
||||
of: find.text(title),
|
||||
matching: find.byType(SettingsRow),
|
||||
);
|
||||
expect(row, findsOneWidget, reason: '"$title" is a row');
|
||||
expect(
|
||||
tester.getSize(row).height,
|
||||
greaterThanOrEqualTo(ButtonSizes.minTapTarget),
|
||||
reason: '"$title" is smaller than a fingertip',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('the icons do not outrank the titles they mark', (tester) async {
|
||||
await pump(tester, prefs: rider);
|
||||
|
||||
// Twelve glyphs in `onSurface` read as loudly as the twelve things they
|
||||
// were labelling. The title leads; the icon locates it.
|
||||
final icon = tester.widget<Icon>(
|
||||
find
|
||||
.descendant(
|
||||
of: find.ancestor(
|
||||
of: find.text('Saved Address'),
|
||||
matching: find.byType(SettingsRow),
|
||||
),
|
||||
matching: find.byType(Icon),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
expect(icon.color, ColorConstants.secondaryText);
|
||||
expect(icon.color, isNot(ColorConstants.onSurface));
|
||||
});
|
||||
|
||||
testWidgets('a skeleton fills what it is given and stops', (tester) async {
|
||||
// Five placeholder cards are ~820pt tall and the body of a pushed page on
|
||||
// the shortest supported phone is ~768, so the screen whose whole job is to
|
||||
// say "this is coming" greeted the rider with an overflow band.
|
||||
tester.view.physicalSize = const Size(390 * 3, 700 * 3);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(390, 844),
|
||||
builder: (_, _) =>
|
||||
const MaterialApp(home: Scaffold(body: SkeletonList(count: 5))),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user