// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/targets/shader_compiler.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/resident_devtools_handler.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:test/fake.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import '../src/common.dart'; import '../src/fake_vm_services.dart'; final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( id: '1', pauseEvent: vm_service.Event( kind: vm_service.EventKind.kResume, timestamp: 0 ), breakpoints: <vm_service.Breakpoint>[], extensionRPCs: <String>[], libraries: <vm_service.LibraryRef>[ vm_service.LibraryRef( id: '1', uri: 'file:///hello_world/main.dart', name: '', ), ], livePorts: 0, name: 'test', number: '1', pauseOnExit: false, runnable: true, startTime: 0, isSystemIsolate: false, isolateFlags: <vm_service.IsolateFlag>[], ); final FlutterView fakeFlutterView = FlutterView( id: 'a', uiIsolate: fakeUnpausedIsolate, ); final FakeVmServiceRequest listViews = FakeVmServiceRequest( method: kListViewsMethod, jsonResponse: <String, Object>{ 'views': <Object>[ fakeFlutterView.toJson(), ], }, ); void main() { testWithoutContext('keyboard input handling single help character', () async { final TestRunner testRunner = TestRunner(); final Logger logger = BufferLogger.test(); final Signals signals = Signals.test(); final Terminal terminal = Terminal.test(); final MemoryFileSystem fs = MemoryFileSystem.test(); final ProcessInfo processInfo = ProcessInfo.test(fs); final TerminalHandler terminalHandler = TerminalHandler( testRunner, logger: logger, signals: signals, terminal: terminal, processInfo: processInfo, reportReady: false, ); expect(testRunner.hasHelpBeenPrinted, false); await terminalHandler.processTerminalInput('h'); expect(testRunner.hasHelpBeenPrinted, true); }); testWithoutContext('keyboard input handling help character surrounded with newlines', () async { final TestRunner testRunner = TestRunner(); final Logger logger = BufferLogger.test(); final Signals signals = Signals.test(); final Terminal terminal = Terminal.test(); final MemoryFileSystem fs = MemoryFileSystem.test(); final ProcessInfo processInfo = ProcessInfo.test(fs); final TerminalHandler terminalHandler = TerminalHandler( testRunner, logger: logger, signals: signals, terminal: terminal, processInfo: processInfo, reportReady: false, ); expect(testRunner.hasHelpBeenPrinted, false); await terminalHandler.processTerminalInput('\nh\n'); expect(testRunner.hasHelpBeenPrinted, true); }); group('keycode verification, brought to you by the letter', () { testWithoutContext('a, can handle trailing newlines', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('a\n'); expect(terminalHandler.lastReceivedCommand, 'a'); }); testWithoutContext('n, can handle trailing only newlines', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]); await terminalHandler.processTerminalInput('\n\n'); expect(terminalHandler.lastReceivedCommand, ''); }); testWithoutContext('a - debugToggleProfileWidgetBuilds', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'enabled': 'false', }, ), const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: <String, Object>{ 'isolateId': '1', 'enabled': 'true', }, jsonResponse: <String, Object>{ 'enabled': 'true', }, ), ]); await terminalHandler.processTerminalInput('a'); }); testWithoutContext('a - debugToggleProfileWidgetBuilds with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'enabled': 'false', }, ), const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: <String, Object>{ 'isolateId': '1', 'enabled': 'true', }, jsonResponse: <String, Object>{ 'enabled': 'true', }, ), ], web: true); await terminalHandler.processTerminalInput('a'); }); testWithoutContext('j unsupported jank metrics for web', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], web: true); await terminalHandler.processTerminalInput('j'); expect(terminalHandler.logger.warningText.contains('Unable to get jank metrics for web'), true); }); testWithoutContext('a - debugToggleProfileWidgetBuilds without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('a'); }); testWithoutContext('b - debugToggleBrightness', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'value': 'Brightness.light', } ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: <String, Object>{ 'isolateId': '1', 'value': 'Brightness.dark', }, jsonResponse: <String, Object>{ 'value': 'Brightness.dark', } ), ]); await terminalHandler.processTerminalInput('b'); expect(terminalHandler.logger.statusText, contains('Changed brightness to Brightness.dark')); }); testWithoutContext('b - debugToggleBrightness with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'value': 'Brightness.light', } ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: <String, Object>{ 'isolateId': '1', 'value': 'Brightness.dark', }, jsonResponse: <String, Object>{ 'value': 'Brightness.dark', } ), ], web: true); await terminalHandler.processTerminalInput('b'); expect(terminalHandler.logger.statusText, contains('Changed brightness to Brightness.dark')); }); testWithoutContext('b - debugToggleBrightness without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('b'); }); testWithoutContext('d,D - detach', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await terminalHandler.processTerminalInput('d'); expect(runner.calledDetach, true); runner.calledDetach = false; await terminalHandler.processTerminalInput('D'); expect(runner.calledDetach, true); }); testWithoutContext('h,H,? - printHelp', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await terminalHandler.processTerminalInput('h'); expect(runner.calledPrintWithDetails, true); runner.calledPrintWithDetails = false; await terminalHandler.processTerminalInput('H'); expect(runner.calledPrintWithDetails, true); runner.calledPrintWithDetails = false; await terminalHandler.processTerminalInput('?'); expect(runner.calledPrintWithDetails, true); }); testWithoutContext('i - debugToggleWidgetInspector', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.inspector.show', args: <String, Object>{ 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('i'); }); testWithoutContext('i - debugToggleWidgetInspector with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.inspector.show', args: <String, Object>{ 'isolateId': '1', }, ), ], web: true); await terminalHandler.processTerminalInput('i'); }); testWithoutContext('i - debugToggleWidgetInspector without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('i'); }); testWithoutContext('I - debugToggleInvertOversizedImages', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.invertOversizedImages', args: <String, Object>{ 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('I - debugToggleInvertOversizedImages with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.invertOversizedImages', args: <String, Object>{ 'isolateId': '1', }, ), ], web: true); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('I - debugToggleInvertOversizedImages without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('I - debugToggleInvertOversizedImages in profile mode is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], buildMode: BuildMode.profile); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('L - debugDumpLayerTree', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpLayerTree', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'LAYER TREE', } ), ]); await terminalHandler.processTerminalInput('L'); expect(terminalHandler.logger.statusText, contains('LAYER TREE')); }); testWithoutContext('L - debugDumpLayerTree with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpLayerTree', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'LAYER TREE', } ), ], web: true); await terminalHandler.processTerminalInput('L'); expect(terminalHandler.logger.statusText, contains('LAYER TREE')); }); testWithoutContext('L - debugDumpLayerTree with service protocol and profile mode is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], buildMode: BuildMode.profile); await terminalHandler.processTerminalInput('L'); }); testWithoutContext('L - debugDumpLayerTree without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('L'); }); testWithoutContext('o,O - debugTogglePlatform', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ // Request 1. listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'value': 'iOS', }, ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', 'value': 'fuchsia', }, jsonResponse: <String, Object>{ 'value': 'fuchsia', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'value': 'android', }, ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', 'value': 'iOS', }, jsonResponse: <String, Object>{ 'value': 'iOS', }, ), ]); await terminalHandler.processTerminalInput('o'); await terminalHandler.processTerminalInput('O'); expect(terminalHandler.logger.statusText, contains('Switched operating system to fuchsia')); expect(terminalHandler.logger.statusText, contains('Switched operating system to iOS')); }); testWithoutContext('o,O - debugTogglePlatform with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ // Request 1. listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'value': 'iOS', }, ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', 'value': 'fuchsia', }, jsonResponse: <String, Object>{ 'value': 'fuchsia', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'value': 'android', }, ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: <String, Object>{ 'isolateId': '1', 'value': 'iOS', }, jsonResponse: <String, Object>{ 'value': 'iOS', }, ), ], web: true); await terminalHandler.processTerminalInput('o'); await terminalHandler.processTerminalInput('O'); expect(terminalHandler.logger.statusText, contains('Switched operating system to fuchsia')); expect(terminalHandler.logger.statusText, contains('Switched operating system to iOS')); }); testWithoutContext('o,O - debugTogglePlatform without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('o'); await terminalHandler.processTerminalInput('O'); }); testWithoutContext('p - debugToggleDebugPaintSizeEnabled', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugPaint', args: <String, Object>{ 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('p'); }); testWithoutContext('p - debugToggleDebugPaintSizeEnabled with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugPaint', args: <String, Object>{ 'isolateId': '1', }, ), ], web: true); await terminalHandler.processTerminalInput('p'); }); testWithoutContext('p - debugToggleDebugPaintSizeEnabled without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('p'); }); testWithoutContext('P - debugTogglePerformanceOverlayOverride', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.showPerformanceOverlay', args: <String, Object>{ 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('P'); }); testWithoutContext('P - debugTogglePerformanceOverlayOverride with web target is skipped ', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], web: true); await terminalHandler.processTerminalInput('P'); }); testWithoutContext('P - debugTogglePerformanceOverlayOverride without service protocol is skipped ', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('P'); }); testWithoutContext('S - debugDumpSemanticsTreeInTraversalOrder', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'SEMANTICS DATA', }, ), ]); await terminalHandler.processTerminalInput('S'); expect(terminalHandler.logger.statusText, contains('SEMANTICS DATA')); }); testWithoutContext('S - debugDumpSemanticsTreeInTraversalOrder with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'SEMANTICS DATA', }, ), ], web: true); await terminalHandler.processTerminalInput('S'); expect(terminalHandler.logger.statusText, contains('SEMANTICS DATA')); }); testWithoutContext('S - debugDumpSemanticsTreeInTraversalOrder without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('S'); }); testWithoutContext('U - debugDumpSemanticsTreeInInverseHitTestOrder', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'SEMANTICS DATA', }, ), ]); await terminalHandler.processTerminalInput('U'); expect(terminalHandler.logger.statusText, contains('SEMANTICS DATA')); }); testWithoutContext('U - debugDumpSemanticsTreeInInverseHitTestOrder with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'SEMANTICS DATA', }, ), ], web: true); await terminalHandler.processTerminalInput('U'); expect(terminalHandler.logger.statusText, contains('SEMANTICS DATA')); }); testWithoutContext('U - debugDumpSemanticsTreeInInverseHitTestOrder without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('U'); }); testWithoutContext('t,T - debugDumpRenderTree', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'RENDER DATA 1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'RENDER DATA 2', }, ), ]); await terminalHandler.processTerminalInput('t'); await terminalHandler.processTerminalInput('T'); expect(terminalHandler.logger.statusText, contains('RENDER DATA 1')); expect(terminalHandler.logger.statusText, contains('RENDER DATA 2')); }); testWithoutContext('t,T - debugDumpRenderTree with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'RENDER DATA 1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'RENDER DATA 2', }, ), ], web: true); await terminalHandler.processTerminalInput('t'); await terminalHandler.processTerminalInput('T'); expect(terminalHandler.logger.statusText, contains('RENDER DATA 1')); expect(terminalHandler.logger.statusText, contains('RENDER DATA 2')); }); testWithoutContext('t,T - debugDumpRenderTree without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('t'); await terminalHandler.processTerminalInput('T'); }); testWithoutContext('w,W - debugDumpApp', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'WIDGET DATA 1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'WIDGET DATA 2', }, ), ]); await terminalHandler.processTerminalInput('w'); await terminalHandler.processTerminalInput('W'); expect(terminalHandler.logger.statusText, contains('WIDGET DATA 1')); expect(terminalHandler.logger.statusText, contains('WIDGET DATA 2')); }); testWithoutContext('w,W - debugDumpApp with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'WIDGET DATA 1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: <String, Object>{ 'isolateId': '1', }, jsonResponse: <String, Object>{ 'data': 'WIDGET DATA 2', }, ), ], web: true); await terminalHandler.processTerminalInput('w'); await terminalHandler.processTerminalInput('W'); expect(terminalHandler.logger.statusText, contains('WIDGET DATA 1')); expect(terminalHandler.logger.statusText, contains('WIDGET DATA 2')); }); testWithoutContext('v - launchDevToolsInBrowser', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; final FakeResidentDevtoolsHandler devtoolsHandler = runner.residentDevtoolsHandler as FakeResidentDevtoolsHandler; expect(devtoolsHandler.calledLaunchDevToolsInBrowser, isFalse); await terminalHandler.processTerminalInput('v'); expect(devtoolsHandler.calledLaunchDevToolsInBrowser, isTrue); }); testWithoutContext('w,W - debugDumpApp without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('w'); await terminalHandler.processTerminalInput('W'); }); testWithoutContext('z,Z - debugToggleDebugCheckElevationsEnabled', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: <String, Object>{ 'isolateId': '1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: <String, Object>{ 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('z'); await terminalHandler.processTerminalInput('Z'); }); testWithoutContext('z,Z - debugToggleDebugCheckElevationsEnabled with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: <String, Object>{ 'isolateId': '1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: <String, Object>{ 'isolateId': '1', }, ), ], web: true); await terminalHandler.processTerminalInput('z'); await terminalHandler.processTerminalInput('Z'); }); testWithoutContext('z,Z - debugToggleDebugCheckElevationsEnabled without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('z'); await terminalHandler.processTerminalInput('Z'); }); testWithoutContext('q,Q - exit', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await terminalHandler.processTerminalInput('q'); expect(runner.calledExit, true); runner.calledExit = false; await terminalHandler.processTerminalInput('Q'); expect(runner.calledExit, true); }); testWithoutContext('r - hotReload unsupported', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsHotReload: false); await terminalHandler.processTerminalInput('r'); }); testWithoutContext('R - hotRestart unsupported', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], supportsRestart: false); await terminalHandler.processTerminalInput('R'); }); testWithoutContext('r - hotReload', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await terminalHandler.processTerminalInput('r'); expect(runner.calledReload, true); expect(runner.calledRestart, false); }); testWithoutContext('R - hotRestart', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[]); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await terminalHandler.processTerminalInput('R'); expect(runner.calledReload, false); expect(runner.calledRestart, true); }); testWithoutContext('r - hotReload with non-fatal error', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], reloadExitCode: 1); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await terminalHandler.processTerminalInput('r'); expect(runner.calledReload, true); expect(runner.calledRestart, false); expect(terminalHandler.logger.statusText, contains('Try again after fixing the above error(s).')); }); testWithoutContext('R - hotRestart with non-fatal error', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], reloadExitCode: 1); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await terminalHandler.processTerminalInput('R'); expect(runner.calledReload, false); expect(runner.calledRestart, true); expect(terminalHandler.logger.statusText, contains('Try again after fixing the above error(s).')); }); testWithoutContext('r - hotReload with fatal error', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], reloadExitCode: 1, fatalReloadError: true); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await expectLater(() => terminalHandler.processTerminalInput('r'), throwsToolExit()); expect(runner.calledReload, true); expect(runner.calledRestart, false); }); testWithoutContext('R - hotRestart with fatal error', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], reloadExitCode: 1, fatalReloadError: true); final FakeResidentRunner runner = terminalHandler.residentRunner as FakeResidentRunner; await expectLater(() => terminalHandler.processTerminalInput('R'), throwsToolExit()); expect(runner.calledReload, false); expect(runner.calledRestart, true); }); }); testWithoutContext('ResidentRunner clears the screen when it should', () async { final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[], reloadExitCode: 1, fatalReloadError: true); const String message = 'This should be cleared'; expect(terminalHandler.logger.statusText, equals('')); terminalHandler.logger.printStatus(message); expect(terminalHandler.logger.statusText, equals('$message\n')); // printStatus makes a newline await terminalHandler.processTerminalInput('c'); expect(terminalHandler.logger.statusText, equals('')); }); testWithoutContext('s, can take screenshot on debug device that supports screenshot', () async { final BufferLogger logger = BufferLogger.test(); final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'false', }, ), FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'true', }, ), ], logger: logger, supportsScreenshot: true); await terminalHandler.processTerminalInput('s'); expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)')); }); testWithoutContext('s, can take screenshot on debug device that does not support screenshot', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'false', }, ), FakeVmServiceRequest( method: '_flutter.screenshot', args: <String, Object>{}, jsonResponse: <String, Object>{ 'screenshot': base64.encode(<int>[1, 2, 3, 4]), }, ), FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'true', }, ), ], logger: logger, fileSystem: fileSystem); await terminalHandler.processTerminalInput('s'); expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)')); expect(fileSystem.currentDirectory.childFile('flutter_01.png').readAsBytesSync(), <int>[1, 2, 3, 4]); }); testWithoutContext('s, can take screenshot on debug web device that does not support screenshot', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler(<FakeVmServiceRequest>[ listViews, FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'false', }, ), FakeVmServiceRequest( method: 'ext.dwds.screenshot', args: <String, Object>{}, jsonResponse: <String, Object>{ 'data': base64.encode(<int>[1, 2, 3, 4]), }, ), FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'true', }, ), ], logger: logger, web: true, fileSystem: fileSystem); await terminalHandler.processTerminalInput('s'); expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)')); expect(fileSystem.currentDirectory.childFile('flutter_01.png').readAsBytesSync(), <int>[1, 2, 3, 4]); }); testWithoutContext('s, can take screenshot on device that does not support service protocol', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler( <FakeVmServiceRequest>[], logger: logger, supportsScreenshot: true, supportsServiceProtocol: false, fileSystem: fileSystem, ); await terminalHandler.processTerminalInput('s'); expect(logger.statusText, contains('Screenshot written to flutter_01.png (0kB)')); expect(fileSystem.currentDirectory.childFile('flutter_01.png').readAsBytesSync(), <int>[1, 2, 3, 4]); }); testWithoutContext('s, does not take a screenshot on a device that does not support screenshot or the service protocol', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler( <FakeVmServiceRequest>[], logger: logger, supportsServiceProtocol: false, fileSystem: fileSystem, ); await terminalHandler.processTerminalInput('s'); expect(logger.statusText, '\n'); expect(fileSystem.currentDirectory.childFile('flutter_01.png'), isNot(exists)); }); testWithoutContext('s, does not take a screenshot on a web device that does not support screenshot or the service protocol', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler( <FakeVmServiceRequest>[], logger: logger, supportsServiceProtocol: false, web: true, fileSystem: fileSystem, ); await terminalHandler.processTerminalInput('s'); expect(logger.statusText, '\n'); expect(fileSystem.currentDirectory.childFile('flutter_01.png'), isNot(exists)); }); testWithoutContext('s, bails taking screenshot on debug device if debugAllowBanner throws RpcError', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler( <FakeVmServiceRequest>[ listViews, FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'false', }, // Failed response, errorCode: RPCErrorCodes.kInternalError, ), ], logger: logger, fileSystem: fileSystem, ); await terminalHandler.processTerminalInput('s'); expect(logger.errorText, contains('Error')); }); testWithoutContext('s, bails taking screenshot on debug device if flutter.screenshot throws RpcError, restoring banner', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler( <FakeVmServiceRequest>[ listViews, FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'false', }, ), const FakeVmServiceRequest( method: '_flutter.screenshot', // Failed response, errorCode: RPCErrorCodes.kInternalError, ), FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'true', }, ), ], logger: logger, fileSystem: fileSystem, ); await terminalHandler.processTerminalInput('s'); expect(logger.errorText, contains('Error')); }); testWithoutContext('s, bails taking screenshot on debug device if dwds.screenshot throws RpcError, restoring banner', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler( <FakeVmServiceRequest>[ listViews, FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'false', }, ), const FakeVmServiceRequest( method: 'ext.dwds.screenshot', // Failed response, errorCode: RPCErrorCodes.kInternalError, ), FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'true', }, ), ], logger: logger, web: true, fileSystem: fileSystem, ); await terminalHandler.processTerminalInput('s'); expect(logger.errorText, contains('Error')); }); testWithoutContext('s, bails taking screenshot on debug device if debugAllowBanner during second request', () async { final BufferLogger logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final TerminalHandler terminalHandler = setUpTerminalHandler( <FakeVmServiceRequest>[ listViews, FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'false', }, ), FakeVmServiceRequest( method: 'ext.flutter.debugAllowBanner', args: <String, Object?>{ 'isolateId': fakeUnpausedIsolate.id, 'enabled': 'true', }, // Failed response, errorCode: RPCErrorCodes.kInternalError, ), ], logger: logger, supportsScreenshot: true, fileSystem: fileSystem, ); await terminalHandler.processTerminalInput('s'); expect(logger.errorText, contains('Error')); }); testWithoutContext('pidfile creation', () { final BufferLogger testLogger = BufferLogger.test(); final Signals signals = _TestSignals(Signals.defaultExitSignals); final Terminal terminal = Terminal.test(); final MemoryFileSystem fs = MemoryFileSystem.test(); final ProcessInfo processInfo = ProcessInfo.test(fs); final FakeResidentRunner residentRunner = FakeResidentRunner( FlutterDevice( FakeDevice(), buildInfo: BuildInfo.debug, generator: FakeResidentCompiler(), developmentShaderCompiler: const FakeShaderCompiler(), ), testLogger, fs, ); residentRunner ..supportsRestart = true ..supportsServiceProtocol = true ..stayResident = true; const String filename = 'test.pid'; final TerminalHandler terminalHandler = TerminalHandler( residentRunner, logger: testLogger, signals: signals, terminal: terminal, processInfo: processInfo, reportReady: false, pidFile: filename, ); expect(fs.file(filename), isNot(exists)); terminalHandler.setupTerminal(); terminalHandler.registerSignalHandlers(); expect(fs.file(filename), exists); terminalHandler.stop(); expect(fs.file(filename), isNot(exists)); }); } class FakeResidentRunner extends ResidentHandlers { FakeResidentRunner(FlutterDevice device, this.logger, this.fileSystem) : flutterDevices = <FlutterDevice>[device]; bool calledDetach = false; bool calledPrint = false; bool calledExit = false; bool calledPrintWithDetails = false; bool calledReload = false; bool calledRestart = false; int reloadExitCode = 0; bool fatalReloadError = false; @override final Logger logger; @override final FileSystem fileSystem; @override final List<FlutterDevice> flutterDevices; @override bool canHotReload = true; @override bool hotMode = true; @override bool isRunningDebug = true; @override bool isRunningProfile = false; @override bool isRunningRelease = false; @override bool stayResident = true; @override bool supportsRestart = true; @override bool supportsServiceProtocol = true; @override bool supportsWriteSkSL = true; @override Future<void> cleanupAfterSignal() async { } @override Future<void> detach() async { calledDetach = true; } @override Future<void> exit() async { calledExit = true; } @override void printHelp({required bool details}) { if (details) { calledPrintWithDetails = true; } else { calledPrint = true; } } @override Future<void> runSourceGenerators() async { } @override Future<OperationResult> restart({bool fullRestart = false, bool pause = false, String? reason}) async { if (fullRestart && !supportsRestart) { throw StateError('illegal restart'); } if (!fullRestart && !canHotReload) { throw StateError('illegal reload'); } if (fullRestart) { calledRestart = true; } else { calledReload = true; } return OperationResult(reloadExitCode, '', fatal: fatalReloadError); } @override ResidentDevtoolsHandler get residentDevtoolsHandler => _residentDevtoolsHandler; final ResidentDevtoolsHandler _residentDevtoolsHandler = FakeResidentDevtoolsHandler(); } class FakeResidentDevtoolsHandler extends Fake implements ResidentDevtoolsHandler { bool calledLaunchDevToolsInBrowser = false; @override bool launchDevToolsInBrowser({List<FlutterDevice?>? flutterDevices}) { return calledLaunchDevToolsInBrowser = true; } } // Unfortunately Device, despite not being immutable, has an `operator ==`. // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types class FakeDevice extends Fake implements Device { @override bool isSupported() => true; @override bool supportsScreenshot = false; @override String get name => 'Fake Device'; @override Future<void> takeScreenshot(File file) async { if (!supportsScreenshot) { throw StateError('illegal screenshot attempt'); } file.writeAsBytesSync(<int>[1, 2, 3, 4]); } } TerminalHandler setUpTerminalHandler(List<FakeVmServiceRequest> requests, { bool supportsRestart = true, bool supportsServiceProtocol = true, bool supportsHotReload = true, bool web = false, bool fatalReloadError = false, bool supportsScreenshot = false, int reloadExitCode = 0, BuildMode buildMode = BuildMode.debug, Logger? logger, FileSystem? fileSystem, }) { final Logger testLogger = logger ?? BufferLogger.test(); final Signals signals = Signals.test(); final Terminal terminal = Terminal.test(); final FileSystem localFileSystem = fileSystem ?? MemoryFileSystem.test(); final ProcessInfo processInfo = ProcessInfo.test(MemoryFileSystem.test()); final FlutterDevice device = FlutterDevice( FakeDevice()..supportsScreenshot = supportsScreenshot, buildInfo: BuildInfo(buildMode, '', treeShakeIcons: false), generator: FakeResidentCompiler(), developmentShaderCompiler: const FakeShaderCompiler(), targetPlatform: web ? TargetPlatform.web_javascript : TargetPlatform.android_arm, ); device.vmService = FakeVmServiceHost(requests: requests).vmService; final FakeResidentRunner residentRunner = FakeResidentRunner(device, testLogger, localFileSystem) ..supportsServiceProtocol = supportsServiceProtocol ..supportsRestart = supportsRestart ..canHotReload = supportsHotReload ..fatalReloadError = fatalReloadError ..reloadExitCode = reloadExitCode; switch (buildMode) { case BuildMode.debug: residentRunner ..isRunningDebug = true ..isRunningProfile = false ..isRunningRelease = false; break; case BuildMode.profile: residentRunner ..isRunningDebug = false ..isRunningProfile = true ..isRunningRelease = false; break; case BuildMode.release: residentRunner ..isRunningDebug = false ..isRunningProfile = false ..isRunningRelease = true; break; } return TerminalHandler( residentRunner, logger: testLogger, signals: signals, terminal: terminal, processInfo: processInfo, reportReady: false, ); } class FakeResidentCompiler extends Fake implements ResidentCompiler { } class TestRunner extends Fake implements ResidentRunner { bool hasHelpBeenPrinted = false; String? receivedCommand; @override Future<void> cleanupAfterSignal() async { } @override Future<void> cleanupAtFinish() async { } @override void printHelp({ bool? details }) { hasHelpBeenPrinted = true; } @override Future<int?> run({ Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool enableDevTools = false, String? route, }) async => null; @override Future<int?> attach({ Completer<DebugConnectionInfo>? connectionInfoCompleter, Completer<void>? appStartedCompleter, bool allowExistingDdsInstance = false, bool enableDevTools = false, bool needsFullRestart = true, }) async => null; } class _TestSignals implements Signals { _TestSignals(this.exitSignals); final List<ProcessSignal> exitSignals; final Map<ProcessSignal, Map<Object, SignalHandler>> _handlersTable = <ProcessSignal, Map<Object, SignalHandler>>{}; @override Object addHandler(ProcessSignal signal, SignalHandler handler) { final Object token = Object(); _handlersTable.putIfAbsent(signal, () => <Object, SignalHandler>{})[token] = handler; return token; } @override Future<bool> removeHandler(ProcessSignal signal, Object token) async { if (!_handlersTable.containsKey(signal)) { return false; } if (!_handlersTable[signal]!.containsKey(token)) { return false; } _handlersTable[signal]!.remove(token); return true; } @override Stream<Object> get errors => _errors.stream; final StreamController<Object> _errors = StreamController<Object>(); } class FakeShaderCompiler implements DevelopmentShaderCompiler { const FakeShaderCompiler(); @override void configureCompiler(TargetPlatform? platform, { required bool enableImpeller }) { } @override Future<DevFSContent> recompileShader(DevFSContent inputShader) { throw UnimplementedError(); } }