Files
doormile_customer_app/test/sheet_stability_test.dart
Thiru-tenext c7a74c57b8 BOOK NOW, OpenStreetMap, and one segmented control instead of two
── The circle says what pressing it does ──

"ONE TOUCH" named the *mechanism* — one gesture, no form — which is something
the product team knows and a customer has to be taught. Nobody arrives at that
screen wanting a touch; they want a parcel collected. The caption under the
sphere still carries what makes it different from the form below.

Renamed in the comments too. A codebase explaining "One Touch" against a button
that says BOOK NOW is a trap for whoever reads it next.

── OpenStreetMap everywhere ──

One line: the default provider was CARTO, it is `osm`. Nothing else moves —
`DmMapTiles` already reads the template, subdomains, retina flag and
attribution off the provider, so the credit line follows on its own.

One thing recorded on the provider itself rather than left to be discovered:
these are donated servers and the OSM Foundation's tile policy does not permit
a distributed app to lean on them. A block looks like every tile turning into
the ground colour at once, with no other symptom. Moving off it is one define —
`DM_MAP_PROVIDER=carto|maptiler|stadia`, all serving OpenStreetMap data — and
the map_config test now asserts the identifying User-Agent rather than only the
URL, because that is what attributable traffic depends on.

── Orders had a second copy of the segmented control ──

Its own `_Tab`, a pill radius, 3pt of padding and the count folded into the
label's text — beside the pickup window's day switcher, which is DmChoiceChip
in a rounded groove with 4pt of padding and the count in a bubble. Two controls
doing one job, drifting apart a padding value at a time.

It is the same control now, and `_Tab` is gone. DmChoiceChip's horizontal
padding drops 12 → 9: three of them split a 390pt phone and "Cancelled"
truncated to "Cancell…" at the old value. The day switcher has two chips and
acres of room, so it loses nothing.
2026-09-30 11:19:30 +05:30

141 lines
4.9 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:doormile_cx/ui/widgets/states.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// A SHEET DOES NOT CHANGE SIZE WHILE IT IS OPENING
///
/// Tapping BOOK NOW opened a tall sheet that snapped shorter a few frames
/// later. Nothing was wrong with either height — the sheet was simply as tall
/// as whatever [DmAsyncList] happened to be rendering, and that changes twice
/// before the customer has done anything: four skeleton rows at 302pt, then
/// the states, which are nearer 200.
///
/// The modal is still animating up while that swap happens, so the resize is
/// not a subtle reflow. It is the first thing the customer sees.
///
/// Both halves of the fix are held here: the box the list lives in keeps one
/// height across every state the list can be in, and a list handed cached
/// items renders them on the first frame instead of a skeleton over data it
/// already has.
/// ─────────────────────────────────────────────────────────────────────────
void main() {
Widget host(Widget child) => MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(width: 360, child: child),
),
),
);
/// A load that never finishes, so the widget stays in its loading state.
Future<List<String>> never({bool refresh = false}) =>
Completer<List<String>>().future;
group('a fixed box holds its height', () {
testWidgets('loading and loaded measure the same', (tester) async {
const box = 300.0;
await tester.pumpWidget(
host(
SizedBox(
height: box,
child: DmAsyncList<String>(
skeletonRows: 3,
load: never,
emptyTitle: 'none',
emptyMessage: 'none',
builder: (_, items) => Column(children: [for (final i in items) Text(i)]),
),
),
),
);
await tester.pump();
final loading = tester.getSize(find.byType(DmAsyncList<String>));
await tester.pumpWidget(
host(
SizedBox(
height: box,
child: DmAsyncList<String>(
skeletonRows: 3,
initialItems: const ['Tamil Nadu'],
load: never,
emptyTitle: 'none',
emptyMessage: 'none',
builder: (_, items) => Column(children: [for (final i in items) Text(i)]),
),
),
),
);
await tester.pump();
final loaded = tester.getSize(find.byType(DmAsyncList<String>));
expect(loaded, loading, reason: 'the sheet would resize as it opened');
expect(loading.height, box);
});
testWidgets('three skeleton rows fit the smallest box the clamp allows', (
tester,
) async {
// The destination sheet floors its list box at 240. Four rows are 302 and
// overflowed it, which is why that sheet asks for three.
await tester.pumpWidget(
host(const SizedBox(height: 240, child: DmSkeleton(rows: 3))),
);
await tester.pump();
expect(tester.takeException(), isNull);
});
});
group('cached items skip the skeleton', () {
testWidgets('the first frame is the content, not a shimmer', (
tester,
) async {
await tester.pumpWidget(
host(
SizedBox(
height: 300,
child: DmAsyncList<String>(
initialItems: const ['Kerala'],
load: never,
emptyTitle: 'none',
emptyMessage: 'none',
builder: (_, items) =>
Column(children: [for (final i in items) Text(i)]),
),
),
),
);
// No settle, no extra pump: this is the frame the sheet opens on.
expect(find.text('Kerala'), findsOneWidget);
expect(find.byType(DmSkeleton), findsNothing);
});
testWidgets('without them it still loads honestly', (tester) async {
await tester.pumpWidget(
host(
SizedBox(
height: 300,
child: DmAsyncList<String>(
// Three for the same reason the destination sheet asks for
// three: the default of four is 302pt and does not fit here.
skeletonRows: 3,
load: never,
emptyTitle: 'none',
emptyMessage: 'none',
builder: (_, items) =>
Column(children: [for (final i in items) Text(i)]),
),
),
),
);
await tester.pump();
expect(find.byType(DmSkeleton), findsOneWidget);
});
});
}