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,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),
),
), ),
), ),
], ],

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