blob: f5f185b044f609b754d12f97f891f139de79e83f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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:io';
import 'package:devtools_app/devtools_app.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
const skipForCustomerTestsTag = 'skip-for-flutter-customer-tests';
const shortPumpDuration = Duration(seconds: 1);
const safePumpDuration = Duration(seconds: 3);
const longPumpDuration = Duration(seconds: 6);
const veryLongPumpDuration = Duration(seconds: 9);
final screenIds = <String>[
AppSizeScreen.id,
DebuggerScreen.id,
DeepLinksScreen.id,
InspectorScreen.id,
LoggingScreen.id,
MemoryScreen.id,
NetworkScreen.id,
PerformanceScreen.id,
ProfilerScreen.id,
ProviderScreen.id,
VMDeveloperToolsScreen.id,
];
/// Scoping method which registers `listener` as a listener for `listenable`,
/// invokes `callback`, and then removes the `listener`.
///
/// Tests that `listener` has actually been invoked.
Future<void> addListenerScope({
required Listenable listenable,
required void Function() listener,
required Future<void> Function() callback,
}) async {
bool listenerCalled = false;
void listenerWrapped() {
listenerCalled = true;
listener();
}
listenable.addListener(listenerWrapped);
await callback();
expect(listenerCalled, true);
listenable.removeListener(listenerWrapped);
}
/// Returns a future that completes when a listenable has a value that satisfies
/// [condition].
Future<T> whenMatches<T>(
ValueListenable<T> listenable,
bool Function(T) condition,
) {
final completer = Completer<T>();
void listener() {
if (condition(listenable.value)) {
completer.complete(listenable.value);
listenable.removeListener(listener);
}
}
listenable.addListener(listener);
listener();
return completer.future;
}
/// Workaround to initialize the live widget binding with assets.
///
/// The [LiveTestWidgetsFlutterBinding] is useful for unit tests that need to
/// perform true async operations such as communicating with the VM Service.
/// Unfortunately the default implementation doesn't work with the patterns we
/// use to load assets in the devtools application.
/// TODO(jacobr): consider writing proper integration tests instead rather than
/// using this code path.
void initializeLiveTestWidgetsFlutterBindingWithAssets() {
TestWidgetsFlutterBinding.ensureInitialized({'FLUTTER_TEST': 'false'});
_mockFlutterAssets();
}
// Copied from _binding_io.dart from package:flutter_test,
// This code is typically used to load assets in regular unittests but not
// unittests run with the LiveTestWidgetsFlutterBinding. Assets should be able
// to load normally when running unittests using the
// LiveTestWidgetsFlutterBinding but that is not the case at least for the
// devtools app so we use this workaround.
void _mockFlutterAssets() {
if (!Platform.environment.containsKey('UNIT_TEST_ASSETS')) {
return;
}
final String? assetFolderPath = Platform.environment['UNIT_TEST_ASSETS'];
assert(Platform.environment['APP_NAME'] != null);
final String prefix = 'packages/${Platform.environment['APP_NAME']}/';
/// Navigation related actions (pop, push, replace) broadcasts these actions via
/// platform messages.
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.navigation, null);
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMessageHandler(
'flutter/assets',
(ByteData? message) async {
assert(message != null);
String key = utf8.decode(message!.buffer.asUint8List());
File asset = File(path.join(assetFolderPath!, key));
if (!asset.existsSync()) {
// For tests in package, it will load assets with its own package prefix.
// In this case, we do a best-effort look up.
if (!key.startsWith(prefix)) {
return null;
}
key = key.replaceFirst(prefix, '');
asset = File(path.join(assetFolderPath, key));
if (!asset.existsSync()) {
return null;
}
}
final Uint8List encoded = Uint8List.fromList(asset.readAsBytesSync());
return Future<ByteData>.value(encoded.buffer.asByteData());
},
);
}
// TODO(https://github.com/flutter/devtools/issues/6215): remove this helper.
/// Load fonts used by the devtool for golden-tests to use them
Future<void> loadFonts() async {
// source: https://medium.com/swlh/test-your-flutter-widgets-using-golden-files-b533ac0de469
//https://github.com/flutter/flutter/issues/20907
if (Directory.current.path.endsWith('/test')) {
Directory.current = Directory.current.parent;
}
const fonts = {
'Roboto': [
'fonts/Roboto/Roboto-Thin.ttf',
'fonts/Roboto/Roboto-Light.ttf',
'fonts/Roboto/Roboto-Regular.ttf',
'fonts/Roboto/Roboto-Medium.ttf',
'fonts/Roboto/Roboto-Bold.ttf',
'fonts/Roboto/Roboto-Black.ttf',
],
'RobotoMono': [
'fonts/Roboto_Mono/RobotoMono-Thin.ttf',
'fonts/Roboto_Mono/RobotoMono-Light.ttf',
'fonts/Roboto_Mono/RobotoMono-Regular.ttf',
'fonts/Roboto_Mono/RobotoMono-Medium.ttf',
'fonts/Roboto_Mono/RobotoMono-Bold.ttf',
],
'Octicons': ['fonts/Octicons.ttf'],
// 'Codicon': ['packages/codicon/font/codicon.ttf']
};
final loadFontsFuture = fonts.entries.map((entry) async {
final loader = FontLoader(entry.key);
for (final path in entry.value) {
final fontData = File(path).readAsBytes().then((bytes) {
return ByteData.view(Uint8List.fromList(bytes).buffer);
});
loader.addFont(fontData);
}
await loader.load();
});
await Future.wait(loadFontsFuture);
}
void verifyIsSearchMatch(
List<SearchableDataMixin> data,
List<SearchableDataMixin> matches,
) {
for (final request in data) {
if (matches.contains(request)) {
expect(request.isSearchMatch, isTrue);
} else {
expect(request.isSearchMatch, isFalse);
}
}
}
void verifyIsSearchMatchForTreeData<T extends TreeDataSearchStateMixin<T>>(
List<T> data,
List<T> matches,
) {
for (final node in data) {
breadthFirstTraversal<T>(
node,
action: (T e) {
if (matches.contains(e)) {
expect(e.isSearchMatch, isTrue);
} else {
expect(e.isSearchMatch, isFalse);
}
},
);
}
}
void logStatus(String log) {
// ignore: avoid_print, intentional print for test output
print('TEST STATUS: $log');
}