import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ground_suite/app/app.dart'; import 'package:ground_suite/app/app_shell.dart'; import 'package:ground_suite/app/theme.dart'; import 'package:ground_suite/features/command/presentation/tactical_command_page.dart'; import 'package:ground_suite/features/command/presentation/tactical_controller.dart'; import 'package:ground_suite/features/vehicle_workspace/domain/vehicle_catalog.dart'; import 'package:ground_suite/features/vehicle_workspace/presentation/vehicle_workspace_controller.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; void main() { testWidgets('desktop command dashboard opens the grouped tactical controls', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); final app = tester.widget(find.byType(MaterialApp)); expect(app.themeMode, ThemeMode.dark); expect(find.text('TACTICAL COMMAND'), findsOneWidget); expect(find.text('SIMULATED CAMERA CONCEPT'), findsOneWidget); expect(find.text('DEMO-1021'), findsAtLeastNWidgets(1)); expect(find.byKey(const Key('units-menu')), findsOneWidget); expect(find.byKey(const Key('cursor-menu')), findsOneWidget); expect(find.byKey(const Key('view-menu')), findsOneWidget); expect(find.text('UNITS'), findsNothing); expect(find.text('MODE: SELECTION'), findsNothing); expect(find.text('VIEW'), findsNothing); expect( tester.getSize(find.byKey(const Key('units-menu'))), const Size(48, 48), ); expect( tester.getSize(find.byKey(const Key('cursor-menu'))), const Size(48, 48), ); expect( tester.getSize(find.byKey(const Key('view-menu'))), const Size(48, 48), ); await tester.tap(find.byKey(const Key('units-menu'))); await tester.pump(); expect(find.text('Add local unit'), findsOneWidget); expect( find.text('Requires verified source-to-unit binding'), findsOneWidget, ); await tester.tap(find.text('Add local unit')); await tester.pumpAndSettle(); expect(find.text('UNIT HANGAR'), findsOneWidget); expect(find.text('LOCAL SCENE'), findsAtLeastNWidgets(1)); expect(find.byKey(const Key('unit-hangar-strip')), findsOneWidget); expect( find.byKey(const Key('hangar-card-kaptan.vehicle.quadcopter.local')), findsOneWidget, ); expect( find.byKey(const Key('hangar-card-kaptan.vehicle.fixed-wing.local')), findsOneWidget, ); expect( find.byKey(const Key('hangar-card-kaptan.vehicle.tiltrotor-vtol.local')), findsOneWidget, ); expect( find.byKey(const Key('hangar-card-kaptan.vehicle.helicopter.local')), findsOneWidget, ); expect( find.byKey(const Key('hangar-card-kaptan.vehicle.rover.local')), findsOneWidget, ); expect( find.byKey(const Key('hangar-card-kaptan.vehicle.surface-vessel.local')), findsOneWidget, ); await tester.tap( find.byKey(const Key('hangar-card-kaptan.vehicle.quadcopter.local')), ); await tester.pumpAndSettle(); expect(find.text('PREPARE UNIT'), findsOneWidget); expect(find.text('QUADCOPTER-01'), findsNothing); expect( ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider).unitDraft, isNotNull, ); final rectangle = await tester.startGesture(const Offset(620, 500)); await rectangle.moveTo(const Offset(760, 560)); await rectangle.up(); await tester.pumpAndSettle(); final draftState = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider); expect(draftState.panel, TacticalPanel.unitHangar); expect(draftState.unitDraft?.definition.name, 'Quadcopter'); await tester.tap(find.text('PREPARE UNIT')); await tester.pumpAndSettle(); expect(find.text('UNIT SETUP'), findsOneWidget); expect(find.text('VEHICLE CONFIGURATION UNAVAILABLE'), findsOneWidget); expect( find.text( 'Unavailable until verified binding, compatible schema, command authority, and safe activation are available.', ), findsOneWidget, ); await tester.tap(find.text('ADD TO LOCAL SCENE')); await tester.pumpAndSettle(); expect( ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider).mode, isA(), ); expect(find.text('QUADCOPTER-01'), findsAtLeastNWidgets(1)); await tester.tap(find.text('SET UP')); await tester.pumpAndSettle(); expect(find.text('LOCAL UNIT SETUP'), findsOneWidget); expect(find.byKey(const Key('local-unit-name-field')), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('dark tactical theme is fixed and uses the palette tokens', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); final context = tester.element(find.byType(AppShell)); expect( tester.widget(find.byType(MaterialApp)).themeMode, ThemeMode.dark, ); expect(Theme.of(context).brightness, Brightness.dark); expect(Theme.of(context).cardColor, panelBackground); expect( tester .widget( find.text( 'LOCAL SCENE DATA · NO VEHICLE LINK · COMMAND AUTHORITY UNAVAILABLE', ), ) .style ?.color, accentOrange, ); }); testWidgets('selection console stays local and exposes no vehicle action', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('unit-card-DEMO-1023'))); await tester.pumpAndSettle(); expect( find.text('LOCAL VISUAL PREVIEW · NO VEHICLE ACTION'), findsOneWidget, ); expect(find.text('DEPLOY'), findsNothing); expect(find.text('CONFIGURE'), findsNothing); await tester.tap(find.text('EVENT STREAM')); await tester.pumpAndSettle(); expect(find.text('Event timeline'), findsOneWidget); await tester.tap(find.text('Tactical workspace')); await tester.pumpAndSettle(); expect(find.text('TACTICAL COMMAND'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('pencil cursor draws directly on the map with its toolbar', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider.notifier).beginPencil(); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsOneWidget); expect(find.byKey(const Key('pencil-hue')), findsOneWidget); expect(find.byKey(const Key('pencil-saturation')), findsOneWidget); expect(find.byKey(const Key('pencil-brightness')), findsOneWidget); expect(find.byKey(const Key('pencil-thickness')), findsOneWidget); await tester.tap(find.byKey(const Key('pencil-swatch-282'))); await tester.drag( find.byKey(const Key('pencil-thickness')), const Offset(70, 0), ); await tester.pump(); final pencil = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider).mode as TacticalPencilMode; expect(pencil.hue, 282); expect(pencil.thickness, greaterThan(3)); final stroke = await tester.startGesture(const Offset(620, 500)); await stroke.moveTo(const Offset(680, 540)); await tester.pump(const Duration(milliseconds: 20)); await stroke.moveTo(const Offset(740, 490)); await tester.pump(const Duration(milliseconds: 20)); await stroke.up(); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsOneWidget); final container = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); expect(container.read(tacticalControllerProvider).drawings, hasLength(1)); expect(find.text('DRAWING'), findsNothing); expect( tester.getSize(find.byKey(const Key('tactical-map-overlay-painter'))), isNot(Size.zero), ); expect(tester.takeException(), isNull); }); testWidgets('marker cursor names a clicked local marker separately', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('cursor-menu'))); await tester.pump(); await tester.tap(find.text('Marker cursor')); await tester.pumpAndSettle(); expect(find.text('MARKER CURSOR'), findsOneWidget); expect(find.byKey(const Key('marker-hue')), findsOneWidget); expect(find.byKey(const Key('marker-saturation')), findsOneWidget); expect(find.byKey(const Key('marker-brightness')), findsOneWidget); await tester.tap(find.byKey(const Key('marker-swatch-208'))); await tester.tapAt(const Offset(700, 520)); await tester.pumpAndSettle(); expect(find.byKey(const Key('marker-name-field')), findsOneWidget); await tester.enterText( find.byKey(const Key('marker-name-field')), 'North staging note', ); await tester.tap(find.text('SAVE MARKER')); await tester.pumpAndSettle(); final state = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider); expect(state.markers, hasLength(1)); expect(state.markers.single.name, 'North staging note'); expect(state.markers.single.hue, 208); expect(state.drawings, isEmpty); expect(find.text('North staging note'), findsAtLeastNWidgets(1)); expect(tester.takeException(), isNull); }); testWidgets('a Pencil click alone never creates a marker or drawing', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); final container = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); container.read(tacticalControllerProvider.notifier).beginPencil(); await tester.pumpAndSettle(); await tester.tapAt(const Offset(700, 520)); await tester.pumpAndSettle(); final state = container.read(tacticalControllerProvider); expect(state.drawings, isEmpty); expect(state.markers, isEmpty); expect(find.byKey(const Key('marker-name-field')), findsNothing); }); testWidgets('map marker selection synchronizes the local selection console', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('map-unit-DEMO-1023'))); await tester.pumpAndSettle(); expect(find.text('DEMO-1023'), findsAtLeastNWidgets(1)); expect( ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider).selectedUnitIds, {'DEMO-1023'}, ); }); testWidgets('dragging the tactical canvas creates a local group selection', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); final gesture = await tester.startGesture(const Offset(660, 520)); await gesture.moveTo(const Offset(960, 745)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); final selected = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider).selectedUnitIds; expect(selected, containsAll(['DEMO-1021', 'DEMO-1023'])); expect(find.text('LOCAL GROUP SELECTION'), findsOneWidget); }); testWidgets( 'map deselection and hidden demo overlays never select invisibly', (tester) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('map-unit-DEMO-1023'))); await tester.pump(); await tester.tapAt(const Offset(600, 500)); await tester.pumpAndSettle(); final container = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); expect( container.read(tacticalControllerProvider).selectedUnitIds, isEmpty, ); await tester.tap(find.byKey(const Key('view-menu'))); await tester.pump(); await tester.tap(find.text('Hide Drones')); await tester.pumpAndSettle(); final gesture = await tester.startGesture(const Offset(660, 520)); await gesture.moveTo(const Offset(960, 745)); await gesture.up(); await tester.pumpAndSettle(); expect( container.read(tacticalControllerProvider).selectedUnitIds, isEmpty, ); }, ); testWidgets('selection cursor ends pencil mode and restores selection', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('cursor-menu'))); await tester.pump(); await tester.tap(find.text('Pencil cursor')); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsOneWidget); await tester.tap(find.text('SELECTION CURSOR')); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsNothing); expect(FocusManager.instance.primaryFocus, isNotNull); expect(tester.takeException(), isNull); }); testWidgets('hangar selection prepares a local unit before confirmation', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('units-menu'))); await tester.pump(); await tester.tap(find.text('Add local unit')); await tester.pumpAndSettle(); await tester.tap( find.byKey(const Key('hangar-card-kaptan.vehicle.quadcopter.local')), ); await tester.pumpAndSettle(); final scope = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); expect(scope.read(tacticalControllerProvider).unitDraft, isNotNull); expect(scope.read(vehicleWorkspaceProvider).units, isEmpty); await tester.tap(find.text('PREPARE UNIT')); await tester.pumpAndSettle(); final name = find.byKey(const Key('local-unit-name-field')); await tester.tap(name); await tester.enterText(name, 'North survey'); await tester.pumpAndSettle(); await tester.tap(find.text('ADD TO LOCAL SCENE')); await tester.pumpAndSettle(); expect(find.text('North survey'), findsAtLeastNWidgets(1)); expect(scope.read(vehicleWorkspaceProvider).units, hasLength(1)); expect( scope.read(vehicleWorkspaceProvider).units.single.displayName, 'North survey', ); expect( ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider).mode, isA(), ); }); testWidgets('hangar keeps a draft on back and blocks an invalid local name', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('units-menu'))); await tester.pump(); await tester.tap(find.text('Add local unit')); await tester.pumpAndSettle(); await tester.tap( find.byKey(const Key('hangar-card-kaptan.vehicle.fixed-wing.local')), ); await tester.pumpAndSettle(); await tester.tap(find.text('PREPARE UNIT')); await tester.pumpAndSettle(); final name = find.byKey(const Key('local-unit-name-field')); await tester.tap(name); await tester.enterText(name, ' '); await tester.tap(find.text('ADD TO LOCAL SCENE')); await tester.pumpAndSettle(); expect( find.text('Enter a local unit name of up to 32 characters.'), findsOneWidget, ); final scope = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); expect(scope.read(vehicleWorkspaceProvider).units, isEmpty); await tester.tap(find.text('BACK TO HANGAR')); await tester.pumpAndSettle(); expect(find.text('FIXED-WING SELECTED'), findsOneWidget); expect( scope.read(tacticalControllerProvider).unitDraft?.definition.name, 'Fixed-wing', ); }); testWidgets('hangar supports arrow selection and Escape focus return', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('units-menu'))); await tester.pump(); await tester.tap(find.text('Add local unit')); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); await tester.pumpAndSettle(); final scope = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); expect( scope.read(tacticalControllerProvider).unitDraft?.definition.name, 'Fixed-wing', ); await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pumpAndSettle(); expect(find.text('UNIT HANGAR'), findsNothing); final trigger = find.descendant( of: find.byKey(const Key('units-menu')), matching: find.byType(TextButton), ); expect(tester.widget(trigger).focusNode?.hasFocus, isTrue); }); testWidgets('hangar and setup remain usable at 200 percent text scale', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1440, 960)); tester.binding.platformDispatcher.textScaleFactorTestValue = 2; addTearDown(() { tester.binding.setSurfaceSize(null); tester.binding.platformDispatcher.clearTextScaleFactorTestValue(); }); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); final scope = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); final controller = scope.read(tacticalControllerProvider.notifier); controller.showUnitHangar(); await tester.pumpAndSettle(); expect(find.byKey(const Key('unit-hangar-strip')), findsOneWidget); controller.chooseUnitDefinition( supportedVehicleDefinitions.first, displayName: 'QUADCOPTER-01', ); controller.beginUnitSetup(); await tester.pumpAndSettle(); expect(find.text('UNIT SETUP'), findsOneWidget); expect(find.byKey(const Key('local-unit-name-field')), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('hangar stays available in the short desktop arrangement', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 760)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); final scope = ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ); scope.read(tacticalControllerProvider.notifier).showUnitHangar(); await tester.pumpAndSettle(); expect(find.text('UNIT HANGAR'), findsOneWidget); expect(find.byKey(const Key('unit-hangar-strip')), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('secondary click cancels the pencil cursor', (tester) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('cursor-menu'))); await tester.pump(); await tester.tap(find.text('Pencil cursor')); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsOneWidget); final pointer = TestPointer(42, PointerDeviceKind.mouse); await tester.sendEventToBinding( pointer.down(const Offset(700, 520), buttons: kSecondaryButton), ); await tester.sendEventToBinding(pointer.up()); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsNothing); expect(tester.takeException(), isNull); }); testWidgets('keyboard pencil keeps a visible draft before finishing', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1600, 960)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('cursor-menu'))); await tester.pump(); await tester.tap(find.text('Pencil cursor')); await tester.pumpAndSettle(); for (var index = 0; index < 3; index++) { await tester.tap(find.text('ADD LOCAL POINT')); await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); } await tester.pumpAndSettle(); expect(find.text('LOCAL POINTS: 3'), findsOneWidget); expect(find.text('FINISH DRAWING'), findsOneWidget); await tester.tap(find.text('FINISH DRAWING')); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsOneWidget); }); testWidgets('compact command dashboard remains usable without overflow', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(480, 800)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); expect(find.text('SIMULATED CAMERA CONCEPT'), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('command dashboard handles 200 percent text scale', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1440, 960)); tester.binding.platformDispatcher.textScaleFactorTestValue = 2; addTearDown(() { tester.binding.setSurfaceSize(null); tester.binding.platformDispatcher.clearTextScaleFactorTestValue(); }); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); expect( find.text( 'LOCAL SCENE DATA · NO VEHICLE LINK · COMMAND AUTHORITY UNAVAILABLE', ), findsOneWidget, ); expect(tester.takeException(), isNull); }); testWidgets('pencil toolbar reflows at 200 percent text scale', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1440, 960)); tester.binding.platformDispatcher.textScaleFactorTestValue = 2; addTearDown(() { tester.binding.setSurfaceSize(null); tester.binding.platformDispatcher.clearTextScaleFactorTestValue(); }); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); ProviderScope.containerOf( tester.element(find.byType(TacticalCommandPage)), ).read(tacticalControllerProvider.notifier).beginPencil(); await tester.pumpAndSettle(); expect(find.text('PENCIL CURSOR'), findsOneWidget); expect(find.byKey(const Key('pencil-thickness')), findsOneWidget); expect(tester.takeException(), isNull); }); testWidgets('short desktop window uses the non-overlapping command layout', ( tester, ) async { await tester.binding.setSurfaceSize(const Size(1366, 768)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(const GroundSuiteApp()); await tester.pumpAndSettle(); expect(find.text('SIMULATED CAMERA CONCEPT'), findsOneWidget); expect(tester.takeException(), isNull); }); }