// Copyright 2015 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 'package:flutter_test/flutter_test.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import '../widgets/semantics_tester.dart'; import 'feedback_tester.dart'; // This file uses "as dynamic" in a few places to defeat the static // analysis. In general you want to avoid using this style in your // code, as it will cause the analyzer to be unable to help you catch // errors. // // In this case, we do it because we are trying to call internal // methods of the tooltip code in order to test it. Normally, the // state of a tooltip is a private class, but by using a GlobalKey we // can get a handle to that object and by using "as dynamic" we can // bypass the analyzer's type checks and call methods that we aren't // supposed to be able to know about. // // It's ok to do this in tests, but you really don't want to do it in // production code. const String tooltipText = 'TIP'; void main() { testWidgets('Does tooltip end up in the right place - center', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 300.0, top: 0.0, child: new Tooltip( key: key, message: tooltipText, height: 20.0, padding: const EdgeInsets.all(5.0), verticalOffset: 20.0, preferBelow: false, child: new Container( width: 0.0, height: 0.0 ) ) ), ] ); } ), ] ) ); (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen * o * y=0 * | * }- 20.0 vertical offset, of which 10.0 is in the screen edge margin * +----+ * \- (5.0 padding in height) * | | * |- 20 height * +----+ * /- (5.0 padding in height) * * *********************/ final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent.parent.parent.parent.parent; final Offset tipInGlobal = tip.localToGlobal(tip.size.topCenter(Offset.zero)); // The exact position of the left side depends on the font the test framework // happens to pick, so we don't test that. expect(tipInGlobal.dx, 300.0); expect(tipInGlobal.dy, 20.0); }); testWidgets('Does tooltip end up in the right place - top left', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 0.0, top: 0.0, child: new Tooltip( key: key, message: tooltipText, height: 20.0, padding: const EdgeInsets.all(5.0), verticalOffset: 20.0, preferBelow: false, child: new Container( width: 0.0, height: 0.0 ) ) ), ] ); } ), ] ) ); (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen *o * y=0 *| * }- 20.0 vertical offset, of which 10.0 is in the screen edge margin *+----+ * \- (5.0 padding in height) *| | * |- 20 height *+----+ * /- (5.0 padding in height) * * *********************/ final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent.parent.parent.parent.parent; expect(tip.size.height, equals(20.0)); // 10.0 height + 5.0 padding * 2 (top, bottom) expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)), equals(const Offset(10.0, 20.0))); }); testWidgets('Does tooltip end up in the right place - center prefer above fits', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 400.0, top: 300.0, child: new Tooltip( key: key, message: tooltipText, height: 100.0, padding: const EdgeInsets.all(0.0), verticalOffset: 100.0, preferBelow: false, child: new Container( width: 0.0, height: 0.0 ) ) ), ] ); } ), ] ) ); (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen * ___ * }- 10.0 margin * |___| * }-100.0 height * | * }-100.0 vertical offset * o * y=300.0 * * * * * * *********************/ final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent; expect(tip.size.height, equals(100.0)); expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(100.0)); expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(200.0)); }); testWidgets('Does tooltip end up in the right place - center prefer above does not fit', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 400.0, top: 299.0, child: new Tooltip( key: key, message: tooltipText, height: 190.0, padding: const EdgeInsets.all(0.0), verticalOffset: 100.0, preferBelow: false, child: new Container( width: 0.0, height: 0.0 ) ) ), ] ); } ), ] ) ); (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) // we try to put it here but it doesn't fit: /********************* 800x600 screen * ___ * }- 10.0 margin * |___| * }-190.0 height (starts at y=9.0) * | * }-100.0 vertical offset * o * y=299.0 * * * * * * *********************/ // so we put it here: /********************* 800x600 screen * * * * * o * y=299.0 * _|_ * }-100.0 vertical offset * |___| * }-190.0 height * * }- 10.0 margin *********************/ final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent; expect(tip.size.height, equals(190.0)); expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(399.0)); expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(589.0)); }); testWidgets('Does tooltip end up in the right place - center prefer below fits', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 400.0, top: 300.0, child: new Tooltip( key: key, message: tooltipText, height: 190.0, padding: const EdgeInsets.all(0.0), verticalOffset: 100.0, preferBelow: true, child: new Container( width: 0.0, height: 0.0 ) ) ), ] ); } ), ] ) ); (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen * * * * * o * y=300.0 * _|_ * }-100.0 vertical offset * |___| * }-190.0 height * * }- 10.0 margin *********************/ final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent; expect(tip.size.height, equals(190.0)); expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(400.0)); expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(590.0)); }); testWidgets('Does tooltip end up in the right place - way off to the right', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 1600.0, top: 300.0, child: new Tooltip( key: key, message: tooltipText, height: 10.0, padding: const EdgeInsets.all(0.0), verticalOffset: 10.0, preferBelow: true, child: new Container( width: 0.0, height: 0.0 ) ) ), ] ); } ), ] ) ); (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen * * * * * * y=300.0; target --> o * ___| * }-10.0 vertical offset * |___| * }-10.0 height * * * * }-10.0 margin *********************/ final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent; expect(tip.size.height, equals(10.0)); expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0)); expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0)); expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(320.0)); }); testWidgets('Does tooltip end up in the right place - near the edge', (WidgetTester tester) async { final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 780.0, top: 300.0, child: new Tooltip( key: key, message: tooltipText, height: 10.0, padding: const EdgeInsets.all(0.0), verticalOffset: 10.0, preferBelow: true, child: new Container( width: 0.0, height: 0.0 ) ) ), ] ); } ), ] ) ); (key.currentState as dynamic).ensureTooltipVisible(); // before using "as dynamic" in your code, see note top of file await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) /********************* 800x600 screen * * * * * o * y=300.0 * __| * }-10.0 vertical offset * |___| * }-10.0 height * * * * }-10.0 margin *********************/ final RenderBox tip = tester.renderObject(find.text(tooltipText)).parent; expect(tip.size.height, equals(10.0)); expect(tip.localToGlobal(tip.size.topLeft(Offset.zero)).dy, equals(310.0)); expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dx, equals(790.0)); expect(tip.localToGlobal(tip.size.bottomRight(Offset.zero)).dy, equals(320.0)); }); testWidgets('Tooltip stays around', (WidgetTester tester) async { await tester.pumpWidget( new MaterialApp( home: new Center( child: new Tooltip( message: tooltipText, child: new Container( width: 100.0, height: 100.0, color: Colors.green[500], ) ) ) ) ); final Finder tooltip = find.byType(Tooltip); TestGesture gesture = await tester.startGesture(tester.getCenter(tooltip)); await tester.pump(kLongPressTimeout); await tester.pump(const Duration(milliseconds: 10)); await gesture.up(); expect(find.text(tooltipText), findsOneWidget); await tester.tap(tooltip); await tester.pump(const Duration(milliseconds: 10)); gesture = await tester.startGesture(tester.getCenter(tooltip)); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); expect(find.text(tooltipText), findsNothing); await tester.pump(kLongPressTimeout); expect(find.text(tooltipText), findsOneWidget); await tester.pump(kLongPressTimeout); expect(find.text(tooltipText), findsOneWidget); gesture.up(); }); testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async { final SemanticsTester semantics = new SemanticsTester(tester); final GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( initialEntries: <OverlayEntry>[ new OverlayEntry( builder: (BuildContext context) { return new Stack( children: <Widget>[ new Positioned( left: 780.0, top: 300.0, child: new Tooltip( key: key, message: tooltipText, child: new Container(width: 0.0, height: 0.0) ) ), ] ); } ), ] ) ); expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText))); // before using "as dynamic" in your code, see note top of file (key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) expect(semantics, hasSemantics(new TestSemantics.root(label: tooltipText))); semantics.dispose(); }); testWidgets('Tooltip overlay does not update', (WidgetTester tester) async { Widget buildApp(String text) { return new MaterialApp( home: new Center( child: new Tooltip( message: text, child: new Container( width: 100.0, height: 100.0, color: Colors.green[500], ) ) ) ); } await tester.pumpWidget(buildApp(tooltipText)); await tester.longPress(find.byType(Tooltip)); expect(find.text(tooltipText), findsOneWidget); await tester.pumpWidget(buildApp('NEW')); expect(find.text(tooltipText), findsOneWidget); await tester.tapAt(const Offset(5.0, 5.0)); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text(tooltipText), findsNothing); await tester.longPress(find.byType(Tooltip)); expect(find.text(tooltipText), findsNothing); }); testWidgets('Haptic feedback', (WidgetTester tester) async { final FeedbackTester feedback = new FeedbackTester(); await tester.pumpWidget(new MaterialApp( home: new Center( child: new Tooltip( message: 'Foo', child: new Container( width: 100.0, height: 100.0, color: Colors.green[500], ) ) ) ) ); await tester.longPress(find.byType(Tooltip)); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(feedback.hapticCount, 1); feedback.dispose(); }); }