blob: ed8ad74ea9f3d69ee853a673724e06c780aae6d9 [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/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
import 'package:vm_snapshot_analysis/treemap.dart';
final screenIds = <String>[
AppSizeScreen.id,
DebuggerScreen.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 Function listener,
required Function callback,
}) async {
bool listenerCalled = false;
final 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 condition(T)) {
final completer = Completer<T>();
void listener() {
if (condition(listenable.value)) {
completer.complete(listenable.value);
listenable.removeListener(listener);
}
}
listenable.addListener(listener);
listener();
return completer.future;
}
Future<TreemapNode> loadSnapshotJsonAsTree(String snapshotJson) async {
final treemapTestData = jsonDecode(snapshotJson);
if (treemapTestData is Map<String, dynamic> &&
treemapTestData['type'] == 'apk') {
return generateTree(treemapTestData);
} else {
final processedTestData = treemapFromJson(treemapTestData);
processedTestData['n'] = 'Root';
return generateTree(processedTestData);
}
}
/// Builds a tree with [TreemapNode] from [treeJson] which represents
/// the hierarchical structure of the tree.
TreemapNode generateTree(Map<String, dynamic> treeJson) {
var treemapNodeName = treeJson['n'];
if (treemapNodeName == '') treemapNodeName = 'Unnamed';
final rawChildren = treeJson['children'];
final treemapNodeChildren = <TreemapNode>[];
int treemapNodeSize = 0;
if (rawChildren != null) {
// If not a leaf node, build all children then take the sum of the
// children's sizes as its own size.
for (var child in rawChildren) {
final childTreemapNode = generateTree(child);
treemapNodeChildren.add(childTreemapNode);
treemapNodeSize += childTreemapNode.byteSize;
}
treemapNodeSize = treemapNodeSize;
} else {
// If a leaf node, just take its own size.
// Defaults to 0 if a leaf node has a size of null.
treemapNodeSize = treeJson['value'] ?? 0;
}
return TreemapNode(name: treemapNodeName, byteSize: treemapNodeSize)
..addAllChildren(treemapNodeChildren);
}
Finder findSubstring(String text) {
return find.byWidgetPredicate((widget) {
if (widget is Text) {
if (widget.data != null) return widget.data!.contains(text);
return widget.textSpan!.toPlainText().contains(text);
} else if (widget is RichText) {
return widget.text.toPlainText().contains(text);
} else if (widget is SelectableText) {
if (widget.data != null) return widget.data!.contains(text);
}
return false;
});
}
extension RichTextChecking on CommonFinders {
Finder richText(String text) {
return find.byWidgetPredicate(
(widget) => widget is RichText && widget.text.toPlainText() == text,
);
}
Finder richTextContaining(String text) {
return find.byWidgetPredicate(
(widget) =>
widget is RichText && widget.text.toPlainText().contains(text),
);
}
}
extension SelectableTextChecking on CommonFinders {
Finder selectableText(String text) {
return find.byWidgetPredicate(
(widget) =>
widget is SelectableText &&
(widget.data == text || widget.textSpan?.toPlainText() == text),
);
}
Finder selectableTextContaining(String text) {
return find.byWidgetPredicate(
(widget) =>
widget is SelectableText &&
((widget.data?.contains(text) ?? false) ||
(widget.textSpan?.toPlainText().contains(text) ?? false)),
);
}
}
/// 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());
},
);
}
/// 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);
}
/// Helper method for finding widgets by type when they have a generic type.
///
/// E.g. `find.byType(typeOf<MyTypeWithAGeneric<String>>());`
Type typeOf<T>() => T;
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);
}
},
);
}
}