Files
flutterApp/test/device_task_control_tile_test.dart

240 lines
8.1 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/autonomous_work/domain/entities/work_session_models.dart';
import 'package:maibu_satabot_v2/features/autonomous_work/domain/repositories/autonomous_work_gateway.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/widgets/device_task_control_tile.dart';
import 'support/region_test_localizations.dart';
WorkTaskSnapshot task({
String id = '42',
String device = 'SN-1',
WorkRunState state = WorkRunState.running,
String rawStatus = '',
}) => WorkTaskSnapshot(
taskId: id,
deviceId: device,
state: state,
rawStatus: rawStatus,
routeName: 'Route $id',
);
class FakeGateway implements AutonomousWorkGateway, WorkTaskQueueSource {
List<WorkTaskSnapshot> tasks = [task()];
final changes = StreamController<WorkTaskSnapshot?>.broadcast(sync: true);
final calls = <String>[];
bool loadFails = false;
bool commandFails = false;
int loads = 0;
Completer<WorkResult<WorkTaskSnapshot>>? pending;
@override
WorkSceneCapabilities get capabilities => const WorkSceneCapabilities();
@override
Stream<WorkTaskSnapshot?> watchTaskChanges({required String deviceId}) =>
changes.stream;
@override
Future<WorkResult<List<WorkTaskSnapshot>>> loadTasks({
required String deviceId,
}) async {
loads++;
return loadFails
? const WorkResult.fail('offline')
: WorkResult.ok([...tasks]);
}
Future<WorkResult<WorkTaskSnapshot>> command(
String deviceId,
WorkTaskSnapshot selected,
bool resume,
) async {
calls.add('${resume ? 'resume' : 'pause'}/$deviceId/${selected.taskId}');
if (pending != null) return pending!.future;
if (commandFails) return const WorkResult.fail('command failed');
final updated = selected.copyWith(
state: resume ? WorkRunState.running : WorkRunState.paused,
);
tasks = [
for (final t in tasks)
if (t.taskId == updated.taskId) updated else t,
];
changes.add(updated);
return WorkResult.ok(updated);
}
@override
Future<WorkResult<WorkTaskSnapshot>> pauseWork({
required String deviceId,
required WorkTaskSnapshot task,
}) => command(deviceId, task, false);
@override
Future<WorkResult<WorkTaskSnapshot>> resumeWork({
required String deviceId,
required WorkTaskSnapshot task,
}) => command(deviceId, task, true);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUpAll(RegionTestLocalizations.loadAssets);
Future<void> mount(WidgetTester tester, FakeGateway gateway) async {
addTearDown(gateway.changes.close);
await tester.pumpWidget(
MaterialApp(
locale: AppLocalizations.chinese,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: const [
RegionTestLocalizations(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: Scaffold(
body: SizedBox(
width: 160,
child: DeviceTaskControlTile(deviceId: 'SN-1', gateway: gateway),
),
),
),
);
await tester.pumpAndSettle();
}
Future<void> open(WidgetTester tester) async {
await tester.tap(find.byType(DeviceTaskControlTile));
await tester.pumpAndSettle();
}
Future<void> confirm(WidgetTester tester, String label) async {
await tester.tap(find.text(label));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
}
Color? background(WidgetTester tester) =>
(tester
.widget<Container>(
find.byKey(
const ValueKey('device-task-control-background'),
),
)
.decoration
as BoxDecoration)
.color;
testWidgets('pause turns yellow and next action resumes the same task', (
tester,
) async {
final gateway = FakeGateway();
await mount(tester, gateway);
await open(tester);
expect(gateway.calls, isEmpty);
await confirm(tester, '确认暂停');
expect(gateway.calls, ['pause/SN-1/42']);
expect(background(tester), const Color(0xFFFFF3CD));
expect(find.text('恢复任务'), findsOneWidget);
await open(tester);
await confirm(tester, '确认继续作业');
expect(gateway.calls, ['pause/SN-1/42', 'resume/SN-1/42']);
expect(background(tester), Colors.white);
expect(gateway.loads, 5);
});
testWidgets('other devices and ended tasks cannot be operated', (
tester,
) async {
final gateway = FakeGateway()
..tasks = [task(device: 'SN-other'), task(state: WorkRunState.finished)];
await mount(tester, gateway);
await open(tester);
expect(find.text('当前没有任务'), findsOneWidget);
expect(gateway.calls, isEmpty);
});
testWidgets('task ends while confirming: no pause is sent', (tester) async {
final gateway = FakeGateway();
await mount(tester, gateway);
await open(tester);
gateway.tasks = [];
gateway.changes.add(task(state: WorkRunState.finished));
await confirm(tester, '确认暂停');
expect(find.text('当前没有任务'), findsOneWidget);
expect(gateway.calls, isEmpty);
expect(background(tester), Colors.white);
});
testWidgets('pool error is distinct from no task and cannot send commands', (
tester,
) async {
final gateway = FakeGateway()..loadFails = true;
await mount(tester, gateway);
await open(tester);
expect(find.text('任务池查询失败,请稍后重试。'), findsOneWidget);
expect(gateway.calls, isEmpty);
});
testWidgets('failed resume preserves yellow paused state', (tester) async {
final gateway = FakeGateway()
..tasks = [task(state: WorkRunState.paused)]
..commandFails = true;
await mount(tester, gateway);
expect(background(tester), const Color(0xFFFFF3CD));
await open(tester);
await confirm(tester, '确认继续作业');
expect(find.text('恢复失败,请重试。'), findsOneWidget);
expect(background(tester), const Color(0xFFFFF3CD));
});
testWidgets('multiple tasks require an explicit selection', (tester) async {
final gateway = FakeGateway()..tasks = [task(), task(id: '43')];
await mount(tester, gateway);
await open(tester);
expect(find.text('选择要暂停或恢复的任务'), findsOneWidget);
await tester.tap(find.text('Route 43'));
await tester.pumpAndSettle();
await confirm(tester, '确认暂停');
expect(gateway.calls, ['pause/SN-1/43']);
expect(background(tester), const Color(0xFFFFF3CD));
await open(tester);
expect(find.text('选择要暂停或恢复的任务'), findsNothing);
await confirm(tester, '确认继续作业');
expect(gateway.calls, ['pause/SN-1/43', 'resume/SN-1/43']);
});
testWidgets('pending command blocks duplicate taps and terminal event wins', (
tester,
) async {
final gateway = FakeGateway()..pending = Completer();
await mount(tester, gateway);
await open(tester);
await confirm(tester, '确认暂停');
await tester.tap(find.byType(DeviceTaskControlTile));
await tester.pump();
expect(gateway.calls, ['pause/SN-1/42']);
gateway.changes.add(task(state: WorkRunState.finished));
gateway.pending!.complete(WorkResult.ok(task(state: WorkRunState.paused)));
await tester.pumpAndSettle();
expect(background(tester), Colors.white);
expect(find.text('任务暂停'), findsOneWidget);
});
testWidgets('queued task cannot be paused', (tester) async {
final gateway = FakeGateway()..tasks = [task(state: WorkRunState.queued)];
await mount(tester, gateway);
await open(tester);
expect(find.text('当前任务尚未执行或正在结束,暂不能暂停或恢复。'), findsOneWidget);
expect(gateway.calls, isEmpty);
});
}