fix(social): prevent horizontal overflow in GPX export snackbar (#52)
- Wrap GPX export snackbar message Text in Expanded with maxLines: 2 and TextOverflow.ellipsis
- Add ValueKey('gpx_export_button_<id>') for deterministic testing
- Wrap narrow feed header/metric/empty-state Row labels in Flexible to prevent overflow under 320px/360px at 1.5x text scale
- Add BUG-005 widget tests across 320x640, 360x800, and 800x480 viewports at 1.5x text scale
This commit is contained in:
parent
1820739a49
commit
eb4cd74f02
|
|
@ -252,38 +252,44 @@ class _SocialFeedScreenState extends ConsumerState<SocialFeedScreen> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Status LED pill
|
// Status LED pill
|
||||||
Container(
|
Flexible(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
color: colors.surfaceCardSecondary,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
color: colors.surfaceCardSecondary,
|
||||||
border: Border.all(
|
borderRadius: BorderRadius.circular(20),
|
||||||
color: accentColor.withValues(alpha: isLight ? 0.5 : 0.3),
|
border: Border.all(
|
||||||
|
color: accentColor.withValues(alpha: isLight ? 0.5 : 0.3),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Row(
|
||||||
child: Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
width: 8,
|
||||||
width: 8,
|
height: 8,
|
||||||
height: 8,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: accentColor,
|
||||||
color: accentColor,
|
shape: BoxShape.circle,
|
||||||
shape: BoxShape.circle,
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
Flexible(
|
||||||
Text(
|
child: Text(
|
||||||
'SQUAD & CANYON FEED',
|
'SQUAD & CANYON FEED',
|
||||||
style: TextStyle(
|
maxLines: 1,
|
||||||
fontFamily: AppTypography.fontTelemetry,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 11,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
fontFamily: AppTypography.fontTelemetry,
|
||||||
letterSpacing: 0.6,
|
fontSize: 11,
|
||||||
color: accentColor,
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
color: accentColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -1358,19 +1364,26 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
|
||||||
|
|
||||||
// GPX Export Button
|
// GPX Export Button
|
||||||
InkWell(
|
InkWell(
|
||||||
|
key: ValueKey('gpx_export_button_${item.id}'),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
backgroundColor: const Color(0xFF1D2025),
|
backgroundColor: colors.surfaceCardSecondary,
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.download_done_rounded,
|
Icon(
|
||||||
color: AppColors.electricCyan),
|
Icons.download_done_rounded,
|
||||||
|
color: accentColor,
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Expanded(
|
||||||
'GPX track exported for ${item.rideTitle}',
|
child: Text(
|
||||||
style: const TextStyle(color: Colors.white),
|
'GPX track exported for ${item.rideTitle}',
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(color: colors.textPrimary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -1519,15 +1532,17 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
|
||||||
),
|
),
|
||||||
if (unit.isNotEmpty) ...[
|
if (unit.isNotEmpty) ...[
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Flexible(
|
||||||
unit,
|
child: Text(
|
||||||
maxLines: 1,
|
unit,
|
||||||
overflow: TextOverflow.ellipsis,
|
maxLines: 1,
|
||||||
style: TextStyle(
|
overflow: TextOverflow.ellipsis,
|
||||||
fontFamily: AppTypography.fontTelemetry,
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontFamily: AppTypography.fontTelemetry,
|
||||||
fontWeight: FontWeight.w700,
|
fontSize: 10,
|
||||||
color: secondaryTextColor,
|
fontWeight: FontWeight.w700,
|
||||||
|
color: secondaryTextColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -1568,6 +1583,7 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
key: ValueKey('feed_route_no_gps_state_${item.id}'),
|
key: ValueKey('feed_route_no_gps_state_${item.id}'),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 12,
|
||||||
vertical: 6,
|
vertical: 6,
|
||||||
|
|
@ -1588,14 +1604,18 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
|
||||||
color: Color(0xFF94A3B8),
|
color: Color(0xFF94A3B8),
|
||||||
),
|
),
|
||||||
SizedBox(width: 6),
|
SizedBox(width: 6),
|
||||||
Text(
|
Flexible(
|
||||||
'NO GPS TRACK RECORDED',
|
child: Text(
|
||||||
style: TextStyle(
|
'NO GPS TRACK RECORDED',
|
||||||
fontFamily: AppTypography.fontTelemetry,
|
maxLines: 1,
|
||||||
fontSize: 10,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w700,
|
style: TextStyle(
|
||||||
letterSpacing: 0.5,
|
fontFamily: AppTypography.fontTelemetry,
|
||||||
color: Color(0xFFCBD5E1),
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
color: Color(0xFFCBD5E1),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -901,4 +901,83 @@ void main() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('BUG-005: GPX Export Snackbar Horizontal Overflow Tests', () {
|
||||||
|
for (final viewport in const [
|
||||||
|
Size(320, 640),
|
||||||
|
Size(360, 800),
|
||||||
|
Size(800, 480),
|
||||||
|
]) {
|
||||||
|
testWidgets(
|
||||||
|
'GPX export snackbar renders long rideTitle without RenderFlex overflow at ${viewport.width.toInt()}x${viewport.height.toInt()} (1.5x text scale)',
|
||||||
|
(tester) async {
|
||||||
|
await tester.binding.setSurfaceSize(viewport);
|
||||||
|
|
||||||
|
final service = SocialFeedService();
|
||||||
|
addTearDown(service.dispose);
|
||||||
|
|
||||||
|
const longRideTitle =
|
||||||
|
'Ultra Long Sunday Skyline Boulevard & Pescadero Creek Canyon Telemetry Endurance Carve Session With Full Squad Pack';
|
||||||
|
|
||||||
|
final post = await service.shareRecentRide(
|
||||||
|
authorUid: 'uid_overflow_test',
|
||||||
|
authorDisplayName: 'Marcus Vance',
|
||||||
|
authorHandle: 'ApexMarcus',
|
||||||
|
locationName: 'Skyline Blvd',
|
||||||
|
rideTitle: longRideTitle,
|
||||||
|
bikeModel: '2024 Ducati Streetfighter V4',
|
||||||
|
distanceMiles: 68.4,
|
||||||
|
durationSeconds: 4440,
|
||||||
|
maxLeanAngleDeg: 51.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
socialFeedServiceProvider.overrideWithValue(service),
|
||||||
|
],
|
||||||
|
child: MediaQuery(
|
||||||
|
data: MediaQueryData(
|
||||||
|
size: viewport,
|
||||||
|
textScaler: const TextScaler.linear(1.5),
|
||||||
|
),
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: ThemeData.dark().copyWith(
|
||||||
|
extensions: const [MotorSignalThemeColors.dark],
|
||||||
|
),
|
||||||
|
home: const SocialFeedScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
|
|
||||||
|
final gpxButton = find.byKey(
|
||||||
|
ValueKey('gpx_export_button_${post.id}'),
|
||||||
|
);
|
||||||
|
await tester.scrollUntilVisible(
|
||||||
|
gpxButton,
|
||||||
|
120.0,
|
||||||
|
scrollable: find.descendant(
|
||||||
|
of: find.byKey(const ValueKey('social_feed_list_view')),
|
||||||
|
matching: find.byType(Scrollable),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(gpxButton);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
|
// Must not throw any RenderFlex overflow exception
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(
|
||||||
|
find.text('GPX track exported for $longRideTitle'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue