1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6import 'dart:convert' show JsonEncoder, JsonDecoder; 7 8import 'package:file/file.dart'; 9import 'package:file/local.dart'; 10import 'package:flutter_driver/flutter_driver.dart'; 11import 'package:path/path.dart' as path; 12import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; 13 14const FileSystem _fs = LocalFileSystem(); 15 16// Demos for which timeline data will be collected using 17// FlutterDriver.traceAction(). 18// 19// Warning: The number of tests executed with timeline collection enabled 20// significantly impacts heap size of the running app. When run with 21// --trace-startup, as we do in this test, the VM stores trace events in an 22// endless buffer instead of a ring buffer. 23// 24// These names must match GalleryItem titles from kAllGalleryDemos 25// in examples/flutter_gallery/lib/gallery/demos.dart 26const List<String> kProfiledDemos = <String>[ 27 'Shrine@Studies', 28 'Contact profile@Studies', 29 'Animation@Studies', 30 'Bottom navigation@Material', 31 'Buttons@Material', 32 'Cards@Material', 33 'Chips@Material', 34 'Dialogs@Material', 35 'Pickers@Material', 36]; 37 38// There are 3 places where the Gallery demos are traversed. 39// 1- In widget tests such as examples/flutter_gallery/test/smoke_test.dart 40// 2- In driver tests such as examples/flutter_gallery/test_driver/transitions_perf_test.dart 41// 3- In on-device instrumentation tests such as examples/flutter_gallery/test/live_smoketest.dart 42// 43// If you change navigation behavior in the Gallery or in the framework, make 44// sure all 3 are covered. 45 46// Demos that will be backed out of within FlutterDriver.runUnsynchronized(); 47// 48// These names must match GalleryItem titles from kAllGalleryDemos 49// in examples/flutter_gallery/lib/gallery/demos.dart 50const List<String> kUnsynchronizedDemos = <String>[ 51 'Progress indicators@Material', 52 'Activity Indicator@Cupertino', 53 'Video@Media', 54]; 55 56const List<String> kSkippedDemos = <String>[]; 57 58// All of the gallery demos, identified as "title@category". 59// 60// These names are reported by the test app, see _handleMessages() 61// in transitions_perf.dart. 62List<String> _allDemos = <String>[]; 63 64/// Extracts event data from [events] recorded by timeline, validates it, turns 65/// it into a histogram, and saves to a JSON file. 66Future<void> saveDurationsHistogram(List<Map<String, dynamic>> events, String outputPath) async { 67 final Map<String, List<int>> durations = <String, List<int>>{}; 68 Map<String, dynamic> startEvent; 69 70 // Save the duration of the first frame after each 'Start Transition' event. 71 for (Map<String, dynamic> event in events) { 72 final String eventName = event['name']; 73 if (eventName == 'Start Transition') { 74 assert(startEvent == null); 75 startEvent = event; 76 } else if (startEvent != null && eventName == 'Frame') { 77 final String routeName = startEvent['args']['to']; 78 durations[routeName] ??= <int>[]; 79 durations[routeName].add(event['dur']); 80 startEvent = null; 81 } 82 } 83 84 // Verify that the durations data is valid. 85 if (durations.keys.isEmpty) 86 throw 'no "Start Transition" timeline events found'; 87 final Map<String, int> unexpectedValueCounts = <String, int>{}; 88 durations.forEach((String routeName, List<int> values) { 89 if (values.length != 2) { 90 unexpectedValueCounts[routeName] = values.length; 91 } 92 }); 93 94 if (unexpectedValueCounts.isNotEmpty) { 95 final StringBuffer error = StringBuffer('Some routes recorded wrong number of values (expected 2 values/route):\n\n'); 96 unexpectedValueCounts.forEach((String routeName, int count) { 97 error.writeln(' - $routeName recorded $count values.'); 98 }); 99 error.writeln('\nFull event sequence:'); 100 final Iterator<Map<String, dynamic>> eventIter = events.iterator; 101 String lastEventName = ''; 102 String lastRouteName = ''; 103 while (eventIter.moveNext()) { 104 final String eventName = eventIter.current['name']; 105 106 if (!<String>['Start Transition', 'Frame'].contains(eventName)) 107 continue; 108 109 final String routeName = eventName == 'Start Transition' 110 ? eventIter.current['args']['to'] 111 : ''; 112 113 if (eventName == lastEventName && routeName == lastRouteName) { 114 error.write('.'); 115 } else { 116 error.write('\n - $eventName $routeName .'); 117 } 118 119 lastEventName = eventName; 120 lastRouteName = routeName; 121 } 122 throw error; 123 } 124 125 // Save the durations Map to a file. 126 final File file = await _fs.file(outputPath).create(recursive: true); 127 await file.writeAsString(const JsonEncoder.withIndent(' ').convert(durations)); 128} 129 130/// Scrolls each demo menu item into view, launches it, then returns to the 131/// home screen twice. 132Future<void> runDemos(List<String> demos, FlutterDriver driver) async { 133 final SerializableFinder demoList = find.byValueKey('GalleryDemoList'); 134 String currentDemoCategory; 135 136 for (String demo in demos) { 137 if (kSkippedDemos.contains(demo)) 138 continue; 139 140 final String demoName = demo.substring(0, demo.indexOf('@')); 141 final String demoCategory = demo.substring(demo.indexOf('@') + 1); 142 print('> $demo'); 143 144 if (currentDemoCategory == null) { 145 await driver.tap(find.text(demoCategory)); 146 } else if (currentDemoCategory != demoCategory) { 147 await driver.tap(find.byTooltip('Back')); 148 await driver.tap(find.text(demoCategory)); 149 // Scroll back to the top 150 await driver.scroll(demoList, 0.0, 10000.0, const Duration(milliseconds: 100)); 151 } 152 currentDemoCategory = demoCategory; 153 154 final SerializableFinder demoItem = find.text(demoName); 155 await driver.scrollUntilVisible(demoList, demoItem, 156 dyScroll: -48.0, 157 alignment: 0.5, 158 timeout: const Duration(seconds: 30), 159 ); 160 161 for (int i = 0; i < 2; i += 1) { 162 await driver.tap(demoItem); // Launch the demo 163 164 if (kUnsynchronizedDemos.contains(demo)) { 165 await driver.runUnsynchronized<void>(() async { 166 await driver.tap(find.pageBack()); 167 }); 168 } else { 169 await driver.tap(find.pageBack()); 170 } 171 } 172 173 print('< Success'); 174 } 175 176 // Return to the home screen 177 await driver.tap(find.byTooltip('Back')); 178} 179 180void main([List<String> args = const <String>[]]) { 181 group('flutter gallery transitions', () { 182 FlutterDriver driver; 183 setUpAll(() async { 184 driver = await FlutterDriver.connect(); 185 186 // Wait for the first frame to be rasterized. 187 await driver.waitUntilFirstFrameRasterized(); 188 189 if (args.contains('--with_semantics')) { 190 print('Enabeling semantics...'); 191 await driver.setSemantics(true); 192 } 193 194 // See _handleMessages() in transitions_perf.dart. 195 _allDemos = List<String>.from(const JsonDecoder().convert(await driver.requestData('demoNames'))); 196 if (_allDemos.isEmpty) 197 throw 'no demo names found'; 198 }); 199 200 tearDownAll(() async { 201 if (driver != null) 202 await driver.close(); 203 }); 204 205 test('all demos', () async { 206 // Collect timeline data for just a limited set of demos to avoid OOMs. 207 final Timeline timeline = await driver.traceAction( 208 () async { 209 await runDemos(kProfiledDemos, driver); 210 }, 211 streams: const <TimelineStream>[ 212 TimelineStream.dart, 213 TimelineStream.embedder, 214 ], 215 ); 216 217 // Save the duration (in microseconds) of the first timeline Frame event 218 // that follows a 'Start Transition' event. The Gallery app adds a 219 // 'Start Transition' event when a demo is launched (see GalleryItem). 220 final TimelineSummary summary = TimelineSummary.summarize(timeline); 221 await summary.writeSummaryToFile('transitions', pretty: true); 222 final String histogramPath = path.join(testOutputsDirectory, 'transition_durations.timeline.json'); 223 await saveDurationsHistogram( 224 List<Map<String, dynamic>>.from(timeline.json['traceEvents']), 225 histogramPath); 226 227 // Execute the remaining tests. 228 final Set<String> unprofiledDemos = Set<String>.from(_allDemos)..removeAll(kProfiledDemos); 229 await runDemos(unprofiledDemos.toList(), driver); 230 231 }, timeout: const Timeout(Duration(minutes: 5))); 232 }); 233} 234