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
186 lines
6.6 KiB
Dart
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");
|
|
});
|
|
}
|
|
}
|