import 'package:commons_core/commons_core.dart'; import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.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/chat_database.dart'; 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/services/sharing_switch.dart'; import 'package:tane/services/social_settings.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'; /// A fresh in-memory database for host tests (unencrypted; encryption is /// verified separately in the SQLCipher-only security test). AppDatabase newTestDatabase() => AppDatabase(NativeDatabase.memory()); /// A fresh in-memory chat cache for host tests (unencrypted; see above). ChatDatabase newTestChatDatabase() => ChatDatabase(NativeDatabase.memory()); /// Unmounts the widget tree *inside* the test and drains the zero-duration /// timer Drift schedules when its stream subscription is cancelled. Without /// this, flutter_test reports "a Timer is still pending after disposal". Call /// at the end of any widget test that mounts an [InventoryCubit]. Future disposeTree(WidgetTester tester) async { await tester.pumpWidget(const SizedBox.shrink()); await tester.pump(const Duration(milliseconds: 10)); } VarietyRepository newTestRepository( AppDatabase db, { String nodeId = 'test-node', }) => VarietyRepository(db, idGen: IdGen(), nodeId: nodeId); SpeciesRepository newTestSpeciesRepository(AppDatabase db) => SpeciesRepository(db, idGen: IdGen()); /// An in-memory [SecretStore] for tests (no OS keystore). class InMemorySecretStore implements SecretStore { final Map _values = {}; @override Future read(String key) async => _values[key]; @override Future write(String key, String value) async => _values[key] = value; } /// A fresh [OnboardingStore] backed by in-memory storage. Defaults to /// "intro already seen" so app-level tests land on home, not the carousel. OnboardingStore newTestOnboardingStore({bool introSeen = true}) { final store = InMemorySecretStore(); if (introSeen) store.write('tane.intro_seen', '1'); return OnboardingStore(store); } /// A [SharingSwitch] over in-memory storage and no relay connection, for /// screens that only care whether sharing is on. Defaults to on, which is what /// most screen tests want (the social entries live, nothing dialled). SharingSwitch newTestSharingSwitch({bool enabled = true}) => SharingSwitch( settings: SocialSettings(InMemorySecretStore()), enabled: enabled, ); /// Wraps [child] with the providers a screen expects (repository, inventory /// cubit) plus i18n and Material localizations, pinned to [locale]. Widget wrapScreen({ required VarietyRepository repository, required Widget child, AppLocale locale = AppLocale.en, TextDirection? textDirection, }) { LocaleSettings.setLocaleSync(locale); return TranslationProvider( child: RepositoryProvider.value( value: repository, child: BlocProvider( create: (_) => InventoryCubit(repository), child: MaterialApp( locale: materialLocaleFor(locale.flutterLocale), supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], home: textDirection == null ? child : Directionality(textDirection: textDirection, child: child), ), ), ), ); } /// Wraps the [VarietyDetailScreen] for [varietyId] with its cubit + i18n. Widget wrapDetail({ required VarietyRepository repository, required String varietyId, SpeciesRepository? species, AppLocale locale = AppLocale.en, TextDirection? textDirection, }) { LocaleSettings.setLocaleSync(locale); return TranslationProvider( child: MultiRepositoryProvider( providers: [ RepositoryProvider.value(value: repository), if (species != null) RepositoryProvider.value(value: species), ], child: BlocProvider( create: (_) => VarietyDetailCubit(repository, varietyId), child: MaterialApp( locale: materialLocaleFor(locale.flutterLocale), supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], home: textDirection == null ? const VarietyDetailScreen() : Directionality( textDirection: textDirection, child: const VarietyDetailScreen(), ), ), ), ), ); }