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
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user