255 lines
8.8 KiB
Dart
255 lines
8.8 KiB
Dart
|
|
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);
|
|||
|
|
});
|
|||
|
|
}
|