blob: 20f16ad8817262914d8e957170ba97103d8b055d [file] [log] [blame]
// Copyright 2019 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
// ignore_for_file: implementation_imports, invalid_use_of_visible_for_testing_member, fine for test only package.
/// @docImport 'package:devtools_app/src/shared/primitives/message_bus.dart';
library;
import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/shared/primitives/query_parameters.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
import 'package:provider/provider.dart';
/// The RouterDelegate must use the same NavigatorKey when building in order
/// for widget state to be preserved.
final _testNavigatorKey = GlobalKey<NavigatorState>();
/// Wraps [widget] with the build context it needs to load in a test as well as
/// the [DevToolsRouterDelegate].
///
/// This includes a [MaterialApp] to provide context like [Theme.of], a
/// [Material] to support elements like [TextField] that draw ink effects, and a
/// [Directionality] to support [RenderFlex] widgets like [Row] and [Column].
Widget wrap(Widget widget, {DevToolsQueryParams? queryParams}) {
setGlobal(GlobalKey<NavigatorState>, _testNavigatorKey);
return MaterialApp.router(
theme: themeFor(
isDarkTheme: false,
ideTheme: IdeTheme(),
theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
),
routerDelegate: DevToolsRouterDelegate.test(
(context, page, args, state) => MaterialPage(
child: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Provider<HoverCardController>.value(
value: HoverCardController(),
child: widget,
),
),
),
),
_testNavigatorKey,
),
routeInformationParser: DevToolsRouteInformationParser.test(
DevToolsQueryParams({
'uri': 'http://test/uri',
}).withUpdates(queryParams?.params),
),
);
}
/// Wraps [widget] with the build context it needs to load in a test.
///
/// This includes a [MaterialApp] to provide context like [Theme.of], a
/// [Material] to support elements like [TextField] that draw ink effects, and a
/// [Directionality] to support [RenderFlex] widgets like [Row] and [Column].
Widget wrapSimple(Widget widget) {
return MaterialApp(
theme: themeFor(
isDarkTheme: false,
ideTheme: IdeTheme(),
theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme),
),
home: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: Provider<HoverCardController>.value(
value: HoverCardController(),
child: widget,
),
),
),
);
}
Widget wrapWithControllers(
Widget widget, {
InspectorScreenController? inspector,
LoggingController? logging,
MemoryController? memory,
PerformanceController? performance,
ProfilerScreenController? profiler,
DebuggerController? debugger,
DeepLinksController? deepLink,
NetworkController? network,
AppSizeController? appSize,
AnalyticsController? analytics,
ReleaseNotesController? releaseNotes,
VMDeveloperToolsController? vmDeveloperTools,
DTDToolsController? dtdTools,
bool includeRouter = true,
DevToolsQueryParams? queryParams,
}) {
// The [ScreenControllers] class uses the [OfflineDataController] class to
// determine which type of controller should be returned from
// [ScreenController.lookup]. Set the global here if it is not already set.
if (globals[OfflineDataController] == null) {
setGlobal(OfflineDataController, OfflineDataController());
}
final offline = offlineDataController.showingOfflineData.value;
setGlobal(ScreenControllers, ScreenControllers());
if (inspector != null) {
screenControllers.register<InspectorScreenController>(
() => inspector,
offline: offline,
);
}
if (logging != null) {
screenControllers.register<LoggingController>(
() => logging,
offline: offline,
);
}
if (memory != null) {
screenControllers.register<MemoryController>(
() => memory,
offline: offline,
);
}
if (performance != null) {
screenControllers.register<PerformanceController>(
() => performance,
offline: offline,
);
}
if (profiler != null) {
screenControllers.register<ProfilerScreenController>(
() => profiler,
offline: offline,
);
}
if (network != null) {
screenControllers.register<NetworkController>(
() => network,
offline: offline,
);
}
if (debugger != null) {
screenControllers.register<DebuggerController>(
() => debugger,
offline: offline,
);
}
if (deepLink != null) {
screenControllers.register<DeepLinksController>(
() => deepLink,
offline: offline,
);
}
if (appSize != null) {
screenControllers.register<AppSizeController>(
() => appSize,
offline: offline,
);
}
if (vmDeveloperTools != null) {
screenControllers.register<VMDeveloperToolsController>(
() => vmDeveloperTools,
offline: offline,
);
}
if (dtdTools != null) {
screenControllers.register<DTDToolsController>(
() => dtdTools,
offline: offline,
);
}
var child = wrapWithNotifications(widget);
final providers = [
if (analytics != null)
Provider<AnalyticsController>.value(value: analytics),
if (releaseNotes != null)
Provider<ReleaseNotesController>.value(value: releaseNotes),
];
if (providers.isNotEmpty) {
child = MultiProvider(providers: providers, child: child);
}
return includeRouter
? wrap(child, queryParams: queryParams)
: wrapSimple(child);
}
Widget wrapWithNotifications(Widget child) {
return NotificationsView(child: child);
}
Widget wrapWithInspectorControllers(Widget widget) {
return wrapWithControllers(
widget,
debugger: DebuggerController(),
inspector: InspectorScreenController(),
);
}
/// Call [testWidgets], allowing the test to set specific values for app globals
/// ([MessageBus], ...).
void testWidgetsWithContext(
String description,
WidgetTesterCallback callback, {
Map<Type, dynamic> context = const {},
}) {
testWidgets(description, (WidgetTester widgetTester) async {
// set up the context
final oldValues = <Type, Object?>{};
for (final type in context.keys) {
oldValues[type] = globals[type];
setGlobal(type, context[type]);
}
try {
await callback(widgetTester);
} finally {
// restore previous global values
for (final type in oldValues.keys) {
final oldGlobal = oldValues[type];
if (oldGlobal != null) {
setGlobal(type, oldGlobal);
} else {
globals.remove(type);
}
}
}
});
}
/// Runs a test with the size of the app window under test to [windowSize].
@isTest
void testWidgetsWithWindowSize(
String name,
Size windowSize,
WidgetTesterCallback test, {
bool skip = false,
}) {
testWidgets(name, (WidgetTester tester) async {
await _setWindowSize(tester, windowSize);
await test(tester);
await _resetWindowSize(tester);
}, skip: skip);
}
Future<void> _setWindowSize(WidgetTester tester, Size windowSize) async {
final binding = TestWidgetsFlutterBinding.ensureInitialized();
await binding.setSurfaceSize(windowSize);
tester.view.physicalSize = windowSize;
tester.view.devicePixelRatio = 1.0;
}
Future<void> _resetWindowSize(WidgetTester tester) async {
await _setWindowSize(tester, const Size(800.0, 600.0));
}