163 lines
6.9 KiB
Dart
163 lines
6.9 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:miler/views/helpers/constants/app_theme.dart';
|
||
|
||
/// The stop card's CTA is an `ElevatedButton.icon` inside a fixed-height
|
||
/// `SizedBox`, and the app theme styles `ElevatedButton` for full-width FORM
|
||
/// buttons: `padding: vertical 18` and a 56dp `minimumSize`.
|
||
///
|
||
/// Those two defaults are a trap for any button in a list row. `styleFrom`
|
||
/// replaces only the keys it is handed, so a card that overrides colour, shape
|
||
/// and elevation still inherits the form metrics. The padding then eats the
|
||
/// height from the inside: at 48dp the content band is 48 − 36 = **12px**, and
|
||
/// the label and icon are squeezed into it — glyphs cut through the middle,
|
||
/// icon painted at 12px instead of its declared 19px.
|
||
///
|
||
/// Two things make it worth a test. It is **silent** — no overflow exception,
|
||
/// nothing in the console, the button just renders half a word — and it is
|
||
/// invisible at 56dp, where the band is exactly 20px and one line happens to
|
||
/// fit. So the bug is dormant in any card using the theme's metrics and wakes
|
||
/// up the moment someone changes the height.
|
||
///
|
||
/// `PickupCard` is private to the `pickups.dart` library and cannot be
|
||
/// imported, so — as in `card_stripe_layout_test.dart` — these tests pin the
|
||
/// idiom rather than the widget.
|
||
void main() {
|
||
const iconSize = 19.0;
|
||
const labelFontSize = 14.5;
|
||
const label = 'Start Combined Stop';
|
||
|
||
/// The card's CTA, at the height the card gives it.
|
||
Widget cta({required bool withOverrides, double height = 48}) {
|
||
return MaterialApp(
|
||
theme: AppTheme.light,
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 268, // CTA share of a 390pt-wide card, beside the ⋯ button
|
||
child: SizedBox(
|
||
height: height,
|
||
child: ElevatedButton.icon(
|
||
onPressed: () {},
|
||
icon: const Icon(Icons.arrow_forward_rounded, size: iconSize),
|
||
label: const Text(
|
||
label,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.w800, fontSize: labelFontSize),
|
||
),
|
||
style: withOverrides
|
||
? ElevatedButton.styleFrom(
|
||
elevation: 0,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
)
|
||
: ElevatedButton.styleFrom(elevation: 0),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
testWidgets('the themed form metrics squeeze a 48dp CTA', (tester) async {
|
||
await tester.pumpWidget(cta(withOverrides: false));
|
||
|
||
// Nothing is thrown — this is the failure mode that ships.
|
||
expect(tester.takeException(), isNull);
|
||
|
||
// 48 − (18 + 18) of inherited vertical padding leaves a 12px content band,
|
||
// and everything in the button is crushed into it. The icon is the clean
|
||
// witness: it asked for 19px and cannot have been drawn correctly at 12.
|
||
final content = tester.getRect(find.byIcon(Icons.arrow_forward_rounded));
|
||
expect(content.height, lessThan(iconSize),
|
||
reason: 'this is the squeeze that cut the button label in half');
|
||
expect(tester.getRect(find.text(label)).height, lessThan(labelFontSize),
|
||
reason: 'the label box is shorter than one line of its own text');
|
||
});
|
||
|
||
testWidgets('overriding padding and minimumSize lets the label fit',
|
||
(tester) async {
|
||
await tester.pumpWidget(cta(withOverrides: true));
|
||
expect(tester.takeException(), isNull);
|
||
|
||
final button = tester.getRect(find.byType(ElevatedButton));
|
||
final icon = tester.getRect(find.byIcon(Icons.arrow_forward_rounded));
|
||
final text = tester.getRect(find.text(label));
|
||
|
||
// The SizedBox is the single source of truth for the height.
|
||
expect(button.height, 48);
|
||
|
||
// Nothing is squeezed: the icon gets the size it asked for, and the label
|
||
// gets at least a full line box.
|
||
expect(icon.height, iconSize);
|
||
expect(text.height, greaterThanOrEqualTo(labelFontSize));
|
||
|
||
// And both sit fully inside the button rather than being clipped by it.
|
||
for (final part in [icon, text]) {
|
||
expect(part.top, greaterThanOrEqualTo(button.top));
|
||
expect(part.bottom, lessThanOrEqualTo(button.bottom));
|
||
expect(part.left, greaterThanOrEqualTo(button.left));
|
||
expect(part.right, lessThanOrEqualTo(button.right));
|
||
}
|
||
});
|
||
|
||
// ── The same trap, on OutlinedButton ──
|
||
//
|
||
// Not a hypothetical: an audit found `sheet.dart` giving a 48dp box to an
|
||
// `OutlinedButton.icon` whose style overrode only the side and the shape. The
|
||
// theme's `outlinedButtonTheme` carries `vertical: 16`, leaving a 16px content
|
||
// band, and the row's 18sp icon was measured painting at **16px**. Silently —
|
||
// this produces no overflow error, which is why it survived.
|
||
group('OutlinedButton inherits the same form metrics', () {
|
||
Widget outlined({required bool withOverrides, double height = 48}) {
|
||
return MaterialApp(
|
||
theme: AppTheme.light,
|
||
home: Scaffold(
|
||
body: Center(
|
||
child: SizedBox(
|
||
width: 220,
|
||
height: height,
|
||
child: OutlinedButton.icon(
|
||
onPressed: () {},
|
||
icon: const Icon(Icons.close_rounded, size: 18),
|
||
label: const Text('Skip stop',
|
||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700)),
|
||
style: withOverrides
|
||
? OutlinedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
)
|
||
: null,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
testWidgets('squeezes the icon without the overrides', (tester) async {
|
||
await tester.pumpWidget(outlined(withOverrides: false));
|
||
expect(tester.takeException(), isNull, reason: 'it fails silently');
|
||
expect(tester.getRect(find.byType(Icon)).height, lessThan(18),
|
||
reason: 'this is the shape that shrank the icon to 16px');
|
||
});
|
||
|
||
testWidgets('and does not with them', (tester) async {
|
||
await tester.pumpWidget(outlined(withOverrides: true));
|
||
expect(tester.takeException(), isNull);
|
||
|
||
final button = tester.getRect(find.byType(OutlinedButton));
|
||
final icon = tester.getRect(find.byType(Icon));
|
||
expect(button.height, 48);
|
||
expect(icon.height, 18, reason: 'the icon gets the size it asked for');
|
||
expect(icon.top, greaterThanOrEqualTo(button.top - 0.01));
|
||
expect(icon.bottom, lessThanOrEqualTo(button.bottom + 0.01));
|
||
});
|
||
});
|
||
}
|