import 'dart:async'; import 'package:fpdart/fpdart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/features/device_control_mode/domain/entities/device_control_mode.dart'; import 'package:maibu_satabot_v2/features/device_control_mode/domain/usecases/device_control_mode_usecases.dart'; import 'package:maibu_satabot_v2/features/device_control_mode/presentation/cubit/control_mode_entry_cubit.dart'; import 'package:maibu_satabot_v2/features/device_control_mode/presentation/widgets/control_mode_entry_gate.dart'; import 'support/control_mode_fakes.dart'; final _translations = {}; class _LoadedTranslations extends LocalizationsDelegate { const _LoadedTranslations(); @override bool isSupported(Locale locale) => _translations.containsKey(locale.toString()); @override Future load(Locale locale) => SynchronousFuture(_translations[locale.toString()]!); @override bool shouldReload(_LoadedTranslations old) => false; } Widget app(Widget home, {Locale locale = const Locale('zh', 'CN')}) => MaterialApp( locale: locale, supportedLocales: const [Locale('zh', 'CN'), Locale('en', 'US')], localizationsDelegates: const [ _LoadedTranslations(), GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], home: home, ); Future openDialog( WidgetTester tester, ControlModeEntryCubit cubit, ValueChanged result, { Locale locale = const Locale('zh', 'CN'), Size size = const Size(800, 360), }) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( app( Builder( builder: (context) => Scaffold( body: TextButton( onPressed: () async => result( await showControlModeEntryDialog( context, deviceId: cubit.deviceId, target: cubit.target, deviceName: '测试车', cubit: cubit, ), ), child: const Text('open'), ), ), ), locale: locale, ), ); await tester.pumpAndSettle(); await tester.tap(find.text('open')); await tester.pump(); await tester.pump(const Duration(milliseconds: 250)); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() async { // 真实资产I/O在测试假时钟外完成,弹窗测试使用同一套真实中英文文案。 for (final locale in [const Locale('zh', 'CN'), const Locale('en', 'US')]) { final translations = AppLocalizations(locale); await translations.load(); _translations[locale.toString()] = translations; await GlobalMaterialLocalizations.delegate.load(locale); await GlobalCupertinoLocalizations.delegate.load(locale); } }); late FakeModes repo; late ControlModeEntryCubit cubit; setUp(() { repo = FakeModes(); cubit = modeCubit(repo); }); tearDown(() async { if (!cubit.isClosed) await cubit.close(); await repo.reports.close(); await sl.reset(); }); testWidgets('进入弹窗显示实时模式,已匹配时确定仅进入、不调用切换', (tester) async { repo.initial = '2'; bool? result; await openDialog(tester, cubit, (value) => result = value); expect(find.text('控制模式确认'), findsOneWidget); expect(find.textContaining('当前实时模式:自主任务'), findsOneWidget); expect(repo.requests, isEmpty); expect(result, isNull); await tester.tap(find.text('确定')); await tester.pumpAndSettle(); expect(result, isTrue); expect(repo.requests, isEmpty); expect(cubit.isClosed, isTrue); }); testWidgets('确定才下发,接口成功立即关闭,不等模式回推', (tester) async { repo.initial = '3'; repo.pending = Completer(); bool? result; await openDialog(tester, cubit, (value) => result = value); expect(repo.requests, isEmpty); await tester.tap(find.text('确定')); await tester.pump(); expect(repo.requests.single, ('SN002', ControlModeTarget.autonomous)); expect(result, isNull); repo.pending!.complete(const Right(unit)); await tester.pumpAndSettle(); expect(result, isTrue); expect(cubit.state.reading!.value, 3); }); testWidgets('返回不会下发切换,关闭后后续实时数据不会触发进入', (tester) async { repo.initial = '1'; bool? result; await openDialog(tester, cubit, (value) => result = value); await tester.tap(find.text('返回')); await tester.pumpAndSettle(); repo.report('2'); await tester.pump(); expect(result, isFalse); expect(repo.requests, isEmpty); expect(cubit.isClosed, isTrue); }); testWidgets('无模式数据不转圈且确定可点击,成功后直接进入', (tester) async { bool? result; await openDialog(tester, cubit, (value) => result = value); expect(find.byType(LinearProgressIndicator), findsNothing); expect(find.textContaining('暂未收到当前模式'), findsOneWidget); expect( tester.widget(find.byType(FilledButton)).onPressed, isNotNull, ); await tester.tap(find.text('确定')); await tester.pumpAndSettle(); expect(result, isTrue); expect(repo.requests.single, ('SN002', ControlModeTarget.autonomous)); }); testWidgets('英文小横屏可以滚动查看内容且无布局溢出', (tester) async { repo.initial = '1'; await openDialog( tester, cubit, (_) {}, locale: const Locale('en', 'US'), size: const Size(560, 280), ); expect(find.text('Confirm control mode'), findsOneWidget); expect(tester.takeException(), isNull); await tester.tap(find.text('Back')); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); }); testWidgets('遥控入口只在模式确认后激活一次,确认前页面动作被阻止', (tester) async { repo.initial = '2'; repo.pending = Completer(); sl.registerSingleton(WatchDeviceControlModeUseCase(repo)); sl.registerSingleton(ChangeDeviceControlModeUseCase(repo)); var activations = 0; var drives = 0; await tester.pumpWidget( app( ControlModeEntryGate( deviceId: 'SN002', target: ControlModeTarget.remote, onReady: () => activations++, child: Scaffold( body: TextButton( key: const Key('drive'), onPressed: () => drives++, child: const Text('drive'), ), ), ), ), ); await tester.pumpAndSettle(); expect(activations, 0); await tester.tap(find.byKey(const Key('drive')), warnIfMissed: false); expect(drives, 0); await tester.tap(find.text('确定')); await tester.pump(); expect(repo.requests.single, ('SN002', ControlModeTarget.remote)); expect(activations, 0); repo.pending!.complete(const Right(unit)); await tester.pumpAndSettle(); expect(activations, 1); expect(drives, 0); await tester.tap(find.byKey(const Key('drive'))); expect(drives, 1); }); testWidgets('取消遥控模式确认返回来源页,不激活控制或下发切换', (tester) async { repo.initial = '2'; sl.registerSingleton(WatchDeviceControlModeUseCase(repo)); sl.registerSingleton(ChangeDeviceControlModeUseCase(repo)); var activations = 0; await tester.pumpWidget( app( Builder( builder: (context) => Scaffold( body: TextButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => ControlModeEntryGate( deviceId: 'SN002', target: ControlModeTarget.remote, onReady: () => activations++, child: const Scaffold(body: Text('remote')), ), ), ), child: const Text('open remote'), ), ), ), ), ); await tester.pumpAndSettle(); await tester.tap(find.text('open remote')); await tester.pumpAndSettle(); await tester.tap(find.text('返回')); await tester.pumpAndSettle(); expect(find.text('open remote'), findsOneWidget); expect(find.text('remote'), findsNothing); expect(activations, 0); expect(repo.requests, isEmpty); }); }