diff --git a/packages/flutter/lib/src/painting/debug.dart b/packages/flutter/lib/src/painting/debug.dart index de0ff3d0314..3ffb639e621 100644 --- a/packages/flutter/lib/src/painting/debug.dart +++ b/packages/flutter/lib/src/painting/debug.dart @@ -127,6 +127,37 @@ class ImageSizeInfo { /// time. PaintImageCallback debugOnPaintImage; +/// If true, the framework will color invert and horizontally flip images that +/// have been decoded to a size taking at least [debugImageOverheadAllowance] +/// bytes more than necessary. +/// +/// It will also call [FlutterError.reportError] with information about the +/// image's decoded size and its display size, which can be used resize the +/// asset before shipping it, apply `cacheHeight` or `cacheWidth` parameters, or +/// directly use a [ResizeImage]. Whenever possible, resizing the image asset +/// itself should be preferred, to avoid unnecessary network traffic, disk space +/// usage, and other memory overhead incurred during decoding. +/// +/// Developers using this flag should test their application on appropriate +/// devices and display sizes for their expected deployment targets when using +/// these parameters. For example, an application that responsively resizes +/// images for a desktop and mobile layout should avoid decoding all images at +/// sizes appropriate for mobile when on desktop. Applications should also avoid +/// animating these parameters, as each change will result in a newly decoded +/// image. For example, an image that always grows into view should decode only +/// at its largest size, whereas an image that normally is a thumbnail and then +/// pops into view should be decoded at its smallest size for the thumbnail and +/// the largest size when needed. +/// +/// This has no effect unless asserts are enabled. +bool debugInvertOversizedImages = false; + +/// The number of bytes an image must use before it triggers inversion when +/// [debugInvertOversizedImages] is true. +/// +/// Default is 1024 (1kb). +int debugImageOverheadAllowance = 1024; + /// Returns true if none of the painting library debug variables have been changed. /// /// This function is used by the test framework to ensure that debug variables @@ -142,7 +173,9 @@ bool debugAssertAllPaintingVarsUnset(String reason, { bool debugDisableShadowsOv assert(() { if (debugDisableShadows != debugDisableShadowsOverride || debugNetworkImageHttpClientProvider != null || - debugOnPaintImage != null) { + debugOnPaintImage != null || + debugInvertOversizedImages == true || + debugImageOverheadAllowance != 1024) { throw FlutterError(reason); } return true; diff --git a/packages/flutter/lib/src/painting/decoration_image.dart b/packages/flutter/lib/src/painting/decoration_image.dart index 11071992756..6779d92c484 100644 --- a/packages/flutter/lib/src/painting/decoration_image.dart +++ b/packages/flutter/lib/src/painting/decoration_image.dart @@ -456,42 +456,6 @@ void paintImage({ assert(sourceSize == inputSize, 'centerSlice was used with a BoxFit that does not guarantee that the image is fully visible.'); } - // Output size is fully calculated. - if (!kReleaseMode) { - final ImageSizeInfo sizeInfo = ImageSizeInfo( - // Some ImageProvider implementations may not have given this. - source: debugImageLabel ?? '', - imageSize: Size(image.width.toDouble(), image.height.toDouble()), - displaySize: outputSize, - ); - // Avoid emitting events that are the same as those emitted in the last frame. - if (!_lastFrameImageSizeInfo.contains(sizeInfo)) { - final ImageSizeInfo existingSizeInfo = _pendingImageSizeInfo[sizeInfo.source]; - if (existingSizeInfo == null || existingSizeInfo.displaySizeInBytes < sizeInfo.displaySizeInBytes) { - _pendingImageSizeInfo[sizeInfo.source] = sizeInfo; - } - // _pendingImageSizeInfo.add(sizeInfo); - if (debugOnPaintImage != null) { - debugOnPaintImage(sizeInfo); - } - SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - _lastFrameImageSizeInfo = _pendingImageSizeInfo.values.toSet(); - if (_pendingImageSizeInfo.isEmpty) { - return; - } - developer.postEvent( - 'Flutter.ImageSizesForFrame', - { - for (ImageSizeInfo imageSizeInfo in _pendingImageSizeInfo.values) - imageSizeInfo.source: imageSizeInfo.toJson() - }, - ); - _pendingImageSizeInfo = {}; - }); - - } - } - if (repeat != ImageRepeat.noRepeat && destinationSize == outputSize) { // There's no need to repeat the image because we're exactly filling the // output rect with the image. @@ -510,6 +474,79 @@ void paintImage({ final double dy = halfHeightDelta + alignment.y * halfHeightDelta; final Offset destinationPosition = rect.topLeft.translate(dx, dy); final Rect destinationRect = destinationPosition & destinationSize; + + // Set to true if we added a saveLayer to the canvas to invert/flip the image. + bool invertedCanvas = false; + // Output size and destination rect are fully calculated. + if (!kReleaseMode) { + final ImageSizeInfo sizeInfo = ImageSizeInfo( + // Some ImageProvider implementations may not have given this. + source: debugImageLabel ?? '', + imageSize: Size(image.width.toDouble(), image.height.toDouble()), + displaySize: outputSize, + ); + assert(() { + if (debugInvertOversizedImages && + sizeInfo.decodedSizeInBytes > sizeInfo.displaySizeInBytes + debugImageOverheadAllowance) { + final int overheadInKilobytes = (sizeInfo.decodedSizeInBytes - sizeInfo.displaySizeInBytes) ~/ 1024; + final int outputWidth = outputSize.width.toInt(); + final int outputHeight = outputSize.height.toInt(); + FlutterError.reportError(FlutterErrorDetails( + exception: 'Image $debugImageLabel has a display size of ' + '$outputWidth×$outputHeight but a decode size of ' + '${image.width}×${image.height}, which uses an additional ' + '${overheadInKilobytes}kb.\n\n' + 'Consider resizing the asset ahead of time, supplying a cacheWidth ' + 'parameter of $outputWidth, a cacheHeight parameter of ' + '$outputHeight, or using a ResizeImage.', + library: 'painting library', + context: ErrorDescription('while painting an image'), + )); + // Invert the colors of the canvas. + canvas.saveLayer( + destinationRect, + Paint()..colorFilter = const ColorFilter.matrix([ + -1, 0, 0, 0, 255, + 0, -1, 0, 0, 255, + 0, 0, -1, 0, 255, + 0, 0, 0, 1, 0, + ]), + ); + // Flip the canvas vertically. + final double dy = -(rect.top + rect.height / 2.0); + canvas.translate(0.0, -dy); + canvas.scale(1.0, -1.0); + canvas.translate(0.0, dy); + invertedCanvas = true; + } + return true; + }()); + // Avoid emitting events that are the same as those emitted in the last frame. + if (!_lastFrameImageSizeInfo.contains(sizeInfo)) { + final ImageSizeInfo existingSizeInfo = _pendingImageSizeInfo[sizeInfo.source]; + if (existingSizeInfo == null || existingSizeInfo.displaySizeInBytes < sizeInfo.displaySizeInBytes) { + _pendingImageSizeInfo[sizeInfo.source] = sizeInfo; + } + if (debugOnPaintImage != null) { + debugOnPaintImage(sizeInfo); + } + SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { + _lastFrameImageSizeInfo = _pendingImageSizeInfo.values.toSet(); + if (_pendingImageSizeInfo.isEmpty) { + return; + } + developer.postEvent( + 'Flutter.ImageSizesForFrame', + { + for (ImageSizeInfo imageSizeInfo in _pendingImageSizeInfo.values) + imageSizeInfo.source: imageSizeInfo.toJson() + }, + ); + _pendingImageSizeInfo = {}; + }); + } + } + final bool needSave = repeat != ImageRepeat.noRepeat || flipHorizontally; if (needSave) canvas.save(); @@ -541,6 +578,10 @@ void paintImage({ } if (needSave) canvas.restore(); + + if (invertedCanvas) { + canvas.restore(); + } } Iterable _generateImageTileRects(Rect outputRect, Rect fundamentalRect, ImageRepeat repeat) sync* { diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 8ed120487d5..64de57e9794 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -448,6 +448,18 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB } assert(() { + registerBoolServiceExtension( + name: 'invertOversizedImages', + getter: () async => debugInvertOversizedImages, + setter: (bool value) async { + if (debugInvertOversizedImages != value) { + debugInvertOversizedImages = value; + return _forceRebuild(); + } + return Future.value(); + }, + ); + registerBoolServiceExtension( name: 'debugAllowBanner', getter: () => Future.value(WidgetsApp.debugAllowBannerOverride), diff --git a/packages/flutter/test/foundation/service_extensions_test.dart b/packages/flutter/test/foundation/service_extensions_test.dart index f60855a8336..1158bd83580 100644 --- a/packages/flutter/test/foundation/service_extensions_test.dart +++ b/packages/flutter/test/foundation/service_extensions_test.dart @@ -171,7 +171,7 @@ void main() { const int disabledExtensions = kIsWeb ? 2 : 0; // If you add a service extension... TEST IT! :-) // ...then increment this number. - expect(binding.extensions.length, 28 + widgetInspectorExtensionCount - disabledExtensions); + expect(binding.extensions.length, 29 + widgetInspectorExtensionCount - disabledExtensions); expect(console, isEmpty); debugPrint = debugPrintThrottled; @@ -401,6 +401,29 @@ void main() { expect(binding.frameScheduled, isFalse); }); + test('Service extensions - invertOversizedImages', () async { + Map result; + + expect(binding.frameScheduled, isFalse); + expect(debugInvertOversizedImages, false); + result = await binding.testExtension('invertOversizedImages', {}); + expect(result, {'enabled': 'false'}); + expect(debugInvertOversizedImages, false); + result = await binding.testExtension('invertOversizedImages', {'enabled': 'true'}); + expect(result, {'enabled': 'true'}); + expect(debugInvertOversizedImages, true); + result = await binding.testExtension('invertOversizedImages', {}); + expect(result, {'enabled': 'true'}); + expect(debugInvertOversizedImages, true); + result = await binding.testExtension('invertOversizedImages', {'enabled': 'false'}); + expect(result, {'enabled': 'false'}); + expect(debugInvertOversizedImages, false); + result = await binding.testExtension('invertOversizedImages', {}); + expect(result, {'enabled': 'false'}); + expect(debugInvertOversizedImages, false); + expect(binding.frameScheduled, isFalse); + }); + test('Service extensions - profileWidgetBuilds', () async { Map result; diff --git a/packages/flutter/test/painting/paint_image_test.dart b/packages/flutter/test/painting/paint_image_test.dart index fbec016f84b..32bdcf55b62 100644 --- a/packages/flutter/test/painting/paint_image_test.dart +++ b/packages/flutter/test/painting/paint_image_test.dart @@ -8,6 +8,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/painting.dart'; @@ -64,6 +65,66 @@ void main() { expect(command.positionalArguments[2], equals(const Rect.fromLTWH(50.0, 75.0, 200.0, 100.0))); }); + test('debugInvertOversizedImages', () { + debugInvertOversizedImages = true; + final FlutterExceptionHandler oldFlutterError = FlutterError.onError; + + final List messages = []; + FlutterError.onError = (FlutterErrorDetails details) { + messages.add(details.exceptionAsString()); + }; + + final TestImage image = TestImage(width: 300, height: 300); + final TestCanvas canvas = TestCanvas(); + const Rect rect = Rect.fromLTWH(50.0, 50.0, 200.0, 100.0); + + paintImage( + canvas: canvas, + rect: rect, + image: image, + debugImageLabel: 'TestImage', + fit: BoxFit.fill, + ); + + final List commands = canvas.invocations + .skipWhile((Invocation invocation) => invocation.memberName != #saveLayer) + .take(4) + .toList(); + + expect(commands[0].positionalArguments[0], rect); + final Paint paint = commands[0].positionalArguments[1] as Paint; + expect( + paint.colorFilter, + const ColorFilter.matrix([ + -1, 0, 0, 0, 255, + 0, -1, 0, 0, 255, + 0, 0, -1, 0, 255, + 0, 0, 0, 1, 0, + ]), + ); + expect(commands[1].memberName, #translate); + expect(commands[1].positionalArguments[0], 0.0); + expect(commands[1].positionalArguments[1], 100.0); + + expect(commands[2].memberName, #scale); + expect(commands[2].positionalArguments[0], 1.0); + expect(commands[2].positionalArguments[1], -1.0); + + + expect(commands[3].memberName, #translate); + expect(commands[3].positionalArguments[0], 0.0); + expect(commands[3].positionalArguments[1], -100.0); + + expect( + messages.single, + 'Image TestImage has a display size of 200×100 but a decode size of 300×300, which uses an additional 364kb.\n\n' + 'Consider resizing the asset ahead of time, supplying a cacheWidth parameter of 200, a cacheHeight parameter of 100, or using a ResizeImage.', + ); + + debugInvertOversizedImages = false; + FlutterError.onError = oldFlutterError; + }); + testWidgets('Reports Image painting', (WidgetTester tester) async { ImageSizeInfo imageSizeInfo; int count = 0; diff --git a/packages/flutter_tools/lib/src/base/command_help.dart b/packages/flutter_tools/lib/src/base/command_help.dart index 128458168ec..1a21b59104d 100644 --- a/packages/flutter_tools/lib/src/base/command_help.dart +++ b/packages/flutter_tools/lib/src/base/command_help.dart @@ -13,6 +13,7 @@ import 'terminal.dart'; // ignore_for_file: non_constant_identifier_names const String fire = '🔥'; +const String image = '🖼️'; const int maxLineWidth = 84; /// Encapsulates the help text construction and printing. @@ -35,6 +36,13 @@ class CommandHelp { final OutputPreferences _outputPreferences; + CommandHelpOption _I; + CommandHelpOption get I => _I ??= _makeOption( + 'I', + 'Toggle oversized image inversion $image.', + 'debugInvertOversizedImages', + ); + CommandHelpOption _L; CommandHelpOption get L => _L ??= _makeOption( 'L', diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index 20ab0e1b45c..f0c56f422cc 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -364,6 +364,7 @@ class WindowsStdoutLogger extends StdoutLogger { final String windowsMessage = _terminal.supportsEmoji ? message : message.replaceAll('🔥', '') + .replaceAll('🖼️', '') .replaceAll('✗', 'X') .replaceAll('✓', '√') .replaceAll('🔨', ''); diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index b5caf86bfc5..6321228de00 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart @@ -354,6 +354,18 @@ abstract class ResidentWebRunner extends ResidentRunner { } } + @override + Future debugToggleInvertOversizedImages() async { + try { + await _vmService + ?.flutterToggleInvertOversizedImages( + isolateId: null, + ); + } on vmservice.RPCError { + return; + } + } + @override Future debugToggleProfileWidgetBuilds() async { try { diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 1efc91fc13a..49c4addaea3 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -436,6 +436,15 @@ class FlutterDevice { } } + Future toggleInvertOversizedImages() async { + final List views = await vmService.getFlutterViews(); + for (final FlutterView view in views) { + await vmService.flutterToggleInvertOversizedImages( + isolateId: view.uiIsolate.id, + ); + } + } + Future toggleProfileWidgetBuilds() async { final List views = await vmService.getFlutterViews(); for (final FlutterView view in views) { @@ -1009,6 +1018,12 @@ abstract class ResidentRunner { } } + Future debugToggleInvertOversizedImages() async { + for (final FlutterDevice device in flutterDevices) { + await device.toggleInvertOversizedImages(); + } + } + Future debugToggleProfileWidgetBuilds() async { for (final FlutterDevice device in flutterDevices) { await device.toggleProfileWidgetBuilds(); @@ -1289,6 +1304,7 @@ abstract class ResidentRunner { commandHelp.S.print(); commandHelp.U.print(); commandHelp.i.print(); + commandHelp.I.print(); commandHelp.p.print(); commandHelp.o.print(); commandHelp.z.print(); @@ -1443,12 +1459,17 @@ class TerminalHandler { residentRunner.printHelp(details: true); return true; case 'i': - case 'I': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugToggleWidgetInspector(); return true; } return false; + case 'I': + if (residentRunner.supportsServiceProtocol && residentRunner.isRunningDebug) { + await residentRunner.debugToggleInvertOversizedImages(); + return true; + } + return false; case 'k': if (residentRunner.supportsCanvasKit) { final bool result = await residentRunner.toggleCanvaskit(); @@ -1499,13 +1520,6 @@ class TerminalHandler { // exit await residentRunner.exit(); return true; - case 's': - for (final FlutterDevice device in residentRunner.flutterDevices) { - if (device.device.supportsScreenshot) { - await residentRunner.screenshot(device); - } - } - return true; case 'r': if (!residentRunner.canHotReload) { return false; @@ -1531,6 +1545,13 @@ class TerminalHandler { globals.printStatus('Try again after fixing the above error(s).', emphasis: true); } return true; + case 's': + for (final FlutterDevice device in residentRunner.flutterDevices) { + if (device.device.supportsScreenshot) { + await residentRunner.screenshot(device); + } + } + return true; case 'S': if (residentRunner.supportsServiceProtocol) { await residentRunner.debugDumpSemanticsTreeInTraversalOrder(); diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 9ef2f7f3837..31a5ede978b 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -601,6 +601,10 @@ extension FlutterVmService on vm_service.VmService { @required String isolateId, }) => _flutterToggle('inspector.show', isolateId: isolateId); + Future> flutterToggleInvertOversizedImages({ + @required String isolateId, + }) => _flutterToggle('invertOversizedImages', isolateId: isolateId); + Future> flutterToggleProfileWidgetBuilds({ @required String isolateId, }) => _flutterToggle('profileWidgetBuilds', isolateId: isolateId); diff --git a/packages/flutter_tools/test/general.shard/base/command_help_test.dart b/packages/flutter_tools/test/general.shard/base/command_help_test.dart index 5116c976eb0..5c67519f967 100644 --- a/packages/flutter_tools/test/general.shard/base/command_help_test.dart +++ b/packages/flutter_tools/test/general.shard/base/command_help_test.dart @@ -50,6 +50,10 @@ void _testMessageLength({ expectedWidth += ansiMetaCharactersLength; } + expect( + commandHelp.I.toString().length, + lessThanOrEqualTo(expectedWidth), + ); expect(commandHelp.L.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.P.toString().length, lessThanOrEqualTo(expectedWidth)); expect(commandHelp.R.toString().length, lessThanOrEqualTo(expectedWidth)); diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 17918bd43a8..027de7c7577 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -858,6 +858,7 @@ void main() { commandHelp.S, commandHelp.U, commandHelp.i, + commandHelp.I, commandHelp.p, commandHelp.o, commandHelp.z, @@ -1284,6 +1285,36 @@ void main() { expect(fakeVmServiceHost.hasRemainingExpectations, false); })); + testUsingContext('ResidentRunner debugToggleInvertOversizedImages calls flutter device', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: []); + await residentRunner.debugToggleInvertOversizedImages(); + + verify(mockFlutterDevice.toggleInvertOversizedImages()).called(1); + })); + + testUsingContext('FlutterDevice.toggleInvertOversizedImages invokes correct VM service request', () => testbed.run(() async { + fakeVmServiceHost = FakeVmServiceHost(requests: [ + listViews, + const FakeVmServiceRequest( + method: 'ext.flutter.invertOversizedImages', + args: { + 'isolateId': '1', + }, + jsonResponse: { + 'value': 'false' + }, + ), + ]); + final FlutterDevice device = FlutterDevice( + mockDevice, + buildInfo: BuildInfo.debug, + ); + device.vmService = fakeVmServiceHost.vmService; + + await device.toggleInvertOversizedImages(); + expect(fakeVmServiceHost.hasRemainingExpectations, false); + })); + testUsingContext('ResidentRunner debugToggleDebugCheckElevationsEnabled calls flutter device', () => testbed.run(() async { fakeVmServiceHost = FakeVmServiceHost(requests: []); await residentRunner.debugToggleDebugCheckElevationsEnabled(); diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 5963f908049..67fb1e3fd4f 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -1179,6 +1179,47 @@ void main() { Platform: () => FakePlatform(operatingSystem: 'linux', environment: {}), }); + testUsingContext('debugToggleInvertOversizedImagesOverride', () async { + final ResidentRunner residentWebRunner = setUpResidentRunner(mockFlutterDevice); + fakeVmServiceHost = FakeVmServiceHost(requests: [ + ...kAttachExpectations, + const FakeVmServiceRequest( + method: 'ext.flutter.invertOversizedImages', + args: { + 'isolateId': null, + }, + jsonResponse: { + 'enabled': 'false' + }, + ), + const FakeVmServiceRequest( + method: 'ext.flutter.invertOversizedImages', + args: { + 'isolateId': null, + 'enabled': 'true', + }, + jsonResponse: { + 'enabled': 'true' + }, + ) + ]); + _setupMocks(); + final Completer connectionInfoCompleter = Completer(); + unawaited(residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + )); + await connectionInfoCompleter.future; + + await residentWebRunner.debugToggleInvertOversizedImages(); + + expect(fakeVmServiceHost.hasRemainingExpectations, false); + }, overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Pub: () => MockPub(), + Platform: () => FakePlatform(operatingSystem: 'linux', environment: {}), + }); + testUsingContext('debugToggleWidgetInspector', () async { final ResidentRunner residentWebRunner = setUpResidentRunner(mockFlutterDevice); fakeVmServiceHost = FakeVmServiceHost(requests: [ diff --git a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart index e65bc483e7e..eaacbcb44aa 100644 --- a/packages/flutter_tools/test/general.shard/terminal_handler_test.dart +++ b/packages/flutter_tools/test/general.shard/terminal_handler_test.dart @@ -122,21 +122,39 @@ void main() { verify(mockResidentRunner.toggleCanvaskit()).called(1); }); - testUsingContext('i, I - debugToggleWidgetInspector with service protocol', () async { + testUsingContext('i - debugToggleWidgetInspector with service protocol', () async { await terminalHandler.processTerminalInput('i'); - await terminalHandler.processTerminalInput('I'); - verify(mockResidentRunner.debugToggleWidgetInspector()).called(2); + verify(mockResidentRunner.debugToggleWidgetInspector()).called(1); }); - testUsingContext('i, I - debugToggleWidgetInspector without service protocol', () async { + testUsingContext('i - debugToggleWidgetInspector without service protocol', () async { when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); await terminalHandler.processTerminalInput('i'); - await terminalHandler.processTerminalInput('I'); verifyNever(mockResidentRunner.debugToggleWidgetInspector()); }); + testUsingContext('I - debugToggleInvertOversizedImages with service protocol/debug', () async { + when(mockResidentRunner.isRunningDebug).thenReturn(true); + await terminalHandler.processTerminalInput('I'); + + verify(mockResidentRunner.debugToggleInvertOversizedImages()).called(1); + }); + + testUsingContext('I - debugToggleInvertOversizedImages with service protocol/ndebug', () async { + when(mockResidentRunner.isRunningDebug).thenReturn(false); + await terminalHandler.processTerminalInput('I'); + + verifyNever(mockResidentRunner.debugToggleInvertOversizedImages()); + }); + + testUsingContext('I - debugToggleInvertOversizedImages without service protocol', () async { + when(mockResidentRunner.supportsServiceProtocol).thenReturn(false); + await terminalHandler.processTerminalInput('I'); + + }); + testUsingContext('l - list flutter views', () async { final MockFlutterDevice mockFlutterDevice = MockFlutterDevice(); when(mockResidentRunner.isRunningDebug).thenReturn(true);