| // Copyright 2016 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' show JsonEncoder; |
| |
| import 'package:file/file.dart'; |
| import 'package:file/local.dart'; |
| import 'package:flutter_driver/flutter_driver.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:test/test.dart'; |
| |
| class Demo { |
| const Demo(this.title, {this.synchronized = true, this.profiled = false}); |
| |
| /// The title of the demo. |
| final String title; |
| |
| /// True if frameSync should be enabled for this test. |
| final bool synchronized; |
| |
| // True if timeline data should be collected for this test. |
| // |
| // Warning: The number of tests executed with timeline collection enabled |
| // significantly impacts heap size of the running app. When run with |
| // --trace-startup, as we do in this test, the VM stores trace events in an |
| // endless buffer instead of a ring buffer. |
| final bool profiled; |
| } |
| |
| // Warning: this list must be kept in sync with the value of |
| // kAllGalleryItems.map((GalleryItem item) => item.title).toList(); |
| const List<Demo> demos = const <Demo>[ |
| // Demos |
| const Demo('Shrine', profiled: true), |
| const Demo('Contact profile', profiled: true), |
| const Demo('Animation', profiled: true), |
| |
| // Material Components |
| const Demo('Bottom navigation', profiled: true), |
| const Demo('Buttons', profiled: true), |
| const Demo('Cards', profiled: true), |
| const Demo('Chips', profiled: true), |
| const Demo('Date and time pickers', profiled: true), |
| const Demo('Dialog', profiled: true), |
| const Demo('Drawer'), |
| const Demo('Expand/collapse list control'), |
| const Demo('Expansion panels'), |
| const Demo('Floating action button'), |
| const Demo('Grid'), |
| const Demo('Icons'), |
| const Demo('Leave-behind list items'), |
| const Demo('List'), |
| const Demo('Menus'), |
| const Demo('Modal bottom sheet'), |
| const Demo('Page selector'), |
| const Demo('Persistent bottom sheet'), |
| const Demo('Progress indicators', synchronized: false), |
| const Demo('Pull to refresh'), |
| const Demo('Scrollable tabs'), |
| const Demo('Selection controls'), |
| const Demo('Sliders'), |
| const Demo('Snackbar'), |
| const Demo('Tabs'), |
| const Demo('Text fields'), |
| const Demo('Tooltips'), |
| |
| // Cupertino Components |
| const Demo('Activity Indicator', synchronized: false), |
| const Demo('Buttons'), |
| const Demo('Dialogs'), |
| const Demo('Navigation'), |
| const Demo('Pickers'), |
| const Demo('Sliders'), |
| const Demo('Switches'), |
| |
| // Media |
| const Demo('Animated images'), |
| |
| // Style |
| const Demo('Colors'), |
| const Demo('Typography'), |
| ]; |
| |
| const FileSystem _fs = const LocalFileSystem(); |
| |
| const Duration kWaitBetweenActions = const Duration(milliseconds: 250); |
| |
| /// Extracts event data from [events] recorded by timeline, validates it, turns |
| /// it into a histogram, and saves to a JSON file. |
| Future<Null> saveDurationsHistogram(List<Map<String, dynamic>> events, String outputPath) async { |
| final Map<String, List<int>> durations = <String, List<int>>{}; |
| Map<String, dynamic> startEvent; |
| |
| // Save the duration of the first frame after each 'Start Transition' event. |
| for (Map<String, dynamic> event in events) { |
| final String eventName = event['name']; |
| if (eventName == 'Start Transition') { |
| assert(startEvent == null); |
| startEvent = event; |
| } else if (startEvent != null && eventName == 'Frame') { |
| final String routeName = startEvent['args']['to']; |
| durations[routeName] ??= <int>[]; |
| durations[routeName].add(event['dur']); |
| startEvent = null; |
| } |
| } |
| |
| // Verify that the durations data is valid. |
| if (durations.keys.isEmpty) |
| throw 'no "Start Transition" timeline events found'; |
| final Map<String, int> unexpectedValueCounts = <String, int>{}; |
| durations.forEach((String routeName, List<int> values) { |
| if (values.length != 2) { |
| unexpectedValueCounts[routeName] = values.length; |
| } |
| }); |
| |
| if (unexpectedValueCounts.isNotEmpty) { |
| final StringBuffer error = new StringBuffer('Some routes recorded wrong number of values (expected 2 values/route):\n\n'); |
| unexpectedValueCounts.forEach((String routeName, int count) { |
| error.writeln(' - $routeName recorded $count values.'); |
| }); |
| error.writeln('\nFull event sequence:'); |
| final Iterator<Map<String, dynamic>> eventIter = events.iterator; |
| String lastEventName = ''; |
| String lastRouteName = ''; |
| while (eventIter.moveNext()) { |
| final String eventName = eventIter.current['name']; |
| |
| if (!<String>['Start Transition', 'Frame'].contains(eventName)) |
| continue; |
| |
| final String routeName = eventName == 'Start Transition' |
| ? eventIter.current['args']['to'] |
| : ''; |
| |
| if (eventName == lastEventName && routeName == lastRouteName) { |
| error.write('.'); |
| } else { |
| error.write('\n - $eventName $routeName .'); |
| } |
| |
| lastEventName = eventName; |
| lastRouteName = routeName; |
| } |
| throw error; |
| } |
| |
| // Save the durations Map to a file. |
| final File file = await _fs.file(outputPath).create(recursive: true); |
| await file.writeAsString(const JsonEncoder.withIndent(' ').convert(durations)); |
| } |
| |
| /// Scrolls each demo menu item into view, launches it, then returns to the |
| /// home screen twice. |
| Future<Null> runDemos(Iterable<Demo> demos, FlutterDriver driver) async { |
| for (Demo demo in demos) { |
| print('Testing "${demo.title}" demo'); |
| final SerializableFinder menuItem = find.text(demo.title); |
| await driver.scrollIntoView(menuItem, alignment: 0.5); |
| await new Future<Null>.delayed(kWaitBetweenActions); |
| |
| for (int i = 0; i < 2; i += 1) { |
| await driver.tap(menuItem); // Launch the demo |
| await new Future<Null>.delayed(kWaitBetweenActions); |
| if (demo.synchronized) { |
| await driver.tap(find.byTooltip('Back')); |
| } else { |
| await driver.runUnsynchronized<Future<Null>>(() async { |
| await new Future<Null>.delayed(kWaitBetweenActions); |
| await driver.tap(find.byTooltip('Back')); |
| }); |
| } |
| await new Future<Null>.delayed(kWaitBetweenActions); |
| } |
| print('Success'); |
| } |
| } |
| |
| void main([List<String> args = const <String>[]]) { |
| group('flutter gallery transitions', () { |
| FlutterDriver driver; |
| setUpAll(() async { |
| driver = await FlutterDriver.connect(); |
| if (args.contains('--with_semantics')) { |
| print('Enabeling semantics...'); |
| await driver.setSemantics(true); |
| } |
| }); |
| |
| tearDownAll(() async { |
| if (driver != null) |
| await driver.close(); |
| }); |
| |
| test('all demos', () async { |
| // Collect timeline data for just a limited set of demos to avoid OOMs. |
| final Timeline timeline = await driver.traceAction(() async { |
| final Iterable<Demo> profiledDemos = demos.where((Demo demo) => demo.profiled); |
| await runDemos(profiledDemos, driver); |
| }, |
| streams: const <TimelineStream>[ |
| TimelineStream.dart, |
| TimelineStream.embedder, |
| ]); |
| |
| // Save the duration (in microseconds) of the first timeline Frame event |
| // that follows a 'Start Transition' event. The Gallery app adds a |
| // 'Start Transition' event when a demo is launched (see GalleryItem). |
| final TimelineSummary summary = new TimelineSummary.summarize(timeline); |
| await summary.writeSummaryToFile('transitions', pretty: true); |
| final String histogramPath = path.join(testOutputsDirectory, 'transition_durations.timeline.json'); |
| await saveDurationsHistogram(timeline.json['traceEvents'], histogramPath); |
| |
| // Execute the remaining tests. |
| final Iterable<Demo> unprofiledDemos = demos.where((Demo demo) => !demo.profiled); |
| await runDemos(unprofiledDemos, driver); |
| |
| }, timeout: const Timeout(const Duration(minutes: 5))); |
| }); |
| } |