From 8315d20f734c41d58e6e0e2824f445db915879d1 Mon Sep 17 00:00:00 2001 From: vjrj Date: Sat, 11 Jul 2026 00:53:44 +0200 Subject: [PATCH] test(ui): small-phone overflow guard; fix Home clipping on small screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds test/ui/small_screen_overflow_test.dart: pumps the full screens at a 320x568 (iPhone-SE-class) viewport in the longest-text locales (es/pt/ast); a RenderFlex overflow fails the test, so a clipped title or save button is caught. It found — and this fixes — a real bug: the Home menu (a fixed, non-scrolling Center>Column) clipped ~80px on small phones in every locale. Home is now wrapped in LayoutBuilder + SingleChildScrollView + ConstrainedBox(minHeight: maxHeight): centered when it fits, scrolls when it doesn't, never clipped. Also makes the test harness faithful: wrapScreen/wrapDetail now map the locale through materialLocaleFor (ast->es) exactly like the app, so Asturian screens don't throw on MaterialLocalizations (AppBar tooltips) — that had masqueraded as an overflow. Scope notes in the test: full screens only (modal bottom sheets get an unbounded height in widget tests -> false huge overflow; they already scroll their body), and the variety-detail screen is skipped (its ~2px hit is on a DISPOSED/DEFUNCT RenderFlex — a transient during the cubit rebuild, not the stable layout). --- apps/app_seeds/lib/ui/home_screen.dart | 19 ++- apps/app_seeds/test/support/test_support.dart | 5 +- .../test/ui/small_screen_overflow_test.dart | 115 ++++++++++++++++++ 3 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 apps/app_seeds/test/ui/small_screen_overflow_test.dart diff --git a/apps/app_seeds/lib/ui/home_screen.dart b/apps/app_seeds/lib/ui/home_screen.dart index 73befef..93c7b81 100644 --- a/apps/app_seeds/lib/ui/home_screen.dart +++ b/apps/app_seeds/lib/ui/home_screen.dart @@ -40,10 +40,18 @@ class HomeScreen extends StatelessWidget { body: Stack( children: [ const Positioned.fill(child: _SeedWatermark()), - Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 440), - child: Padding( + // Scroll when the menu doesn't fit a short screen (small phones), so + // the market card / buttons are never clipped; stays centered when it + // does fit. + LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Padding( padding: const EdgeInsets.fromLTRB(24, 24, 24, 32), child: Column( mainAxisSize: MainAxisSize.min, @@ -109,6 +117,9 @@ class HomeScreen extends StatelessWidget { ), ), ), + ), + ), + ), ], ), ); diff --git a/apps/app_seeds/test/support/test_support.dart b/apps/app_seeds/test/support/test_support.dart index c1e9685..3c17884 100644 --- a/apps/app_seeds/test/support/test_support.dart +++ b/apps/app_seeds/test/support/test_support.dart @@ -10,6 +10,7 @@ import 'package:tane/db/database.dart'; import 'package:tane/i18n/strings.g.dart'; import 'package:tane/security/secret_store.dart'; import 'package:tane/services/onboarding_store.dart'; +import 'package:tane/app.dart' show materialLocaleFor; import 'package:tane/state/inventory_cubit.dart'; import 'package:tane/state/variety_detail_cubit.dart'; import 'package:tane/ui/variety_detail_screen.dart'; @@ -69,7 +70,7 @@ Widget wrapScreen({ child: BlocProvider( create: (_) => InventoryCubit(repository), child: MaterialApp( - locale: locale.flutterLocale, + locale: materialLocaleFor(locale.flutterLocale), supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, @@ -103,7 +104,7 @@ Widget wrapDetail({ child: BlocProvider( create: (_) => VarietyDetailCubit(repository, varietyId), child: MaterialApp( - locale: locale.flutterLocale, + locale: materialLocaleFor(locale.flutterLocale), supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, diff --git a/apps/app_seeds/test/ui/small_screen_overflow_test.dart b/apps/app_seeds/test/ui/small_screen_overflow_test.dart new file mode 100644 index 0000000..1c70f40 --- /dev/null +++ b/apps/app_seeds/test/ui/small_screen_overflow_test.dart @@ -0,0 +1,115 @@ +import 'package:commons_core/commons_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tane/db/enums.dart'; +import 'package:tane/i18n/strings.g.dart'; +import 'package:tane/ui/home_screen.dart'; +import 'package:tane/ui/inventory_list_screen.dart'; +import 'package:tane/ui/settings_screen.dart'; + +import '../support/test_support.dart'; + +/// Small-phone layout guard. A Flutter `RenderFlex overflowed` (a clipped title, +/// a save button pushed off a non-scrolling column, a Row too wide) is reported +/// as an error and FAILS the test — so pumping each screen at a tiny viewport, +/// in the languages whose text runs longest (es/pt/ast), catches exactly the +/// "the save button / title isn't fully visible on a small phone" reports. +/// +/// FULL SCREENS ONLY. Modal bottom sheets (quick-add, the lot editor) are NOT +/// checked here: `showModalBottomSheet` gets an unbounded height in a widget +/// test, which fabricates a huge false overflow. Those sheets already wrap their +/// body in a `SingleChildScrollView`, so their save buttons stay reachable; +/// verify them on a real small device. +void main() { + // iPhone-SE-class logical viewport: narrow AND short, where clipping shows. + const small = Size(320, 568); + // The locales whose strings are longest — English rarely overflows first. + const longLocales = [AppLocale.es, AppLocale.pt, AppLocale.ast]; + + Future pumpSmall(WidgetTester tester, Widget widget) async { + tester.view.physicalSize = small; + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + await tester.pumpWidget(widget); + await tester.pumpAndSettle(); + } + + for (final locale in longLocales) { + final tag = locale.languageCode; + + testWidgets('home menu fits a small screen ($tag)', (tester) async { + final db = newTestDatabase(); + addTearDown(db.close); + await pumpSmall( + tester, + wrapScreen( + repository: newTestRepository(db), + locale: locale, + child: const HomeScreen(marketEnabled: true), + ), + ); + await disposeTree(tester); + }); + + testWidgets('settings (language, backup, about) fits ($tag)', (tester) async { + final db = newTestDatabase(); + addTearDown(db.close); + await pumpSmall( + tester, + wrapScreen( + repository: newTestRepository(db), + locale: locale, + child: const SettingsScreen(), + ), + ); + await disposeTree(tester); + }); + } + + testWidgets('inventory (empty) fits a small screen', (tester) async { + final db = newTestDatabase(); + addTearDown(db.close); + await pumpSmall( + tester, + wrapScreen( + repository: newTestRepository(db), + locale: AppLocale.es, + child: const InventoryListScreen(), + ), + ); + await disposeTree(tester); + }); + + testWidgets('inventory with a long-named seed fits a small screen', + (tester) async { + final db = newTestDatabase(); + addTearDown(db.close); + final repo = newTestRepository(db); + final id = await repo.addQuickVariety( + label: 'Tomate rosa de Barbastro extraordinariamente larguísimo', + category: 'Solanáceas', + ); + await repo.addLot( + varietyId: id, + harvestYear: 2024, + quantity: const Quantity(kind: QuantityKind.handful), + offerStatus: OfferStatus.shared, + ); + await pumpSmall( + tester, + wrapScreen( + repository: repo, + locale: AppLocale.es, + child: const InventoryListScreen(), + ), + ); + await disposeTree(tester); + }); + + // NOTE: the variety-detail screen is deliberately NOT overflow-checked here. + // It reports a ~2px overflow on a RenderFlex that is already DISPOSED/DEFUNCT + // — a transient stale frame while its cubit's Drift stream rebuilds during + // pumpAndSettle, not the stable rendered layout. Guarding it would be flaky; + // its RTL/build coverage lives in rtl_smoke_test.dart. +}