diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart index 9cca8f7021f..a3a32e79c83 100644 --- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart @@ -174,10 +174,8 @@ class ResidentWebRunner extends ResidentRunner { debuggingOptions.buildInfo.ddcModuleFormat != DdcModuleFormat.ddc || debuggingOptions.buildInfo.canaryFeatures != true; - // TODO(srujzs): Return true when web supports detaching. - // https://github.com/flutter/flutter/issues/163329 @override - bool get supportsDetach => false; + bool get supportsDetach => stopAppDuringCleanup; ConnectionResult? _connectionResult; StreamSubscription? _stdOutSub; @@ -220,7 +218,11 @@ class ResidentWebRunner extends ResidentRunner { await _stdErrSub?.cancel(); await _serviceSub?.cancel(); await _extensionEventSub?.cancel(); - await device!.device!.stopApp(null); + + if (stopAppDuringCleanup) { + await device!.device!.stopApp(null); + } + _registeredMethodsForService.clear(); try { _generatedEntrypointDirectory?.deleteSync(recursive: true); @@ -808,7 +810,9 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive). @override Future exitApp() async { - await device!.exitApps(); + if (stopAppDuringCleanup) { + await device!.exitApps(); + } appFinished(); } diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 1fb6909f60e..185201281fa 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -970,9 +970,6 @@ abstract class ResidentHandlers { Future cleanupAfterSignal(); /// Tear down the runner and leave the application running. - /// - /// This is not supported on web devices where the runner is running - /// the application server as well. Future detach(); /// Tear down the runner and exit the application. diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 5fef0e13540..f3c4ecde757 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -405,6 +405,90 @@ void main() { }, ); + testUsingContext( + 'Detach keeps device running', + () async { + final BufferLogger logger = BufferLogger.test(); + fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); + setupMocks(); + fileSystem.directory('web').deleteSync(recursive: true); + final ResidentWebRunner residentWebRunner = ResidentWebRunner( + flutterDevice, + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + fileSystem: fileSystem, + logger: logger, + terminal: Terminal.test(), + platform: FakePlatform(), + outputPreferences: OutputPreferences.test(), + analytics: globals.analytics, + systemClock: globals.systemClock, + devtoolsHandler: createNoOpHandler, + ); + + mockDevice.dds = DartDevelopmentService(logger: logger); + + expect(mockDevice.isRunning, false); + final Completer connectionInfoCompleter = + Completer(); + unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter)); + await connectionInfoCompleter.future; + expect(mockDevice.isRunning, true); + await residentWebRunner.detach(); + expect(residentWebRunner.stopAppDuringCleanup, false); + await residentWebRunner.exit(); + await residentWebRunner.cleanupAtFinish(); + expect(mockDevice.isRunning, true); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + FeatureFlags: enableExplicitPackageDependencies, + Pub: FakePubWithPrimedDeps.new, + }, + ); + + testUsingContext( + 'Quit stops device', + () async { + final BufferLogger logger = BufferLogger.test(); + fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList()); + setupMocks(); + fileSystem.directory('web').deleteSync(recursive: true); + final ResidentWebRunner residentWebRunner = ResidentWebRunner( + flutterDevice, + flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + fileSystem: fileSystem, + logger: logger, + terminal: Terminal.test(), + platform: FakePlatform(), + outputPreferences: OutputPreferences.test(), + analytics: globals.analytics, + systemClock: globals.systemClock, + devtoolsHandler: createNoOpHandler, + ); + + mockDevice.dds = DartDevelopmentService(logger: logger); + + expect(mockDevice.isRunning, false); + final Completer connectionInfoCompleter = + Completer(); + unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter)); + await connectionInfoCompleter.future; + expect(mockDevice.isRunning, true); + expect(residentWebRunner.stopAppDuringCleanup, true); + await residentWebRunner.cleanupAtFinish(); + expect(mockDevice.isRunning, false); + }, + overrides: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + FeatureFlags: enableExplicitPackageDependencies, + Pub: FakePubWithPrimedDeps.new, + }, + ); + testUsingContext( 'Listens to stdout and stderr streams before running main', () async { @@ -1593,6 +1677,8 @@ class FakeDevice extends Fake implements Device { int count = 0; + bool isRunning = false; + @override Future get sdkNameAndVersion async => 'SDK Name and Version'; @@ -1613,6 +1699,7 @@ class FakeDevice extends Fake implements Device { bool ipv6 = false, String? userIdentifier, }) async { + isRunning = true; return LaunchResult.succeeded(); } @@ -1622,6 +1709,7 @@ class FakeDevice extends Fake implements Device { throw StateError('stopApp called more than once.'); } count += 1; + isRunning = false; return true; } }