Files
flutterApp/test/control_mode_entry_widget_test.dart
2026-11-06 08:34:19 +08:00

255 lines
8.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
}