blob: 30b9658836f0a1610f82015984c434f529ed2994 [file] [log] [blame]
// Copyright 2014 The Flutter 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 'dart:ui';
import 'package:flutter/services.dart';
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 '../rendering/mock_canvas.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';
Finder _findTooltipContainer(String tooltipText) {
return find.ancestor(
of: find.text(tooltipText),
matching: find.byType(Container),
);
}
void main() {
testWidgets('Does tooltip end up in the right place - center', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 300.0,
top: 0.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 20.0,
padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0,
preferBelow: false,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the 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(
_findTooltipContainer(tooltipText),
);
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 - center with padding outside overlay', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Padding(
padding: const EdgeInsets.all(20),
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 300.0,
top: 0.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 20.0,
padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0,
preferBelow: false,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
/************************ 800x600 screen
* ________________ * }- 20.0 padding outside overlay
* | 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)
* |________________| *
* * } - 20.0 padding outside overlay
************************/
final RenderBox tip = tester.renderObject(
_findTooltipContainer(tooltipText),
);
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, 320.0);
expect(tipInGlobal.dy, 40.0);
});
testWidgets('Does tooltip end up in the right place - top left', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 0.0,
top: 0.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 20.0,
padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0,
preferBelow: false,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the 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(
_findTooltipContainer(tooltipText),
);
expect(tip.size.height, equals(24.0)); // 14.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 = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 400.0,
top: 300.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 100.0,
padding: const EdgeInsets.all(0.0),
verticalOffset: 100.0,
preferBelow: false,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the 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(
_findTooltipContainer(tooltipText),
);
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 = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 400.0,
top: 299.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 190.0,
padding: const EdgeInsets.all(0.0),
verticalOffset: 100.0,
preferBelow: false,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the 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(
_findTooltipContainer(tooltipText),
);
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 = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 400.0,
top: 300.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 190.0,
padding: const EdgeInsets.all(0.0),
verticalOffset: 100.0,
preferBelow: true,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the 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(
_findTooltipContainer(tooltipText),
);
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 = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 1600.0,
top: 300.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 10.0,
padding: const EdgeInsets.all(0.0),
verticalOffset: 10.0,
preferBelow: true,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the 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(
_findTooltipContainer(tooltipText),
);
expect(tip.size.height, equals(14.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(324.0));
});
testWidgets('Does tooltip end up in the right place - near the edge', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 780.0,
top: 300.0,
child: Tooltip(
key: key,
message: tooltipText,
height: 10.0,
padding: const EdgeInsets.all(0.0),
verticalOffset: 10.0,
preferBelow: true,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
),
),
],
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the 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(
_findTooltipContainer(tooltipText),
);
expect(tip.size.height, equals(14.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(324.0));
});
testWidgets('Custom tooltip margin', (WidgetTester tester) async {
const double _customMarginValue = 10.0;
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: key,
message: tooltipText,
padding: const EdgeInsets.all(0.0),
margin: const EdgeInsets.all(_customMarginValue),
child: const SizedBox(
width: 0.0,
height: 0.0,
),
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final Offset topLeftTipInGlobal = tester.getTopLeft(
_findTooltipContainer(tooltipText),
);
final Offset topLeftTooltipContentInGlobal = tester.getTopLeft(find.text(tooltipText));
expect(topLeftTooltipContentInGlobal.dx, topLeftTipInGlobal.dx + _customMarginValue);
expect(topLeftTooltipContentInGlobal.dy, topLeftTipInGlobal.dy + _customMarginValue);
final Offset topRightTipInGlobal = tester.getTopRight(
_findTooltipContainer(tooltipText),
);
final Offset topRightTooltipContentInGlobal = tester.getTopRight(find.text(tooltipText));
expect(topRightTooltipContentInGlobal.dx, topRightTipInGlobal.dx - _customMarginValue);
expect(topRightTooltipContentInGlobal.dy, topRightTipInGlobal.dy + _customMarginValue);
final Offset bottomLeftTipInGlobal = tester.getBottomLeft(
_findTooltipContainer(tooltipText),
);
final Offset bottomLeftTooltipContentInGlobal = tester.getBottomLeft(find.text(tooltipText));
expect(bottomLeftTooltipContentInGlobal.dx, bottomLeftTipInGlobal.dx + _customMarginValue);
expect(bottomLeftTooltipContentInGlobal.dy, bottomLeftTipInGlobal.dy - _customMarginValue);
final Offset bottomRightTipInGlobal = tester.getBottomRight(
_findTooltipContainer(tooltipText),
);
final Offset bottomRightTooltipContentInGlobal = tester.getBottomRight(find.text(tooltipText));
expect(bottomRightTooltipContentInGlobal.dx, bottomRightTipInGlobal.dx - _customMarginValue);
expect(bottomRightTooltipContentInGlobal.dy, bottomRightTipInGlobal.dy - _customMarginValue);
});
testWidgets('Default tooltip message textStyle - light', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(
home: Tooltip(
key: key,
message: tooltipText,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
));
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
expect(textStyle.color, Colors.white);
expect(textStyle.fontFamily, 'Roboto');
expect(textStyle.decoration, TextDecoration.none);
expect(textStyle.debugLabel, '((englishLike body1 2014).merge(blackMountainView bodyText2)).copyWith');
});
testWidgets('Default tooltip message textStyle - dark', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
),
home: Tooltip(
key: key,
message: tooltipText,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
));
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
expect(textStyle.color, Colors.black);
expect(textStyle.fontFamily, 'Roboto');
expect(textStyle.decoration, TextDecoration.none);
expect(textStyle.debugLabel, '((englishLike body1 2014).merge(whiteMountainView bodyText2)).copyWith');
});
testWidgets('Custom tooltip message textStyle', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(
home: Tooltip(
key: key,
textStyle: const TextStyle(
color: Colors.orange,
decoration: TextDecoration.underline,
),
message: tooltipText,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
));
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
expect(textStyle.color, Colors.orange);
expect(textStyle.fontFamily, null);
expect(textStyle.decoration, TextDecoration.underline);
});
testWidgets('Tooltip overlay respects ambient Directionality', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/40702.
Widget buildApp(String text, TextDirection textDirection) {
return MaterialApp(
home: Directionality(
textDirection: textDirection,
child: Center(
child: Tooltip(
message: text,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
),
),
);
}
await tester.pumpWidget(buildApp(tooltipText, TextDirection.rtl));
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsOneWidget);
RenderParagraph tooltipRenderParagraph = tester.renderObject<RenderParagraph>(find.text(tooltipText));
expect(tooltipRenderParagraph.textDirection, TextDirection.rtl);
await tester.pumpWidget(buildApp(tooltipText, TextDirection.ltr));
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsOneWidget);
tooltipRenderParagraph = tester.renderObject<RenderParagraph>(find.text(tooltipText));
expect(tooltipRenderParagraph.textDirection, TextDirection.ltr);
});
testWidgets('Tooltip overlay wrapped with a non-fallback DefaultTextStyle widget', (WidgetTester tester) async {
// A Material widget is needed as an ancestor of the Text widget.
// It is invalid to have text in a Material application that
// does not have a Material ancestor.
final GlobalKey key = GlobalKey();
await tester.pumpWidget(MaterialApp(
home: Tooltip(
key: key,
message: tooltipText,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
));
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final TextStyle textStyle = tester.widget<DefaultTextStyle>(
find.ancestor(
of: find.text(tooltipText),
matching: find.byType(DefaultTextStyle),
).first,
).style;
// The default fallback text style results in a text with a
// double underline of Color(0xffffff00).
expect(textStyle.decoration, isNot(TextDecoration.underline));
expect(textStyle.decorationColor, isNot(const Color(0xffffff00)));
expect(textStyle.decorationStyle, isNot(TextDecorationStyle.double));
});
testWidgets('Does tooltip end up with the right default size, shape, and color', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: key,
message: tooltipText,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final RenderBox tip = tester.renderObject(
_findTooltipContainer(tooltipText),
);
expect(tip.size.height, equals(32.0));
expect(tip.size.width, equals(74.0));
expect(tip, paints..rrect(
rrect: RRect.fromRectAndRadius(tip.paintBounds, const Radius.circular(4.0)),
color: const Color(0xe6616161),
));
});
testWidgets('Can tooltip decoration be customized', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
const Decoration customDecoration = ShapeDecoration(
shape: StadiumBorder(),
color: Color(0x80800000),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: key,
decoration: customDecoration,
message: tooltipText,
child: const SizedBox(
width: 0.0,
height: 0.0,
),
);
},
),
],
),
),
);
(key.currentState as dynamic).ensureTooltipVisible(); // Before using "as dynamic" in your code, see note at the top of the file.
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final RenderBox tip = tester.renderObject(
_findTooltipContainer(tooltipText),
);
expect(tip.size.height, equals(32.0));
expect(tip.size.width, equals(74.0));
expect(tip, paints..path(
color: const Color(0x80800000),
));
});
testWidgets('Tooltip stays after long press', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Tooltip(
message: tooltipText,
child: 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));
// long press reveals tooltip
await tester.pump(kLongPressTimeout);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tooltipText), findsOneWidget);
await gesture.up();
// tap (down, up) gesture hides tooltip, since its not
// a long press
await tester.tap(tooltip);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tooltipText), findsNothing);
// long press once more
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);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text(tooltipText), findsOneWidget);
// keep holding the long press, should still show tooltip
await tester.pump(kLongPressTimeout);
expect(find.text(tooltipText), findsOneWidget);
gesture.up();
});
testWidgets('Tooltip shows/hides when hovered', (WidgetTester tester) async {
const Duration waitDuration = Duration(milliseconds: 0);
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(() async {
if (gesture != null)
return gesture.removePointer();
});
await gesture.addPointer();
await gesture.moveTo(const Offset(1.0, 1.0));
await tester.pump();
await gesture.moveTo(Offset.zero);
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: Tooltip(
message: tooltipText,
waitDuration: waitDuration,
child: SizedBox(
width: 100.0,
height: 100.0,
),
),
),
),
);
final Finder tooltip = find.byType(Tooltip);
await gesture.moveTo(Offset.zero);
await tester.pump();
await gesture.moveTo(tester.getCenter(tooltip));
await tester.pump();
// Wait for it to appear.
await tester.pump(waitDuration);
expect(find.text(tooltipText), findsOneWidget);
// Wait a looong time to make sure that it doesn't go away if the mouse is
// still over the widget.
await tester.pump(const Duration(days: 1));
await tester.pumpAndSettle();
expect(find.text(tooltipText), findsOneWidget);
await gesture.moveTo(Offset.zero);
await tester.pump();
// Wait for it to disappear.
await tester.pumpAndSettle();
await gesture.removePointer();
gesture = null;
expect(find.text(tooltipText), findsNothing);
});
testWidgets('Tooltip does not attempt to show after unmount', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/54096.
const Duration waitDuration = Duration(seconds: 1);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(() async {
if (gesture != null)
return gesture.removePointer();
});
await gesture.addPointer();
await gesture.moveTo(const Offset(1.0, 1.0));
await tester.pump();
await gesture.moveTo(Offset.zero);
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: Tooltip(
message: tooltipText,
waitDuration: waitDuration,
child: SizedBox(
width: 100.0,
height: 100.0,
),
),
),
),
);
final Finder tooltip = find.byType(Tooltip);
await gesture.moveTo(Offset.zero);
await tester.pump();
await gesture.moveTo(tester.getCenter(tooltip));
await tester.pump();
// Pump another random widget to unmount the Tooltip widget.
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox(),
),
),
);
// If the issue regresses, an exception will be thrown while we are waiting.
await tester.pump(waitDuration);
});
testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 780.0,
top: 300.0,
child: Tooltip(
key: key,
message: tooltipText,
child: const SizedBox(width: 10.0, height: 10.0),
),
),
],
);
},
),
],
),
),
);
final TestSemantics expected = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 1,
label: 'TIP',
textDirection: TextDirection.ltr,
),
],
);
expect(semantics, hasSemantics(expected, ignoreTransform: true, ignoreRect: true));
// Before using "as dynamic" in your code, see note at the top of the 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(expected, ignoreTransform: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('Tooltip overlay does not update', (WidgetTester tester) async {
Widget buildApp(String text) {
return MaterialApp(
home: Center(
child: Tooltip(
message: text,
child: 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('Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
Widget buildApp(String text, { required double textScaleFactor }) {
return MediaQuery(
data: MediaQueryData(textScaleFactor: textScaleFactor),
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return Center(
child: Tooltip(
message: text,
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
);
}
);
},
),
),
);
}
await tester.pumpWidget(buildApp(tooltipText, textScaleFactor: 1.0));
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsOneWidget);
expect(tester.getSize(find.text(tooltipText)), equals(const Size(42.0, 14.0)));
RenderBox tip = tester.renderObject(
_findTooltipContainer(tooltipText),
);
expect(tip.size.height, equals(32.0));
await tester.pumpWidget(buildApp(tooltipText, textScaleFactor: 4.0));
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsOneWidget);
expect(tester.getSize(find.text(tooltipText)), equals(const Size(168.0, 56.0)));
tip = tester.renderObject(
_findTooltipContainer(tooltipText),
);
expect(tip.size.height, equals(56.0));
});
testWidgets('Haptic feedback', (WidgetTester tester) async {
final FeedbackTester feedback = FeedbackTester();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Tooltip(
message: 'Foo',
child: 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();
});
testWidgets('Semantics included', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: Tooltip(
message: 'Foo',
child: Text('Bar'),
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
label: 'Foo\nBar',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
), ignoreRect: true, ignoreId: true, ignoreTransform: true));
semantics.dispose();
});
testWidgets('Semantics excluded', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: Tooltip(
message: 'Foo',
child: Text('Bar'),
excludeFromSemantics: true,
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
label: 'Bar',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
), ignoreRect: true, ignoreId: true, ignoreTransform: true));
semantics.dispose();
});
testWidgets('has semantic events', (WidgetTester tester) async {
final List<dynamic> semanticEvents = <dynamic>[];
SystemChannels.accessibility.setMockMessageHandler((dynamic message) async {
semanticEvents.add(message);
});
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Tooltip(
message: 'Foo',
child: Container(
width: 100.0,
height: 100.0,
color: Colors.green[500],
),
),
),
),
);
await tester.longPress(find.byType(Tooltip));
final RenderObject object = tester.firstRenderObject(find.byType(Tooltip));
expect(semanticEvents, unorderedEquals(<dynamic>[
<String, dynamic>{
'type': 'longPress',
'nodeId': findDebugSemantics(object).id,
'data': <String, dynamic>{},
},
<String, dynamic>{
'type': 'tooltip',
'data': <String, dynamic>{
'message': 'Foo',
},
},
]));
semantics.dispose();
SystemChannels.accessibility.setMockMessageHandler(null);
});
testWidgets('default Tooltip debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const Tooltip(message: 'message',).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString()).toList();
expect(description, <String>[
'"message"',
]);
});
testWidgets('Tooltip implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
// Not checking controller, inputFormatters, focusNode
const Tooltip(
key: ValueKey<String>('foo'),
message: 'message',
decoration: BoxDecoration(),
waitDuration: Duration(seconds: 1),
showDuration: Duration(seconds: 2),
padding: EdgeInsets.zero,
margin: EdgeInsets.all(5.0),
height: 100.0,
excludeFromSemantics: true,
preferBelow: false,
verticalOffset: 50.0,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString()).toList();
expect(description, <String>[
'"message"',
'height: 100.0',
'padding: EdgeInsets.zero',
'margin: EdgeInsets.all(5.0)',
'vertical offset: 50.0',
'position: above',
'semantics: excluded',
'wait duration: 0:00:01.000000',
'show duration: 0:00:02.000000',
]);
});
}
SemanticsNode findDebugSemantics(RenderObject object) {
if (object.debugSemantics != null)
return object.debugSemantics!;
return findDebugSemantics(object.parent! as RenderObject);
}