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:math' as math; 6 7import 'package:flutter/material.dart'; 8import 'package:flutter/rendering.dart'; 9import 'package:flutter_test/flutter_test.dart'; 10import 'package:flutter_gallery/gallery/demos.dart'; 11import 'package:flutter_gallery/gallery/app.dart' show GalleryApp; 12 13// This title is visible on the home and demo category pages. It's 14// not visible when the demos are running. 15const String kGalleryTitle = 'Flutter gallery'; 16 17// All of the classes printed by debugDump etc, must have toString() 18// values approved by verityToStringOutput(). 19int toStringErrors = 0; 20 21// There are 3 places where the Gallery demos are traversed. 22// 1- In widget tests such as examples/flutter_gallery/test/smoke_test.dart 23// 2- In driver tests such as examples/flutter_gallery/test_driver/transitions_perf_test.dart 24// 3- In on-device instrumentation tests such as examples/flutter_gallery/test/live_smoketest.dart 25// 26// If you change navigation behavior in the Gallery or in the framework, make 27// sure all 3 are covered. 28 29void reportToStringError(String name, String route, int lineNumber, List<String> lines, String message) { 30 // If you're on line 12, then it has index 11. 31 // If you want 1 line before and 1 line after, then you want lines with index 10, 11, and 12. 32 // That's (lineNumber-1)-margin .. (lineNumber-1)+margin, or lineNumber-(margin+1) .. lineNumber+(margin-1) 33 const int margin = 5; 34 final int firstLine = math.max(0, lineNumber - margin); 35 final int lastLine = math.min(lines.length, lineNumber + margin); 36 print('$name : $route : line $lineNumber of ${lines.length} : $message; nearby lines were:\n ${lines.sublist(firstLine, lastLine).join("\n ")}'); 37 toStringErrors += 1; 38} 39 40void verifyToStringOutput(String name, String route, String testString) { 41 int lineNumber = 0; 42 final List<String> lines = testString.split('\n'); 43 if (!testString.endsWith('\n')) 44 reportToStringError(name, route, lines.length, lines, 'does not end with a line feed'); 45 for (String line in lines) { 46 lineNumber += 1; 47 if (line == '' && lineNumber != lines.length) { 48 reportToStringError(name, route, lineNumber, lines, 'found empty line'); 49 } else if (line.contains('Instance of ')) { 50 reportToStringError(name, route, lineNumber, lines, 'found a class that does not have its own toString'); 51 } else if (line.endsWith(' ')) { 52 reportToStringError(name, route, lineNumber, lines, 'found a line with trailing whitespace'); 53 } 54 } 55} 56 57Future<void> smokeDemo(WidgetTester tester, GalleryDemo demo) async { 58 // Don't use pumpUntilNoTransientCallbacks in this function, because some of 59 // the smoketests have infinitely-running animations (e.g. the progress 60 // indicators demo). 61 62 await tester.tap(find.text(demo.title)); 63 await tester.pump(); // Launch the demo. 64 await tester.pump(const Duration(milliseconds: 400)); // Wait until the demo has opened. 65 expect(find.text(kGalleryTitle), findsNothing); 66 67 // Leave the demo on the screen briefly for manual testing. 68 await tester.pump(const Duration(milliseconds: 400)); 69 70 // Scroll the demo around a bit. 71 await tester.flingFrom(const Offset(400.0, 300.0), const Offset(-100.0, 0.0), 500.0); 72 await tester.flingFrom(const Offset(400.0, 300.0), const Offset(0.0, -100.0), 500.0); 73 await tester.pump(); 74 await tester.pump(const Duration(milliseconds: 50)); 75 await tester.pump(const Duration(milliseconds: 200)); 76 await tester.pump(const Duration(milliseconds: 400)); 77 78 // Verify that the dumps are pretty. 79 final String routeName = demo.routeName; 80 verifyToStringOutput('debugDumpApp', routeName, WidgetsBinding.instance.renderViewElement.toStringDeep()); 81 verifyToStringOutput('debugDumpRenderTree', routeName, RendererBinding.instance?.renderView?.toStringDeep()); 82 verifyToStringOutput('debugDumpLayerTree', routeName, RendererBinding.instance?.renderView?.debugLayer?.toStringDeep()); 83 84 // Scroll the demo around a bit more. 85 await tester.flingFrom(const Offset(400.0, 300.0), const Offset(0.0, 400.0), 1000.0); 86 await tester.pump(); 87 await tester.pump(const Duration(milliseconds: 400)); 88 await tester.flingFrom(const Offset(400.0, 300.0), const Offset(-200.0, 0.0), 500.0); 89 await tester.pump(); 90 await tester.pump(const Duration(milliseconds: 50)); 91 await tester.pump(const Duration(milliseconds: 200)); 92 await tester.pump(const Duration(milliseconds: 400)); 93 await tester.flingFrom(const Offset(400.0, 300.0), const Offset(100.0, 0.0), 500.0); 94 await tester.pump(); 95 await tester.pump(const Duration(milliseconds: 400)); 96 97 // Go back 98 await tester.pageBack(); 99 await tester.pumpAndSettle(); 100 await tester.pump(); // Start the pop "back" operation. 101 await tester.pump(); // Complete the willPop() Future. 102 await tester.pump(const Duration(milliseconds: 400)); // Wait until it has finished. 103} 104 105Future<void> smokeOptionsPage(WidgetTester tester) async { 106 final Finder showOptionsPageButton = find.byTooltip('Toggle options page'); 107 108 // Show the options page 109 await tester.tap(showOptionsPageButton); 110 await tester.pumpAndSettle(); 111 112 // Switch to the dark theme: first menu button, choose 'Dark' 113 await tester.tap(find.byIcon(Icons.arrow_drop_down).first); 114 await tester.pumpAndSettle(); 115 await tester.tap(find.text('Dark')); 116 await tester.pumpAndSettle(); 117 118 // Switch back to system theme setting: first menu button, choose 'System Default' 119 await tester.tap(find.byIcon(Icons.arrow_drop_down).first); 120 await tester.pumpAndSettle(); 121 await tester.tap(find.text('System Default').at(1)); 122 await tester.pumpAndSettle(); 123 124 // Switch text direction: first switch 125 await tester.tap(find.byType(Switch).first); 126 await tester.pumpAndSettle(); 127 128 // Switch back to system text direction: first switch control again 129 await tester.tap(find.byType(Switch).first); 130 await tester.pumpAndSettle(); 131 132 // Scroll the 'Send feedback' item into view 133 await tester.drag(find.text('Theme'), const Offset(0.0, -1000.0)); 134 await tester.pumpAndSettle(); 135 await tester.tap(find.text('Send feedback')); 136 await tester.pumpAndSettle(); 137 138 // Close the options page 139 expect(showOptionsPageButton, findsOneWidget); 140 await tester.tap(showOptionsPageButton); 141 await tester.pumpAndSettle(); 142} 143 144Future<void> smokeGallery(WidgetTester tester) async { 145 bool sendFeedbackButtonPressed = false; 146 147 await tester.pumpWidget( 148 GalleryApp( 149 testMode: true, 150 onSendFeedback: () { 151 sendFeedbackButtonPressed = true; // see smokeOptionsPage() 152 }, 153 ), 154 ); 155 await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 156 await tester.pump(); // triggers a frame 157 158 expect(find.text(kGalleryTitle), findsOneWidget); 159 160 for (GalleryDemoCategory category in kAllGalleryDemoCategories) { 161 await Scrollable.ensureVisible(tester.element(find.text(category.name)), alignment: 0.5); 162 await tester.tap(find.text(category.name)); 163 await tester.pumpAndSettle(); 164 for (GalleryDemo demo in kGalleryCategoryToDemos[category]) { 165 await Scrollable.ensureVisible(tester.element(find.text(demo.title)), alignment: 0.0); 166 await smokeDemo(tester, demo); 167 tester.binding.debugAssertNoTransientCallbacks('A transient callback was still active after running $demo'); 168 } 169 await tester.pageBack(); 170 await tester.pumpAndSettle(); 171 } 172 expect(toStringErrors, 0); 173 174 await smokeOptionsPage(tester); 175 expect(sendFeedbackButtonPressed, true); 176} 177 178void main() { 179 testWidgets('Flutter Gallery app smoke test', smokeGallery); 180 181 testWidgets('Flutter Gallery app smoke test with semantics', (WidgetTester tester) async { 182 RendererBinding.instance.setSemanticsEnabled(true); 183 await smokeGallery(tester); 184 RendererBinding.instance.setSemanticsEnabled(false); 185 }); 186} 187