Files
doormile_milderapp/test/account_page_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

281 lines
10 KiB
Dart

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);
});
}