Fix/garage layout batch4 (#86)
* fix(garage): improve landscape layout (#62) * fix(garage): make service dialog responsive (#63) * fix(garage): truncate service type dropdown (#64) * fix(garage): avoid compact landscape card overflow (#62) --------- Co-authored-by: Developer <dev@example.com>
This commit is contained in:
parent
3f64083bdd
commit
74943a8420
|
|
@ -112,14 +112,17 @@ class GarageScreen extends ConsumerWidget {
|
|||
if (isMultiColumn) {
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 550,
|
||||
mainAxisExtent: 420,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: responsive.isShortViewport ? 600 : 550,
|
||||
mainAxisExtent: responsive.isShortViewport ? 300 : 420,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: bikes.length,
|
||||
itemCount: bikes.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == bikes.length) {
|
||||
return _buildAddMotorcycleCard(context, ref, bikes);
|
||||
}
|
||||
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: true);
|
||||
},
|
||||
);
|
||||
|
|
@ -127,8 +130,11 @@ class GarageScreen extends ConsumerWidget {
|
|||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: bikes.length,
|
||||
itemCount: bikes.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == bikes.length) {
|
||||
return _buildAddMotorcycleCard(context, ref, bikes);
|
||||
}
|
||||
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: false);
|
||||
},
|
||||
);
|
||||
|
|
@ -136,19 +142,6 @@ class GarageScreen extends ConsumerWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: bikesAsync.value != null && bikesAsync.value!.isNotEmpty
|
||||
? FloatingActionButton.extended(
|
||||
key: const ValueKey('garage_add_motorcycle_fab'),
|
||||
backgroundColor: colors.cyberCyan,
|
||||
foregroundColor: Colors.black,
|
||||
icon: const Icon(Icons.two_wheeler),
|
||||
label: const Text(
|
||||
'Add Motorcycle',
|
||||
style: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.2),
|
||||
),
|
||||
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +166,27 @@ class GarageScreen extends ConsumerWidget {
|
|||
context.push('/garage/add');
|
||||
}
|
||||
|
||||
Widget _buildAddMotorcycleCard(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
List<GarageMotorcycle> bikes,
|
||||
) {
|
||||
final colors = context.motorColors;
|
||||
return Card(
|
||||
child: Center(
|
||||
child: OutlinedButton.icon(
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add Motorcycle'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: colors.cyberCyan,
|
||||
side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.6)),
|
||||
),
|
||||
onPressed: () => _onAddMotorcycle(context, ref, bikes),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(
|
||||
BuildContext context,
|
||||
MotorSignalThemeColors colors,
|
||||
|
|
@ -306,7 +320,7 @@ class GarageScreen extends ConsumerWidget {
|
|||
year: bike.year,
|
||||
make: bike.makeName,
|
||||
model: bike.modelName,
|
||||
height: 150,
|
||||
height: isGrid ? 100 : 150,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
|
|||
|
|
@ -1207,17 +1207,29 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
builder: (context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final viewInsets = MediaQuery.viewInsetsOf(context);
|
||||
return AlertDialog(
|
||||
scrollable: true,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 560,
|
||||
maxHeight: size.height - viewInsets.bottom - 48,
|
||||
),
|
||||
title: const Text('Log New Service'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: selectedType,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(labelText: 'Service Type'),
|
||||
items: serviceTypes.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
||||
items: serviceTypes
|
||||
.map((t) => DropdownMenuItem(
|
||||
value: t,
|
||||
child: Text(t, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) setState(() => selectedType = val);
|
||||
},
|
||||
|
|
@ -1268,7 +1280,6 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||
ElevatedButton(
|
||||
|
|
@ -1340,8 +1351,14 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
|||
children: [
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: selectedType,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(labelText: 'Service Type'),
|
||||
items: serviceTypes.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
||||
items: serviceTypes
|
||||
.map((t) => DropdownMenuItem(
|
||||
value: t,
|
||||
child: Text(t, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) setState(() => selectedType = val);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'package:motorcycle_dash/features/records/records_service.dart';
|
|||
import 'package:motorcycle_dash/features/garage/garage_screen.dart';
|
||||
import 'package:motorcycle_dash/features/garage/motorcycle_detail_screen.dart';
|
||||
import 'package:motorcycle_dash/features/garage/add_motorcycle_screen.dart';
|
||||
import 'package:motorcycle_dash/features/garage/widgets/motorcycle_photo.dart';
|
||||
|
||||
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
|
||||
@override
|
||||
|
|
@ -340,5 +341,86 @@ void main() {
|
|||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
});
|
||||
|
||||
group('Garage responsive regression coverage', () {
|
||||
testWidgets('BUG-015: landscape garage card and Add Motorcycle remain reachable', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const GarageScreen()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
final grid = tester.widget<GridView>(find.byType(GridView));
|
||||
final delegate = grid.gridDelegate as SliverGridDelegateWithMaxCrossAxisExtent;
|
||||
expect(delegate.mainAxisExtent, lessThanOrEqualTo(320));
|
||||
expect(tester.getSize(find.byType(MotorcyclePhoto).first).height, lessThanOrEqualTo(120));
|
||||
final addMotorcycle = find.text('Add Motorcycle');
|
||||
await tester.ensureVisible(addMotorcycle);
|
||||
expect(addMotorcycle.hitTestable(), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const MotorcycleDetailScreen(motorcycleId: 1)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('SPECIFICATIONS'));
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(tester.getTopLeft(find.text('SPECIFICATIONS')).dy, lessThan(360));
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
|
||||
for (final keyboardInset in [0.0, 180.0]) {
|
||||
testWidgets('BUG-016: landscape service dialog remains usable with keyboard inset $keyboardInset', (tester) async {
|
||||
tester.view.physicalSize = const Size(800, 360);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
tester.view.viewInsets = FakeViewPadding(bottom: keyboardInset);
|
||||
addTearDown(() {
|
||||
tester.view.resetPhysicalSize();
|
||||
tester.view.resetViewInsets();
|
||||
});
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const MotorcycleDetailScreen(motorcycleId: 1)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Log Service'));
|
||||
await tester.tap(find.text('Log Service'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('Log New Service'), findsOneWidget);
|
||||
final notesLabel = find.text('Notes (e.g. Brand, weight, shop)');
|
||||
await tester.ensureVisible(notesLabel);
|
||||
expect(tester.getBottomRight(notesLabel).dy, lessThan(360 - keyboardInset));
|
||||
expect(find.text('Log New Service').hitTestable(), findsNothing);
|
||||
expect(find.text('Cancel').hitTestable(), findsOneWidget);
|
||||
expect(find.text('Save Record').hitTestable(), findsOneWidget);
|
||||
await tester.tap(find.text('Cancel'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Log New Service'), findsNothing);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('BUG-017: narrow portrait service type dropdown truncates label without overflow', (tester) async {
|
||||
tester.view.physicalSize = const Size(360, 640);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(() => tester.view.resetPhysicalSize());
|
||||
|
||||
await tester.pumpWidget(buildTestApp(const MotorcycleDetailScreen(motorcycleId: 1)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Log Service'));
|
||||
await tester.tap(find.text('Log Service'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
final dropdown = tester.widget<DropdownButton<String>>(find.byType(DropdownButton<String>).first);
|
||||
expect(dropdown.isExpanded, isTrue);
|
||||
final label = tester.widget<Text>(find.text('Engine Oil & Filter').first);
|
||||
expect(label.maxLines, 1);
|
||||
expect(label.overflow, TextOverflow.ellipsis);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue