// 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. // @dart = 2.8 import 'dart:async'; import 'package:file/file.dart'; import 'package:vm_service/vm_service.dart' as vm_service; 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/compile.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/vmservice.dart'; import 'package:test/fake.dart'; 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: [], exceptionPauseMode: null, extensionRPCs: [], libraries: [ 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: [], ); final vm_service.VM fakeVM = vm_service.VM( isolates: [fakeUnpausedIsolate], pid: 1, hostCPU: '', isolateGroups: [], targetCPU: '', startTime: 0, name: 'dart', architectureBits: 64, operatingSystem: '', version: '', systemIsolateGroups: [], systemIsolates: [], ); final FlutterView fakeFlutterView = FlutterView( id: 'a', uiIsolate: fakeUnpausedIsolate, ); final FakeVmServiceRequest listViews = FakeVmServiceRequest( method: kListViewsMethod, jsonResponse: { 'views': [ fakeFlutterView.toJson(), ], }, ); final FakeVmServiceRequest getVM = FakeVmServiceRequest( method: 'getVM', jsonResponse: fakeVM.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([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('a\n'); expect(terminalHandler.lastReceivedCommand, 'a'); }); testWithoutContext('n, can handle trailing only newlines', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([]); await terminalHandler.processTerminalInput('\n\n'); expect(terminalHandler.lastReceivedCommand, ''); }); testWithoutContext('a - debugToggleProfileWidgetBuilds', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: { 'isolateId': '1', }, jsonResponse: { 'enabled': 'false' }, ), const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: { 'isolateId': '1', 'enabled': 'true' }, jsonResponse: { 'enabled': 'true' }, ), ]); await terminalHandler.processTerminalInput('a'); }); testWithoutContext('a - debugToggleProfileWidgetBuilds with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: { 'isolateId': '1', }, jsonResponse: { 'enabled': 'false' }, ), const FakeVmServiceRequest( method: 'ext.flutter.profileWidgetBuilds', args: { 'isolateId': '1', 'enabled': 'true' }, jsonResponse: { 'enabled': 'true' }, ), ], web: true); await terminalHandler.processTerminalInput('a'); }); testWithoutContext('a - debugToggleProfileWidgetBuilds without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('a'); }); testWithoutContext('b - debugToggleBrightness', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: { 'isolateId': '1', }, jsonResponse: { 'value': 'Brightness.light', } ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: { 'isolateId': '1', 'value': 'Brightness.dark', }, jsonResponse: { '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([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: { 'isolateId': '1', }, jsonResponse: { 'value': 'Brightness.light', } ), getVM, const FakeVmServiceRequest( method: 'ext.flutter.brightnessOverride', args: { 'isolateId': '1', 'value': 'Brightness.dark', }, jsonResponse: { '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([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('b'); }); testWithoutContext('d,D - detach', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([]); 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([]); 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([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.inspector.show', args: { 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('i'); }); testWithoutContext('i - debugToggleWidgetInspector with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.inspector.show', args: { 'isolateId': '1', }, ), ], web: true); await terminalHandler.processTerminalInput('i'); }); testWithoutContext('i - debugToggleWidgetInspector without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('i'); }); testWithoutContext('I - debugToggleInvertOversizedImages', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.invertOversizedImages', args: { 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('I - debugToggleInvertOversizedImages with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.invertOversizedImages', args: { 'isolateId': '1', }, ), ], web: true); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('I - debugToggleInvertOversizedImages without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('I - debugToggleInvertOversizedImages in profile mode is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], buildMode: BuildMode.profile); await terminalHandler.processTerminalInput('I'); }); testWithoutContext('L - debugDumpLayerTree', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpLayerTree', args: { 'isolateId': '1', }, jsonResponse: { '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([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpLayerTree', args: { 'isolateId': '1', }, jsonResponse: { '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([], buildMode: BuildMode.profile); await terminalHandler.processTerminalInput('L'); }); testWithoutContext('L - debugDumpLayerTree without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('L'); }); testWithoutContext('o,O - debugTogglePlatform', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ // Request 1. listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', }, jsonResponse: { 'value': 'iOS' }, ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', 'value': 'fuchsia', }, jsonResponse: { 'value': 'fuchsia' }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', }, jsonResponse: { 'value': 'android' }, ), listViews, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', 'value': 'iOS', }, jsonResponse: { '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([ // Request 1. getVM, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', }, jsonResponse: { 'value': 'iOS' }, ), getVM, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', 'value': 'fuchsia', }, jsonResponse: { 'value': 'fuchsia' }, ), // Request 2. getVM, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', }, jsonResponse: { 'value': 'android' }, ), getVM, const FakeVmServiceRequest( method: 'ext.flutter.platformOverride', args: { 'isolateId': '1', 'value': 'iOS', }, jsonResponse: { '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([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('o'); await terminalHandler.processTerminalInput('O'); }); testWithoutContext('p - debugToggleDebugPaintSizeEnabled', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugPaint', args: { 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('p'); }); testWithoutContext('p - debugToggleDebugPaintSizeEnabled with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugPaint', args: { 'isolateId': '1', }, ), ], web: true); await terminalHandler.processTerminalInput('p'); }); testWithoutContext('p - debugToggleDebugPaintSizeEnabled without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('p'); }); testWithoutContext('P - debugTogglePerformanceOverlayOverride', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.showPerformanceOverlay', args: { 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('P'); }); testWithoutContext('P - debugTogglePerformanceOverlayOverride with web target is skipped ', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], web: true); await terminalHandler.processTerminalInput('P'); }); testWithoutContext('P - debugTogglePerformanceOverlayOverride without service protocol is skipped ', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('P'); }); testWithoutContext('s - screenshot', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([]); await terminalHandler.processTerminalInput('s'); }); testWithoutContext('S - debugDumpSemanticsTreeInTraversalOrder', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', args: { 'isolateId': '1', }, jsonResponse: { '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([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInTraversalOrder', args: { 'isolateId': '1', }, jsonResponse: { '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([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('S'); }); testWithoutContext('U - debugDumpSemanticsTreeInInverseHitTestOrder', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', args: { 'isolateId': '1', }, jsonResponse: { '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([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder', args: { 'isolateId': '1', }, jsonResponse: { '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([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('U'); }); testWithoutContext('t,T - debugDumpRenderTree', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: { 'isolateId': '1', }, jsonResponse: { 'data': 'RENDER DATA 1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: { 'isolateId': '1', }, jsonResponse: { '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([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: { 'isolateId': '1', }, jsonResponse: { 'data': 'RENDER DATA 1', }, ), // Request 2. getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpRenderTree', args: { 'isolateId': '1', }, jsonResponse: { '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([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('t'); await terminalHandler.processTerminalInput('T'); }); testWithoutContext('w,W - debugDumpApp', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: { 'isolateId': '1', }, jsonResponse: { 'data': 'WIDGET DATA 1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: { 'isolateId': '1', }, jsonResponse: { '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([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: { 'isolateId': '1', }, jsonResponse: { 'data': 'WIDGET DATA 1', }, ), // Request 2. getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugDumpApp', args: { 'isolateId': '1', }, jsonResponse: { '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('w,W - debugDumpApp without service protocol is skipped', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('w'); await terminalHandler.processTerminalInput('W'); }); testWithoutContext('z,Z - debugToggleDebugCheckElevationsEnabled', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: { 'isolateId': '1', }, ), // Request 2. listViews, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: { 'isolateId': '1', }, ), ]); await terminalHandler.processTerminalInput('z'); await terminalHandler.processTerminalInput('Z'); }); testWithoutContext('z,Z - debugToggleDebugCheckElevationsEnabled with web target', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([ getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: { 'isolateId': '1', }, ), // Request 2. getVM, const FakeVmServiceRequest( method: 'ext.flutter.debugCheckElevationsEnabled', args: { '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([], supportsServiceProtocol: false); await terminalHandler.processTerminalInput('z'); await terminalHandler.processTerminalInput('Z'); }); testWithoutContext('q,Q - exit', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([]); 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([], supportsHotReload: false); await terminalHandler.processTerminalInput('r'); }); testWithoutContext('R - hotRestart unsupported', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([], supportsRestart: false); await terminalHandler.processTerminalInput('R'); }); testWithoutContext('r - hotReload', () async { final TerminalHandler terminalHandler = setUpTerminalHandler([]); 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([]); 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([], 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([], 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([], 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([], 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([], 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('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()), 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 = [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 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 cleanupAfterSignal() async { } @override Future detach() async { calledDetach = true; } @override Future exit() async { calledExit = true; } @override void printHelp({bool details}) { if (details) { calledPrintWithDetails = true; } else { calledPrint = true; } } @override Future runSourceGenerators() async { } @override Future 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); } } class FakeDevice extends Fake implements Device { @override bool isSupported() => true; @override bool supportsScreenshot = false; @override Future takeScreenshot(File file) async { if (!supportsScreenshot) { throw StateError('illegal screenshot attempt'); } file.writeAsBytesSync([1, 2, 3, 4]); } } TerminalHandler setUpTerminalHandler(List requests, { bool supportsRestart = true, bool supportsServiceProtocol = true, bool supportsHotReload = true, bool web = false, bool fatalReloadError = false, int reloadExitCode = 0, BuildMode buildMode = BuildMode.debug, }) { final BufferLogger testLogger = BufferLogger.test(); final Signals signals = Signals.test(); final Terminal terminal = Terminal.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final ProcessInfo processInfo = ProcessInfo.test(MemoryFileSystem.test()); final FlutterDevice device = FlutterDevice( FakeDevice(), buildInfo: BuildInfo(buildMode, '', treeShakeIcons: false), generator: FakeResidentCompiler(), targetPlatform: web ? TargetPlatform.web_javascript : TargetPlatform.android_arm, ); device.vmService = FakeVmServiceHost(requests: requests).vmService; final FakeResidentRunner residentRunner = FakeResidentRunner(device, testLogger, fileSystem) ..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 cleanupAfterSignal() async { } @override Future cleanupAtFinish() async { } @override void printHelp({ bool details }) { hasHelpBeenPrinted = true; } @override Future run({ Completer connectionInfoCompleter, Completer appStartedCompleter, bool enableDevTools = false, String route, }) async => null; @override Future attach({ Completer connectionInfoCompleter, Completer appStartedCompleter, bool allowExistingDdsInstance = false, bool enableDevTools = false, }) async => null; } class _TestSignals implements Signals { _TestSignals(this.exitSignals); final List exitSignals; final Map> _handlersTable = >{}; @override Object addHandler(ProcessSignal signal, SignalHandler handler) { final Object token = Object(); _handlersTable.putIfAbsent(signal, () => {})[token] = handler; return token; } @override Future 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 get errors => _errors.stream; final StreamController _errors = StreamController(); }