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>
178 lines
5.7 KiB
Dart
178 lines
5.7 KiB
Dart
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();
|
||
});
|
||
}
|