Files
Garage-SDKs/garage_ui/test/button_centring_test.dart
T
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
2026-09-23 18:49:21 +01:00

186 lines
6.6 KiB
Dart

import "package:flutter/rendering.dart";
import "package:flutter/widgets.dart";
import "package:flutter_test/flutter_test.dart";
import "package:garage_ui/garage_ui.dart";
import "support/test_scheme.dart";
// A centred label sat in an Expanded, so it centred in the space LEFT OVER
// after a leading icon rather than across the button - one icon threw the text
// off centre by half its own width plus the gap.
void main() {
Widget host(Widget child) => Directionality(
textDirection: TextDirection.ltr,
child: GarageTheme(
data: ThemeData(colorScheme: testScheme, density: const Density()),
child: Center(child: SizedBox(width: 300, child: child)),
),
);
Future<double> offset(WidgetTester t, Widget b) async {
await t.pumpWidget(host(b));
return t.getRect(find.text("Label")).center.dx -
t.getRect(find.byType(Button)).center.dx;
}
Button btn({Widget? leading, Widget? trailing, AlignmentGeometry? alignment}) =>
Button(
style: const ButtonStyle.secondary(),
alignment: alignment,
leading: leading,
trailing: trailing,
onPressed: () {},
child: const Text("Label"),
);
const icon = Icon(LucideIcons.mail);
_fieldCentring();
_labelNeverWraps();
testWidgets("centred label is centred with no icons", (t) async {
expect(await offset(t, btn(alignment: Alignment.center)), 0.0);
});
testWidgets("centred label is centred with a leading icon", (t) async {
expect(await offset(t, btn(alignment: Alignment.center, leading: icon)), 0.0);
});
testWidgets("centred label is centred with a trailing icon", (t) async {
expect(await offset(t, btn(alignment: Alignment.center, trailing: icon)), 0.0);
});
testWidgets("centred label is centred with both", (t) async {
expect(
await offset(t,
btn(alignment: Alignment.center, leading: icon, trailing: icon)),
0.0);
});
testWidgets("no alignment means no balancing - the button still shrink-wraps",
(t) async {
// without an alignment there is no centre to miss, and padding the empty
// side out would only make the button wider.
// unconstrained, so the button reports its own intrinsic width
Widget loose(Widget c) => Directionality(
textDirection: TextDirection.ltr,
child: GarageTheme(
data: ThemeData(colorScheme: testScheme, density: const Density()),
child: Center(child: c),
),
);
await t.pumpWidget(loose(btn(leading: icon)));
final unaligned = t.getSize(find.byType(Button)).width;
await t.pumpWidget(loose(btn(alignment: Alignment.center, leading: icon)));
final aligned = t.getSize(find.byType(Button)).width;
// the balanced one is wider by exactly the mirrored icon plus its gap
expect(aligned, greaterThan(unaligned));
expect(aligned - unaligned,
const Density().iconSize + const Density().controlGap);
});
testWidgets("an icon button with a leading icon isnt balanced out", (t) async {
// there is no label in here to sit off centre - both slots are glyphs - so
// the mirror spacer is 15px of nothing. the snap targets button in the
// blender header measured 52 wide for 38 of content because of it.
Widget loose(Widget c) => Directionality(
textDirection: TextDirection.ltr,
child: GarageTheme(
data: ThemeData(colorScheme: testScheme, density: const Density()),
child: Center(child: c),
),
);
await t.pumpWidget(loose(IconButton.outline(
density: ControlDensity.compact,
leading: Icon(LucideIcons.ruler_dimension_line).iconSmall,
icon: Icon(LucideIcons.chevron_down).iconSmall,
onPressed: () {},
)));
const d = Density();
final pad = ControlDensity.compact
.resolveIcon(ThemeData(colorScheme: testScheme, density: d));
expect(
t.getSize(find.byType(IconButton)).width,
pad.horizontal + d.iconSize * 2 + d.controlGap,
);
});
}
// The same defect in TextField: centred text sits in an Expanded, so a leading
// feature pushed it off the field's centre by half that feature plus the gap.
void _fieldCentring() {
Widget host(Widget child) => Directionality(
textDirection: TextDirection.ltr,
child: GarageTheme(
data: ThemeData(colorScheme: testScheme, density: const Density()),
child: Center(child: SizedBox(width: 300, child: child)),
),
);
testWidgets("centred field text is centred with a leading feature",
(t) async {
await t.pumpWidget(host(const TextField(
initialValue: "abc",
readOnly: true,
enabled: false,
textAlign: TextAlign.center,
features: [InputFeature.leading(Icon(LucideIcons.mail))],
)));
final field = t.getRect(find.byType(TextField));
final text = t.getRect(find.text("abc"));
expect((text.center.dx - field.center.dx).abs(), lessThan(0.5));
});
testWidgets("balancing uses a spacer, not a second icon", (t) async {
// a copy of the feature would sit in the tree twice, which is how this
// broke a test that looked for exactly one icon inside a field.
await t.pumpWidget(host(const TextField(
initialValue: "abc",
readOnly: true,
enabled: false,
textAlign: TextAlign.center,
features: [InputFeature.leading(Icon(LucideIcons.mail))],
)));
expect(
find.descendant(
of: find.byType(TextField), matching: find.byIcon(LucideIcons.mail)),
findsOneWidget,
);
});
}
// A leading icon takes width off the label's allowance; Expanded forces the
// label to absorb it, and Text used to resolve that by wrapping - "Use" on one
// line and the rest on a second the button's height hid. It reads as a
// truncation but never was one.
void _labelNeverWraps() {
Widget host(Widget child, double cap) => Directionality(
textDirection: TextDirection.ltr,
child: GarageTheme(
data: ThemeData(colorScheme: testScheme, density: const Density()),
child: Center(child: SizedBox(width: cap, child: Wrap(children: [child]))),
),
);
for (final cap in [400.0, 130.0, 90.0]) {
testWidgets("label stays on one line at width $cap", (t) async {
await t.pumpWidget(host(
Button(
style: const ButtonStyle.primary(),
leading: const Icon(LucideIcons.mail),
onPressed: () {},
child: const Text("Use passkey"),
),
cap,
));
final rp = t.renderObject(find.text("Use passkey")) as RenderParagraph;
expect(rp.size.height, const Density().lineBox,
reason: "a wrapped label is twice this tall");
});
}
}