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>
281 lines
10 KiB
Dart
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);
|
|
});
|
|
}
|