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 5import 'dart:math' as math; 6import 'package:flutter_test/flutter_test.dart'; 7import 'package:flutter/material.dart'; 8import 'package:flutter/widgets.dart'; 9import 'package:flutter/rendering.dart'; 10 11void main() { 12 group('PhysicalShape', () { 13 testWidgets('properties', (WidgetTester tester) async { 14 await tester.pumpWidget( 15 const PhysicalShape( 16 clipper: ShapeBorderClipper(shape: CircleBorder()), 17 elevation: 2.0, 18 color: Color(0xFF0000FF), 19 shadowColor: Color(0xFF00FF00), 20 ) 21 ); 22 final RenderPhysicalShape renderObject = tester.renderObject(find.byType(PhysicalShape)); 23 expect(renderObject.clipper, const ShapeBorderClipper(shape: CircleBorder())); 24 expect(renderObject.color, const Color(0xFF0000FF)); 25 expect(renderObject.shadowColor, const Color(0xFF00FF00)); 26 expect(renderObject.elevation, 2.0); 27 }); 28 29 testWidgets('hit test', (WidgetTester tester) async { 30 await tester.pumpWidget( 31 PhysicalShape( 32 clipper: const ShapeBorderClipper(shape: CircleBorder()), 33 elevation: 2.0, 34 color: const Color(0xFF0000FF), 35 shadowColor: const Color(0xFF00FF00), 36 child: Container(color: const Color(0xFF0000FF)), 37 ) 38 ); 39 40 final RenderPhysicalShape renderPhysicalShape = 41 tester.renderObject(find.byType(PhysicalShape)); 42 43 // The viewport is 800x600, the CircleBorder is centered and fits 44 // the shortest edge, so we get a circle of radius 300, centered at 45 // (400, 300). 46 // 47 // We test by sampling a few points around the left-most point of the 48 // circle (100, 300). 49 50 expect(tester.hitTestOnBinding(const Offset(99.0, 300.0)), doesNotHit(renderPhysicalShape)); 51 expect(tester.hitTestOnBinding(const Offset(100.0, 300.0)), hits(renderPhysicalShape)); 52 expect(tester.hitTestOnBinding(const Offset(100.0, 299.0)), doesNotHit(renderPhysicalShape)); 53 expect(tester.hitTestOnBinding(const Offset(100.0, 301.0)), doesNotHit(renderPhysicalShape)); 54 }, skip: isBrowser); 55 56 }); 57 58 group('FractionalTranslation', () { 59 testWidgets('hit test - entirely inside the bounding box', (WidgetTester tester) async { 60 final GlobalKey key1 = GlobalKey(); 61 bool _pointerDown = false; 62 63 await tester.pumpWidget( 64 Center( 65 child: FractionalTranslation( 66 translation: Offset.zero, 67 transformHitTests: true, 68 child: Listener( 69 onPointerDown: (PointerDownEvent event) { 70 _pointerDown = true; 71 }, 72 child: SizedBox( 73 key: key1, 74 width: 100.0, 75 height: 100.0, 76 child: Container( 77 color: const Color(0xFF0000FF) 78 ), 79 ), 80 ), 81 ), 82 ) 83 ); 84 expect(_pointerDown, isFalse); 85 await tester.tap(find.byKey(key1)); 86 expect(_pointerDown, isTrue); 87 }); 88 89 testWidgets('hit test - partially inside the bounding box', (WidgetTester tester) async { 90 final GlobalKey key1 = GlobalKey(); 91 bool _pointerDown = false; 92 93 await tester.pumpWidget( 94 Center( 95 child: FractionalTranslation( 96 translation: const Offset(0.5, 0.5), 97 transformHitTests: true, 98 child: Listener( 99 onPointerDown: (PointerDownEvent event) { 100 _pointerDown = true; 101 }, 102 child: SizedBox( 103 key: key1, 104 width: 100.0, 105 height: 100.0, 106 child: Container( 107 color: const Color(0xFF0000FF) 108 ), 109 ), 110 ), 111 ), 112 ) 113 ); 114 expect(_pointerDown, isFalse); 115 await tester.tap(find.byKey(key1)); 116 expect(_pointerDown, isTrue); 117 }); 118 119 testWidgets('hit test - completely outside the bounding box', (WidgetTester tester) async { 120 final GlobalKey key1 = GlobalKey(); 121 bool _pointerDown = false; 122 123 await tester.pumpWidget( 124 Center( 125 child: FractionalTranslation( 126 translation: const Offset(1.0, 1.0), 127 transformHitTests: true, 128 child: Listener( 129 onPointerDown: (PointerDownEvent event) { 130 _pointerDown = true; 131 }, 132 child: SizedBox( 133 key: key1, 134 width: 100.0, 135 height: 100.0, 136 child: Container( 137 color: const Color(0xFF0000FF) 138 ), 139 ), 140 ), 141 ), 142 ) 143 ); 144 expect(_pointerDown, isFalse); 145 await tester.tap(find.byKey(key1)); 146 expect(_pointerDown, isTrue); 147 }); 148 }); 149 150 group('Row', () { 151 testWidgets('multiple baseline aligned children', (WidgetTester tester) async { 152 final UniqueKey key1 = UniqueKey(); 153 final UniqueKey key2 = UniqueKey(); 154 const double fontSize1 = 54; 155 const double fontSize2 = 14; 156 157 await tester.pumpWidget( 158 MaterialApp( 159 home: Scaffold( 160 body: Container( 161 child: Row( 162 crossAxisAlignment: CrossAxisAlignment.baseline, 163 textBaseline: TextBaseline.alphabetic, 164 children: <Widget>[ 165 Text('big text', 166 key: key1, 167 style: const TextStyle(fontSize: fontSize1), 168 ), 169 Text('one\ntwo\nthree\nfour\nfive\nsix\nseven', 170 key: key2, 171 style: const TextStyle(fontSize: fontSize2) 172 ), 173 ], 174 ), 175 ), 176 ), 177 ), 178 ); 179 180 final RenderBox textBox1 = tester.renderObject(find.byKey(key1)); 181 final RenderBox textBox2 = tester.renderObject(find.byKey(key2)); 182 final RenderBox rowBox = tester.renderObject(find.byType(Row)); 183 184 // The two Texts are baseline aligned, so some portion of them extends 185 // both above and below the baseline. The first has a huge font size, so 186 // it extends higher above the baseline than usual. The second has many 187 // lines, but being aligned by the first line's baseline, they hang far 188 // below the baseline. The size of the parent row is just enough to 189 // contain both of them. 190 const double ahemBaselineLocation = 0.8; // https://web-platform-tests.org/writing-tests/ahem.html 191 const double aboveBaseline1 = fontSize1 * ahemBaselineLocation; 192 const double belowBaseline1 = fontSize1 * (1 - ahemBaselineLocation); 193 const double aboveBaseline2 = fontSize2 * ahemBaselineLocation; 194 const double belowBaseline2 = fontSize2 * (1 - ahemBaselineLocation) + fontSize2 * 6; 195 final double aboveBaseline = math.max(aboveBaseline1, aboveBaseline2); 196 final double belowBaseline = math.max(belowBaseline1, belowBaseline2); 197 expect(rowBox.size.height, greaterThan(textBox1.size.height)); 198 expect(rowBox.size.height, greaterThan(textBox2.size.height)); 199 expect(rowBox.size.height, closeTo(aboveBaseline + belowBaseline, .001)); 200 expect(tester.getTopLeft(find.byKey(key1)).dy, 0); 201 expect( 202 tester.getTopLeft(find.byKey(key2)).dy, 203 closeTo(aboveBaseline1 - aboveBaseline2, .001), 204 ); 205 }, skip: isBrowser); 206 }); 207 208 test('UnconstrainedBox toString', () { 209 expect( 210 const UnconstrainedBox(constrainedAxis: Axis.vertical,).toString(), 211 equals('UnconstrainedBox(alignment: center, constrainedAxis: vertical)'), 212 ); 213 expect( 214 const UnconstrainedBox(constrainedAxis: Axis.horizontal, textDirection: TextDirection.rtl, alignment: Alignment.topRight).toString(), 215 equals('UnconstrainedBox(alignment: topRight, constrainedAxis: horizontal, textDirection: rtl)'), 216 ); 217 }); 218} 219 220HitsRenderBox hits(RenderBox renderBox) => HitsRenderBox(renderBox); 221 222class HitsRenderBox extends Matcher { 223 const HitsRenderBox(this.renderBox); 224 225 final RenderBox renderBox; 226 227 @override 228 Description describe(Description description) => 229 description.add('hit test result contains ').addDescriptionOf(renderBox); 230 231 @override 232 bool matches(dynamic item, Map<dynamic, dynamic> matchState) { 233 final HitTestResult hitTestResult = item; 234 return hitTestResult.path.where( 235 (HitTestEntry entry) => entry.target == renderBox 236 ).isNotEmpty; 237 } 238} 239 240DoesNotHitRenderBox doesNotHit(RenderBox renderBox) => DoesNotHitRenderBox(renderBox); 241 242class DoesNotHitRenderBox extends Matcher { 243 const DoesNotHitRenderBox(this.renderBox); 244 245 final RenderBox renderBox; 246 247 @override 248 Description describe(Description description) => 249 description.add('hit test result doesn\'t contain ').addDescriptionOf(renderBox); 250 251 @override 252 bool matches(dynamic item, Map<dynamic, dynamic> matchState) { 253 final HitTestResult hitTestResult = item; 254 return hitTestResult.path.where( 255 (HitTestEntry entry) => entry.target == renderBox 256 ).isEmpty; 257 } 258} 259