diff --git a/packages/devtools_app/integration_test/test/live_connection/service_extensions_test.dart b/packages/devtools_app/integration_test/test/live_connection/service_extensions_test.dart index d8f785ac9fb..d76887404be 100644 --- a/packages/devtools_app/integration_test/test/live_connection/service_extensions_test.dart +++ b/packages/devtools_app/integration_test/test/live_connection/service_extensions_test.dart @@ -35,42 +35,53 @@ void main() { await resetHistory(); }); - testWidgets('can call services and service extensions', (tester) async { - await pumpAndConnectDevTools(tester, testApp); - await tester.pump(longDuration); - - // TODO(kenz): re-work this integration test so that we do not have to be - // on the inspector screen for this to pass. - await switchToScreen( - tester, - tabIcon: ScreenMetaData.inspector.icon!, - screenId: ScreenMetaData.inspector.id, - ); - await tester.pump(longDuration); - - // Ensure all futures are completed before running checks. - await serviceConnection.serviceManager.service!.allFuturesCompleted; - - logStatus('verify Flutter framework service extensions'); - await _verifyBooleanExtension(tester); - await _verifyNumericExtension(tester); - await _verifyStringExtension(tester); - - logStatus('verify Flutter engine service extensions'); - expect( - await serviceConnection.queryDisplayRefreshRate, - equals(60), - ); - - logStatus('verify services that are registered to exactly one client'); - await _verifyHotReloadAndHotRestart(); - await expectLater( - serviceConnection.serviceManager.callService('fakeMethod'), - throwsException, - ); - - await disconnectFromTestApp(tester); - }); + testWidgets( + 'can call services and service extensions', + ignoreAllowedExceptions( + (tester) async { + await pumpAndConnectDevTools(tester, testApp); + await tester.pump(longDuration); + + // TODO(kenz): re-work this integration test so that we do not have to be + // on the inspector screen for this to pass. + await switchToScreen( + tester, + tabIcon: ScreenMetaData.inspector.icon!, + screenId: ScreenMetaData.inspector.id, + ); + await tester.pump(longDuration); + + // Ensure all futures are completed before running checks. + await serviceConnection.serviceManager.service!.allFuturesCompleted; + + logStatus('verify Flutter framework service extensions'); + await _verifyBooleanExtension(tester); + await _verifyNumericExtension(tester); + await _verifyStringExtension(tester); + + logStatus('verify Flutter engine service extensions'); + expect( + await serviceConnection.queryDisplayRefreshRate, + equals(60), + ); + + logStatus('verify services that are registered to exactly one client'); + await _verifyHotReloadAndHotRestart(); + await expectLater( + serviceConnection.serviceManager.callService('fakeMethod'), + throwsException, + ); + + await disconnectFromTestApp(tester); + }, + allowedExceptions: [ + AllowedException( + msg: 'A SemanticsHandle was active at the end of the test.', + issue: 'https://github.com/flutter/devtools/issues/8107', + ), + ], + ), + ); testWidgets('loads initial extension states from device', (tester) async { await pumpAndConnectDevTools(tester, testApp); diff --git a/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart b/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart index 945d3d81f3b..cb5e85de3d1 100644 --- a/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart +++ b/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart @@ -2,9 +2,11 @@ // 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 'dart:ui' as ui; +import 'package:collection/collection.dart'; import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/main.dart' as app; import 'package:devtools_app_shared/ui.dart'; @@ -157,3 +159,37 @@ Future verifyScreenshot( }, ); } + +class AllowedException { + AllowedException({required this.msg, required this.issue}); + + final String msg; + final String issue; +} + +/// Wraps the callback to [testWidgets] in a new zone that will catch any +/// exceptions thrown during the test or after the test completes. +/// +/// If the exception is included in [allowedExceptions], the exception will be +/// logged but ignored. Otherwise, the exception will be rethrown. +Future Function(WidgetTester) ignoreAllowedExceptions( + Future Function(WidgetTester) testCallback, { + required List allowedExceptions, +}) { + return (WidgetTester tester) async { + await runZonedGuarded( + () async { + await testCallback(tester); + }, + (e, st) { + final allowed = allowedExceptions + .firstWhereOrNull((allowed) => '$e'.contains(allowed.msg)); + if (allowed == null) { + throw Error.throwWithStackTrace(e, st); + } else { + logStatus('Ignoring exception due to ${allowed.issue}: $e'); + } + }, + ); + }; +}