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,167 @@
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_lucide/flutter_lucide.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:garage_ui/garage_ui.dart";
|
||||
|
||||
import "support/test_scheme.dart";
|
||||
|
||||
Widget _host(Widget child) => Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
// WidgetsLocalizations, which ReorderableList wants for its drag
|
||||
// semantics. A WidgetsApp brings them; this host isnt one, so it says
|
||||
// so itself rather than the widget quietly shadowing an app's locale.
|
||||
child: Localizations(
|
||||
locale: const Locale("en", "US"),
|
||||
delegates: const [DefaultWidgetsLocalizations.delegate],
|
||||
child: GarageTheme(
|
||||
data: ThemeData(colorScheme: testScheme, density: const Density()),
|
||||
// deliberately NO Material and no MaterialApp: garage's app shell is
|
||||
// a WidgetsApp, so anything material PropertiesList needs it has to
|
||||
// bring itself. The Overlay is the one thing a WidgetsApp really does
|
||||
// provide (through its Navigator), so the host provides it too.
|
||||
child: Overlay(initialEntries: [OverlayEntry(builder: (_) => child)]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
PropertiesEntry _entry(String id, {PropertiesPin? pin, bool movable = true}) {
|
||||
return PropertiesEntry(
|
||||
id: id,
|
||||
pin: pin,
|
||||
movable: movable,
|
||||
child: Builder(
|
||||
builder: (context) => PropertiesSection(
|
||||
title: id,
|
||||
scheme: GarageTheme.of(context).colorScheme,
|
||||
collapsed: true,
|
||||
onToggle: () {},
|
||||
rows: const [],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets("every section is movable by default", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
PropertiesList(entries: [_entry("a"), _entry("b")], onReorder: (_) {}),
|
||||
),
|
||||
);
|
||||
|
||||
// one grab handle each - the default is that things move
|
||||
expect(find.byIcon(LucideIcons.grip_horizontal), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets("an unmovable section has no handle", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
PropertiesList(
|
||||
entries: [_entry("a"), _entry("b", movable: false)],
|
||||
onReorder: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byIcon(LucideIcons.grip_horizontal), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("a pinned section sits above the list, out of its reach", (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
PropertiesList(
|
||||
// handed over LAST, and it still comes out first
|
||||
entries: [
|
||||
_entry("a"),
|
||||
_entry("b"),
|
||||
_entry("top", pin: PropertiesPin.top),
|
||||
],
|
||||
onReorder: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final top = tester.getTopLeft(find.text("top")).dy;
|
||||
expect(top, lessThan(tester.getTopLeft(find.text("a")).dy));
|
||||
// two movable, so two handles - the pinned one has none
|
||||
expect(find.byIcon(LucideIcons.grip_horizontal), findsNWidgets(2));
|
||||
|
||||
// and it isnt IN the reorderable list at all, which is what stops the
|
||||
// list shifting it out of the way and then bouncing the drop back.
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ReorderableList),
|
||||
matching: find.text("top"),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ReorderableList),
|
||||
matching: find.text("a"),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("with nothing pinned there is no stray list", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
PropertiesList(entries: [_entry("a"), _entry("b")], onReorder: (_) {}),
|
||||
),
|
||||
);
|
||||
expect(find.byType(ReorderableList), findsOneWidget);
|
||||
});
|
||||
|
||||
test("a section is not rebuilt just because its position moved", () {
|
||||
// the whole reason the index lives in a mutable slot. Notifying on it
|
||||
// rebuilt every section in the list each time the gap shifted mid-drag -
|
||||
// rows, text fields and all - which is what made dragging feel like it
|
||||
// was catching.
|
||||
final slot = PropertiesReorderSlot(0);
|
||||
const child = SizedBox();
|
||||
|
||||
final before = PropertiesReorderScope(
|
||||
slot: slot,
|
||||
draggable: true,
|
||||
child: child,
|
||||
);
|
||||
slot.index = 3;
|
||||
final after = PropertiesReorderScope(
|
||||
slot: slot,
|
||||
draggable: true,
|
||||
child: child,
|
||||
);
|
||||
|
||||
expect(after.updateShouldNotify(before), isFalse);
|
||||
|
||||
// but whether it can be dragged at all still is worth a rebuild - thats
|
||||
// the handle appearing or going.
|
||||
final locked = PropertiesReorderScope(
|
||||
slot: slot,
|
||||
draggable: false,
|
||||
child: child,
|
||||
);
|
||||
expect(locked.updateShouldNotify(after), isTrue);
|
||||
});
|
||||
|
||||
testWidgets("a section on its own has no handle at all", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
Builder(
|
||||
builder: (context) => PropertiesSection(
|
||||
title: "alone",
|
||||
scheme: GarageTheme.of(context).colorScheme,
|
||||
collapsed: true,
|
||||
onToggle: () {},
|
||||
rows: const [],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byIcon(LucideIcons.grip_horizontal), findsNothing);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user