feat(ground-suite): add tactical command workspace
This commit is contained in:
1 parent
00a3a9f908
commit
35e0fd8741
10 files changed
+5430
-869
No files matched your search
@@ -1,96 +1,683 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/shared/widgets/surfaces.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('replay workspace keeps diagnostics distinct and read-only', (
|
||||
testWidgets('desktop command dashboard opens the grouped tactical controls', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(const ProviderScope(child: GroundSuiteApp()));
|
||||
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<MaterialApp>(find.byType(MaterialApp));
|
||||
expect(app.themeMode, ThemeMode.system);
|
||||
expect(app.theme?.brightness, Brightness.light);
|
||||
expect(app.darkTheme?.brightness, Brightness.dark);
|
||||
expect(find.byType(FrostedChrome), findsAtLeastNWidgets(1));
|
||||
expect(find.text('OBSERVATION ONLY'), findsOneWidget);
|
||||
expect(find.text('Replay fixture'), findsAtLeastNWidgets(1));
|
||||
expect(find.text('LAST REPORTED HEARTBEAT'), findsOneWidget);
|
||||
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.byTooltip('Appearance: System default'));
|
||||
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();
|
||||
await tester.tap(find.text('Dark'));
|
||||
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<TacticalBrowseMode>(),
|
||||
);
|
||||
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<MaterialApp>(find.byType(MaterialApp)).themeMode,
|
||||
ThemeMode.dark,
|
||||
);
|
||||
expect(Theme.of(context).brightness, Brightness.dark);
|
||||
expect(Theme.of(context).cardColor, panelBackground);
|
||||
expect(
|
||||
tester
|
||||
.widget<Text>(
|
||||
find.text(
|
||||
'LOCAL SCENE DATA · NO VEHICLE LINK · COMMAND AUTHORITY UNAVAILABLE',
|
||||
),
|
||||
)
|
||||
.style
|
||||
?.color,
|
||||
accentOrange,
|
||||
);
|
||||
});
|
||||
|
||||
await tester.tap(find.byIcon(Icons.segment_outlined));
|
||||
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();
|
||||
|
||||
expect(find.text('Event timeline'), findsOneWidget);
|
||||
await tester.tap(find.text('Errors 1'));
|
||||
await tester.tap(find.byKey(const Key('unit-card-DEMO-1023')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('kaptan_drivers::lidar_lite_v3::error::Error::TransferFault'),
|
||||
find.text('LOCAL VISUAL PREVIEW · NO VEHICLE ACTION'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('DEPLOY'), findsNothing);
|
||||
expect(find.text('CONFIGURE'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Shutdown reports 1'));
|
||||
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();
|
||||
|
||||
expect(find.text('Orderly shutdown report'), findsOneWidget);
|
||||
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(
|
||||
'compact desktop layout uses bottom navigation without overflow',
|
||||
'map deselection and hidden demo overlays never select invisibly',
|
||||
(tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(480, 800));
|
||||
await tester.binding.setSurfaceSize(const Size(1600, 960));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
await tester.pumpWidget(const ProviderScope(child: GroundSuiteApp()));
|
||||
await tester.pumpWidget(const GroundSuiteApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('compact-navigation')), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
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('follows a dark system preference', (tester) async {
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue =
|
||||
Brightness.dark;
|
||||
addTearDown(
|
||||
tester.binding.platformDispatcher.clearPlatformBrightnessTestValue,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(const ProviderScope(child: GroundSuiteApp()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final context = tester.element(find.byType(AppShell));
|
||||
expect(Theme.of(context).brightness, Brightness.dark);
|
||||
});
|
||||
|
||||
testWidgets('large desktop text uses the compact session strip', (
|
||||
testWidgets('selection cursor ends pencil mode and restores selection', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1280, 900));
|
||||
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<TacticalBrowseMode>(),
|
||||
);
|
||||
});
|
||||
|
||||
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<TextButton>(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 ProviderScope(child: GroundSuiteApp()));
|
||||
await tester.pumpWidget(const GroundSuiteApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('OBSERVATION ONLY'), findsOneWidget);
|
||||
expect(find.text('Replay ended 12:00:06Z'), findsNothing);
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user