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

178 lines
5.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:flutter_test/flutter_test.dart';
/// Home's app bar carries the rider's identity **and the ON/OFF duty toggle**,
/// and it used to be the first sliver inside the page's `CustomScrollView` — so
/// it scrolled away the moment he looked at his second stop, taking the duty
/// control with it.
///
/// The fix is structural: the bar becomes the first child of a `Column` and the
/// scrollable takes the rest via `Expanded`. `_HomePageState` cannot be built in
/// a test (Get, SharedPreferences, Geolocator, live fetches), so — as in
/// `card_stripe_layout_test.dart` — these pin the *idiom* rather than the widget.
void main() {
const barKey = ValueKey('appbar');
const contentKey = ValueKey('tall-content');
// The default test viewport is 800×600 and the bar is 96 tall, so this point
// is always inside the scroll area below it. Gestures are aimed here rather
// than at the content's centre: the content is 2000pt tall, so its centre is
// off-screen and a drag there hit-tests against nothing.
const inScrollArea = Offset(400, 350);
/// The shape Home now uses.
Widget pinned({Future<void> Function()? onRefresh}) {
final scroller = CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Container(key: contentKey, height: 2000, color: Colors.grey),
),
],
);
return MaterialApp(
home: Scaffold(
body: Stack(
children: [
Column(
children: [
Container(key: barKey, height: 96, color: Colors.white),
Expanded(
child: onRefresh == null
? scroller
: RefreshIndicator(onRefresh: onRefresh, child: scroller),
),
],
),
],
),
),
);
}
/// The shape it used to use — bar as a sliver.
Widget scrolling() {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Container(key: barKey, height: 96, color: Colors.white),
),
SliverToBoxAdapter(
child: Container(
key: contentKey,
height: 2000,
color: Colors.grey,
),
),
],
),
),
);
}
testWidgets('as a sliver, the bar scrolls off the top', (tester) async {
await tester.pumpWidget(scrolling());
expect(find.byKey(barKey), findsOneWidget);
await tester.dragFrom(inScrollArea, const Offset(0, -300));
await tester.pumpAndSettle();
// Not merely pushed off-screen — a sliver that leaves the viewport and its
// cache extent is unmounted outright, so the duty toggle stopped existing
// rather than just moving. This is the behaviour being fixed.
expect(
find.byKey(barKey),
findsNothing,
reason: 'this is the behaviour that hid the duty toggle',
);
});
testWidgets('as a Column child, the bar does not move', (tester) async {
await tester.pumpWidget(pinned());
final before = tester.getRect(find.byKey(barKey));
final contentBefore = tester.getRect(find.byKey(contentKey)).top;
await tester.dragFrom(inScrollArea, const Offset(0, -300));
await tester.pumpAndSettle();
expect(
tester.getRect(find.byKey(barKey)),
before,
reason: 'the app bar is fixed',
);
// And the page really did scroll underneath it.
expect(
tester.getRect(find.byKey(contentKey)).top,
lessThan(contentBefore - 200),
);
});
testWidgets('the bar stays put through a long scroll and back', (
tester,
) async {
await tester.pumpWidget(pinned());
final rect = tester.getRect(find.byKey(barKey));
for (final dy in [-600.0, -600.0, 400.0, -900.0, 1200.0]) {
await tester.dragFrom(inScrollArea, Offset(0, dy));
await tester.pumpAndSettle();
expect(tester.getRect(find.byKey(barKey)), rect);
}
});
testWidgets('pull to refresh still fires below the fixed bar', (
tester,
) async {
var fired = 0;
await tester.pumpWidget(
pinned(
onRefresh: () async {
fired++;
await Future<void>.delayed(const Duration(milliseconds: 50));
},
),
);
final rect = tester.getRect(find.byKey(barKey));
await tester.flingFrom(inScrollArea, const Offset(0, 320), 1200);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(fired, 1, reason: 'the gesture still reaches the scroll view');
// The spinner must not drag the bar down with it, which is what the old
// unbounded-overscroll page did to the whole header.
expect(tester.getRect(find.byKey(barKey)), rect);
await tester.pumpAndSettle();
});
testWidgets('the refresh spinner comes down below the bar, not behind it', (
tester,
) async {
await tester.pumpWidget(
pinned(
onRefresh: () async {
await Future<void>.delayed(const Duration(milliseconds: 400));
},
),
);
await tester.flingFrom(inScrollArea, const Offset(0, 320), 1200);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
final spinner = find.byType(RefreshProgressIndicator);
expect(spinner, findsOneWidget);
// `edgeOffset` is no longer needed precisely because the scroll viewport now
// starts under the bar — so the indicator is already in clear space.
expect(
tester.getRect(spinner).top,
greaterThanOrEqualTo(tester.getRect(find.byKey(barKey)).bottom - 0.01),
);
await tester.pumpAndSettle();
});
}