blob: 4465cc6b7f0ff37506b52b442422969dfaae5748 [file] [log] [blame]
// Copyright 2023 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: invalid_use_of_visible_for_testing_member, valid use from package:devtools_test
import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'utils.dart';
/// Navigates to each visible DevTools screen.
Future<void> navigateThroughDevToolsScreens(
WidgetController controller, {
bool runWithExpectations = true,
required bool connectedToApp,
}) async {
final visibleScreenIds = generateVisibleScreenIds();
final tabs = controller.widgetList<Tab>(
find.descendant(
of: find.byType(DevToolsAppBar),
matching: find.byType(Tab),
),
);
var numTabs = tabs.length;
if (numTabs < visibleScreenIds.length) {
final tabOverflowMenuFinder = find.descendant(
of: find.byType(TabOverflowButton),
matching: find.byType(MenuAnchor),
);
_maybeExpect(
tabOverflowMenuFinder,
findsOneWidget,
shouldExpect: runWithExpectations,
);
final menuChildren =
controller.widget<MenuAnchor>(tabOverflowMenuFinder).menuChildren;
numTabs += menuChildren.length;
}
_maybeExpect(
numTabs,
visibleScreenIds.length,
shouldExpect: runWithExpectations,
);
final expectedConnectedControllersCount =
devtoolsScreens!.where((s) => s.providesController).length;
final expectedDisconnectedControllersCount =
devtoolsScreens!
.where((s) => s.providesController && !s.screen.requiresConnection)
.length;
_maybeExpect(
screenControllers.controllers.length,
connectedToApp
? expectedConnectedControllersCount
: expectedDisconnectedControllersCount,
shouldExpect: runWithExpectations,
);
_maybeExpect(
screenControllers.offlineControllers.length,
0,
shouldExpect: runWithExpectations,
);
final screens =
(ScreenMetaData.values.toList()
..removeWhere((data) => !visibleScreenIds.contains(data.id)));
for (final screen in screens) {
await switchToScreen(
controller,
tabIcon: screen.icon,
tabIconAsset: screen.iconAsset,
screenId: screen.id,
runWithExpectations: runWithExpectations,
);
}
}
List<String> generateVisibleScreenIds() {
final availableScreenIds = <String>[];
for (final screen in devtoolsScreens!) {
if (shouldShowScreen(screen.screen).show) {
availableScreenIds.add(screen.screen.screenId);
}
}
return availableScreenIds;
}
/// Switches to the DevTools screen with icon [tabIcon] and pumps the tester
/// to settle the UI.
Future<void> switchToScreen(
WidgetController controller, {
required IconData? tabIcon,
required String? tabIconAsset,
required String screenId,
bool warnIfTapMissed = true,
bool runWithExpectations = true,
}) async {
logStatus(
'switching to $screenId screen (icon $tabIcon, iconAsset: $tabIconAsset)',
);
final tabFinder = await findTab(
controller,
icon: tabIcon,
iconAsset: tabIconAsset,
);
_maybeExpect(tabFinder, findsOneWidget, shouldExpect: runWithExpectations);
await controller.tap(tabFinder, warnIfMissed: warnIfTapMissed);
// We use pump here instead of pumpAndSettle because pumpAndSettle will
// never complete if there is an animation (e.g. a progress indicator).
await controller.pump(safePumpDuration);
}
/// Finds the tab with [icon] either in the top-level DevTools tab bar or in the
/// tab overflow menu for tabs that don't fit on screen.
Future<Finder> findTab(
WidgetController controller, {
required IconData? icon,
required String? iconAsset,
}) async {
assert(
icon != null || iconAsset != null,
'At least one of icon or iconAsset must be non-null.',
);
// Open the tab overflow menu before looking for the tab.
final tabOverflowButtonFinder = find.byType(TabOverflowButton);
if (tabOverflowButtonFinder.evaluate().isNotEmpty) {
await controller.tap(tabOverflowButtonFinder);
await controller.pump(shortPumpDuration);
}
if (icon != null) {
return find.widgetWithIcon(Tab, icon);
}
return find.descendant(
of: find.byType(Tab),
matching: find.byWidgetPredicate(
(widget) => widget is AssetImageIcon && widget.asset == iconAsset!,
),
);
}
// ignore: avoid-dynamic, wrapper around `expect`, which uses dynamic types.
void _maybeExpect(dynamic actual, dynamic matcher, {bool shouldExpect = true}) {
if (shouldExpect) {
expect(actual, matcher);
}
}
Future<void> loadSampleData(
WidgetController controller,
String fileName, {
Duration waitTimeForLoad = longPumpDuration,
}) async {
await controller.tap(find.byType(DropdownButton<DevToolsJsonFile>));
await controller.pumpAndSettle();
await controller.tap(find.text(fileName).last);
await controller.pump(safePumpDuration);
await controller.tap(find.text('Load sample data'));
await controller.pump(waitTimeForLoad);
}
/// Scrolls to the end of the first [Scrollable] descendant of the [T] widget.
///
/// For example, if you have some widget in the tree 'Foo' that contains a
/// [Scrollbar] somewhere in its descendants, calling
/// `scrollToEnd<Foo>(controller)` would perform the following steps:
///
/// 1) find the [Scrollbar] widget descending from 'Foo'.
/// 2) access the [Scrollbar] widget's [ScrollController].
/// 3) scroll the scrollable attached to the [ScrollController] to the end of
/// the [ScrollController]'s scroll extent.
Future<void> scrollToEnd<T>(WidgetController controller) async {
final scrollbarFinder = find.descendant(
of: find.byType(T),
matching: find.byType(Scrollbar),
);
final scrollbar = controller.firstWidget<Scrollbar>(scrollbarFinder);
await scrollbar.controller!.animateTo(
scrollbar.controller!.position.maxScrollExtent,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOutCubic,
);
await controller.pump(shortPumpDuration);
}