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

252 lines
9.5 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 'support/control_mode_fakes.dart';
import 'package:dio/dio.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fpdart/fpdart.dart';
import 'package:maibu_satabot_v2/core/di/adapters/device_control_mode_realtime_adapter.dart';
import 'package:maibu_satabot_v2/core/error/failure.dart';
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
import 'package:maibu_satabot_v2/features/device_control_mode/data/datasources/control_mode_remote_datasource.dart';
import 'package:maibu_satabot_v2/features/device_control_mode/data/repositories/device_control_mode_repository_impl.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/devices/domain/entities/gps_entity.dart';
import 'package:maibu_satabot_v2/features/devices/domain/entities/running_status_entity.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
class _Logger implements ILoggerService {
final lines = <String>[];
@override
void logWithLevel(
String message, {
String level = 'INFO',
Map<String, dynamic>? data,
bool shouldLog = false,
}) {
if (shouldLog) lines.add(message);
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class _StatusBloc extends Cubit<DeviceStatusState> implements DeviceStatusBloc {
_StatusBloc(String sn, String raw)
: listeningSn = sn,
super(
DeviceStatusUpdated(
RunningStatusEntity(controlMode: raw),
GPSEntity(0, 0),
),
);
String listeningSn;
final starts = <String>[];
@override
String? get listeningDeviceSn => listeningSn;
@override
void startMqttRealtimeListening(String sn) {
starts.add(sn);
if (listeningSn != sn) {
listeningSn = sn;
emit(DeviceStatusInitial());
}
}
void report(String raw) => emit(
DeviceStatusUpdated(RunningStatusEntity(controlMode: raw), GPSEntity(0, 0)),
);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
test('接口按文档GET下发编码和整数模式,数字data=1不做bool强转', () async {
final dio = Dio();
final requests = <RequestOptions>[];
final logger = _Logger();
Map<String, dynamic> response = {'code': 200, 'msg': '操作成功', 'data': 1};
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
requests.add(options);
handler.resolve(
Response(requestOptions: options, statusCode: 200, data: response),
);
},
),
);
final repo = DeviceControlModeRepositoryImpl(
realtime: (_) => const Stream.empty(),
remote: ControlModeRemoteDatasource(dio, logger),
);
final change = ChangeDeviceControlModeUseCase(repo);
for (final target in ControlModeTarget.values) {
expect((await change(' SN002 ', target)).isRight(), isTrue);
expect(requests.last.method, 'GET');
expect(requests.last.uri.path, '/iot/host/modeChage');
expect(requests.last.queryParameters, {
'deviceId': 'SN002',
'controlMode': target.value,
});
expect(requests.last.data, isNull);
}
response = {'code': 500, 'msg': '设备不在线'};
final failed = await change('SN002', ControlModeTarget.remote);
expect(
failed.fold((failure) => failure.message, (_) => ''),
contains('设备不在线'),
);
expect((await change(' ', ControlModeTarget.remote)).isLeft(), isTrue);
expect(requests, hasLength(3));
expect(logger.lines.any((s) => s.contains('"controlMode":3')), isTrue);
expect(logger.lines.any((s) => s.contains('HTTP 200')), isTrue);
dio.close();
});
test('实时适配器与设备详情共用Bloc,同设备复用快照,切换设备不串模式', () async {
final bloc = _StatusBloc('old', '3');
final readings = <DeviceControlModeReading>[];
final source = DeviceControlModeRealtimeAdapter(bloc);
var sub = source.watch('new').listen(readings.add);
await flushModes();
expect(bloc.starts, ['new']);
expect(readings.every((reading) => reading.rawMode == null), isTrue);
bloc.report('2');
await flushModes();
expect(readings.last.deviceId, 'new');
expect(readings.last.value, 2);
await sub.cancel();
readings.clear();
sub = source.watch('new').listen(readings.add);
await flushModes();
expect(readings.last.value, 2);
bloc.startMqttRealtimeListening('other');
bloc.report('3');
await flushModes();
expect(readings.last.rawMode, isNull);
await sub.cancel();
await bloc.close();
});
group('入口模式确认', () {
late FakeModes repo;
late ControlModeEntryCubit cubit;
setUp(() {
repo = FakeModes();
cubit = modeCubit(repo);
});
tearDown(() async {
if (!cubit.isClosed) await cubit.close();
await repo.reports.close();
});
Future<void> start(String? mode) async {
repo.initial = mode;
await cubit.start();
await flushModes();
}
test('无模式上报也允许用户确定下发,不等待topic', () async {
await start(null);
expect(cubit.state.canConfirm, isTrue);
expect(repo.requests, isEmpty);
await cubit.confirm();
expect(repo.requests.single, ('SN002', ControlModeTarget.autonomous));
expect(cubit.state.phase, ControlModeEntryPhase.confirmed);
expect(cubit.state.reading?.rawMode, isNull);
});
test('已匹配时确定直接进入,不重复切换', () async {
await start('2');
await cubit.confirm();
expect(cubit.state.phase, ControlModeEntryPhase.confirmed);
expect(repo.requests, isEmpty);
});
test('文本模式可展示也可确定,不因非整数被禁用', () async {
await start('本地控制');
expect(cubit.state.reading!.hasReportedMode, isTrue);
expect(cubit.state.canConfirm, isTrue);
await cubit.confirm();
expect(cubit.state.phase, ControlModeEntryPhase.confirmed);
expect(cubit.state.reading!.rawMode, '本地控制');
});
test('接口成功立即进入,保持真实上报值而不伪造目标模式', () async {
await start('1');
await cubit.confirm();
expect(cubit.state.phase, ControlModeEntryPhase.confirmed);
expect(cubit.state.reading!.value, 1);
});
test('发送过程中重复点击只请求一次,成功后无需到达目标模式上报', () async {
await start('1');
repo.pending = Completer();
final first = cubit.confirm();
await cubit.confirm();
expect(cubit.state.busy, isTrue);
expect(repo.requests, hasLength(1));
repo.pending!.complete(const Right(unit));
await first;
expect(cubit.state.phase, ControlModeEntryPhase.confirmed);
});
test('失败后允许重试,同模式实时帧不抹掉失败信息', () async {
await start('1');
repo.failure = Failure('设备拒绝切换');
await cubit.confirm();
repo.report('1');
await flushModes();
expect(cubit.state.error, '设备拒绝切换');
expect(cubit.state.canConfirm, isTrue);
repo.failure = null;
await cubit.confirm();
expect(repo.requests, hasLength(2));
expect(cubit.state.phase, ControlModeEntryPhase.confirmed);
});
test('关闭后迟到HTTP响应不触发进入', () async {
await start('1');
repo.pending = Completer();
final request = cubit.confirm();
await cubit.close();
repo.pending!.complete(const Right(unit));
await request;
expect(cubit.state.phase, ControlModeEntryPhase.submitting);
});
test('远程入口下发3,接口成功后直接进入', () async {
await cubit.close();
cubit = modeCubit(repo, target: ControlModeTarget.remote);
await start('2');
await cubit.confirm();
expect(repo.requests.single, ('SN002', ControlModeTarget.remote));
expect(cubit.state.phase, ControlModeEntryPhase.confirmed);
expect(cubit.state.reading!.value, 2);
});
test('未知或协议外模式仍可手动确定下发', () async {
await start('0');
repo.report('6');
await flushModes();
expect(cubit.state.canConfirm, isTrue);
await cubit.confirm();
expect(repo.requests.single, ('SN002', ControlModeTarget.autonomous));
});
test('其他设备上报不改变本设备模式', () async {
await start('1');
repo.report('2', deviceId: 'other');
await flushModes();
expect(cubit.state.reading!.value, 1);
await cubit.confirm();
expect(repo.requests, hasLength(1));
});
test('请求超时恢复确定按钮,不把迟到响应当成成功', () async {
await cubit.close();
cubit = modeCubit(repo, timeout: const Duration(milliseconds: 40));
await start('1');
repo.pending = Completer();
await cubit.confirm();
expect(cubit.state.errorKey, 'request_timeout');
expect(cubit.state.canConfirm, isTrue);
repo.pending!.complete(const Right(unit));
await flushModes();
expect(cubit.state.phase, ControlModeEntryPhase.failed);
});
});
}