import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:tane/db/enums.dart'; import 'package:tane/i18n/strings.g.dart'; import 'package:tane/services/profile_store.dart'; import 'package:tane/services/social_account_store.dart'; import 'package:tane/services/social_connection.dart'; import 'package:tane/services/social_service.dart'; import 'package:tane/services/social_settings.dart'; import 'package:drift/drift.dart' show Value; import 'package:tane/domain/crop_calendar.dart'; import 'package:tane/ui/about_screen.dart'; import 'package:tane/ui/calendar_screen.dart'; import 'package:tane/ui/chat_screen.dart'; import 'package:tane/ui/home_screen.dart'; import 'package:tane/ui/intro_screen.dart'; import 'package:tane/ui/inventory_list_screen.dart'; import 'package:tane/ui/market_screen.dart'; import 'package:tane/ui/plantares_screen.dart'; import 'package:tane/ui/profile_screen.dart'; import 'package:tane/ui/sales_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]; setUpAll(() => PackageInfo.setMockInitialValues( appName: 'Tanemaki', packageName: 'org.comunes.tane', version: '0.1.0', buildNumber: '1', buildSignature: '', )); Future pumpSmall(WidgetTester tester, Widget widget) async { tester.view.physicalSize = small; tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); // flutter_test's headless engine can't decode our real PNG assets (the // logo), so `Image.asset` throws from the "image resource service". That's // unrelated to layout — swallow ONLY those, and let every other FlutterError // (crucially the "rendering library" RenderFlex overflow this guard exists // to catch) still fail the test. Installed here, inside the running test, // because testWidgets reinstalls its own onError at test start (a setUp // override would be clobbered). final previous = FlutterError.onError; FlutterError.onError = (details) { if (details.library == 'image resource service') return; previous?.call(details); }; addTearDown(() => FlutterError.onError = previous); 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('about (title, version, license) fits ($tag)', (tester) async { final db = newTestDatabase(); addTearDown(db.close); await pumpSmall( tester, wrapScreen( repository: newTestRepository(db), locale: locale, child: const AboutScreen(), ), ); await disposeTree(tester); }); testWidgets('the intro carousel fits ($tag)', (tester) async { final db = newTestDatabase(); addTearDown(db.close); await pumpSmall( tester, wrapScreen( repository: newTestRepository(db), locale: locale, child: IntroScreen(onDone: () {}), ), ); await disposeTree(tester); }); } testWidgets('profile (identity card + save button) fits a small screen', (tester) async { final db = newTestDatabase(); addTearDown(db.close); final social = await SocialService.fromRootSeedHex('00' * 32); final connection = SocialConnection( social: social, settings: SocialSettings(InMemorySecretStore()), open: (_) async => throw StateError('offline'), online: const Stream.empty(), ); await pumpSmall( tester, wrapScreen( repository: newTestRepository(db), locale: AppLocale.es, child: ProfileScreen( social: social, connection: connection, profileStore: ProfileStore(InMemorySecretStore()), accounts: SocialAccountStore(InMemorySecretStore()), ), ), ); await disposeTree(tester); }); // The market/chat are "live" screens; checked here in their OFFLINE state // (the shared connection can't reach a relay) — a static layout, and the one // a user without signal actually sees. Future offlineConn() async { final social = await SocialService.fromRootSeedHex('00' * 32); final settings = SocialSettings(InMemorySecretStore()); await settings.setRelayUrls(const []); return SocialConnection( social: social, settings: settings, open: (_) async => throw StateError('offline'), online: const Stream.empty(), ); } testWidgets('market (offline "set up sharing") fits a small screen', (tester) async { final db = newTestDatabase(); addTearDown(db.close); final connection = await offlineConn(); await pumpSmall( tester, wrapScreen( repository: newTestRepository(db), locale: AppLocale.es, child: MarketScreen( social: await SocialService.fromRootSeedHex('00' * 32), settings: SocialSettings(InMemorySecretStore()), connection: connection, ), ), ); await disposeTree(tester); }); testWidgets('chat (offline "set up sharing") fits a small screen', (tester) async { final db = newTestDatabase(); addTearDown(db.close); final connection = await offlineConn(); await pumpSmall( tester, wrapScreen( repository: newTestRepository(db), locale: AppLocale.es, child: ChatScreen( social: await SocialService.fromRootSeedHex('00' * 32), connection: connection, peerPubkey: 'ab' * 32, ), ), ); 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); }); testWidgets('the Plantares screen (with a commitment) fits a small screen', (tester) async { final db = newTestDatabase(); addTearDown(db.close); final repo = newTestRepository(db); await repo.createPlantare( direction: PlantareDirection.iReturn, counterparty: 'Colectivo semillero de la comarca', owedDescription: 'un puñado la próxima temporada, si germina bien', ); await pumpSmall( tester, wrapScreen( repository: repo, locale: AppLocale.es, child: const PlantaresScreen(), ), ); await disposeTree(tester); }); testWidgets('the Sales screen (with a sale) fits a small screen', (tester) async { final db = newTestDatabase(); addTearDown(db.close); final repo = newTestRepository(db); await repo.createSale( direction: SaleDirection.iSold, counterparty: 'Feria de intercambio de la comarca', amount: 12.5, currency: '€', ); await pumpSmall( tester, wrapScreen( repository: repo, locale: AppLocale.es, child: const SalesScreen(), ), ); await disposeTree(tester); }); testWidgets('the "this month" calendar fits a small screen', (tester) async { final db = newTestDatabase(); addTearDown(db.close); final repo = newTestRepository(db); final id = await repo.addQuickVariety( label: 'Lechuga maravilla de verano', category: 'Asteráceas'); await repo.updateVariety(id: id, sowMonths: Value(monthsToMask([3]))); await pumpSmall( tester, wrapScreen( repository: repo, locale: AppLocale.es, child: const CalendarScreen(initialMonth: 3), ), ); 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. }