blob: 273a7faa472148928f28cfee53163b683fd2e3cf [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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
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 tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 300.0,
top: 0.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 20.0,
padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0,
preferBelow: false,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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 tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Padding(
padding: const EdgeInsets.all(20),
child: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 300.0,
top: 0.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 20.0,
padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0,
preferBelow: false,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Material2 - Does tooltip end up in the right place - top left', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 0.0,
top: 0.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 20.0,
padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0,
preferBelow: false,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Material3 - Does tooltip end up in the right place - top left', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 0.0,
top: 0.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 20.0,
padding: const EdgeInsets.all(5.0),
verticalOffset: 20.0,
preferBelow: false,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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(30.0)); // 20.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 tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 400.0,
top: 300.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 100.0,
padding: EdgeInsets.zero,
verticalOffset: 100.0,
preferBelow: false,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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 tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 400.0,
top: 299.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 190.0,
padding: EdgeInsets.zero,
verticalOffset: 100.0,
preferBelow: false,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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 tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 400.0,
top: 300.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 190.0,
padding: EdgeInsets.zero,
verticalOffset: 100.0,
preferBelow: true,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Material2 - Does tooltip end up in the right place - way off to the right', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 1600.0,
top: 300.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 10.0,
padding: EdgeInsets.zero,
verticalOffset: 10.0,
preferBelow: true,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Material3 - Does tooltip end up in the right place - way off to the right', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 1600.0,
top: 300.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 10.0,
padding: EdgeInsets.zero,
verticalOffset: 10.0,
preferBelow: true,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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(20.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(330.0));
});
testWidgets('Material2 - Does tooltip end up in the right place - near the edge', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 780.0,
top: 300.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 10.0,
padding: EdgeInsets.zero,
verticalOffset: 10.0,
preferBelow: true,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Material3 - Does tooltip end up in the right place - near the edge', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: 780.0,
top: 300.0,
child: Tooltip(
key: tooltipKey,
message: tooltipText,
height: 10.0,
padding: EdgeInsets.zero,
verticalOffset: 10.0,
preferBelow: true,
child: const SizedBox.shrink(),
),
),
],
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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(20.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(330.0));
});
testWidgets('Tooltip should be fully visible when MediaQuery.viewInsets > 0', (
WidgetTester tester,
) async {
// Regression test for https://github.com/flutter/flutter/issues/23666
Widget materialAppWithViewInsets(double viewInsetsHeight) {
final Widget scaffold = Scaffold(
body: const TextField(),
floatingActionButton: FloatingActionButton(
tooltip: tooltipText,
onPressed: () {
/* do nothing */
},
child: const Icon(Icons.add),
),
);
return MediaQuery(
data: MediaQueryData(viewInsets: EdgeInsets.only(bottom: viewInsetsHeight)),
child: MaterialApp(useInheritedMediaQuery: true, home: scaffold),
);
}
// Start with MediaQuery.viewInsets.bottom = 0
await tester.pumpWidget(materialAppWithViewInsets(0));
// Show FAB tooltip
final Finder fabFinder = find.byType(FloatingActionButton);
await tester.longPress(fabFinder);
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(Tooltip), findsOneWidget);
// FAB tooltip should be above FAB
RenderBox tip = tester.renderObject(_findTooltipContainer(tooltipText));
Offset fabTopRight = tester.getTopRight(fabFinder);
Offset tooltipTopRight = tip.localToGlobal(tip.size.topRight(Offset.zero));
expect(tooltipTopRight.dy, lessThan(fabTopRight.dy));
// Simulate Keyboard opening (MediaQuery.viewInsets.bottom = 300))
await tester.pumpWidget(materialAppWithViewInsets(300));
// Wait for the tooltip to dismiss.
await tester.pump(const Duration(days: 1));
await tester.pumpAndSettle();
// Show FAB tooltip
await tester.longPress(fabFinder);
await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(Tooltip), findsOneWidget);
// FAB tooltip should still be above FAB
tip = tester.renderObject(_findTooltipContainer(tooltipText));
fabTopRight = tester.getTopRight(fabFinder);
tooltipTopRight = tip.localToGlobal(tip.size.topRight(Offset.zero));
expect(tooltipTopRight.dy, lessThan(fabTopRight.dy));
});
testWidgets('Custom tooltip margin', (WidgetTester tester) async {
const customMarginValue = 10.0;
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: tooltipKey,
message: tooltipText,
padding: EdgeInsets.zero,
margin: const EdgeInsets.all(customMarginValue),
child: const SizedBox.shrink(),
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Material2 - Default tooltip message textStyle - light', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Tooltip(
key: tooltipKey,
message: tooltipText,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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 bodyMedium 2014).merge(blackMountainView bodyMedium)).copyWith',
);
});
testWidgets('Material3 - Default tooltip message textStyle - light', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
key: tooltipKey,
message: tooltipText,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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 bodyMedium 2021).merge((blackMountainView bodyMedium).apply)).copyWith',
);
});
testWidgets('Material2 - Default tooltip message textStyle - dark', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false, brightness: Brightness.dark),
home: Tooltip(
key: tooltipKey,
message: tooltipText,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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 bodyMedium 2014).merge(whiteMountainView bodyMedium)).copyWith',
);
});
testWidgets('Material3 - Default tooltip message textStyle - dark', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(brightness: Brightness.dark),
home: Tooltip(
key: tooltipKey,
message: tooltipText,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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 bodyMedium 2021).merge((whiteMountainView bodyMedium).apply)).copyWith',
);
});
testWidgets('Custom tooltip message textStyle', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
key: tooltipKey,
textStyle: const TextStyle(color: Colors.orange, decoration: TextDecoration.underline),
message: tooltipText,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Custom tooltip message textAlign', (WidgetTester tester) async {
Future<void> pumpTooltipWithTextAlign({TextAlign? textAlign}) async {
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
key: tooltipKey,
textAlign: textAlign,
message: tooltipText,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
}
// Default value should be TextAlign.start
await pumpTooltipWithTextAlign();
TextAlign textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
expect(textAlign, TextAlign.start);
await pumpTooltipWithTextAlign(textAlign: TextAlign.center);
textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
expect(textAlign, TextAlign.center);
await pumpTooltipWithTextAlign(textAlign: TextAlign.end);
textAlign = tester.widget<Text>(find.text(tooltipText)).textAlign!;
expect(textAlign, TextAlign.end);
});
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 tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
key: tooltipKey,
message: tooltipText,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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('Material2 - Does tooltip end up with the right default size, shape, and color', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: tooltipKey,
message: tooltipText,
child: const SizedBox.shrink(),
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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),
),
);
final Container tooltipContainer = tester.firstWidget<Container>(
_findTooltipContainer(tooltipText),
);
expect(tooltipContainer.padding, const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0));
});
testWidgets('Material3 - Does tooltip end up with the right default size, shape, and color', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: tooltipKey,
message: tooltipText,
child: const SizedBox.shrink(),
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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.75));
expect(
tip,
paints..rrect(
rrect: RRect.fromRectAndRadius(tip.paintBounds, const Radius.circular(4.0)),
color: const Color(0xe6616161),
),
);
final Container tooltipContainer = tester.firstWidget<Container>(
_findTooltipContainer(tooltipText),
);
expect(tooltipContainer.padding, const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0));
});
testWidgets(
'Material2 - Tooltip default size, shape, and color test for Desktop',
(WidgetTester tester) async {
// Regressing test for https://github.com/flutter/flutter/issues/68601
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Tooltip(key: tooltipKey, message: tooltipText, child: const SizedBox.shrink()),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final RenderParagraph tooltipRenderParagraph = tester.renderObject<RenderParagraph>(
find.text(tooltipText),
);
expect(tooltipRenderParagraph.textSize.height, equals(12.0));
final RenderBox tooltipRenderBox = tester.renderObject(_findTooltipContainer(tooltipText));
expect(tooltipRenderBox.size.height, equals(24.0));
expect(
tooltipRenderBox,
paints..rrect(
rrect: RRect.fromRectAndRadius(tooltipRenderBox.paintBounds, const Radius.circular(4.0)),
color: const Color(0xe6616161),
),
);
final Container tooltipContainer = tester.firstWidget<Container>(
_findTooltipContainer(tooltipText),
);
expect(tooltipContainer.padding, const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0));
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.macOS,
TargetPlatform.linux,
TargetPlatform.windows,
}),
);
testWidgets(
'Material3 - Tooltip default size, shape, and color test for Desktop',
(WidgetTester tester) async {
// Regressing test for https://github.com/flutter/flutter/issues/68601
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
home: Tooltip(key: tooltipKey, message: tooltipText, child: const SizedBox.shrink()),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final RenderParagraph tooltipRenderParagraph = tester.renderObject<RenderParagraph>(
find.text(tooltipText),
);
expect(tooltipRenderParagraph.textSize.height, equals(17.0));
final RenderBox tooltipRenderBox = tester.renderObject(_findTooltipContainer(tooltipText));
expect(tooltipRenderBox.size.height, equals(25.0));
expect(
tooltipRenderBox,
paints..rrect(
rrect: RRect.fromRectAndRadius(tooltipRenderBox.paintBounds, const Radius.circular(4.0)),
color: const Color(0xe6616161),
),
);
final Container tooltipContainer = tester.firstWidget<Container>(
_findTooltipContainer(tooltipText),
);
expect(tooltipContainer.padding, const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0));
},
variant: const TargetPlatformVariant(<TargetPlatform>{
TargetPlatform.macOS,
TargetPlatform.linux,
TargetPlatform.windows,
}),
);
testWidgets('Material2 - Can tooltip decoration be customized', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
const Decoration customDecoration = ShapeDecoration(
shape: StadiumBorder(),
color: Color(0x80800000),
);
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: tooltipKey,
decoration: customDecoration,
message: tooltipText,
child: const SizedBox.shrink(),
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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(color: const Color(0x80800000)));
});
testWidgets('Material3 - Can tooltip decoration be customized', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
const Decoration customDecoration = ShapeDecoration(
shape: StadiumBorder(),
color: Color(0x80800000),
);
late final OverlayEntry entry;
addTearDown(
() => entry
..remove()
..dispose(),
);
await tester.pumpWidget(
MaterialApp(
home: Overlay(
initialEntries: <OverlayEntry>[
entry = OverlayEntry(
builder: (BuildContext context) {
return Tooltip(
key: tooltipKey,
decoration: customDecoration,
message: tooltipText,
child: const SizedBox.shrink(),
);
},
),
],
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
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.75));
expect(tip, paints..rrect(color: const Color(0x80800000)));
});
testWidgets('Material2 - Tooltip text scales with textScaler', (WidgetTester tester) async {
Widget buildApp(String text, {required TextScaler textScaler}) {
return MaterialApp(
theme: ThemeData(useMaterial3: false),
home: MediaQuery(
data: MediaQueryData(textScaler: textScaler),
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, textScaler: TextScaler.noScaling));
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, textScaler: const TextScaler.linear(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(64.0));
});
testWidgets('Material3 - Tooltip text scales with textScaleFactor', (WidgetTester tester) async {
Widget buildApp(String text, {required TextScaler textScaler}) {
return MaterialApp(
home: MediaQuery(
data: MediaQueryData(textScaler: textScaler),
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, textScaler: TextScaler.noScaling));
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsOneWidget);
expect(tester.getSize(find.text(tooltipText)).width, equals(42.75));
expect(tester.getSize(find.text(tooltipText)).height, equals(20.0));
RenderBox tip = tester.renderObject(_findTooltipContainer(tooltipText));
expect(tip.size.height, equals(32.0));
await tester.pumpWidget(buildApp(tooltipText, textScaler: const TextScaler.linear(4.0)));
await tester.longPress(find.byType(Tooltip));
expect(find.text(tooltipText), findsOneWidget);
expect(tester.getSize(find.text(tooltipText)).width, equals(168.75));
expect(tester.getSize(find.text(tooltipText)).height, equals(80.0));
tip = tester.renderObject(_findTooltipContainer(tooltipText));
expect(tip.size.height, equals(88.0));
});
testWidgets('Tooltip text displays with richMessage', (WidgetTester tester) async {
final tooltipKey = GlobalKey<TooltipState>();
const textSpan1Text = 'I am a rich tooltip message. ';
const textSpan2Text = 'I am another span of a rich tooltip message';
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
key: tooltipKey,
richMessage: const TextSpan(
text: textSpan1Text,
children: <InlineSpan>[TextSpan(text: textSpan2Text)],
),
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
final RichText richText = tester.widget<RichText>(find.byType(RichText));
expect(richText.text.toPlainText(), equals('$textSpan1Text$textSpan2Text'));
});
testWidgets('Tooltip throws assertion error when both message and richMessage are specified', (
WidgetTester tester,
) async {
expect(() {
MaterialApp(
home: Tooltip(
message: 'I am a tooltip message.',
richMessage: const TextSpan(
text: 'I am a rich tooltip.',
children: <InlineSpan>[TextSpan(text: 'I am another span of a rich tooltip.')],
),
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
);
}, throwsA(const TypeMatcher<AssertionError>()));
});
testWidgets('default Tooltip debugFillProperties', (WidgetTester tester) async {
final 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('default Tooltip debugFillProperties with richMessage', (WidgetTester tester) async {
final builder = DiagnosticPropertiesBuilder();
const Tooltip(
richMessage: TextSpan(
text: 'This is a ',
children: <InlineSpan>[TextSpan(text: 'richMessage')],
),
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description, <String>['"This is a richMessage"']);
});
testWidgets('Tooltip implements debugFillProperties', (WidgetTester tester) async {
final 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,
triggerMode: TooltipTriggerMode.manual,
enableFeedback: true,
).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',
'triggerMode: TooltipTriggerMode.manual',
'enableFeedback: true',
]);
});
testWidgets('Tooltip should not be shown with empty message (with child)', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Tooltip(message: tooltipText, child: Text(tooltipText)),
),
);
expect(find.text(tooltipText), findsOneWidget);
});
testWidgets('Tooltip should not be shown with empty message (without child)', (
WidgetTester tester,
) async {
await tester.pumpWidget(const MaterialApp(home: Tooltip(message: tooltipText)));
expect(find.text(tooltipText), findsNothing);
if (tooltipText.isEmpty) {
expect(find.byType(SizedBox), findsOneWidget);
}
});
testWidgets('Tooltip should not ignore users tap on richMessage', (WidgetTester tester) async {
var isTapped = false;
final recognizer = TapGestureRecognizer();
addTearDown(recognizer.dispose);
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
richMessage: TextSpan(
text: tooltipText,
recognizer: recognizer
..onTap = () {
isTapped = true;
},
),
showDuration: const Duration(seconds: 5),
triggerMode: TooltipTriggerMode.tap,
child: const Icon(Icons.refresh),
),
),
);
final Finder tooltip = find.byType(Tooltip);
expect(find.text(tooltipText), findsNothing);
await _testGestureTap(tester, tooltip);
final Finder textSpan = find.text(tooltipText);
expect(textSpan, findsOneWidget);
await _testGestureTap(tester, textSpan);
expect(isTapped, isTrue);
});
testWidgets('Tooltip does not rebuild for fade in / fade out animation', (
WidgetTester tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: SizedBox.square(
dimension: 10.0,
child: Tooltip(
message: tooltipText,
waitDuration: Duration(seconds: 1),
triggerMode: TooltipTriggerMode.longPress,
child: SizedBox.expand(),
),
),
),
),
);
final TooltipState tooltipState = tester.state(find.byType(Tooltip));
final element = tooltipState.context as Element;
// The Tooltip widget itself is almost stateless thus doesn't need
// rebuilding.
expect(element.dirty, isFalse);
expect(tooltipState.ensureTooltipVisible(), isTrue);
expect(element.dirty, isFalse);
await tester.pump(const Duration(seconds: 1));
expect(element.dirty, isFalse);
expect(Tooltip.dismissAllToolTips(), isTrue);
expect(element.dirty, isFalse);
await tester.pump(const Duration(seconds: 1));
expect(element.dirty, isFalse);
});
testWidgets('Tooltip is not selectable', (WidgetTester tester) async {
const tooltipText = 'AAAAAAAAAAAAAAAAAAAAAAA';
String? selectedText;
await tester.pumpWidget(
MaterialApp(
home: SelectionArea(
onSelectionChanged: (SelectedContent? content) {
selectedText = content?.plainText;
},
child: const Center(
child: Column(
children: <Widget>[
Text('Select Me'),
Tooltip(
message: tooltipText,
waitDuration: Duration(seconds: 1),
triggerMode: TooltipTriggerMode.longPress,
child: SizedBox.square(dimension: 50),
),
],
),
),
),
),
);
final TooltipState tooltipState = tester.state(find.byType(Tooltip));
final Rect textRect = tester.getRect(find.text('Select Me'));
final TestGesture gesture = await tester.startGesture(
Alignment.centerLeft.alongSize(textRect.size) + textRect.topLeft,
);
// Drag from centerLeft to centerRight to select the text.
await tester.pump(const Duration(seconds: 1));
await gesture.moveTo(Alignment.centerRight.alongSize(textRect.size) + textRect.topLeft);
await tester.pump();
tooltipState.ensureTooltipVisible();
await tester.pump();
// Make sure the tooltip becomes visible.
expect(find.text(tooltipText), findsOneWidget);
assert(selectedText != null);
final Rect tooltipTextRect = tester.getRect(find.text(tooltipText));
// Now drag from centerLeft to centerRight to select the tooltip text.
await gesture.moveTo(
Alignment.centerLeft.alongSize(tooltipTextRect.size) + tooltipTextRect.topLeft,
);
await tester.pump();
await gesture.moveTo(
Alignment.centerRight.alongSize(tooltipTextRect.size) + tooltipTextRect.topLeft,
);
await tester.pump();
expect(selectedText, isNot(contains('A')));
});
testWidgets('Tooltip mouse cursor behavior', (WidgetTester tester) async {
const SystemMouseCursor customCursor = SystemMouseCursors.grab;
await tester.pumpWidget(
const MaterialApp(
home: Center(
child: Tooltip(
message: tooltipText,
mouseCursor: customCursor,
child: SizedBox.square(dimension: 50),
),
),
),
);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
pointer: 1,
);
await gesture.addPointer(location: const Offset(10, 10));
await tester.pump();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
final Offset chip = tester.getCenter(find.byType(Tooltip));
await gesture.moveTo(chip);
await tester.pump();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), customCursor);
});
testWidgets('Tooltip overlay ignores pointer by default when passing simple message', (
WidgetTester tester,
) async {
const tooltipMessage = 'Tooltip message';
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Tooltip(
message: tooltipMessage,
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
),
),
),
),
);
final Finder buttonFinder = find.text('Hover me');
expect(buttonFinder, findsOneWidget);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
final Finder tooltipFinder = find.text(tooltipMessage);
expect(tooltipFinder, findsOneWidget);
final Finder ignorePointerFinder = find.byType(IgnorePointer);
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
expect(ignorePointer.ignoring, isTrue);
await gesture.removePointer();
});
testWidgets(
"Tooltip overlay with simple message doesn't ignore pointer when passing ignorePointer: false",
(WidgetTester tester) async {
const tooltipMessage = 'Tooltip message';
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Tooltip(
ignorePointer: false,
message: tooltipMessage,
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
),
),
),
),
);
final Finder buttonFinder = find.text('Hover me');
expect(buttonFinder, findsOneWidget);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
final Finder tooltipFinder = find.text(tooltipMessage);
expect(tooltipFinder, findsOneWidget);
final Finder ignorePointerFinder = find.byType(IgnorePointer);
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
expect(ignorePointer.ignoring, isFalse);
await gesture.removePointer();
},
);
testWidgets("Tooltip overlay doesn't ignore pointer by default when passing rich message", (
WidgetTester tester,
) async {
const InlineSpan richMessage = TextSpan(
children: <InlineSpan>[
TextSpan(
text: 'Rich ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: 'Tooltip'),
],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Tooltip(
richMessage: richMessage,
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
),
),
),
),
);
final Finder buttonFinder = find.text('Hover me');
expect(buttonFinder, findsOneWidget);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
final Finder tooltipFinder = find.textContaining('Rich Tooltip');
expect(tooltipFinder, findsOneWidget);
final Finder ignorePointerFinder = find.byType(IgnorePointer);
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
expect(ignorePointer.ignoring, isFalse);
await gesture.removePointer();
});
testWidgets('Tooltip overlay with richMessage ignores pointer when passing ignorePointer: true', (
WidgetTester tester,
) async {
const InlineSpan richMessage = TextSpan(
children: <InlineSpan>[
TextSpan(
text: 'Rich ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: 'Tooltip'),
],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Tooltip(
ignorePointer: true,
richMessage: richMessage,
child: ElevatedButton(onPressed: () {}, child: const Text('Hover me')),
),
),
),
),
);
final Finder buttonFinder = find.text('Hover me');
expect(buttonFinder, findsOneWidget);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(buttonFinder));
await tester.pumpAndSettle();
final Finder tooltipFinder = find.textContaining('Rich Tooltip');
expect(tooltipFinder, findsOneWidget);
final Finder ignorePointerFinder = find.byType(IgnorePointer);
final IgnorePointer ignorePointer = tester.widget<IgnorePointer>(ignorePointerFinder.last);
expect(ignorePointer.ignoring, isTrue);
await gesture.removePointer();
});
testWidgets('Tooltip should pass its default text style down to widget spans', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
key: tooltipKey,
richMessage: const WidgetSpan(child: Text(tooltipText)),
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
await tester.pump(const Duration(seconds: 2));
final Finder defaultTextStyle = find.ancestor(
of: find.text(tooltipText),
matching: find.byType(DefaultTextStyle),
);
final DefaultTextStyle textStyle = tester.widget<DefaultTextStyle>(defaultTextStyle.first);
expect(textStyle.style.color, Colors.white);
expect(textStyle.style.fontFamily, 'Roboto');
expect(textStyle.style.decoration, TextDecoration.none);
expect(
textStyle.style.debugLabel,
'((englishLike bodyMedium 2021).merge((blackMountainView bodyMedium).apply)).copyWith',
);
});
testWidgets('Tooltip should apply provided text style to rich messages', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
const expectedTextStyle = TextStyle(color: Colors.orange);
await tester.pumpWidget(
MaterialApp(
home: Tooltip(
key: tooltipKey,
richMessage: const TextSpan(text: tooltipText),
textStyle: expectedTextStyle,
child: Container(width: 100.0, height: 100.0, color: Colors.green[500]),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
await tester.pump(const Duration(seconds: 2));
final TextStyle textStyle = tester.widget<Text>(find.text(tooltipText)).style!;
final Finder defaultTextStyleFinder = find.ancestor(
of: find.text(tooltipText),
matching: find.byType(DefaultTextStyle),
);
final TextStyle defaultTextStyle = tester
.widget<DefaultTextStyle>(defaultTextStyleFinder.first)
.style;
expect(textStyle, same(expectedTextStyle));
expect(defaultTextStyle, same(expectedTextStyle));
});
testWidgets('Tooltip respects and prefers the given constraints over theme constraints', (
WidgetTester tester,
) async {
final tooltipKey = GlobalKey<TooltipState>();
const themeConstraints = BoxConstraints.tightFor(width: 300, height: 150);
const tooltipConstraints = BoxConstraints.tightFor(width: 500, height: 250);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(tooltipTheme: const TooltipThemeData(constraints: themeConstraints)),
home: Tooltip(
key: tooltipKey,
message: tooltipText,
constraints: tooltipConstraints,
padding: EdgeInsets.zero,
child: const ColoredBox(color: Colors.green),
),
),
);
tooltipKey.currentState?.ensureTooltipVisible();
await tester.pump(const Duration(seconds: 2));
final Finder textAncestors = find.ancestor(
of: find.text(tooltipText),
matching: find.byWidgetPredicate((_) => true),
);
expect(tester.element(textAncestors.first).size, equals(tooltipConstraints.biggest));
});
// This is a regression test for https://github.com/flutter/flutter/issues/167359.
testWidgets('Tooltip does not show while transitioning from another page', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: const Center(
child: Tooltip(message: 'Hello', child: Text('World')),
),
),
body: Builder(
builder: (BuildContext context) {
return TextButton(
onPressed: () => Navigator.push(
context,
CupertinoPageRoute<void>(
builder: (BuildContext context) =>
Scaffold(appBar: AppBar(title: const Text('Second Page'))),
),
),
child: const Text('Go to Second Page'),
);
},
),
),
),
);
await tester.tap(find.text('Go to Second Page'));
await tester.pumpAndSettle();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await tester.tap(find.byType(BackButton));
await tester.pump(const Duration(milliseconds: 250));
await gesture.moveTo(tester.getCenter(find.text('World')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
// This is a regression test for https://github.com/flutter/flutter/issues/169741.
testWidgets(
'Tooltip does not show while transitioning from another route with secondary animation',
(WidgetTester tester) async {
final observer = TransitionDurationObserver();
await tester.pumpWidget(
MaterialApp(
navigatorObservers: <NavigatorObserver>[observer],
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return TextButton(
onPressed: () => Navigator.push(
context,
CupertinoPageRoute<void>(
builder: (BuildContext context) => Scaffold(
appBar: AppBar(
leading: const Tooltip(message: 'Hello', child: Text('World')),
),
body: TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(appBar: AppBar(title: const Text('Third Page')));
},
),
);
},
child: const Text('Go to Third Page'),
),
),
),
),
child: const Text('Go to Second Page'),
);
},
),
),
),
);
expect(find.text('Go to Second Page'), findsOneWidget);
await tester.tap(find.text('Go to Second Page'));
await tester.pumpAndSettle();
expect(find.text('Go to Third Page'), findsOneWidget);
await tester.tap(find.text('Go to Third Page'));
await tester.pumpAndSettle();
expect(find.text('Third Page'), findsOneWidget);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await tester.tap(find.byType(BackButton));
await observer.pumpPastTransition(tester);
await gesture.moveTo(tester.getCenter(find.text('World')));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
},
);
/// This is a regression test for https://github.com/flutter/flutter/issues/168545
testWidgets('The Tooltip on the ModalBottomSheet can still be displayed after showMenu.', (
WidgetTester tester,
) async {
final navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigatorKey,
home: const Scaffold(body: Placeholder()),
),
);
showModalBottomSheet<void>(
context: navigatorKey.currentContext!,
builder: (_) {
return const Center(
child: Tooltip(message: 'Hello', child: Text('World')),
);
},
);
await tester.pumpAndSettle();
showMenu<void>(
context: navigatorKey.currentContext!,
items: <PopupMenuEntry<int>>[const PopupMenuItem<int>(value: 0, child: Text('item 1'))],
position: RelativeRect.fill,
);
await tester.pumpAndSettle();
navigatorKey.currentState!.pop();
await tester.pumpAndSettle();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.text('World')));
await tester.pumpAndSettle();
expect(find.text('Hello'), findsOne);
await gesture.removePointer();
});
testWidgets('Custom tooltip positioning - positionDelegate parameter', (
WidgetTester tester,
) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Tooltip(
message: tooltipText,
positionDelegate: (TooltipPositionContext context) {
// Align on top right of box with bottom left of tooltip.
return Offset(
context.target.dx + (context.targetSize.width / 2),
context.target.dy - (context.targetSize.height / 2) - context.tooltipSize.height,
);
},
child: const SizedBox(width: 50, height: 50),
),
),
),
),
);
await tester.longPress(find.byType(Tooltip));
await tester.pump(const Duration(seconds: 1));
expect(find.text(tooltipText), findsOneWidget);
final Offset targetCenter = tester.getCenter(find.byType(Tooltip));
final Offset tooltipPosition = tester.getTopLeft(_findTooltipContainer(tooltipText));
// The tooltip should be positioned at target + (25, -25-32).
expect(tooltipPosition.dx, closeTo(targetCenter.dx + 25, 5.0));
expect(tooltipPosition.dy, closeTo(targetCenter.dy - 25 - 32, 5.0));
});
testWidgets('Tooltip does not crash at zero area', (WidgetTester tester) async {
final key = GlobalKey<TooltipState>();
tester.view.physicalSize = Size.zero;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Tooltip(key: key, message: 'X'),
),
),
);
expect(tester.getSize(find.byType(Tooltip)), Size.zero);
key.currentState!.ensureTooltipVisible();
await tester.pumpAndSettle();
expect(tester.getSize(find.byType(Tooltip)), Size.zero);
expect(find.text('X'), findsOne);
});
}
Future<void> _testGestureTap(WidgetTester tester, Finder tooltip) async {
await tester.tap(tooltip);
await tester.pump(const Duration(milliseconds: 10));
}