@Tags(['screenshots']) library; import 'dart:async'; import 'package:commons_core/commons_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tane/data/species_repository.dart'; import 'package:tane/data/variety_repository.dart'; import 'package:tane/db/database.dart'; import 'package:tane/i18n/strings.g.dart'; import 'package:tane/state/offers_cubit.dart'; import 'package:tane/state/variety_detail_cubit.dart'; import 'package:tane/ui/calendar_screen.dart'; import 'package:tane/ui/home_screen.dart'; import 'package:tane/ui/inventory_list_screen.dart'; import 'package:tane/ui/market_screen.dart'; import 'package:tane/ui/variety_detail_screen.dart'; import '../support/test_support.dart'; import 'screenshot_support.dart'; /// Generates localized marketing/store screenshots as golden PNGs. /// /// Run (writes the PNGs) — the `screenshots` tag is skipped by default /// (dart_test.yaml), so `--run-skipped` is required: /// flutter test --update-goldens --run-skipped --tags screenshots test/screenshots/screenshots_test.dart /// /// Output: test/screenshots/goldens/``/``.png for en/es/fr/de/pt/ja, /// plus goldens/rtl/ — a right-to-left *layout* demo (English strings mirrored; /// the app ships no Arabic translation yet, so this proves the design is RTL-safe /// rather than claiming an Arabic locale). These are raw device-canvas frames /// with no marketing chrome; add captions/device frames downstream if a store /// requires them. `tool/collect_screenshots.sh` copies them into site/ and fastlane/. void main() { setUpAll(() async { await loadScreenshotFonts(); // Touch mode never draws focus rings, so no highlight frames the FAB/fields. FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; }); late AppDatabase db; late VarietyRepository repo; late SpeciesRepository species; late SeededInventory seeded; setUp(() async { db = newTestDatabase(); repo = newTestRepository(db); species = newTestSpeciesRepository(db); seeded = await seedShowcase(db, repo); }); tearDown(() => db.close()); Future capture(WidgetTester tester, String dir, String name) async { tester.view.physicalSize = screenshotSize; tester.view.devicePixelRatio = screenshotPixelRatio; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpAndSettle(); await expectLater( find.byType(MaterialApp), matchesGoldenFile('goldens/$dir/$name.png'), ); await disposeTree(tester); } // A POPULATED market: a seeded OffersCubit (no transport) feeding MarketBody, // so the offer list renders with no live discovery stream to hang // pumpAndSettle. (Live discovery is exercised by offers_cubit_test.dart; see // the note in market_screen_test.dart on why we don't drive it via widgets.) Future market() async => Builder( builder: (context) => Scaffold( appBar: AppBar( title: Text(context.t.market.title), actions: [IconButton(icon: const Icon(Icons.tune), onPressed: () {})], ), body: BlocProvider( create: (_) => _SeededOffersCubit(_sampleOffers()), child: MarketBody(onConfigure: () {}), ), ), ); /// Each entry renders one screen; the runner re-seeds and re-locales per shot. final screens = Function()>{ 'home': () async => const HomeScreen(marketEnabled: true), 'inventory': () async => const InventoryListScreen(), 'market': market, 'calendar': () async => const CalendarScreen(initialMonth: 4), 'detail': () async => BlocProvider( create: (_) => VarietyDetailCubit(repo, seeded.heroVarietyId), child: const VarietyDetailScreen(), ), }; for (final locale in screenshotLocales) { for (final entry in screens.entries) { testWidgets('${locale.languageCode}/${entry.key}', (tester) async { await tester.pumpWidget( screenshotApp( repository: repo, species: species, locale: locale, child: await entry.value(), ), ); await capture(tester, locale.languageCode, entry.key); }); } } // RTL layout demo: base (English) strings, forced right-to-left, so the // mirrored inventory proves the design is RTL-safe. Not a translated locale. testWidgets('rtl/inventory', (tester) async { await tester.pumpWidget( screenshotApp( repository: repo, species: species, locale: AppLocale.en, textDirection: TextDirection.rtl, child: const InventoryListScreen(), ), ); await capture(tester, 'rtl', 'inventory'); }); } /// An [OffersCubit] pre-loaded with a fixed offer list. It carries a no-op /// transport (never called — offers are pre-seeded) purely so `isOnline` is /// true and MarketBody renders the list instead of the "can't reach" state. class _SeededOffersCubit extends OffersCubit { _SeededOffersCubit(List offers) : super(_NoopOfferTransport()) { emit(state.copyWith(offers: offers, hasSearched: true, areaGeohash: 'sp3e')); } } /// A transport that is present (so the market reads as online) but never used. class _NoopOfferTransport implements OfferTransport { @override Stream discover(DiscoveryQuery query) => const Stream.empty(); @override Future publish(Offer offer) => throw UnimplementedError(); @override Future retract(String offerId) async {} @override Future close() async {} } /// A small, internationally-neutral set of nearby offers across gift / swap / /// sale and colour-coded botanical families. List _sampleOffers() => [ Offer(id: 'o1', authorPubkeyHex: 'a1' * 32, summary: 'Cherry tomato', type: OfferType.gift, category: 'Solanaceae', approxGeohash: 'sp3e9', isOrganic: true), Offer(id: 'o2', authorPubkeyHex: 'a2' * 32, summary: 'Climbing bean', type: OfferType.exchange, category: 'Fabaceae', approxGeohash: 'sp3e8'), Offer(id: 'o3', authorPubkeyHex: 'a3' * 32, summary: 'Sunflower', type: OfferType.sale, category: 'Asteraceae', approxGeohash: 'sp3ec', priceAmount: 2, priceCurrency: '€'), Offer(id: 'o4', authorPubkeyHex: 'a4' * 32, summary: 'Sweet basil', type: OfferType.gift, category: 'Lamiaceae', approxGeohash: 'sp3e2', isOrganic: true), Offer(id: 'o5', authorPubkeyHex: 'a5' * 32, summary: 'Purple carrot', type: OfferType.exchange, category: 'Apiaceae', approxGeohash: 'sp3ef'), ];