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:
Developer 2026-10-16 23:23:12 -04:00
parent 1820739a49
commit eb4cd74f02
2 changed files with 151 additions and 52 deletions

View File

@ -252,7 +252,8 @@ class _SocialFeedScreenState extends ConsumerState<SocialFeedScreen> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Status LED pill // Status LED pill
Container( Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colors.surfaceCardSecondary, color: colors.surfaceCardSecondary,
@ -273,8 +274,11 @@ class _SocialFeedScreenState extends ConsumerState<SocialFeedScreen> {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Flexible(
child: Text(
'SQUAD & CANYON FEED', 'SQUAD & CANYON FEED',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontTelemetry, fontFamily: AppTypography.fontTelemetry,
fontSize: 11, fontSize: 11,
@ -283,9 +287,11 @@ class _SocialFeedScreenState extends ConsumerState<SocialFeedScreen> {
color: accentColor, color: accentColor,
), ),
), ),
),
], ],
), ),
), ),
),
// Search toggle button // Search toggle button
IconButton( IconButton(
@ -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(
child: Text(
'GPX track exported for ${item.rideTitle}', 'GPX track exported for ${item.rideTitle}',
style: const TextStyle(color: Colors.white), maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: colors.textPrimary),
),
), ),
], ],
), ),
@ -1519,7 +1532,8 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
), ),
if (unit.isNotEmpty) ...[ if (unit.isNotEmpty) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Flexible(
child: Text(
unit, unit,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -1530,6 +1544,7 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
color: secondaryTextColor, 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,8 +1604,11 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
color: Color(0xFF94A3B8), color: Color(0xFF94A3B8),
), ),
SizedBox(width: 6), SizedBox(width: 6),
Text( Flexible(
child: Text(
'NO GPS TRACK RECORDED', 'NO GPS TRACK RECORDED',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontFamily: AppTypography.fontTelemetry, fontFamily: AppTypography.fontTelemetry,
fontSize: 10, fontSize: 10,
@ -1598,6 +1617,7 @@ class _SocialFeedCardState extends ConsumerState<_SocialFeedCard>
color: Color(0xFFCBD5E1), color: Color(0xFFCBD5E1),
), ),
), ),
),
], ],
), ),
), ),

View File

@ -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,
);
},
);
}
});
} }