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)); }); }); }