diff --git a/lib/ui2/screens/cycle_screen.dart b/lib/ui2/screens/cycle_screen.dart index f9f18cdfe..0a985b75c 100644 --- a/lib/ui2/screens/cycle_screen.dart +++ b/lib/ui2/screens/cycle_screen.dart @@ -479,14 +479,24 @@ class _CycleTabState extends State with RevisionReload { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + // A Wrap, not a Row with a Spacer: at large text sizes or in a + // longer language the label and the phase pill do not fit one + // line, and the row overflowed. Side by side when they fit, the + // pill on its own line when they don't. + Wrap( + alignment: WrapAlignment.spaceBetween, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: S.x2, + runSpacing: S.x2, children: [ Text( l?.cycleDayInThisCycle ?? 'DAY IN THIS CYCLE', style: F.over.copyWith(color: p.ink3), ), - const Spacer(), - if (d.phase != 'unknown') Pill(_phaseLabel(l, d.phase), C.pink), + if (d.phase != 'unknown') + // Wraps at large text rather than cutting a long phase name + // short with an ellipsis; one line at default sizes. + Pill(_phaseLabel(l, d.phase), C.pink, wrap: bigText(c)), ], ), const SizedBox(height: S.x2), @@ -496,13 +506,18 @@ class _CycleTabState extends State with RevisionReload { children: [ Text('${d.cycleDay}', style: F.n34.copyWith(color: p.ink)), const SizedBox(width: S.x2), - Text( - d.medianLength == null - ? (l?.cycleCountedFromLastStart ?? - 'counted from your last logged start') - : (l?.cycleOfAboutDays(d.medianLength!.round()) ?? - 'of about ${d.medianLength!.round()}'), - style: F.cap.copyWith(color: p.ink3), + // Wraps under itself rather than running off the card: + // "counted from your last logged start" is most of a line at + // the default size already. + Flexible( + child: Text( + d.medianLength == null + ? (l?.cycleCountedFromLastStart ?? + 'counted from your last logged start') + : (l?.cycleOfAboutDays(d.medianLength!.round()) ?? + 'of about ${d.medianLength!.round()}'), + style: F.cap.copyWith(color: p.ink3), + ), ), ], ), diff --git a/test/cycle_day_row_overflow_test.dart b/test/cycle_day_row_overflow_test.dart new file mode 100644 index 000000000..df014dac2 --- /dev/null +++ b/test/cycle_day_row_overflow_test.dart @@ -0,0 +1,73 @@ +// The cycle screen's "day in this cycle" card must fit a phone at any text +// size. +// +// The headline row put the day number and "counted from your last logged +// start" side by side with nothing allowed to wrap, and the header row put +// its label before a Spacer and the phase pill. At a 390 pt width that +// overflowed by 147 pt in the test font, and by less, but still, with larger +// system text (iOS goes to 3.1x) or a longer language. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:openstrap_edge/data/local_repository.dart'; +import 'package:openstrap_edge/state/app_state.dart'; +import 'package:openstrap_edge/ui2/screens/cycle_screen.dart'; +import 'package:openstrap_edge/ui2/ui2.dart'; + +class _Repo extends LocalRepository { + _Repo({this.median}); + final num? median; + + @override + Future> getCycle() async => { + 'enabled': true, + 'phase': 'follicular', + 'cycle_day': 5, + 'median_length': ?median, + 'logs': [ + {'date': '2026-11-09', 'kind': 'start'}, + ], + }; + + @override + Future>> getCycleSymptoms() async => const {}; +} + +Future _pump(WidgetTester t, {num? median, double scale = 1}) async { + t.view.physicalSize = const Size(390 * 3, 4000 * 3); + t.view.devicePixelRatio = 3; + addTearDown(t.view.reset); + final app = AppState.forTesting()..repo = _Repo(median: median); + addTearDown(app.dispose); + await t.pumpWidget(ChangeNotifierProvider.value( + value: app, + child: MediaQuery( + data: MediaQueryData(textScaler: TextScaler.linear(scale)), + child: MaterialApp( + theme: buildTheme(Brightness.light), + home: const Scaffold(body: CycleTab()), + ), + ), + )); + await t.pumpAndSettle(); +} + +void main() { + for (final scale in const [1.0, 3.1]) { + testWidgets('no median yet, ${scale}x text', (t) async { + // The pump is the assertion: a RenderFlex overflow fails it. + await _pump(t, scale: scale); + expect(find.textContaining('counted from your last logged start'), + findsOneWidget); + }); + + testWidgets('with a median, ${scale}x text', (t) async { + await _pump(t, median: 29, scale: scale); + expect(find.textContaining('of about 29'), findsOneWidget); + // The phase name wraps at large text instead of ending in an ellipsis. + expect(t.widget(find.byType(Pill).first).wrap, scale > 1.3); + }); + } +}