// 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/io.dart'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:path/path.dart' as path; import 'package:test/test.dart'; // Warning: this list must be kept in sync with the value of // kAllGalleryItems.map((GalleryItem item) => item.category)).toList(); final List demoCategories = [ 'Demos', 'Components', 'Style' ]; // Warning: this list must be kept in sync with the value of // kAllGalleryItems.map((GalleryItem item) => item.title).toList(); final List demoTitles = [ // Demos 'Pesto', 'Shrine', 'Contact profile', // Components 'Bottom navigation', 'Buttons', 'Cards', 'Chips', 'Date and time pickers', 'Dialog', 'Drawer', 'Expand/collapse list control', 'Expansion panels', 'Floating action button', 'Grid', 'Icons', 'Leave-behind list items', 'List', 'Menus', 'Modal bottom sheet', 'Over-scroll', 'Page selector', 'Persistent bottom sheet', 'Progress indicators', 'Scrollable tabs', 'Selection controls', 'Sliders', 'Snackbar', 'Tabs', 'Text fields', 'Tooltips', // Style 'Colors', 'Typography' ]; final FileSystem _fs = new LocalFileSystem(); /// Extracts event data from [events] recorded by timeline, validates it, turns /// it into a histogram, and saves to a JSON file. Future saveDurationsHistogram(List> events, String outputPath) async { final Map> durations = >{}; Map startEvent; // Save the duration of the first frame after each 'Start Transition' event. for (Map 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] ??= new List(); 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'; Map unexpectedValueCounts = {}; durations.forEach((String routeName, List values) { if (values.length != 2) { unexpectedValueCounts[routeName] = values.length; } }); if (unexpectedValueCounts.isNotEmpty) { 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:'); Iterator> eventIter = events.iterator; String lastEventName = ''; String lastRouteName = ''; while(eventIter.moveNext()) { String eventName = eventIter.current['name']; if (!['Start Transition', 'Frame'].contains(eventName)) continue; 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(new JsonEncoder.withIndent(' ').convert(durations)); } void main() { group('flutter gallery transitions', () { FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) await driver.close(); }); test('all demos', () async { Timeline timeline = await driver.traceAction(() async { // Expand the demo category submenus. for (String category in demoCategories.reversed) { await driver.tap(find.text(category)); await new Future.delayed(new Duration(milliseconds: 500)); } // Scroll each demo menu item into view, launch the demo and // return to the demo menu 2x. for(String demoTitle in demoTitles) { print('Testing "$demoTitle" demo'); SerializableFinder menuItem = find.text(demoTitle); await driver.scrollIntoView(menuItem); await new Future.delayed(new Duration(milliseconds: 500)); for(int i = 0; i < 2; i += 1) { await driver.tap(menuItem); // Launch the demo await new Future.delayed(new Duration(milliseconds: 500)); await driver.tap(find.byTooltip('Back')); await new Future.delayed(new Duration(milliseconds: 1000)); } print('Success'); } }, streams: const [ 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). TimelineSummary summary = new TimelineSummary.summarize(timeline); await summary.writeSummaryToFile('transitions', pretty: true); String histogramPath = path.join(testOutputsDirectory, 'transition_durations.timeline.json'); await saveDurationsHistogram(timeline.json['traceEvents'], histogramPath); }, timeout: new Timeout(new Duration(minutes: 5))); }); }