blob: e46caf16e54efec09a37df18a30a318e14992890 [file] [log] [blame]
// 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 'dart:ui' show SemanticsFlags, SemanticsAction;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/physics.dart';
import '../rendering/mock_canvas.dart';
import '../rendering/recording_canvas.dart';
import '../widgets/semantics_tester.dart';
class StateMarker extends StatefulWidget {
const StateMarker({ Key key, this.child }) : super(key: key);
final Widget child;
@override
StateMarkerState createState() => new StateMarkerState();
}
class StateMarkerState extends State<StateMarker> {
String marker;
@override
Widget build(BuildContext context) {
if (widget.child != null)
return widget.child;
return new Container();
}
}
Widget buildFrame({
Key tabBarKey,
List<String> tabs,
String value,
bool isScrollable: false,
Color indicatorColor,
}) {
return new Material(
child: new DefaultTabController(
initialIndex: tabs.indexOf(value),
length: tabs.length,
child: new TabBar(
key: tabBarKey,
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
isScrollable: isScrollable,
indicatorColor: indicatorColor,
),
),
);
}
typedef Widget TabControllerFrameBuilder(BuildContext context, TabController controller);
class TabControllerFrame extends StatefulWidget {
const TabControllerFrame({ this.length, this.initialIndex: 0, this.builder });
final int length;
final int initialIndex;
final TabControllerFrameBuilder builder;
@override
TabControllerFrameState createState() => new TabControllerFrameState();
}
class TabControllerFrameState extends State<TabControllerFrame> with SingleTickerProviderStateMixin {
TabController _controller;
@override
void initState() {
super.initState();
_controller = new TabController(
vsync: this,
length: widget.length,
initialIndex: widget.initialIndex,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.builder(context, _controller);
}
}
Widget buildLeftRightApp({ List<String> tabs, String value }) {
return new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new DefaultTabController(
initialIndex: tabs.indexOf(value),
length: tabs.length,
child: new Scaffold(
appBar: new AppBar(
title: const Text('tabs'),
bottom: new TabBar(
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
),
),
body: new TabBarView(
children: <Widget>[
const Center(child: const Text('LEFT CHILD')),
const Center(child: const Text('RIGHT CHILD'))
]
)
)
)
);
}
class TabIndicatorRecordingCanvas extends TestRecordingCanvas {
TabIndicatorRecordingCanvas(this.indicatorColor);
final Color indicatorColor;
Rect indicatorRect;
@override
void drawRect(Rect rect, Paint paint) {
if (paint.color == indicatorColor)
indicatorRect = rect;
}
}
class TestScrollPhysics extends ScrollPhysics {
const TestScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
@override
TestScrollPhysics applyTo(ScrollPhysics ancestor) {
return new TestScrollPhysics(parent: buildParent(ancestor));
}
static final SpringDescription _kDefaultSpring = new SpringDescription.withDampingRatio(
mass: 0.5,
springConstant: 500.0,
ratio: 1.1,
);
@override
SpringDescription get spring => _kDefaultSpring;
}
void main() {
testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
final List<String> tabs = <String>['A', 'B', 'C'];
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
final TabController controller = DefaultTabController.of(tester.element(find.text('A')));
expect(controller, isNotNull);
expect(controller.index, 2);
expect(controller.previousIndex, 2);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
await tester.tap(find.text('B'));
await tester.pump();
expect(controller.indexIsChanging, true);
await tester.pump(const Duration(seconds: 1)); // finish the animation
expect(controller.index, 1);
expect(controller.previousIndex, 2);
expect(controller.indexIsChanging, false);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
await tester.tap(find.text('C'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(controller.index, 2);
expect(controller.previousIndex, 1);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
await tester.tap(find.text('A'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(controller.index, 0);
expect(controller.previousIndex, 2);
});
testWidgets('Scrollable TabBar tap selects tab', (WidgetTester tester) async {
final List<String> tabs = <String>['A', 'B', 'C'];
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true));
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsOneWidget);
expect(find.text('C'), findsOneWidget);
final TabController controller = DefaultTabController.of(tester.element(find.text('A')));
expect(controller.index, 2);
expect(controller.previousIndex, 2);
await tester.tap(find.text('C'));
await tester.pumpAndSettle();
expect(controller.index, 2);
await tester.tap(find.text('B'));
await tester.pumpAndSettle();
expect(controller.index, 1);
await tester.tap(find.text('A'));
await tester.pumpAndSettle();
expect(controller.index, 0);
});
testWidgets('Scrollable TabBar tap centers selected tab', (WidgetTester tester) async {
final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
final Key tabBarKey = const Key('TabBar');
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
expect(controller, isNotNull);
expect(controller.index, 0);
expect(tester.getSize(find.byKey(tabBarKey)).width, equals(800.0));
// The center of the FFFFFF item is to the right of the TabBar's center
expect(tester.getCenter(find.text('FFFFFF')).dx, greaterThan(401.0));
await tester.tap(find.text('FFFFFF'));
await tester.pumpAndSettle();
expect(controller.index, 5);
// The center of the FFFFFF item is now at the TabBar's center
expect(tester.getCenter(find.text('FFFFFF')).dx, closeTo(400.0, 1.0));
});
testWidgets('TabBar can be scrolled independent of the selection', (WidgetTester tester) async {
final List<String> tabs = <String>['AAAA', 'BBBB', 'CCCC', 'DDDD', 'EEEE', 'FFFF', 'GGGG', 'HHHH', 'IIII', 'JJJJ', 'KKKK', 'LLLL'];
final Key tabBarKey = const Key('TabBar');
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAA', isScrollable: true, tabBarKey: tabBarKey));
final TabController controller = DefaultTabController.of(tester.element(find.text('AAAA')));
expect(controller, isNotNull);
expect(controller.index, 0);
// Fling-scroll the TabBar to the left
expect(tester.getCenter(find.text('HHHH')).dx, lessThan(700.0));
await tester.fling(find.byKey(tabBarKey), const Offset(-200.0, 0.0), 10000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(tester.getCenter(find.text('HHHH')).dx, lessThan(500.0));
// Scrolling the TabBar doesn't change the selection
expect(controller.index, 0);
});
testWidgets('TabBarView maintains state', (WidgetTester tester) async {
final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE'];
String value = tabs[0];
Widget builder() {
return new Material(
child: new DefaultTabController(
initialIndex: tabs.indexOf(value),
length: tabs.length,
child: new TabBarView(
children: tabs.map((String name) {
return new StateMarker(
child: new Text(name)
);
}).toList()
),
),
);
}
StateMarkerState findStateMarkerState(String name) {
return tester.state(find.widgetWithText(StateMarker, name));
}
await tester.pumpWidget(builder());
final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
TestGesture gesture = await tester.startGesture(tester.getCenter(find.text(tabs[0])));
await gesture.moveBy(const Offset(-600.0, 0.0));
await tester.pump();
expect(value, equals(tabs[0]));
findStateMarkerState(tabs[1]).marker = 'marked';
await gesture.up();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
value = tabs[controller.index];
expect(value, equals(tabs[1]));
await tester.pumpWidget(builder());
expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
// Move to the third tab.
gesture = await tester.startGesture(tester.getCenter(find.text(tabs[1])));
await gesture.moveBy(const Offset(-600.0, 0.0));
await gesture.up();
await tester.pump();
expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
await tester.pump(const Duration(seconds: 1));
value = tabs[controller.index];
expect(value, equals(tabs[2]));
await tester.pumpWidget(builder());
// The state is now gone.
expect(find.text(tabs[1]), findsNothing);
// Move back to the second tab.
gesture = await tester.startGesture(tester.getCenter(find.text(tabs[2])));
await gesture.moveBy(const Offset(600.0, 0.0));
await tester.pump();
final StateMarkerState markerState = findStateMarkerState(tabs[1]);
expect(markerState.marker, isNull);
markerState.marker = 'marked';
await gesture.up();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
value = tabs[controller.index];
expect(value, equals(tabs[1]));
await tester.pumpWidget(builder());
expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
});
testWidgets('TabBar left/right fling', (WidgetTester tester) async {
final List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller.index, 0);
// Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pumpAndSettle();
expect(controller.index, 1);
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
// Fling to the right, switch back to the 'LEFT' tab
flingStart = tester.getCenter(find.text('RIGHT CHILD'));
await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pumpAndSettle();
expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
});
testWidgets('TabBar left/right fling reverse (1)', (WidgetTester tester) async {
final List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller.index, 0);
final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
});
testWidgets('TabBar left/right fling reverse (2)', (WidgetTester tester) async {
final List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller.index, 0);
final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump();
// this is similar to a test above, but that one does many more pumps
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 1);
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
});
// A regression test for https://github.com/flutter/flutter/issues/5095
testWidgets('TabBar left/right fling reverse (2)', (WidgetTester tester) async {
final List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller.index, 0);
final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
final TestGesture gesture = await tester.startGesture(flingStart);
for (int index = 0; index > 50; index += 1) {
await gesture.moveBy(const Offset(-10.0, 0.0));
await tester.pump(const Duration(milliseconds: 1));
}
// End the fling by reversing direction. This should cause not cause
// a change to the selected tab, everything should just settle back to
// to where it started.
for (int index = 0; index > 50; index += 1) {
await gesture.moveBy(const Offset(10.0, 0.0));
await tester.pump(const Duration(milliseconds: 1));
}
await gesture.up();
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, 0);
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
});
// A regression test for https://github.com/flutter/flutter/issues/7133
testWidgets('TabBar fling velocity', (WidgetTester tester) async {
final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
int index = 0;
await tester.pumpWidget(
new MaterialApp(
home: new Align(
alignment: FractionalOffset.topLeft,
child: new SizedBox(
width: 300.0,
height: 200.0,
child: new DefaultTabController(
length: tabs.length,
child: new Scaffold(
appBar: new AppBar(
title: const Text('tabs'),
bottom: new TabBar(
isScrollable: true,
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
),
),
body: new TabBarView(
children: tabs.map((String name) => new Text('${index++}')).toList(),
),
),
),
),
),
),
);
// After a small slow fling to the left, we expect the second item to still be visible.
await tester.fling(find.text('AAAAAA'), const Offset(-25.0, 0.0), 100.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
final RenderBox box = tester.renderObject(find.text('BBBBBB'));
expect(box.localToGlobal(Offset.zero).dx, greaterThan(0.0));
});
testWidgets('TabController change notification', (WidgetTester tester) async {
final List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
expect(controller, isNotNull);
expect(controller.index, 0);
String value;
controller.addListener(() {
value = tabs[controller.index];
});
await tester.tap(find.text('RIGHT'));
await tester.pumpAndSettle();
expect(value, 'RIGHT');
await tester.tap(find.text('LEFT'));
await tester.pumpAndSettle();
expect(value, 'LEFT');
final Offset leftFlingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(leftFlingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pumpAndSettle();
expect(value, 'RIGHT');
final Offset rightFlingStart = tester.getCenter(find.text('RIGHT CHILD'));
await tester.flingFrom(rightFlingStart, const Offset(200.0, 0.0), 10000.0);
await tester.pumpAndSettle();
expect(value, 'LEFT');
});
testWidgets('Explicit TabController', (WidgetTester tester) async {
final List<String> tabs = <String>['LEFT', 'RIGHT'];
TabController tabController;
Widget buildTabControllerFrame(BuildContext context, TabController controller) {
tabController = controller;
return new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new Scaffold(
appBar: new AppBar(
title: const Text('tabs'),
bottom: new TabBar(
controller: controller,
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
),
),
body: new TabBarView(
controller: controller,
children: <Widget>[
const Center(child: const Text('LEFT CHILD')),
const Center(child: const Text('RIGHT CHILD'))
]
),
),
);
}
await tester.pumpWidget(new TabControllerFrame(
builder: buildTabControllerFrame,
length: tabs.length,
initialIndex: 1,
));
expect(find.text('LEFT'), findsOneWidget);
expect(find.text('RIGHT'), findsOneWidget);
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
expect(tabController.index, 1);
expect(tabController.previousIndex, 1);
expect(tabController.indexIsChanging, false);
expect(tabController.animation.value, 1.0);
expect(tabController.animation.status, AnimationStatus.completed);
tabController.index = 0;
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('LEFT CHILD'), findsOneWidget);
expect(find.text('RIGHT CHILD'), findsNothing);
tabController.index = 1;
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('LEFT CHILD'), findsNothing);
expect(find.text('RIGHT CHILD'), findsOneWidget);
});
testWidgets('TabController listener resets index', (WidgetTester tester) async {
// This is a regression test for the scenario brought up here
// https://github.com/flutter/flutter/pull/7387#pullrequestreview-15630946
final List<String> tabs = <String>['A', 'B', 'C'];
TabController tabController;
Widget buildTabControllerFrame(BuildContext context, TabController controller) {
tabController = controller;
return new MaterialApp(
theme: new ThemeData(platform: TargetPlatform.android),
home: new Scaffold(
appBar: new AppBar(
title: const Text('tabs'),
bottom: new TabBar(
controller: controller,
tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
),
),
body: new TabBarView(
controller: controller,
children: <Widget>[
const Center(child: const Text('CHILD A')),
const Center(child: const Text('CHILD B')),
const Center(child: const Text('CHILD C')),
]
),
),
);
}
await tester.pumpWidget(new TabControllerFrame(
builder: buildTabControllerFrame,
length: tabs.length,
));
tabController.animation.addListener(() {
if (tabController.animation.status == AnimationStatus.forward)
tabController.index = 2;
expect(tabController.indexIsChanging, true);
});
expect(tabController.index, 0);
expect(tabController.indexIsChanging, false);
tabController.animateTo(1, duration: const Duration(milliseconds: 200), curve: Curves.linear);
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
expect(tabController.index, 2);
expect(tabController.indexIsChanging, false);
});
testWidgets('TabBarView child disposed during animation', (WidgetTester tester) async {
// This is a regression test for the scenario brought up here
// https://github.com/flutter/flutter/pull/7387#discussion_r95089191x
final List<String> tabs = <String>['LEFT', 'RIGHT'];
await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
// Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
});
testWidgets('TabBar unselectedLabelColor control test', (WidgetTester tester) async {
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 2,
);
Color firstColor;
Color secondColor;
await tester.pumpWidget(
new Material(
child: new TabBar(
controller: controller,
labelColor: Colors.green[500],
unselectedLabelColor: Colors.blue[500],
tabs: <Widget>[
new Builder(
builder: (BuildContext context) {
firstColor = IconTheme.of(context).color;
return const Text('First');
}
),
new Builder(
builder: (BuildContext context) {
secondColor = IconTheme.of(context).color;
return const Text('Second');
}
),
],
),
),
);
expect(firstColor, equals(Colors.green[500]));
expect(secondColor, equals(Colors.blue[500]));
});
testWidgets('TabBarView page left and right test', (WidgetTester tester) async {
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 2,
);
await tester.pumpWidget(
new Material(
child: new TabBarView(
controller: controller,
children: <Widget>[ const Text('First'), const Text('Second') ],
),
),
);
expect(controller.index, equals(0));
TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
expect(controller.index, equals(0));
// Drag to the left and right, by less than the TabBarView's width.
// The selected index (controller.index) should not change.
await gesture.moveBy(const Offset(-100.0, 0.0));
await gesture.moveBy(const Offset(100.0, 0.0));
expect(controller.index, equals(0));
expect(find.text('First'), findsOneWidget);
expect(find.text('Second'), findsNothing);
// Drag more than the TabBarView's width to the right. This forces
// the selected index to change to 1.
await gesture.moveBy(const Offset(-500.0, 0.0));
await gesture.up();
await tester.pump(); // start the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, equals(1));
expect(find.text('First'), findsNothing);
expect(find.text('Second'), findsOneWidget);
gesture = await tester.startGesture(const Offset(100.0, 100.0));
expect(controller.index, equals(1));
// Drag to the left and right, by less than the TabBarView's width.
// The selected index (controller.index) should not change.
await gesture.moveBy(const Offset(-100.0, 0.0));
await gesture.moveBy(const Offset(100.0, 0.0));
expect(controller.index, equals(1));
expect(find.text('First'), findsNothing);
expect(find.text('Second'), findsOneWidget);
// Drag more than the TabBarView's width to the left. This forces
// the selected index to change back to 0.
await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up();
await tester.pump(); // start the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
expect(controller.index, equals(0));
expect(find.text('First'), findsOneWidget);
expect(find.text('Second'), findsNothing);
});
testWidgets('TabBar tap animates the selection indicator', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/7479
final List<String> tabs = <String>['A', 'B'];
const Color indicatorColor = const Color(0xFFFF0000);
await tester.pumpWidget(buildFrame(tabs: tabs, value: 'A', indicatorColor: indicatorColor));
final RenderBox box = tester.renderObject(find.byType(TabBar));
final TabIndicatorRecordingCanvas canvas = new TabIndicatorRecordingCanvas(indicatorColor);
final TestRecordingPaintingContext context = new TestRecordingPaintingContext(canvas);
box.paint(context, Offset.zero);
final Rect indicatorRect0 = canvas.indicatorRect;
expect(indicatorRect0.left, 0.0);
expect(indicatorRect0.width, 400.0);
expect(indicatorRect0.height, 2.0);
await tester.tap(find.text('B'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
box.paint(context, Offset.zero);
final Rect indicatorRect1 = canvas.indicatorRect;
expect(indicatorRect1.left, greaterThan(indicatorRect0.left));
expect(indicatorRect1.right, lessThan(800.0));
expect(indicatorRect1.height, 2.0);
await tester.pump(const Duration(milliseconds: 300));
box.paint(context, Offset.zero);
final Rect indicatorRect2 = canvas.indicatorRect;
expect(indicatorRect2.left, 400.0);
expect(indicatorRect2.width, 400.0);
expect(indicatorRect2.height, 2.0);
});
testWidgets('TabBarView child disposed during animation', (WidgetTester tester) async {
// This is a regression test for this patch:
// https://github.com/flutter/flutter/pull/9015
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 2,
);
Widget buildFrame() {
return new Material(
child: new TabBar(
key: new UniqueKey(),
controller: controller,
tabs: <Widget>[ const Text('A'), const Text('B') ],
),
);
}
await tester.pumpWidget(buildFrame());
// The original TabBar will be disposed. The controller should no
// longer have any listeners from the original TabBar.
await tester.pumpWidget(buildFrame());
controller.index = 1;
await tester.pump(const Duration(milliseconds: 300));
});
testWidgets('TabBarView scrolls end very VERY close to a new page', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/9375
final TabController tabController = new TabController(
vsync: const TestVSync(),
initialIndex: 1,
length: 3,
);
await tester.pumpWidget(
new SizedBox.expand(
child: new Center(
child: new SizedBox(
width: 400.0,
height: 400.0,
child: new TabBarView(
controller: tabController,
children: <Widget>[
const Center(child: const Text('0')),
const Center(child: const Text('1')),
const Center(child: const Text('2')),
],
),
),
),
),
);
expect(tabController.index, 1);
final PageView pageView = tester.widget(find.byType(PageView));
final PageController pageController = pageView.controller;
final ScrollPosition position = pageController.position;
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
// page 1 is at 400.0, page 2 is at 800.0.
expect(position.pixels, 400.0);
// Not close enough to switch to page 2
pageController.jumpTo(800.0 - 1.25 * position.physics.tolerance.distance);
expect(tabController.index, 1);
// Close enough to switch to page 2
pageController.jumpTo(800.0 - 0.75 * position.physics.tolerance.distance);
expect(tabController.index, 2);
});
testWidgets('TabBarView scrolls end very close to a new page with custom physics', (WidgetTester tester) async {
final TabController tabController = new TabController(
vsync: const TestVSync(),
initialIndex: 1,
length: 3,
);
await tester.pumpWidget(
new SizedBox.expand(
child: new Center(
child: new SizedBox(
width: 400.0,
height: 400.0,
child: new TabBarView(
controller: tabController,
physics: const TestScrollPhysics(),
children: <Widget>[
const Center(child: const Text('0')),
const Center(child: const Text('1')),
const Center(child: const Text('2')),
],
),
),
),
),
);
expect(tabController.index, 1);
final PageView pageView = tester.widget(find.byType(PageView));
final PageController pageController = pageView.controller;
final ScrollPosition position = pageController.position;
// The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
// page 1 is at 400.0, page 2 is at 800.0.
expect(position.pixels, 400.0);
// Not close enough to switch to page 2
pageController.jumpTo(800.0 - 1.25 * position.physics.tolerance.distance);
expect(tabController.index, 1);
// Close enough to switch to page 2
pageController.jumpTo(800.0 - 0.75 * position.physics.tolerance.distance);
expect(tabController.index, 2);
});
testWidgets('Scrollable TabBar with a non-zero TabController initialIndex', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/9374
final List<Tab> tabs = new List<Tab>.generate(20, (int index) {
return new Tab(text: 'TAB #$index');
});
final TabController controller = new TabController(
vsync: const TestVSync(),
length: tabs.length,
initialIndex: tabs.length - 1,
);
await tester.pumpWidget(
new Material(
child: new TabBar(
isScrollable: true,
controller: controller,
tabs: tabs,
),
),
);
// The initialIndex tab should be visible and right justified
expect(find.text('TAB #19'), findsOneWidget);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0);
});
testWidgets('TabBar with indicatorWeight, indicatorPadding', (WidgetTester tester) async {
const Color color = const Color(0xFF00FF00);
const double height = 100.0;
const double weight = 8.0;
const double padLeft = 8.0;
const double padRight = 4.0;
final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
return new Container(
key: new ValueKey<int>(index),
height: height,
);
});
final TabController controller = new TabController(
vsync: const TestVSync(),
length: tabs.length,
);
await tester.pumpWidget(
new Material(
child: new Column(
children: <Widget>[
new TabBar(
indicatorWeight: 8.0,
indicatorColor: color,
indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight),
controller: controller,
tabs: tabs,
),
new Flexible(child: new Container()),
],
),
),
);
final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
// Selected tab dimensions
double tabWidth = tester.getSize(find.byKey(const ValueKey<int>(0))).width;
double tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(0))).dx;
double tabRight = tabLeft + tabWidth;
expect(tabBarBox, paints..rect(
style: PaintingStyle.fill,
color: color,
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight)
));
// Select tab 3
controller.index = 3;
await tester.pumpAndSettle();
tabWidth = tester.getSize(find.byKey(const ValueKey<int>(3))).width;
tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(3))).dx;
tabRight = tabLeft + tabWidth;
expect(tabBarBox, paints..rect(
style: PaintingStyle.fill,
color: color,
rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight)
));
});
testWidgets('correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
final List<Tab> tabs = new List<Tab>.generate(2, (int index) {
return new Tab(text: 'TAB #$index');
});
final TabController controller = new TabController(
vsync: const TestVSync(),
length: tabs.length,
initialIndex: 0,
);
await tester.pumpWidget(
new Material(
child: new Semantics(
container: true,
child: new TabBar(
isScrollable: true,
controller: controller,
tabs: tabs,
),
),
),
);
final TestSemantics expectedSemantics = new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: 1,
rect: TestSemantics.fullScreen,
children: <TestSemantics>[
new TestSemantics(
id: 2,
actions: SemanticsAction.tap.index,
flags: SemanticsFlags.isSelected.index,
label: 'TAB #0\nTab 1 of 2',
rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
transform: new Matrix4.translationValues(0.0, 276.0, 0.0),
),
new TestSemantics(
id: 5,
actions: SemanticsAction.tap.index,
label: 'TAB #1\nTab 2 of 2',
rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
transform: new Matrix4.translationValues(108.0, 276.0, 0.0),
),
]),
],
);
expect(semantics, hasSemantics(expectedSemantics));
semantics.dispose();
});
testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 0,
);
await tester.pumpWidget(
new Material(
child: new Column(
children: <Widget>[
new TabBar(
controller: controller,
tabs: const <Widget>[],
),
new Flexible(
child: new TabBarView(
controller: controller,
children: const <Widget>[],
),
),
],
),
),
);
expect(controller.index, 0);
expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0));
expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0));
// A fling in the TabBar or TabBarView, shouldn't do anything.
await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0));
await(tester.pumpAndSettle());
await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0));
await(tester.pumpAndSettle());
expect(controller.index, 0);
});
testWidgets('TabBar etc with one tab', (WidgetTester tester) async {
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 1,
);
await tester.pumpWidget(
new Material(
child: new Column(
children: <Widget>[
new TabBar(
controller: controller,
tabs: const <Widget>[const Tab(text: 'TAB')],
),
new Flexible(
child: new TabBarView(
controller: controller,
children: const <Widget>[const Text('PAGE')],
),
),
],
),
),
);
expect(controller.index, 0);
expect(find.text('TAB'), findsOneWidget);
expect(find.text('PAGE'), findsOneWidget);
expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0));
expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0));
// The one tab spans the app's width
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
// A fling in the TabBar or TabBarView, shouldn't move the tab.
await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0));
await(tester.pump(const Duration(milliseconds: 50)));
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
await(tester.pumpAndSettle());
await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0));
await(tester.pump(const Duration(milliseconds: 50)));
expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
await(tester.pumpAndSettle());
expect(controller.index, 0);
expect(find.text('TAB'), findsOneWidget);
expect(find.text('PAGE'), findsOneWidget);
});
testWidgets('can tap on indicator at very bottom of TabBar to switch tabs', (WidgetTester tester) async {
final TabController controller = new TabController(
vsync: const TestVSync(),
length: 2,
initialIndex: 0,
);
await tester.pumpWidget(
new Material(
child: new Column(
children: <Widget>[
new TabBar(
controller: controller,
indicatorWeight: 30.0,
tabs: const <Widget>[const Tab(text: 'TAB1'), const Tab(text: 'TAB2')],
),
new Flexible(
child: new TabBarView(
controller: controller,
children: const <Widget>[const Text('PAGE1'), const Text('PAGE2')],
),
),
],
),
),
);
expect(controller.index, 0);
final Offset bottomRight = tester.getBottomRight(find.byType(TabBar)) - const Offset(1.0, 1.0);
final TestGesture gesture = await tester.startGesture(bottomRight);
await gesture.up();
await tester.pumpAndSettle();
expect(controller.index, 1);
});
}