1// Copyright 2017 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 5// ATTENTION! 6// 7// This file is not named "*_test.dart", and as such will not run when you run 8// "flutter test". It is only intended to be run as part of the 9// flutter_gallery_instrumentation_test devicelab test. 10 11import 'dart:async'; 12 13import 'package:flutter/cupertino.dart'; 14import 'package:flutter/material.dart'; 15import 'package:flutter/scheduler.dart'; 16import 'package:flutter/services.dart'; 17import 'package:flutter/widgets.dart'; 18import 'package:flutter/gestures.dart' show kPrimaryButton; 19import 'package:flutter_test/flutter_test.dart'; 20 21import 'package:flutter_gallery/gallery/demos.dart'; 22import 'package:flutter_gallery/gallery/app.dart' show GalleryApp; 23 24// Reports success or failure to the native code. 25const MethodChannel _kTestChannel = MethodChannel('io.flutter.demo.gallery/TestLifecycleListener'); 26 27// We don't want to wait for animations to complete before tapping the 28// back button in the demos with these titles. 29const List<String> _kUnsynchronizedDemoTitles = <String>[ 30 'Progress indicators', 31 'Activity Indicator', 32 'Video', 33]; 34 35// These demos can't be backed out of by tapping a button whose 36// tooltip is 'Back'. 37const List<String> _kSkippedDemoTitles = <String>[ 38 'Progress indicators', 39 'Activity Indicator', 40 'Video', 41]; 42 43// There are 3 places where the Gallery demos are traversed. 44// 1- In widget tests such as examples/flutter_gallery/test/smoke_test.dart 45// 2- In driver tests such as examples/flutter_gallery/test_driver/transitions_perf_test.dart 46// 3- In on-device instrumentation tests such as examples/flutter_gallery/test/live_smoketest.dart 47// 48// If you change navigation behavior in the Gallery or in the framework, make 49// sure all 3 are covered. 50 51Future<void> main() async { 52 try { 53 // Verify that _kUnsynchronizedDemos and _kSkippedDemos identify 54 // demos that actually exist. 55 final List<String> allDemoTitles = kAllGalleryDemos.map((GalleryDemo demo) => demo.title).toList(); 56 if (!Set<String>.from(allDemoTitles).containsAll(_kUnsynchronizedDemoTitles)) 57 fail('Unrecognized demo titles in _kUnsynchronizedDemosTitles: $_kUnsynchronizedDemoTitles'); 58 if (!Set<String>.from(allDemoTitles).containsAll(_kSkippedDemoTitles)) 59 fail('Unrecognized demo names in _kSkippedDemoTitles: $_kSkippedDemoTitles'); 60 61 print('Starting app...'); 62 runApp(const GalleryApp(testMode: true)); 63 final _LiveWidgetController controller = _LiveWidgetController(WidgetsBinding.instance); 64 for (GalleryDemoCategory category in kAllGalleryDemoCategories) { 65 print('Tapping "${category.name}" section...'); 66 await controller.tap(find.text(category.name)); 67 for (GalleryDemo demo in kGalleryCategoryToDemos[category]) { 68 final Finder demoItem = find.text(demo.title); 69 print('Scrolling to "${demo.title}"...'); 70 await controller.scrollIntoView(demoItem, alignment: 0.5); 71 if (_kSkippedDemoTitles.contains(demo.title)) 72 continue; 73 for (int i = 0; i < 2; i += 1) { 74 print('Tapping "${demo.title}"...'); 75 await controller.tap(demoItem); // Launch the demo 76 controller.frameSync = !_kUnsynchronizedDemoTitles.contains(demo.title); 77 print('Going back to demo list...'); 78 await controller.tap(backFinder); 79 controller.frameSync = true; 80 } 81 } 82 print('Going back to home screen...'); 83 await controller.tap(find.byTooltip('Back')); 84 } 85 print('Finished successfully!'); 86 _kTestChannel.invokeMethod<void>('success'); 87 } catch (error, stack) { 88 print('Caught error: $error\n$stack'); 89 _kTestChannel.invokeMethod<void>('failure'); 90 } 91} 92 93final Finder backFinder = find.byElementPredicate( 94 (Element element) { 95 final Widget widget = element.widget; 96 if (widget is Tooltip) 97 return widget.message == 'Back'; 98 if (widget is CupertinoNavigationBarBackButton) 99 return true; 100 return false; 101 }, 102 description: 'Material or Cupertino back button', 103); 104 105class _LiveWidgetController extends LiveWidgetController { 106 _LiveWidgetController(WidgetsBinding binding) : super(binding); 107 108 /// With [frameSync] enabled, Flutter Driver will wait to perform an action 109 /// until there are no pending frames in the app under test. 110 bool frameSync = true; 111 112 /// Waits until at the end of a frame the provided [condition] is [true]. 113 Future<void> _waitUntilFrame(bool condition(), [Completer<void> completer]) { 114 completer ??= Completer<void>(); 115 if (!condition()) { 116 SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { 117 _waitUntilFrame(condition, completer); 118 }); 119 } else { 120 completer.complete(); 121 } 122 return completer.future; 123 } 124 125 /// Runs `finder` repeatedly until it finds one or more [Element]s. 126 Future<Finder> _waitForElement(Finder finder) async { 127 if (frameSync) 128 await _waitUntilFrame(() => binding.transientCallbackCount == 0); 129 await _waitUntilFrame(() => finder.precache()); 130 if (frameSync) 131 await _waitUntilFrame(() => binding.transientCallbackCount == 0); 132 return finder; 133 } 134 135 @override 136 Future<void> tap(Finder finder, { int pointer, int buttons = kPrimaryButton }) async { 137 await super.tap(await _waitForElement(finder), pointer: pointer, buttons: buttons); 138 } 139 140 Future<void> scrollIntoView(Finder finder, {double alignment}) async { 141 final Finder target = await _waitForElement(finder); 142 await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: alignment); 143 } 144} 145