diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart index 29cb6276574..c3b49317b6f 100644 --- a/packages/devtools_app/lib/src/shared/framework/screen.dart +++ b/packages/devtools_app/lib/src/shared/framework/screen.dart @@ -32,6 +32,7 @@ enum ScreenMetaData { 'home', iconAsset: 'icons/app_bar/devtools.png', requiresConnection: false, + supportsWebServerDevice: true, tutorialVideoTimestamp: '?t=0', ), inspector( @@ -40,6 +41,7 @@ enum ScreenMetaData { iconAsset: 'icons/app_bar/inspector.png', requiresFlutter: true, requiresDebugBuild: true, + supportsWebServerDevice: true, tutorialVideoTimestamp: '?t=172', ), performance( @@ -89,6 +91,7 @@ enum ScreenMetaData { 'logging', title: 'Logging', iconAsset: 'icons/app_bar/logging.png', + supportsWebServerDevice: true, tutorialVideoTimestamp: '?t=558', ), provider( @@ -138,6 +141,7 @@ enum ScreenMetaData { this.requiresFlutter = false, this.requiresDebugBuild = false, this.requiresAdvancedDeveloperMode = false, + this.supportsWebServerDevice = false, this.worksWithOfflineData = false, this.requiresLibrary, this.tutorialVideoTimestamp, @@ -155,6 +159,7 @@ enum ScreenMetaData { final bool requiresFlutter; final bool requiresDebugBuild; final bool requiresAdvancedDeveloperMode; + final bool supportsWebServerDevice; final bool worksWithOfflineData; final String? requiresLibrary; @@ -204,6 +209,7 @@ abstract class Screen { this.requiresFlutter = false, this.requiresDebugBuild = false, this.requiresAdvancedDeveloperMode = false, + this.supportsWebServerDevice = false, this.worksWithOfflineData = false, this.showFloatingDebuggerControls = true, }) : assert( @@ -223,6 +229,7 @@ abstract class Screen { bool requiresFlutter = false, bool requiresDebugBuild = false, bool requiresAdvancedDeveloperMode = false, + bool supportsWebServerDevice = false, bool worksWithOfflineData = false, bool Function(FlutterVersion? currentVersion)? shouldShowForFlutterVersion, bool showFloatingDebuggerControls = true, @@ -239,6 +246,7 @@ abstract class Screen { requiresFlutter: requiresFlutter, requiresDebugBuild: requiresDebugBuild, requiresAdvancedDeveloperMode: requiresAdvancedDeveloperMode, + supportsWebServerDevice: supportsWebServerDevice, worksWithOfflineData: worksWithOfflineData, showFloatingDebuggerControls: showFloatingDebuggerControls, title: title, @@ -262,6 +270,7 @@ abstract class Screen { requiresFlutter: metadata.requiresFlutter, requiresDebugBuild: metadata.requiresDebugBuild, requiresAdvancedDeveloperMode: metadata.requiresAdvancedDeveloperMode, + supportsWebServerDevice: metadata.supportsWebServerDevice, worksWithOfflineData: metadata.worksWithOfflineData, shouldShowForFlutterVersion: shouldShowForFlutterVersion, showFloatingDebuggerControls: showFloatingDebuggerControls, @@ -339,6 +348,10 @@ abstract class Screen { /// is enabled. final bool requiresAdvancedDeveloperMode; + /// Whether this screen should be included when the app is a web app without full debugging + /// support. + final bool supportsWebServerDevice; + /// Whether this screen works offline and should show in offline mode even if conditions are not met. final bool worksWithOfflineData; @@ -490,9 +503,11 @@ abstract class Screen { } } + final serviceManager = serviceConnection.serviceManager; + final connectedApp = serviceManager.connectedApp; final serviceReady = - serviceConnection.serviceManager.isServiceAvailable && - serviceConnection.serviceManager.connectedApp!.connectedAppInitialized; + serviceManager.isServiceAvailable && + connectedApp!.connectedAppInitialized; if (!serviceReady) { if (!screen.requiresConnection) { _log.finest('screen does not require connection: returning true'); @@ -509,42 +524,42 @@ abstract class Screen { } } - if (screen.requiresLibrary != null) { - if (serviceConnection.serviceManager.isolateManager.mainIsolate.value == - null || - !serviceConnection.serviceManager.libraryUriAvailableNow( - screen.requiresLibrary, - )) { - _log.finest( - 'screen requires library ${screen.requiresLibrary}: returning false', - ); - return ( - show: false, - disabledReason: ScreenDisabledReason.requiresDartLibrary, - ); - } + if (screen.requiresLibrary != null && + (serviceManager.isolateManager.mainIsolate.value == null || + !serviceManager.libraryUriAvailableNow(screen.requiresLibrary))) { + _log.finest( + 'screen requires library ${screen.requiresLibrary}: returning false', + ); + return ( + show: false, + disabledReason: ScreenDisabledReason.requiresDartLibrary, + ); } - if (screen.requiresDartVm) { - if (serviceConnection.serviceManager.connectedApp!.isRunningOnDartVM != - true) { - _log.finest('screen requires Dart VM: returning false'); - return (show: false, disabledReason: ScreenDisabledReason.requiresDartVm); - } + if (screen.requiresDartVm && connectedApp.isRunningOnDartVM != true) { + _log.finest('screen requires Dart VM: returning false'); + return (show: false, disabledReason: ScreenDisabledReason.requiresDartVm); } - if (screen.requiresFlutter && - serviceConnection.serviceManager.connectedApp!.isFlutterAppNow == false) { + if (screen.requiresFlutter && connectedApp.isFlutterAppNow == false) { _log.finest('screen requires Flutter: returning false'); return (show: false, disabledReason: ScreenDisabledReason.requiresFlutter); } - if (screen.requiresDebugBuild) { - if (serviceConnection.serviceManager.connectedApp!.isProfileBuildNow == - true) { - _log.finest('screen requires debug build: returning false'); - return ( - show: false, - disabledReason: ScreenDisabledReason.requiresDebugBuild, - ); - } + if (screen.requiresDebugBuild && connectedApp.isProfileBuildNow == true) { + _log.finest('screen requires debug build: returning false'); + return ( + show: false, + disabledReason: ScreenDisabledReason.requiresDebugBuild, + ); + } + if (!screen.supportsWebServerDevice && + connectedApp.isDartWebAppNow == true && + !connectedApp.isDebuggableWebApp) { + _log.finest( + 'screen requires a debuggable web application: returning false', + ); + return ( + show: false, + disabledReason: ScreenDisabledReason.requiresDebuggableWebApp, + ); } _log.finest('${screen.screenId} screen supported: returning true'); return (show: true, disabledReason: null); @@ -573,6 +588,9 @@ enum ScreenDisabledReason { requiresAdvancedDeveloperMode( 'only works when Advanced Developer Mode is enabled', ), + requiresDebuggableWebApp( + 'only works with web applications with full debugging support.', + ), serviceNotReady( 'requires a connected application, but there is no connection available.', ); diff --git a/packages/devtools_app/test/shared/framework/visible_screens_test.dart b/packages/devtools_app/test/shared/framework/visible_screens_test.dart index 4e1a97c4470..4cc7425d5cd 100644 --- a/packages/devtools_app/test/shared/framework/visible_screens_test.dart +++ b/packages/devtools_app/test/shared/framework/visible_screens_test.dart @@ -41,6 +41,7 @@ void main() { void setupMockConnectedApp({ bool web = false, + bool debuggableWeb = true, bool flutter = false, bool debugMode = true, SemanticVersion? flutterVersion, @@ -55,6 +56,7 @@ void main() { isFlutterApp: flutter, isProfileBuild: !debugMode, isWebApp: web, + isDebuggableWebApp: debuggableWeb, ); if (flutter) { fakeServiceConnection.serviceManager.availableLibraries.add( @@ -114,6 +116,31 @@ void main() { ); }); + testWidgets('are correct for Dart Web app (DWDS websocket mode)', ( + WidgetTester tester, + ) async { + setupMockConnectedApp(web: true, debuggableWeb: false); + + expect( + visibleScreenTypes, + equals([ + HomeScreen, + // InspectorScreen, + // LegacyPerformanceScreen, + // PerformanceScreen, + // ProfilerScreen, + // MemoryScreen, + // DebuggerScreen, + // NetworkScreen, + LoggingScreen, + // AppSizeScreen, + // DeepLinksScreen, + // VMDeveloperToolsScreen, + // DTDToolsScreen, + ]), + ); + }); + testWidgets('are correct for Flutter (non-web) debug app', ( WidgetTester tester, ) async { @@ -189,6 +216,31 @@ void main() { ); }); + testWidgets('are correct for Flutter web debug app (DWDS websocket mode)', ( + WidgetTester tester, + ) async { + setupMockConnectedApp(flutter: true, web: true, debuggableWeb: false); + + expect( + visibleScreenTypes, + equals([ + HomeScreen, + InspectorScreen, + // LegacyPerformanceScreen, + // PerformanceScreen, + // ProfilerScreen, + // MemoryScreen, + // DebuggerScreen, + // NetworkScreen, + LoggingScreen, + // AppSizeScreen, + // DeepLinksScreen, + // VMDeveloperToolsScreen, + // DTDToolsScreen, + ]), + ); + }); + testWidgets('are correct for Flutter app on old Flutter version', ( WidgetTester tester, ) async { diff --git a/packages/devtools_app_shared/lib/src/service/connected_app.dart b/packages/devtools_app_shared/lib/src/service/connected_app.dart index 17a625e69b0..39f1fc22475 100644 --- a/packages/devtools_app_shared/lib/src/service/connected_app.dart +++ b/packages/devtools_app_shared/lib/src/service/connected_app.dart @@ -101,6 +101,16 @@ class ConnectedApp { bool get isDebugFlutterAppNow => isFlutterAppNow! && !isProfileBuildNow!; + /// Returns true is the connected application's VM has the name + /// [dwdsChromeDebugProxyDeviceName], indicating that DWDS has an active + /// Chrome debugger connection with support for expression evaluation, + /// object inspection, and setting breakpoints. + /// + /// If false, the connected application supports a reduced subset of the VM + /// service protocol. + bool get isDebuggableWebApp => + serviceManager!.vm!.name == dwdsChromeDebugProxyDeviceName; + bool? get isRunningOnDartVM { final name = serviceManager!.vm!.name; // These are the two possible VM names returned by DWDS. @@ -133,24 +143,6 @@ class ConnectedApp { shouldLogError: false, ); return !(value?.kind == 'Bool'); - - // TODO(terry): Disabled below code, it will hang if flutter run --start-paused - // see issue https://github.com/flutter/devtools/issues/2082. - // Currently, if eval (see above) doesn't work then we're - // running in Profile mode. - /* - assert(serviceConnectionManager.isServiceAvailable); - // Only flutter apps have profile and non-profile builds. If this changes in - // the future (flutter web), we can modify this check. - if (!isRunningOnDartVM || !await isFlutterApp) return false; - - await serviceConnectionManager.manager.serviceExtensionManager.extensionStatesUpdated.future; - - // The debugAllowBanner extension is only available in debug builds - final hasDebugExtension = serviceConnectionManager.manager.serviceExtensionManager - .isServiceExtensionAvailable(extensions.debugAllowBanner.extension); - return !hasDebugExtension; - */ } Future initializeValues({void Function()? onComplete}) async { diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart index d2f2b10c98d..fee470e121e 100644 --- a/packages/devtools_test/lib/src/mocks/mocks.dart +++ b/packages/devtools_test/lib/src/mocks/mocks.dart @@ -146,6 +146,7 @@ void mockConnectedApp( bool isFlutterApp = true, bool isProfileBuild = false, bool isWebApp = false, + bool isDebuggableWebApp = true, String os = 'ios', String flutterVersion = '2.10.0', }) { @@ -160,6 +161,9 @@ void mockConnectedApp( when( connectedApp.isFlutterNativeAppNow, ).thenReturn(isFlutterApp && !isWebApp); + when( + connectedApp.isDebuggableWebApp, + ).thenReturn(isWebApp && isDebuggableWebApp); if (isFlutterApp) { when(connectedApp.flutterVersionNow).thenReturn( FlutterVersion.parse({