对接连续任务列表接口和连续任务下发,便捷操作中暂停任务的对接,底部导航tab项的设置根据用户角色显示和设置
This commit is contained in:
239
test/device_task_control_tile_test.dart
Normal file
239
test/device_task_control_tile_test.dart
Normal file
@@ -0,0 +1,239 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user