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 tasks = [task()]; final changes = StreamController.broadcast(sync: true); final calls = []; bool loadFails = false; bool commandFails = false; int loads = 0; Completer>? pending; @override WorkSceneCapabilities get capabilities => const WorkSceneCapabilities(); @override Stream watchTaskChanges({required String deviceId}) => changes.stream; @override Future>> loadTasks({ required String deviceId, }) async { loads++; return loadFails ? const WorkResult.fail('offline') : WorkResult.ok([...tasks]); } Future> 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> pauseWork({ required String deviceId, required WorkTaskSnapshot task, }) => command(deviceId, task, false); @override Future> 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 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 open(WidgetTester tester) async { await tester.tap(find.byType(DeviceTaskControlTile)); await tester.pumpAndSettle(); } Future 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( 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); }); }