Files
flutterApp/test/control_mode_entry_widget_test.dart

255 lines
8.8 KiB
Dart
Raw Normal View History

2026-11-06 08:34:19 +08:00
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 = <String, AppLocalizations>{};
class _LoadedTranslations extends LocalizationsDelegate<AppLocalizations> {
const _LoadedTranslations();
@override
bool isSupported(Locale locale) =>
_translations.containsKey(locale.toString());
@override
Future<AppLocalizations> 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<void> openDialog(
WidgetTester tester,
ControlModeEntryCubit cubit,
ValueChanged<bool> 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<FilledButton>(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);
});
}