One settings table in CameraBridge watches every camera value into the telemetry tick and backs a generic setCameraValue, so the tabbed camera menu (Exposure / Photo / Video / Other) is built from row specs and the camera's own capability ranges. Writes show at once and fall back to what the camera reports. Adds the quick exposure panel (Auto/A/S/M, ISO slider, shutter and aperture wheels, EV strip, metered EV in M), a new camera column with record timer and interval/pano progress, a zoom column for zoom cameras, tap-to-focus / tap-to-meter, and a live histogram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
423 lines
20 KiB
Dart
423 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:pilotvault_fly/flight_model.dart';
|
||
import 'package:pilotvault_fly/fly/camera_exposure.dart';
|
||
import 'package:pilotvault_fly/fly/camera_menu.dart';
|
||
import 'package:pilotvault_fly/fly/fly_prefs.dart';
|
||
import 'package:pilotvault_fly/fly/product_profile.dart';
|
||
import 'package:pilotvault_fly/fly/state/camera_state.dart';
|
||
import 'package:pilotvault_fly/fly/telemetry_reducer.dart';
|
||
import 'package:pilotvault_fly/ui/fly/dialogs/slide_to_confirm.dart';
|
||
import 'package:pilotvault_fly/ui/fly/fly_screen.dart';
|
||
import 'package:pilotvault_fly/ui/fly/panels/camera_menu/camera_menu_sheet.dart';
|
||
import 'package:pilotvault_fly/ui/fly/panels/camera_write.dart';
|
||
|
||
import 'fake_dji.dart';
|
||
import 'profile_fixtures.dart';
|
||
import 'telemetry_fixtures.dart';
|
||
|
||
/// A connected model whose camera has reported [settings] on top of the
|
||
/// fixture tick's, plus a system state and an exposure read-back.
|
||
FlightModel cameraModel(
|
||
Map<String, dynamic> profile, {
|
||
Map<String, Object?> settings = const <String, Object?>{},
|
||
Map<String, Object?> system = const <String, Object?>{},
|
||
}) {
|
||
final FlightModel m = FlightModel()
|
||
..connected = true
|
||
..profile = ProductProfile.fromBridge(profile);
|
||
final TelemetryReducer r = TelemetryReducer(m);
|
||
final Map<String, dynamic> t = telemetryTick();
|
||
t['cameraSettings'] = <Object?, Object?>{
|
||
...(t['cameraSettings'] as Map<Object?, Object?>),
|
||
'exposureMode': 'PROGRAM',
|
||
'isoSetting': 'AUTO',
|
||
'shootPhotoMode': 'SINGLE',
|
||
'meteringMode': 'CENTER',
|
||
'whiteBalancePreset': 'AUTO',
|
||
'histogramEnabled': false,
|
||
...settings,
|
||
};
|
||
r.apply(t);
|
||
r.apply(<String, dynamic>{'type': 'camera', 'isRecording': false, 'mode': 'SHOOT_PHOTO', ...system});
|
||
r.apply(<String, dynamic>{'type': 'exposure', 'iso': '100', 'shutter': '1/240', 'aperture': 'f/2.8', 'ev': '0.0'});
|
||
return m;
|
||
}
|
||
|
||
/// Re-sends the camera's settings group, as the next telemetry tick would.
|
||
void reportSettings(FlightModel m, Map<String, Object?> settings) {
|
||
m.camera.applyTelemetry(settings: <String, dynamic>{...m.camera.reportedSettings, ...settings});
|
||
}
|
||
|
||
void main() {
|
||
group('Exposure rules', () {
|
||
test('A only on adjustable-aperture lenses', () {
|
||
const List<String> all = <String>['PROGRAM', 'SHUTTER_PRIORITY', 'APERTURE_PRIORITY', 'MANUAL'];
|
||
expect(ExposureOptions.modes(const CameraProfile(exposureModes: all)),
|
||
<ExposureMode>[ExposureMode.auto, ExposureMode.shutter, ExposureMode.manual]);
|
||
expect(ExposureOptions.modes(const CameraProfile(exposureModes: all, adjustableApertureSupported: true)), ExposureMode.values);
|
||
expect(ExposureOptions.modes(ProductProfile.fromBridge(phantom4).camera), <ExposureMode>[ExposureMode.auto, ExposureMode.manual],
|
||
reason: 'only what the camera reports');
|
||
expect(ExposureOptions.modes(null), ExposureMode.values, reason: 'preview with nothing connected');
|
||
});
|
||
|
||
test('what each mode lets the pilot set', () {
|
||
expect(ExposureMode.auto.isoEditable, isFalse);
|
||
expect(ExposureMode.auto.evEditable, isTrue);
|
||
expect(ExposureMode.shutter.shutterEditable, isTrue);
|
||
expect(ExposureMode.shutter.apertureEditable, isFalse);
|
||
expect(ExposureMode.aperture.apertureEditable, isTrue);
|
||
expect(ExposureMode.manual.evEditable, isFalse, reason: 'EV is the meter in M');
|
||
expect(ExposureMode.fromWire('SHUTTER_PRIORITY'), ExposureMode.shutter);
|
||
});
|
||
|
||
test('option lists come from the camera, with fallbacks before the handshake', () {
|
||
final CameraProfile? c = ProductProfile.fromBridge(phantom4).camera;
|
||
expect(ExposureOptions.isos(c), <String>['100', '200', '400', '800', '1600'], reason: 'AUTO is its own toggle');
|
||
expect(ExposureOptions.shutters(c), <String>['1/8000', '1/1000', '1/100', '1/30']);
|
||
expect(ExposureOptions.isos(const CameraProfile()), isNotEmpty);
|
||
expect(ExposureOptions.evs(c), <String>['-3.0', '0.0', '+3.0']);
|
||
});
|
||
|
||
test('EV steps to the neighbour in the camera range and stops at the ends', () {
|
||
final List<String> evs = ExposureOptions.evs(null);
|
||
expect(ExposureOptions.stepEv('0.0', evs, 1), '+0.3');
|
||
expect(ExposureOptions.stepEv('0.0', evs, -1), '-0.3');
|
||
expect(ExposureOptions.stepEv('+3.0', evs, 1), isNull);
|
||
expect(ExposureOptions.stepEv(null, evs, 1), '+0.3');
|
||
});
|
||
|
||
test('a wheel starts on the nearest value', () {
|
||
const List<String> s = <String>['1/1000', '1/500', '1/240', '1/120'];
|
||
expect(ExposureOptions.nearest(s, '1/240'), 2);
|
||
expect(ExposureOptions.nearest(s, '1/250'), 2);
|
||
expect(ExposureOptions.nearest(const <String>['f/2.8', 'f/4', 'f/5.6'], 'f/4.5'), 1);
|
||
});
|
||
|
||
test('meter verdict', () {
|
||
final CameraState c = CameraState()..applyExposure(<String, dynamic>{'ev': '-1.0'});
|
||
expect(meterVerdict(c), MeterVerdict.under);
|
||
c.applyExposure(<String, dynamic>{'ev': '+0.3'});
|
||
expect(meterVerdict(c), MeterVerdict.ok);
|
||
});
|
||
});
|
||
|
||
group('Zoom', () {
|
||
test('factor, steps and the digital range', () {
|
||
final CameraState c = CameraState()
|
||
..applyTelemetry(settings: <String, dynamic>{
|
||
'zoomFocalLength': 240,
|
||
'zoomMin': 240,
|
||
'zoomMax': 960,
|
||
'zoomOpticalMax': 480,
|
||
'zoomStep': 10,
|
||
});
|
||
final ZoomInfo z = c.zoom!;
|
||
expect(z.factor, 1.0);
|
||
expect(z.maxFactor, 4.0);
|
||
expect(z.stepped(true), 330);
|
||
expect(z.stepped(false), 240, reason: 'clamped at the wide end');
|
||
expect(z.digital, isFalse);
|
||
c.applyTelemetry(settings: <String, dynamic>{...c.reportedSettings, 'zoomFocalLength': 600});
|
||
expect(c.zoom!.digital, isTrue);
|
||
});
|
||
|
||
test('no zoom without a spec', () {
|
||
expect((CameraState()..applyTelemetry(settings: <String, dynamic>{'zoomFocalLength': 240})).zoom, isNull);
|
||
});
|
||
});
|
||
|
||
group('Pending camera writes', () {
|
||
test('show at once, clear when the camera reports the setting moved', () {
|
||
final FlightModel m = cameraModel(phantom4);
|
||
final CameraState c = m.camera;
|
||
c.setPending(<String, Object?>{'photoAspectRatio': 'RATIO_16_9'});
|
||
expect(c.reportedSettings['photoAspectRatio'], 'RATIO_3_2');
|
||
expect(c.photoAspectRatio, 'RATIO_16_9');
|
||
expect(c.isPending('photoAspectRatio'), isTrue);
|
||
|
||
// A tick that only moved storage leaves the pending value up.
|
||
c.applyTelemetry(storage: <String, dynamic>{'inserted': true}, settings: c.reportedSettings);
|
||
expect(c.isPending('photoAspectRatio'), isTrue);
|
||
|
||
reportSettings(m, <String, Object?>{'photoAspectRatio': 'RATIO_16_9'});
|
||
expect(c.isPending('photoAspectRatio'), isFalse);
|
||
expect(c.setting('photoAspectRatio'), 'RATIO_16_9');
|
||
});
|
||
|
||
test('roll back to the reported value', () {
|
||
final CameraState c = cameraModel(phantom4).camera;
|
||
c.setPending(<String, Object?>{'photoAspectRatio': 'RATIO_16_9'});
|
||
c.dropPending(<String>['photoAspectRatio']);
|
||
expect(c.setting('photoAspectRatio'), 'RATIO_3_2');
|
||
});
|
||
|
||
test('a refused write rolls back and reports', () async {
|
||
final CameraState c = cameraModel(phantom4).camera;
|
||
final FakeDjiService dji = FakeDjiService()..failWith = PlatformException(code: 'DJI_ERROR', message: 'nope');
|
||
String? failed;
|
||
await writeCameraSetting(
|
||
c,
|
||
label: 'Image size',
|
||
pending: <String, Object?>{'photoAspectRatio': 'RATIO_16_9'},
|
||
send: () => dji.setCameraValue('photoAspectRatio', 'RATIO_16_9'),
|
||
onError: (String label, _) => failed = label,
|
||
);
|
||
expect(failed, 'Image size');
|
||
expect(c.setting('photoAspectRatio'), 'RATIO_3_2');
|
||
});
|
||
});
|
||
|
||
group('Camera menu rows', () {
|
||
List<String> ids(CameraMenuTab tab, FlightModel m) =>
|
||
CameraMenu.rowsFor(tab, m.profile, m.camera).map((CameraRow r) => r.id).toList();
|
||
|
||
test('photo-mode sub-rows follow the photo mode', () {
|
||
FlightModel m = cameraModel(phantom4, settings: <String, Object?>{'burstCount': 'BURST_COUNT_3', 'intervalSeconds': 5});
|
||
expect(ids(CameraMenuTab.photo, m), <String>['shootPhotoMode', 'photoAspectRatio', 'photoFileFormat']);
|
||
m = cameraModel(phantom4, settings: <String, Object?>{'shootPhotoMode': 'BURST', 'burstCount': 'BURST_COUNT_3'});
|
||
expect(ids(CameraMenuTab.photo, m), contains('burstCount'));
|
||
m = cameraModel(phantom4, settings: <String, Object?>{'shootPhotoMode': 'INTERVAL', 'intervalSeconds': 5});
|
||
expect(ids(CameraMenuTab.photo, m), contains('intervalSeconds'));
|
||
});
|
||
|
||
test('a setting the camera never reported has no row', () {
|
||
final FlightModel m = cameraModel(phantom4);
|
||
expect(ids(CameraMenuTab.video, m), isNot(contains('videoCaption')));
|
||
expect(ids(CameraMenuTab.video, cameraModel(phantom4, settings: <String, Object?>{'videoCaption': true})), contains('videoCaption'));
|
||
});
|
||
|
||
test('product predicates hide rows', () {
|
||
expect(ids(CameraMenuTab.exposure, cameraModel(phantom4, settings: <String, Object?>{'focusMode': 'AFC'})),
|
||
isNot(contains('focusMode')), reason: 'fixed-focus lens');
|
||
expect(ids(CameraMenuTab.exposure, cameraModel(inspire2X5s)), contains('focusMode'));
|
||
});
|
||
|
||
test('custom white balance shows the temperature stepper', () {
|
||
expect(ids(CameraMenuTab.video, cameraModel(phantom4)), isNot(contains('whiteBalanceKelvin')));
|
||
expect(
|
||
ids(CameraMenuTab.video, cameraModel(phantom4, settings: <String, Object?>{'whiteBalancePreset': 'CUSTOM', 'whiteBalanceKelvin': 5600})),
|
||
contains('whiteBalanceKelvin'),
|
||
);
|
||
});
|
||
|
||
test('options come from the camera range when it published one', () {
|
||
final FlightModel m = cameraModel(phantom4);
|
||
final ChoiceRow wb = CameraMenu.all.whereType<ChoiceRow>().firstWhere((ChoiceRow r) => r.id == 'whiteBalancePreset');
|
||
expect(wb.options(m.profile, m.camera).map((CameraOption o) => o.value), <String>['AUTO', 'SUNNY', 'CLOUDY', 'CUSTOM']);
|
||
final ChoiceRow size = CameraMenu.all.whereType<ChoiceRow>().firstWhere((ChoiceRow r) => r.id == 'videoSize');
|
||
expect(size.options(m.profile, m.camera).map((CameraOption o) => o.label), <String>['4K 29.97p', '1080p 59.94p']);
|
||
expect(size.current(m.camera), 'RESOLUTION_3840x2160@FRAME_RATE_29_DOT_970_FPS');
|
||
});
|
||
|
||
test('labels', () {
|
||
expect(CameraLabels.of('RATIO_16_9'), '16:9');
|
||
expect(CameraLabels.of('BURST_COUNT_7'), '7');
|
||
expect(CameraLabels.of('FILM_A'), 'Film a');
|
||
expect(CameraLabels.fpsLabel('FRAME_RATE_30_FPS'), '30p');
|
||
});
|
||
|
||
test('Format and Reset wait for shooting to stop', () {
|
||
final ActionRow format = CameraMenu.all.whereType<ActionRow>().firstWhere((ActionRow r) => r.id == 'formatSd');
|
||
final FlightModel m = cameraModel(phantom4, system: <String, Object?>{'isRecording': true});
|
||
expect(format.blockedReason(m.camera), 'Stop shooting first');
|
||
expect(format.blockedReason(cameraModel(phantom4).camera), isNull);
|
||
});
|
||
});
|
||
|
||
group('Camera HUD', () {
|
||
Future<FakeDjiService> pump(WidgetTester tester, FlightModel m, {Size size = const Size(844, 390)}) async {
|
||
tester.view.physicalSize = size;
|
||
tester.view.devicePixelRatio = 1;
|
||
addTearDown(tester.view.reset);
|
||
final FakeDjiService dji = FakeDjiService();
|
||
// A fresh navigator, so a sheet left open by an earlier pump is gone.
|
||
await tester.pumpWidget(const SizedBox());
|
||
await tester.pumpWidget(MaterialApp(home: FlyScreen(model: m, dji: dji)));
|
||
await tester.pump(const Duration(milliseconds: 50));
|
||
return dji;
|
||
}
|
||
|
||
Future<void> openMenu(WidgetTester tester, String tab) async {
|
||
await tester.tap(find.byKey(const ValueKey<String>('camera-menu')));
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
await tester.tap(find.byKey(ValueKey<String>('tab-$tab')));
|
||
await tester.pump();
|
||
}
|
||
|
||
testWidgets('a menu row writes, shows the pending value, then what the camera reports', (WidgetTester tester) async {
|
||
final FlightModel m = cameraModel(phantom4);
|
||
final FakeDjiService dji = await pump(tester, m);
|
||
await openMenu(tester, 'photo');
|
||
expect(find.text('3:2'), findsOneWidget);
|
||
await tester.tap(find.byKey(const ValueKey<String>('row-photoAspectRatio')));
|
||
await tester.pump();
|
||
await tester.tap(find.byKey(const ValueKey<String>('opt-RATIO_16_9')));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setCameraValue(photoAspectRatio, RATIO_16_9)'));
|
||
expect(find.text('16:9'), findsOneWidget);
|
||
// The camera never confirmed: the row returns to the real value.
|
||
await tester.pump(cameraSettle);
|
||
expect(find.text('3:2'), findsOneWidget);
|
||
expect(tester.takeException(), isNull);
|
||
});
|
||
|
||
testWidgets('the camera confirming keeps the new value', (WidgetTester tester) async {
|
||
final FlightModel m = cameraModel(phantom4);
|
||
await pump(tester, m);
|
||
await openMenu(tester, 'photo');
|
||
await tester.tap(find.byKey(const ValueKey<String>('row-photoFileFormat')));
|
||
await tester.pump();
|
||
await tester.tap(find.byKey(const ValueKey<String>('opt-JPEG')));
|
||
await tester.pump();
|
||
reportSettings(m, <String, Object?>{'photoFileFormat': 'JPEG'});
|
||
await tester.pump(cameraSettle);
|
||
expect(find.text('JPEG'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('a refused write snaps back and says why', (WidgetTester tester) async {
|
||
final FlightModel m = cameraModel(phantom4);
|
||
final FakeDjiService dji = await pump(tester, m);
|
||
dji.failWith = PlatformException(code: 'DJI_ERROR', message: 'Not supported');
|
||
await openMenu(tester, 'video');
|
||
await tester.tap(find.byKey(const ValueKey<String>('row-videoFileFormat')));
|
||
await tester.pump();
|
||
await tester.tap(find.byKey(const ValueKey<String>('opt-MP4')));
|
||
await tester.pump();
|
||
await tester.pump();
|
||
expect(find.text('MOV'), findsOneWidget);
|
||
expect(find.textContaining('Not supported'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('quick exposure: switching to M turns EV into the meter', (WidgetTester tester) async {
|
||
final FlightModel m = cameraModel(phantom4);
|
||
final FakeDjiService dji = await pump(tester, m);
|
||
await tester.tap(find.byKey(const ValueKey<String>('camera-info')));
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
expect(find.text('EV'), findsWidgets);
|
||
await tester.tap(find.byKey(const ValueKey<String>('ev-plus')));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setEV(+3.0)'), reason: 'next step in the camera range');
|
||
await tester.tap(find.byKey(const ValueKey<String>('exposure-manual')));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setCameraValue(exposureMode, MANUAL)'));
|
||
expect(find.text('METERED'), findsOneWidget);
|
||
dji.calls.clear();
|
||
await tester.tap(find.byKey(const ValueKey<String>('ev-plus')));
|
||
await tester.pump();
|
||
expect(dji.calls, isEmpty, reason: 'EV is not settable in M');
|
||
await tester.pump(cameraSettle);
|
||
});
|
||
|
||
testWidgets('no zoom column without a zoom camera; stepped zoom on hybrid', (WidgetTester tester) async {
|
||
await pump(tester, cameraModel(phantom4));
|
||
expect(find.byKey(const ValueKey<String>('zoom-in')), findsNothing);
|
||
|
||
final FakeDjiService dji = await pump(
|
||
tester,
|
||
cameraModel(mavic2Zoom, settings: <String, Object?>{
|
||
'zoomFocalLength': 240,
|
||
'zoomMin': 240,
|
||
'zoomMax': 960,
|
||
'zoomOpticalMax': 480,
|
||
'zoomStep': 10,
|
||
}),
|
||
);
|
||
expect(find.text('1.0×'), findsOneWidget);
|
||
await tester.tap(find.byKey(const ValueKey<String>('zoom-in')));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setZoomFocalLength(330)'));
|
||
expect(tester.takeException(), isNull);
|
||
});
|
||
|
||
testWidgets('tap focuses on a focusing lens, meters on a fixed one; × cancels metering', (WidgetTester tester) async {
|
||
FakeDjiService dji = await pump(tester, cameraModel(inspire2X5s));
|
||
await tester.tapAt(const Offset(422, 195));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setFocusTarget(0.50, 0.50)'));
|
||
await tester.pump(const Duration(seconds: 2));
|
||
|
||
final FlightModel m = cameraModel(phantom4);
|
||
dji = await pump(tester, m);
|
||
await tester.tapAt(const Offset(422, 195));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setSpotMeteringTarget(0.50, 0.50)'));
|
||
reportSettings(m, <String, Object?>{'meteringMode': 'SPOT'});
|
||
await tester.pump();
|
||
expect(find.text('Exposure'), findsOneWidget);
|
||
await tester.tap(find.byKey(const ValueKey<String>('meter-cancel')));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setCameraValue(meteringMode, CENTER)'));
|
||
expect(find.text('Exposure'), findsNothing);
|
||
await tester.pump(cameraSettle);
|
||
});
|
||
|
||
testWidgets('long press follows the setting', (WidgetTester tester) async {
|
||
addTearDown(() => FlyPrefs.longPress.value = LongPressAction.meter);
|
||
final FakeDjiService dji = await pump(tester, cameraModel(inspire2X5s));
|
||
await tester.longPressAt(const Offset(422, 195));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setSpotMeteringTarget(0.50, 0.50)'));
|
||
FlyPrefs.longPress.value = LongPressAction.off;
|
||
await tester.pump();
|
||
dji.calls.clear();
|
||
await tester.longPressAt(const Offset(422, 195));
|
||
await tester.pump();
|
||
expect(dji.calls, isEmpty);
|
||
});
|
||
|
||
testWidgets('the shutter stops a running interval and shows the mode', (WidgetTester tester) async {
|
||
final FlightModel m = cameraModel(phantom4,
|
||
settings: <String, Object?>{'shootPhotoMode': 'INTERVAL', 'intervalSeconds': 5}, system: <String, Object?>{'shootingInterval': true});
|
||
final FakeDjiService dji = await pump(tester, m);
|
||
expect(find.text('INTERVAL 5s'), findsOneWidget);
|
||
await tester.tap(find.byKey(const ValueKey<String>('shutter')));
|
||
await tester.pump();
|
||
expect(dji.calls, <String>['stopShootPhoto']);
|
||
});
|
||
|
||
testWidgets('AE lock toggles from the column', (WidgetTester tester) async {
|
||
final FakeDjiService dji = await pump(tester, cameraModel(phantom4));
|
||
await tester.tap(find.byKey(const ValueKey<String>('ae-lock')));
|
||
await tester.pump();
|
||
expect(dji.calls, contains('setCameraValue(aeLock, true)'));
|
||
await tester.pump(cameraSettle);
|
||
});
|
||
|
||
testWidgets('Format SD from the menu is slide-confirmed', (WidgetTester tester) async {
|
||
final FakeDjiService dji = await pump(tester, cameraModel(phantom4));
|
||
await openMenu(tester, 'other');
|
||
final Finder format = find.byKey(const ValueKey<String>('action-formatSd'));
|
||
await tester.scrollUntilVisible(format, 60,
|
||
scrollable: find.descendant(of: find.byType(CameraMenuSheet), matching: find.byType(Scrollable)));
|
||
await tester.tap(format);
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
expect(find.textContaining('This cannot be undone'), findsOneWidget);
|
||
expect(dji.calls, isNot(contains('formatStorage')));
|
||
await tester.drag(find.byKey(SlideToConfirmTrack.thumbKey), const Offset(600, 0));
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 500));
|
||
expect(dji.calls, contains('formatStorage'));
|
||
});
|
||
|
||
testWidgets('lays out without overflow on a small phone and a tablet, menu open', (WidgetTester tester) async {
|
||
for (final Size s in const <Size>[Size(740, 340), Size(1280, 800)]) {
|
||
await pump(tester, cameraModel(inspire2X5s), size: s);
|
||
expect(tester.takeException(), isNull, reason: '$s');
|
||
await tester.tap(find.byKey(const ValueKey<String>('camera-menu')));
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
for (final String t in <String>['exposure', 'photo', 'video', 'other']) {
|
||
await tester.tap(find.byKey(ValueKey<String>('tab-$t')));
|
||
await tester.pump();
|
||
expect(tester.takeException(), isNull, reason: '$s $t');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|