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

259 lines
9.7 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 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:miler/views/Dashboard/profile/informations/faq.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/views/helpers/constants/miler_type.dart';
import 'package:miler/views/helpers/widgets/app_widgets.dart';
/// The pages you can navigate *into* from Account.
///
/// ── Why these needed a test ──
///
/// Eight pushed pages had six different app bars between them: four solid brand
/// red with white text, two white with black text, one grey, one white with a
/// red title. The back arrow came in four different icons at four sizes in three
/// colours. Two shared helpers already existed and, between them, were used by
/// three pages.
///
/// And the FAQ page **overflowed at any text scale at or above 1.5** — three
/// separate places, on every screen width — which is a black-and-yellow band
/// across a help page for the riders most likely to need help reading it.
///
/// Both classes of bug are invisible in a screenshot taken at default settings
/// on the developer's own phone, so both are measured here instead.
void main() {
Future<void> pump(
WidgetTester tester,
Widget page, {
double width = 390,
double textScale = 1.0,
}) async {
tester.view.physicalSize = Size(width * 3, 800 * 3);
tester.view.devicePixelRatio = 3.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
ScreenUtilInit(
designSize: const Size(390, 844),
builder: (_, _) => MaterialApp(
builder: (context, child) => MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.linear(textScale)),
child: child!,
),
home: page,
),
),
);
await tester.pumpAndSettle();
}
group('the shared pushed-page bar', () {
// ── The wash gave way to the brand, everywhere at once ──
//
// This bar wore a frosted 7% brand wash flattened against white while Home
// and the four tabs are solid brand. So pushing Edit Profile, Saved
// Addresses, Alert Sound or Notifications changed the colour of the top of
// the phone AND the colour of the status-bar glyphs — which reads as
// leaving the app rather than opening a page inside it.
//
// One ground now, and everything on it inverts. The contract this test
// still holds is the one that matters: whatever the bar's surface is, the
// type on it has to clear contrast against it, and every bar in the app has
// to agree on the answer.
testWidgets('is solid brand with a white title', (tester) async {
await pump(
tester,
Scaffold(appBar: milerPageBar('Saved Addresses', onBack: () {})),
);
final bar = tester.widget<AppBar>(find.byType(AppBar));
expect(bar.backgroundColor, ColorConstants.primary);
expect(
bar.flexibleSpace,
isNull,
reason: 'a solid ground needs no surface layered over it',
);
expect(
bar.systemOverlayStyle?.statusBarIconBrightness,
Brightness.light,
reason:
'dark glyphs on maroon is the one contrast failure a rider '
'cannot work around by tilting the phone',
);
final title = tester.widget<Text>(find.text('Saved Addresses'));
expect(title.style!.color, Colors.white);
expect(
title.style!.fontSize,
MilerType.pageTitleFixed.fontSize,
reason: 'one page-title size across tabs and pushed screens',
);
// The back control inverts with it, or it disappears into the ground.
final back = tester.widget<Icon>(find.byIcon(LucideIcons.arrowLeft));
expect(back.color, Colors.white);
});
testWidgets('carries a back affordance when given one', (tester) async {
var popped = false;
await pump(
tester,
Scaffold(
appBar: milerPageBar('Notifications', onBack: () => popped = true),
),
);
// ── An arrow, because these pages are pushed ──
//
// The cross promised dismissal on nine screens you actually go *back*
// from, and it was one of three back glyphs in the app. `close_rounded`
// survives behind `isModal: true`, for a sheet that genuinely dismisses.
expect(find.byIcon(LucideIcons.arrowLeft), findsOneWidget);
expect(find.byIcon(LucideIcons.x), findsNothing);
await tester.tap(find.byIcon(LucideIcons.arrowLeft));
await tester.pumpAndSettle();
expect(popped, isTrue);
});
testWidgets('and none when it is not', (tester) async {
await pump(tester, Scaffold(appBar: milerPageBar('Terms')));
expect(find.byIcon(LucideIcons.arrowLeft), findsNothing);
expect(find.byIcon(LucideIcons.x), findsNothing);
});
testWidgets('a modal presentation still gets a cross', (tester) async {
await pump(
tester,
Scaffold(
appBar: milerPageBar('Skip stop', onBack: () {}, isModal: true),
),
);
expect(find.byIcon(LucideIcons.x), findsOneWidget);
expect(find.byIcon(LucideIcons.arrowLeft), findsNothing);
});
testWidgets('survives a long title and a large font', (tester) async {
await pump(
tester,
Scaffold(
appBar: milerPageBar(
'A Very Long Page Title That Will Not Fit',
onBack: () {},
),
),
width: 320,
textScale: 2.0,
);
expect(tester.takeException(), isNull);
});
// ── One line, not two ──
//
// The bar used to carry a caption under the title. It was the smallest type
// on the screen in the one place a rider reads exactly once, and every
// caption duplicated something the page said louder further down — two
// sources for one number, which is how they drift apart.
testWidgets('carries the page name and nothing under it', (tester) async {
await pump(tester, Scaffold(appBar: milerPageBar('Notifications')));
final texts = find.byType(Text);
expect(tester.widgetList<Text>(texts), hasLength(1));
expect(find.text('Notifications'), findsOneWidget);
});
// ── The × is stacked ABOVE the heading, on the same margin ──
//
// It used to share the row: `[×] Edit Profile`. That pushed every title on
// every pushed page 52pt in from the margin, out of line with the content
// beneath it, and a control sitting at a title's left at the same optical
// weight reads as part of the title rather than as something you press.
testWidgets('stacks the close above the title', (tester) async {
await pump(
tester,
Scaffold(appBar: milerPageBar('Edit Profile', onBack: () {})),
);
final close = tester.getRect(find.byIcon(LucideIcons.arrowLeft));
final title = tester.getRect(find.text('Edit Profile'));
expect(
close.bottom,
lessThanOrEqualTo(title.top),
reason: 'the back control must sit on its own row above the heading',
);
expect(
close.left,
closeTo(title.left, 0.5),
reason:
'they share the page margin — an IconButton could not do this, '
'which is why the close is hand-rolled. See _TopBarClose.',
);
});
testWidgets('gives the back control a full 48pt target', (tester) async {
await pump(
tester,
Scaffold(appBar: milerPageBar('Edit Profile', onBack: () {})),
);
final box = tester.getSize(
find
.ancestor(
of: find.byIcon(LucideIcons.arrowLeft),
matching: find.byType(SizedBox),
)
.first,
);
// 48, not the 44pt accessibility floor: this is the control a rider hits
// one-handed, and the glyph inside stays 24 so only the target grew.
expect(box.width, 48);
expect(box.height, 48);
});
});
group('FAQ lays out at every font scale', () {
// It did not. `Container(height: 160.h)` around the hero overflowed by 89px
// at 1.5x; the category grid's `childAspectRatio` derived cell height from
// the column *width*, so all four tiles overflowed by ~10px; and the SOS
// row's two unflexed children ran 30px past the card.
for (final width in [320.0, 360.0, 390.0, 430.0]) {
for (final scale in [1.0, 1.3, 1.5, 2.0]) {
testWidgets('at ${width.toInt()}pt / ${scale}x', (tester) async {
await pump(tester, const FaqPage(), width: width, textScale: scale);
expect(
tester.takeException(),
isNull,
reason: 'FAQ overflowed at ${width.toInt()}pt / ${scale}x',
);
// Opening the accordions is where the page grows the most.
final tiles = find.byType(ExpansionTile);
for (var i = 0; i < tiles.evaluate().length; i++) {
await tester.tap(tiles.at(i), warnIfMissed: false);
await tester.pumpAndSettle();
expect(
tester.takeException(),
isNull,
reason:
'FAQ overflowed opening accordion $i at '
'${width.toInt()}pt / ${scale}x',
);
}
});
}
}
});
// Terms & Conditions is deliberately not laid out here: it hosts a
// `WebView`, and `WebViewPlatform.instance` has no implementation in a headless
// unit test, so the page cannot be built at all. Its app bar is the shared one
// — that much is covered by the group above — but the body needs a device or
// an integration test.
}