// 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 'dart:convert'; import 'package:build_daemon/client.dart'; import 'package:dwds/dwds.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/net.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_runner/resident_web_runner.dart'; import 'package:flutter_tools/src/build_runner/web_fs.dart'; import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/web/chrome.dart'; import 'package:flutter_tools/src/web/web_device.dart'; import 'package:meta/meta.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/testbed.dart'; void main() { Testbed testbed; MockFlutterWebFs mockWebFs; ResidentWebRunner residentWebRunner; MockDebugConnection mockDebugConnection; MockVmService mockVmService; MockChromeDevice mockChromeDevice; MockAppConnection mockAppConnection; MockFlutterDevice mockFlutterDevice; MockWebDevFS mockWebDevFS; MockResidentCompiler mockResidentCompiler; MockChrome mockChrome; MockChromeConnection mockChromeConnection; MockChromeTab mockChromeTab; MockWipConnection mockWipConnection; MockWipDebugger mockWipDebugger; MockWebServerDevice mockWebServerDevice; bool didSkipDwds; setUp(() { resetChromeForTesting(); mockWebFs = MockFlutterWebFs(); mockDebugConnection = MockDebugConnection(); mockVmService = MockVmService(); mockChromeDevice = MockChromeDevice(); mockAppConnection = MockAppConnection(); mockFlutterDevice = MockFlutterDevice(); mockWebDevFS = MockWebDevFS(); mockResidentCompiler = MockResidentCompiler(); mockChrome = MockChrome(); mockChromeConnection = MockChromeConnection(); mockChromeTab = MockChromeTab(); mockWipConnection = MockWipConnection(); mockWipDebugger = MockWipDebugger(); mockWebServerDevice = MockWebServerDevice(); when(mockFlutterDevice.device).thenReturn(mockChromeDevice); testbed = Testbed( setup: () { residentWebRunner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, stayResident: true, dartDefines: const <String>[], urlTunneller: null, ) as ResidentWebRunner; }, overrides: <Type, Generator>{ WebFsFactory: () => ({ @required String target, @required FlutterProject flutterProject, @required BuildInfo buildInfo, @required bool skipDwds, @required bool initializePlatform, @required String hostname, @required String port, @required List<String> dartDefines, @required UrlTunneller urlTunneller, }) async { didSkipDwds = skipDwds; return mockWebFs; }, }, ); }); void _setupMocks() { globals.fs.file('pubspec.yaml').createSync(); globals.fs.file(globals.fs.path.join('lib', 'main.dart')).createSync(recursive: true); globals.fs.file(globals.fs.path.join('web', 'index.html')).createSync(recursive: true); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { return ConnectionResult(mockAppConnection, mockDebugConnection); }); when(mockWebFs.recompile()).thenAnswer((Invocation _) { return Future<bool>.value(false); }); when(mockWebFs.uri).thenReturn('http://localhost:8765/app/'); when(mockDebugConnection.vmService).thenReturn(mockVmService); when(mockDebugConnection.onDone).thenAnswer((Invocation invocation) { return Completer<void>().future; }); when(mockVmService.onStdoutEvent).thenAnswer((Invocation _) { return const Stream<Event>.empty(); }); when(mockVmService.onDebugEvent).thenAnswer((Invocation _) { return const Stream<Event>.empty(); }); when(mockDebugConnection.uri).thenReturn('ws://127.0.0.1/abcd/'); when(mockFlutterDevice.devFS).thenReturn(mockWebDevFS); when(mockWebDevFS.sources).thenReturn(<Uri>[]); when(mockFlutterDevice.generator).thenReturn(mockResidentCompiler); when(mockChrome.chromeConnection).thenReturn(mockChromeConnection); when(mockChromeConnection.getTab(any)).thenAnswer((Invocation invocation) async { return mockChromeTab; }); when(mockChromeTab.connect()).thenAnswer((Invocation invocation) async { return mockWipConnection; }); when(mockWipConnection.debugger).thenReturn(mockWipDebugger); } test('runner with web server device does not support debugging without --start-paused', () => testbed.run(() { when(mockFlutterDevice.device).thenReturn(WebServerDevice()); final ResidentRunner profileResidentWebRunner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, stayResident: true, dartDefines: const <String>[], urlTunneller: null, ) as ResidentWebRunner; expect(profileResidentWebRunner.debuggingEnabled, false); when(mockFlutterDevice.device).thenReturn(MockChromeDevice()); expect(residentWebRunner.debuggingEnabled, true); })); test('runner with web server device does not initialize dwds', () => testbed.run(() async { _setupMocks(); when(mockFlutterDevice.device).thenReturn(WebServerDevice()); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect(didSkipDwds, true); })); test('runner with web server device supports debugging with --start-paused', () => testbed.run(() { when(mockFlutterDevice.device).thenReturn(WebServerDevice()); final ResidentRunner profileResidentWebRunner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, startPaused: true), ipv6: true, stayResident: true, dartDefines: <String>[], urlTunneller: null, ); expect(profileResidentWebRunner.debuggingEnabled, true); })); test('runner with web server device uses debug extension with --start-paused', () => testbed.run(() async { _setupMocks(); when(mockFlutterDevice.device).thenReturn(WebServerDevice()); final ResidentWebRunner runner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, startPaused: true), ipv6: true, stayResident: true, dartDefines: <String>[], urlTunneller: null, ) as ResidentWebRunner; final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(runner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; // Check connect() was told to use the debug extension. verify(mockWebFs.connect(true)).called(1); // And ensure the debug services was started. expect(testLogger.statusText, contains('Debug service listening on')); })); test('profile does not supportsServiceProtocol', () => testbed.run(() { when(mockFlutterDevice.device).thenReturn(mockChromeDevice); final ResidentRunner profileResidentWebRunner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile), ipv6: true, stayResident: true, dartDefines: const <String>[], urlTunneller: null, ); expect(profileResidentWebRunner.supportsServiceProtocol, false); expect(residentWebRunner.supportsServiceProtocol, true); })); test('Exits on run if application does not support the web', () => testbed.run(() async { globals.fs.file('pubspec.yaml').createSync(); expect(await residentWebRunner.run(), 1); expect(testLogger.errorText, contains('This application is not configured to build on the web')); })); test('Exits on run if target file does not exist', () => testbed.run(() async { globals.fs.file('pubspec.yaml').createSync(); globals.fs.file(globals.fs.path.join('web', 'index.html')).createSync(recursive: true); expect(await residentWebRunner.run(), 1); final String absoluteMain = globals.fs.path.absolute(globals.fs.path.join('lib', 'main.dart')); expect(testLogger.errorText, contains('Tried to run $absoluteMain, but that file does not exist.')); })); test('Can successfully run and connect to vmservice', () => testbed.run(() async { _setupMocks(); final DelegateLogger delegateLogger = globals.logger as DelegateLogger; final BufferLogger bufferLogger = delegateLogger.delegate as BufferLogger; final MockStatus status = MockStatus(); delegateLogger.status = status; final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future; verify(mockAppConnection.runMain()).called(1); verify(mockVmService.registerService('reloadSources', 'FlutterTools')).called(1); verify(status.stop()).called(2); expect(bufferLogger.statusText, contains('Debug service listening on ws://127.0.0.1/abcd/')); expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/'); }, overrides: <Type, Generator>{ Logger: () => DelegateLogger(BufferLogger( terminal: AnsiTerminal( stdio: null, platform: const LocalPlatform(), ), outputPreferences: OutputPreferences.test(), )), })); test('Can successfully run and disconnect with --no-resident', () => testbed.run(() async { _setupMocks(); residentWebRunner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, stayResident: false, dartDefines: const <String>[], urlTunneller: null, ) as ResidentWebRunner; expect(await residentWebRunner.run(), 0); })); test('Listens to stdout streams before running main', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final StreamController<Event> controller = StreamController<Event>.broadcast(); when(mockVmService.onStdoutEvent).thenAnswer((Invocation _) { return controller.stream; }); when(mockAppConnection.runMain()).thenAnswer((Invocation invocation) { controller.add(Event.parse(<String, Object>{ 'type': 'Event', 'kind': 'WriteEvent', 'timestamp': 1569473488296, 'bytes': base64.encode('THIS MESSAGE IS IMPORTANT'.codeUnits), })); }); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect(testLogger.statusText, contains('THIS MESSAGE IS IMPORTANT')); })); test('Does not run main with --start-paused', () => testbed.run(() async { residentWebRunner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, startPaused: true), ipv6: true, stayResident: true, dartDefines: const <String>[], urlTunneller: null, ) as ResidentWebRunner; _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final StreamController<Event> controller = StreamController<Event>.broadcast(); when(mockVmService.onStdoutEvent).thenAnswer((Invocation _) { return controller.stream; }); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; verifyNever(mockAppConnection.runMain()); })); test('Can hot reload after attaching', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockWebFs.recompile()).thenAnswer((Invocation invocation) async { return true; }); when(mockVmService.callServiceExtension('hotRestart')).thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'type': 'Success'}); }); final OperationResult result = await residentWebRunner.restart(fullRestart: false); expect(testLogger.statusText, contains('Reloaded application in')); expect(result.code, 0); // ensure that analytics are sent. verify(Usage.instance.sendEvent('hot', 'restart', parameters: <String, String>{ 'cd27': 'web-javascript', 'cd28': null, 'cd29': 'false', 'cd30': 'true', })).called(1); verify(Usage.instance.sendTiming('hot', 'web-restart', any)).called(1); verify(Usage.instance.sendTiming('hot', 'web-refresh', any)).called(1); verify(Usage.instance.sendTiming('hot', 'web-recompile', any)).called(1); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), })); test('Can hot reload after attaching - experimental', () => testbed.run(() async { _setupMocks(); launchChromeInstance(mockChrome); when(mockWebDevFS.update( mainPath: anyNamed('mainPath'), target: anyNamed('target'), bundle: anyNamed('bundle'), firstBuildTime: anyNamed('firstBuildTime'), bundleFirstUpload: anyNamed('bundleFirstUpload'), generator: anyNamed('generator'), fullRestart: anyNamed('fullRestart'), dillOutputPath: anyNamed('dillOutputPath'), trackWidgetCreation: anyNamed('trackWidgetCreation'), projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), )).thenAnswer((Invocation invocation) async { return UpdateFSReport(success: true) ..invalidatedModules = <String>['example']; }); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future; expect(debugConnectionInfo, isNotNull); final OperationResult result = await residentWebRunner.restart(fullRestart: false); expect(testLogger.statusText, contains('Reloaded application in')); expect(result.code, 0); verify(mockResidentCompiler.accept()).called(2); // ensure that analytics are sent. verify(Usage.instance.sendEvent('hot', 'restart', parameters: <String, String>{ 'cd27': 'web-javascript', 'cd28': null, 'cd29': 'false', 'cd30': 'true', })).called(1); verify(Usage.instance.sendTiming('hot', 'web-incremental-restart', any)).called(1); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), FeatureFlags: () => TestFeatureFlags(isWebIncrementalCompilerEnabled: true), })); test('Can hot restart after attaching - experimental', () => testbed.run(() async { _setupMocks(); launchChromeInstance(mockChrome); when(mockWebDevFS.update( mainPath: anyNamed('mainPath'), target: anyNamed('target'), bundle: anyNamed('bundle'), firstBuildTime: anyNamed('firstBuildTime'), bundleFirstUpload: anyNamed('bundleFirstUpload'), generator: anyNamed('generator'), fullRestart: anyNamed('fullRestart'), dillOutputPath: anyNamed('dillOutputPath'), trackWidgetCreation: anyNamed('trackWidgetCreation'), projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), )).thenAnswer((Invocation invocation) async { return UpdateFSReport(success: true) ..invalidatedModules = <String>['example']; }); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; final OperationResult result = await residentWebRunner.restart(fullRestart: true); expect(testLogger.statusText, contains('Restarted application in')); expect(result.code, 0); verify(mockResidentCompiler.accept()).called(2); // ensure that analytics are sent. verify(Usage.instance.sendEvent('hot', 'restart', parameters: <String, String>{ 'cd27': 'web-javascript', 'cd28': null, 'cd29': 'false', 'cd30': 'true', })).called(1); verifyNever(Usage.instance.sendTiming('hot', 'web-incremental-restart', any)); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), FeatureFlags: () => TestFeatureFlags(isWebIncrementalCompilerEnabled: true), })); test('Can hot restart after attaching - experimental with web-server device', () => testbed.run(() async { _setupMocks(); when(mockFlutterDevice.device).thenReturn(mockWebServerDevice); when(mockWebDevFS.update( mainPath: anyNamed('mainPath'), target: anyNamed('target'), bundle: anyNamed('bundle'), firstBuildTime: anyNamed('firstBuildTime'), bundleFirstUpload: anyNamed('bundleFirstUpload'), generator: anyNamed('generator'), fullRestart: anyNamed('fullRestart'), dillOutputPath: anyNamed('dillOutputPath'), trackWidgetCreation: anyNamed('trackWidgetCreation'), projectRootPath: anyNamed('projectRootPath'), pathToReload: anyNamed('pathToReload'), invalidatedFiles: anyNamed('invalidatedFiles'), )).thenAnswer((Invocation invocation) async { return UpdateFSReport(success: true) ..invalidatedModules = <String>['example']; }); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; final OperationResult result = await residentWebRunner.restart(fullRestart: true); expect(testLogger.statusText, contains('Restarted application in')); expect(result.code, 0); verify(mockResidentCompiler.accept()).called(2); // ensure that analytics are sent. verify(Usage.instance.sendEvent('hot', 'restart', parameters: <String, String>{ 'cd27': 'web-javascript', 'cd28': null, 'cd29': 'false', 'cd30': 'true', })).called(1); verifyNever(Usage.instance.sendTiming('hot', 'web-incremental-restart', any)); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), FeatureFlags: () => TestFeatureFlags(isWebIncrementalCompilerEnabled: true), })); test('experimental resident runner is not debuggable', () => testbed.run(() { expect(residentWebRunner.debuggingEnabled, false); }, overrides: <Type, Generator>{ FeatureFlags: () => TestFeatureFlags(isWebIncrementalCompilerEnabled: true), })); test('Can hot restart after attaching', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockWebFs.recompile()).thenAnswer((Invocation invocation) async { return true; }); when(mockVmService.callServiceExtension('fullReload')).thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'type': 'Success'}); }); final OperationResult result = await residentWebRunner.restart(fullRestart: true); expect(testLogger.statusText, contains('Restarted application in')); expect(result.code, 0); // ensure that analytics are sent. verify(Usage.instance.sendEvent('hot', 'restart', parameters: <String, String>{ 'cd27': 'web-javascript', 'cd28': null, 'cd29': 'false', 'cd30': 'true', })).called(1); verifyNever(Usage.instance.sendTiming('hot', 'web-restart', any)); verifyNever(Usage.instance.sendTiming('hot', 'web-refresh', any)); verify(Usage.instance.sendTiming('hot', 'web-recompile', any)).called(1); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), })); test('Selects Dwds runner in profile mode with incremental compiler enabled', () => testbed.run(() async { final ResidentWebRunner residentWebRunner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.profile), ipv6: true, stayResident: true, dartDefines: const <String>[], urlTunneller: null, ) as ResidentWebRunner; expect(residentWebRunner.runtimeType.toString(), '_DwdsResidentWebRunner'); }, overrides: <Type, Generator>{ FeatureFlags: () => TestFeatureFlags(isWebIncrementalCompilerEnabled: true), })); test('Fails on compilation errors in hot restart', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockWebFs.recompile()).thenAnswer((Invocation _) async { return false; }); final OperationResult result = await residentWebRunner.restart(fullRestart: true); expect(result.code, 1); expect(result.message, contains('Failed to recompile application.')); verifyNever(Usage.instance.sendTiming('hot', 'web-restart', any)); verifyNever(Usage.instance.sendTiming('hot', 'web-refresh', any)); verifyNever(Usage.instance.sendTiming('hot', 'web-recompile', any)); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), })); test('Fails on vmservice response error for hot restart', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockWebFs.recompile()).thenAnswer((Invocation _) async { return true; }); when(mockVmService.callServiceExtension('fullReload')).thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'type': 'Failed'}); }); final OperationResult result = await residentWebRunner.restart(fullRestart: true); expect(result.code, 1); expect(result.message, contains('Failed')); verifyNever(Usage.instance.sendTiming('hot', 'web-restart', any)); verifyNever(Usage.instance.sendTiming('hot', 'web-refresh', any)); verify(Usage.instance.sendTiming('hot', 'web-recompile', any)).called(1); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), })); test('Fails on vmservice response error for hot reload', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockWebFs.recompile()).thenAnswer((Invocation _) async { return true; }); when(mockVmService.callServiceExtension('hotRestart')).thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'type': 'Failed'}); }); final OperationResult result = await residentWebRunner.restart(fullRestart: false); expect(result.code, 1); expect(result.message, contains('Failed')); verifyNever(Usage.instance.sendTiming('hot', 'web-restart', any)); verifyNever(Usage.instance.sendTiming('hot', 'web-refresh', any)); verify(Usage.instance.sendTiming('hot', 'web-recompile', any)).called(1); }, overrides: <Type, Generator>{ Usage: () => MockFlutterUsage(), })); test('Fails on vmservice RpcError', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockWebFs.recompile()).thenAnswer((Invocation _) async { return true; }); when(mockVmService.callServiceExtension('hotRestart')).thenThrow(RPCError('', 2, '123')); final OperationResult result = await residentWebRunner.restart(fullRestart: false); expect(result.code, 1); expect(result.message, contains('Page requires refresh')); })); test('printHelp without details has web warning', () => testbed.run(() async { residentWebRunner.printHelp(details: false); expect(testLogger.statusText, contains('Warning')); expect(testLogger.statusText, contains('https://flutter.dev/web')); expect(testLogger.statusText, isNot(contains('https://flutter.dev/web.'))); })); test('debugDumpApp', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await residentWebRunner.debugDumpApp(); verify(mockVmService.callServiceExtension('ext.flutter.debugDumpApp')).called(1); })); test('debugDumpLayerTree', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await residentWebRunner.debugDumpLayerTree(); verify(mockVmService.callServiceExtension('ext.flutter.debugDumpLayerTree')).called(1); })); test('debugDumpRenderTree', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await residentWebRunner.debugDumpRenderTree(); verify(mockVmService.callServiceExtension('ext.flutter.debugDumpRenderTree')).called(1); })); test('debugDumpSemanticsTreeInTraversalOrder', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await residentWebRunner.debugDumpSemanticsTreeInTraversalOrder(); verify(mockVmService.callServiceExtension('ext.flutter.debugDumpSemanticsTreeInTraversalOrder')).called(1); })); test('debugDumpSemanticsTreeInInverseHitTestOrder', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await residentWebRunner.debugDumpSemanticsTreeInInverseHitTestOrder(); verify(mockVmService.callServiceExtension('ext.flutter.debugDumpSemanticsTreeInInverseHitTestOrder')).called(1); })); test('debugToggleDebugPaintSizeEnabled', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockVmService.callServiceExtension('ext.flutter.debugPaint')) .thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'enabled': false}); }); await residentWebRunner.debugToggleDebugPaintSizeEnabled(); verify(mockVmService.callServiceExtension('ext.flutter.debugPaint', args: <String, Object>{'enabled': true})).called(1); })); test('debugTogglePerformanceOverlayOverride', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockVmService.callServiceExtension('ext.flutter.showPerformanceOverlay')) .thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'enabled': false}); }); await residentWebRunner.debugTogglePerformanceOverlayOverride(); verify(mockVmService.callServiceExtension('ext.flutter.showPerformanceOverlay', args: <String, Object>{'enabled': true})).called(1); })); test('debugToggleWidgetInspector', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockVmService.callServiceExtension('ext.flutter.debugToggleWidgetInspector')) .thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'enabled': false}); }); await residentWebRunner.debugToggleWidgetInspector(); verify(mockVmService.callServiceExtension('ext.flutter.debugToggleWidgetInspector', args: <String, Object>{'enabled': true})).called(1); })); test('debugToggleProfileWidgetBuilds', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockVmService.callServiceExtension('ext.flutter.profileWidgetBuilds')) .thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'enabled': false}); }); await residentWebRunner.debugToggleProfileWidgetBuilds(); verify(mockVmService.callServiceExtension('ext.flutter.profileWidgetBuilds', args: <String, Object>{'enabled': true})).called(1); })); test('debugTogglePlatform', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; when(mockVmService.callServiceExtension('ext.flutter.platformOverride')) .thenAnswer((Invocation _) async { return Response.parse(<String, Object>{'value': 'iOS'}); }); await residentWebRunner.debugTogglePlatform(); expect(testLogger.statusText, contains('Switched operating system to fuchsia')); verify(mockVmService.callServiceExtension('ext.flutter.platformOverride', args: <String, Object>{'value': 'fuchsia'})).called(1); })); test('cleanup of resources is safe to call multiple times', () => testbed.run(() async { _setupMocks(); bool debugClosed = false; when(mockChromeDevice.stopApp(any)).thenAnswer((Invocation invocation) async { if (debugClosed) { throw StateError('debug connection closed twice'); } debugClosed = true; return true; }); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; await residentWebRunner.exit(); await residentWebRunner.exit(); verifyNever(mockDebugConnection.close()); })); test('cleans up Chrome if tab is closed', () => testbed.run(() async { _setupMocks(); final Completer<void> onDone = Completer<void>(); when(mockDebugConnection.onDone).thenAnswer((Invocation invocation) { return onDone.future; }); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Future<int> result = residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ); await connectionInfoCompleter.future; onDone.complete(); await result; verify(mockWebFs.stop()).called(1); })); test('Prints target and device name on run', () => testbed.run(() async { _setupMocks(); when(mockChromeDevice.name).thenReturn('Chromez'); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; expect(testLogger.statusText, contains('Launching ${globals.fs.path.join('lib', 'main.dart')} on Chromez in debug mode')); })); test('Sends launched app.webLaunchUrl event for Chrome device', () => testbed.run(() async { _setupMocks(); when(mockFlutterDevice.device).thenReturn(ChromeDevice()); final DelegateLogger delegateLogger = globals.logger as DelegateLogger; final MockStatus mockStatus = MockStatus(); delegateLogger.status = mockStatus; final ResidentWebRunner runner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, stayResident: true, dartDefines: const <String>[], urlTunneller: null, ) as ResidentWebRunner; final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(runner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; // Ensure we got the URL and that it was already launched. verify(globals.logger.sendEvent( 'app.webLaunchUrl', argThat(allOf( containsPair('url', 'http://localhost:8765/app/'), containsPair('launched', true), )) )); }, overrides: <Type, Generator>{ Logger: () => DelegateLogger(MockLogger()), ChromeLauncher: () => MockChromeLauncher(), })); test('Sends unlaunched app.webLaunchUrl event for Web Server device', () => testbed.run(() async { _setupMocks(); when(mockFlutterDevice.device).thenReturn(WebServerDevice()); final DelegateLogger delegateLogger = globals.logger as DelegateLogger; final MockStatus mockStatus = MockStatus(); delegateLogger.status = mockStatus; final ResidentWebRunner runner = DwdsWebRunnerFactory().createWebRunner( mockFlutterDevice, flutterProject: FlutterProject.current(), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), ipv6: true, stayResident: true, dartDefines: const <String>[], urlTunneller: null, ) as ResidentWebRunner; final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); unawaited(runner.run( connectionInfoCompleter: connectionInfoCompleter, )); await connectionInfoCompleter.future; // Ensure we got the URL and that it was not already launched. verify(globals.logger.sendEvent( 'app.webLaunchUrl', argThat(allOf( containsPair('url', 'http://localhost:8765/app/'), containsPair('launched', false), )) )); }, overrides: <Type, Generator>{ Logger: () => DelegateLogger(MockLogger()) })); test('Successfully turns WebSocketException into ToolExit', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw const WebSocketException(); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<ToolExit>())); unhandledErrorCompleter.complete(); await expectation; })); test('Successfully turns AppConnectionException into ToolExit', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw AppConnectionException('Could not connect to application with appInstanceId: c0ae0750-ee91-11e9-cea6-35d95a968356'); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<ToolExit>())); unhandledErrorCompleter.complete(); await expectation; })); test('Successfully turns ChromeDebugError into ToolExit', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw ChromeDebugException(<String, dynamic>{}); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<ToolExit>())); unhandledErrorCompleter.complete(); await expectation; })); test('Successfully turns OptionsSkew error into ToolExit', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw OptionsSkew(); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<ToolExit>())); unhandledErrorCompleter.complete(); await expectation; })); test('Successfully turns VersionSkew error into ToolExit', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw VersionSkew(); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<ToolExit>())); unhandledErrorCompleter.complete(); await expectation; })); test('Successfully turns failed startup StateError error into ToolExit', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw StateError('Unable to start build daemon'); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<ToolExit>())); unhandledErrorCompleter.complete(); await expectation; })); test('Rethrows Exception type', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw Exception('Something went wrong'); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<Exception>())); unhandledErrorCompleter.complete(); await expectation; })); test('Successfully turns MissingPortFile error into ToolExit', () => testbed.run(() async { _setupMocks(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw MissingPortFile(); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<ToolExit>())); unhandledErrorCompleter.complete(); await expectation; })); test('Rethrows unknown exception type from web tooling', () => testbed.run(() async { _setupMocks(); final DelegateLogger delegateLogger = globals.logger as DelegateLogger; final MockStatus mockStatus = MockStatus(); delegateLogger.status = mockStatus; final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<void> unhandledErrorCompleter = Completer<void>(); when(mockWebFs.connect(any)).thenAnswer((Invocation _) async { unawaited(unhandledErrorCompleter.future.then((void value) { throw StateError('Something went wrong'); })); return ConnectionResult(mockAppConnection, mockDebugConnection); }); final Future<void> expectation = expectLater(() => residentWebRunner.run( connectionInfoCompleter: connectionInfoCompleter, ), throwsA(isInstanceOf<StateError>())); unhandledErrorCompleter.complete(); await expectation; verify(mockStatus.stop()).called(2); }, overrides: <Type, Generator>{ Logger: () => DelegateLogger(BufferLogger( terminal: AnsiTerminal( stdio: null, platform: const LocalPlatform(), ), outputPreferences: OutputPreferences.test(), )) })); } class MockChromeLauncher extends Mock implements ChromeLauncher {} class MockFlutterUsage extends Mock implements Usage {} class MockChromeDevice extends Mock implements ChromeDevice {} class MockBuildDaemonCreator extends Mock implements BuildDaemonCreator {} class MockFlutterWebFs extends Mock implements WebFs {} class MockDebugConnection extends Mock implements DebugConnection {} class MockAppConnection extends Mock implements AppConnection {} class MockVmService extends Mock implements VmService {} class MockStatus extends Mock implements Status {} class MockFlutterDevice extends Mock implements FlutterDevice {} class MockWebDevFS extends Mock implements DevFS {} class MockResidentCompiler extends Mock implements ResidentCompiler {} class MockChrome extends Mock implements Chrome {} class MockChromeConnection extends Mock implements ChromeConnection {} class MockChromeTab extends Mock implements ChromeTab {} class MockWipConnection extends Mock implements WipConnection {} class MockWipDebugger extends Mock implements WipDebugger {} class MockLogger extends Mock implements Logger {} class MockWebServerDevice extends Mock implements WebServerDevice {}