| // 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' as ui show ParagraphBuilder; |
| |
| 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'; |
| |
| import '../rendering/mock_canvas.dart'; |
| import '../rendering/recording_canvas.dart'; |
| import '../widgets/semantics_tester.dart'; |
| import 'feedback_tester.dart'; |
| |
| Widget boilerplate({ Widget? child, TextDirection textDirection = TextDirection.ltr, bool? useMaterial3, TabBarTheme? tabBarTheme }) { |
| return Theme( |
| data: ThemeData(useMaterial3: useMaterial3, tabBarTheme: tabBarTheme), |
| child: Localizations( |
| locale: const Locale('en', 'US'), |
| delegates: const <LocalizationsDelegate<dynamic>>[ |
| DefaultMaterialLocalizations.delegate, |
| DefaultWidgetsLocalizations.delegate, |
| ], |
| child: Directionality( |
| textDirection: textDirection, |
| child: Material( |
| child: child, |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| class StateMarker extends StatefulWidget { |
| const StateMarker({ super.key, this.child }); |
| |
| final Widget? child; |
| |
| @override |
| StateMarkerState createState() => StateMarkerState(); |
| } |
| |
| class StateMarkerState extends State<StateMarker> { |
| String? marker; |
| |
| @override |
| Widget build(BuildContext context) { |
| if (widget.child != null) { |
| return widget.child!; |
| } |
| return Container(); |
| } |
| } |
| |
| class AlwaysKeepAliveWidget extends StatefulWidget { |
| const AlwaysKeepAliveWidget({ super.key}); |
| static String text = 'AlwaysKeepAlive'; |
| @override |
| AlwaysKeepAliveState createState() => AlwaysKeepAliveState(); |
| } |
| |
| class AlwaysKeepAliveState extends State<AlwaysKeepAliveWidget> |
| with AutomaticKeepAliveClientMixin { |
| @override |
| bool get wantKeepAlive => true; |
| |
| @override |
| Widget build(BuildContext context) { |
| super.build(context); |
| return Text(AlwaysKeepAliveWidget.text); |
| } |
| } |
| |
| class _NestedTabBarContainer extends StatelessWidget { |
| const _NestedTabBarContainer({ |
| this.tabController, |
| }); |
| |
| final TabController? tabController; |
| |
| @override |
| Widget build(BuildContext context) { |
| return ColoredBox( |
| color: Colors.blue, |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| controller: tabController, |
| tabs: const <Tab>[ |
| Tab(text: 'Yellow'), |
| Tab(text: 'Grey'), |
| ], |
| ), |
| Expanded( |
| child: TabBarView( |
| controller: tabController, |
| children: <Widget>[ |
| Container(color: Colors.yellow), |
| Container(color: Colors.grey), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| Widget buildFrame({ |
| Key? tabBarKey, |
| bool secondaryTabBar = false, |
| required List<String> tabs, |
| required String value, |
| bool isScrollable = false, |
| Color? indicatorColor, |
| Duration? animationDuration, |
| EdgeInsetsGeometry? padding, |
| TextDirection textDirection = TextDirection.ltr, |
| TabAlignment? tabAlignment, |
| TabBarTheme? tabBarTheme, |
| bool? useMaterial3, |
| }) { |
| if (secondaryTabBar) { |
| return boilerplate( |
| useMaterial3: useMaterial3, |
| tabBarTheme: tabBarTheme, |
| textDirection: textDirection, |
| child: DefaultTabController( |
| animationDuration: animationDuration, |
| initialIndex: tabs.indexOf(value), |
| length: tabs.length, |
| child: TabBar.secondary( |
| key: tabBarKey, |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| isScrollable: isScrollable, |
| indicatorColor: indicatorColor, |
| padding: padding, |
| tabAlignment: tabAlignment, |
| ), |
| ), |
| ); |
| } |
| |
| return boilerplate( |
| useMaterial3: useMaterial3, |
| tabBarTheme: tabBarTheme, |
| textDirection: textDirection, |
| child: DefaultTabController( |
| animationDuration: animationDuration, |
| initialIndex: tabs.indexOf(value), |
| length: tabs.length, |
| child: TabBar( |
| key: tabBarKey, |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| isScrollable: isScrollable, |
| indicatorColor: indicatorColor, |
| padding: padding, |
| tabAlignment: tabAlignment, |
| ), |
| ), |
| ); |
| } |
| |
| typedef TabControllerFrameBuilder = Widget Function(BuildContext context, TabController controller); |
| |
| class TabControllerFrame extends StatefulWidget { |
| const TabControllerFrame({ |
| super.key, |
| required this.length, |
| this.initialIndex = 0, |
| required this.builder, |
| }); |
| |
| final int length; |
| final int initialIndex; |
| final TabControllerFrameBuilder builder; |
| |
| @override |
| TabControllerFrameState createState() => TabControllerFrameState(); |
| } |
| |
| class TabControllerFrameState extends State<TabControllerFrame> with SingleTickerProviderStateMixin { |
| late TabController _controller; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _controller = 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({required List<String> tabs, required String value, bool automaticIndicatorColorAdjustment = true, ThemeData? themeData}) { |
| return MaterialApp( |
| theme: themeData ?? ThemeData(platform: TargetPlatform.android), |
| home: DefaultTabController( |
| initialIndex: tabs.indexOf(value), |
| length: tabs.length, |
| child: Scaffold( |
| appBar: AppBar( |
| title: const Text('tabs'), |
| bottom: TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| automaticIndicatorColorAdjustment: automaticIndicatorColorAdjustment, |
| ), |
| ), |
| body: const TabBarView( |
| children: <Widget>[ |
| Center(child: Text('LEFT CHILD')), |
| Center(child: Text('RIGHT CHILD')), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| class TabIndicatorRecordingCanvas extends TestRecordingCanvas { |
| TabIndicatorRecordingCanvas(this.indicatorColor); |
| |
| final Color indicatorColor; |
| late Rect indicatorRect; |
| |
| @override |
| void drawLine(Offset p1, Offset p2, Paint paint) { |
| // Assuming that the indicatorWeight is 2.0, the default. |
| const double indicatorWeight = 2.0; |
| if (paint.color == indicatorColor) { |
| indicatorRect = Rect.fromPoints(p1, p2).inflate(indicatorWeight / 2.0); |
| } |
| } |
| } |
| |
| class TestScrollPhysics extends ScrollPhysics { |
| const TestScrollPhysics({ super.parent }); |
| |
| @override |
| TestScrollPhysics applyTo(ScrollPhysics? ancestor) { |
| return TestScrollPhysics(parent: buildParent(ancestor)); |
| } |
| |
| @override |
| double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { |
| return offset == 10 ? 20 : offset; |
| } |
| |
| static final SpringDescription _kDefaultSpring = SpringDescription.withDampingRatio( |
| mass: 0.5, |
| stiffness: 500.0, |
| ratio: 1.1, |
| ); |
| |
| @override |
| SpringDescription get spring => _kDefaultSpring; |
| } |
| |
| RenderParagraph _getText(WidgetTester tester, String text) { |
| return tester.renderObject<RenderParagraph>(find.text(text)); |
| } |
| |
| void main() { |
| setUp(() { |
| debugResetSemanticsIdCounter(); |
| }); |
| |
| testWidgets('indicatorPadding update test', (WidgetTester tester) async { |
| // Regressing test for https://github.com/flutter/flutter/issues/108102 |
| const Tab tab = Tab(text: 'A'); |
| const EdgeInsets indicatorPadding = EdgeInsets.only(left: 7.0, right: 7.0); |
| |
| await tester.pumpWidget(boilerplate( |
| child: const DefaultTabController( |
| length: 1, |
| child: TabBar( |
| tabs: <Tab>[tab], |
| indicatorPadding: indicatorPadding, |
| ), |
| ), |
| )); |
| |
| // Change the indicatorPadding |
| await tester.pumpWidget(boilerplate( |
| child: DefaultTabController( |
| length: 1, |
| child: TabBar( |
| tabs: const <Tab>[tab], |
| indicatorPadding: indicatorPadding + const EdgeInsets.all(7.0), |
| ), |
| ), |
| ), Duration.zero, EnginePhase.build); |
| |
| expect(tester.renderObject(find.byType(CustomPaint)).debugNeedsPaint, true); |
| }); |
| |
| testWidgets('Tab sizing - icon', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp(home: Center(child: Material(child: Tab(icon: SizedBox(width: 10.0, height: 10.0))))), |
| ); |
| expect(tester.getSize(find.byType(Tab)), const Size(10.0, 46.0)); |
| }); |
| |
| testWidgets('Tab sizing - child', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const MaterialApp(home: Center(child: Material(child: Tab(child: SizedBox(width: 10.0, height: 10.0))))), |
| ); |
| expect(tester.getSize(find.byType(Tab)), const Size(10.0, 46.0)); |
| }); |
| |
| testWidgets('Tab sizing - text', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); |
| final bool material3 = theme.useMaterial3; |
| await tester.pumpWidget( |
| MaterialApp(theme: theme, home: const Center(child: Material(child: Tab(text: 'x')))), |
| ); |
| expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).text.style!.fontFamily, 'FlutterTest'); |
| expect(tester.getSize(find.byType(Tab)), material3 ? const Size(15.0, 46.0) : const Size(14.0, 46.0)); |
| }); |
| |
| testWidgets('Tab sizing - icon and text', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); |
| final bool material3 = theme.useMaterial3; |
| await tester.pumpWidget( |
| MaterialApp(theme: theme, home: const Center(child: Material(child: Tab(icon: SizedBox(width: 10.0, height: 10.0), text: 'x')))), |
| ); |
| expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).text.style!.fontFamily, 'FlutterTest'); |
| expect(tester.getSize(find.byType(Tab)), material3 ? const Size(15.0, 72.0) : const Size(14.0, 72.0)); |
| }); |
| |
| testWidgets('Tab sizing - icon, iconMargin and text', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| MaterialApp( |
| theme: ThemeData(fontFamily: 'FlutterTest'), |
| home: const Center( |
| child: Material( |
| child: Tab( |
| icon: SizedBox( |
| width: 10.0, |
| height: 10.0, |
| ), |
| iconMargin: EdgeInsets.symmetric( |
| horizontal: 100.0, |
| ), |
| text: 'x', |
| ), |
| ), |
| ), |
| ), |
| ); |
| expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).text.style!.fontFamily, 'FlutterTest'); |
| expect(tester.getSize(find.byType(Tab)), const Size(210.0, 72.0)); |
| }); |
| |
| testWidgets('Tab sizing - icon and child', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); |
| final bool material3 = theme.useMaterial3; |
| await tester.pumpWidget( |
| MaterialApp(theme: theme, home: const Center(child: Material(child: Tab(icon: SizedBox(width: 10.0, height: 10.0), child: Text('x'))))), |
| ); |
| expect(tester.renderObject<RenderParagraph>(find.byType(RichText)).text.style!.fontFamily, 'FlutterTest'); |
| expect(tester.getSize(find.byType(Tab)), material3 ? const Size(15.0, 72.0) : const Size(14.0, 72.0)); |
| }); |
| |
| testWidgets('Tab color - normal', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(fontFamily: 'FlutterTest'); |
| final bool material3 = theme.useMaterial3; |
| final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: TabController(length: 1, vsync: tester)); |
| await tester.pumpWidget( |
| MaterialApp(theme: theme, home: Material(child: tabBar)), |
| ); |
| expect(find.byType(TabBar), paints..line(color: material3 ? theme.colorScheme.surfaceVariant : Colors.blue[500])); |
| }); |
| |
| testWidgets('Tab color - match', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(); |
| final bool material3 = theme.useMaterial3; |
| final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: TabController(length: 1, vsync: tester)); |
| await tester.pumpWidget( |
| MaterialApp(theme: theme, home: Material(color: const Color(0xff2196f3), child: tabBar)), |
| ); |
| expect(find.byType(TabBar), paints..line(color: material3 ? theme.colorScheme.surfaceVariant : Colors.white)); |
| }); |
| |
| testWidgets('Tab color - transparency', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(); |
| final bool material3 = theme.useMaterial3; |
| final Widget tabBar = TabBar(tabs: const <Widget>[SizedBox.shrink()], controller: TabController(length: 1, vsync: tester)); |
| await tester.pumpWidget( |
| MaterialApp(theme: theme, home: Material(type: MaterialType.transparency, child: tabBar)), |
| ); |
| expect(find.byType(TabBar), paints..line(color: material3 ? theme.colorScheme.surfaceVariant : Colors.blue[500])); |
| }); |
| |
| testWidgets('TabBar default selected/unselected label style (primary)', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| const String selectedValue = 'A'; |
| const String unselectedValue = 'C'; |
| await tester.pumpWidget( |
| buildFrame(tabs: tabs, value: selectedValue, useMaterial3: theme.useMaterial3), |
| ); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsOneWidget); |
| expect(find.text('C'), findsOneWidget); |
| |
| // Test selected label text style. |
| final RenderParagraph selectedLabel = _getText(tester, selectedValue); |
| expect(selectedLabel.text.style!.fontFamily, 'Roboto'); |
| expect(selectedLabel.text.style!.fontSize, 14.0); |
| expect(selectedLabel.text.style!.color, theme.colorScheme.primary); |
| |
| // Test unselected label text style. |
| final RenderParagraph unselectedLabel = _getText(tester, unselectedValue); |
| expect(unselectedLabel.text.style!.fontFamily, 'Roboto'); |
| expect(unselectedLabel.text.style!.fontSize, 14.0); |
| expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant); |
| }); |
| |
| testWidgets('TabBar default selected/unselected label style (secondary)', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| const String selectedValue = 'A'; |
| const String unselectedValue = 'C'; |
| await tester.pumpWidget( |
| buildFrame(tabs: tabs, value: selectedValue, secondaryTabBar: true, useMaterial3: theme.useMaterial3), |
| ); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsOneWidget); |
| expect(find.text('C'), findsOneWidget); |
| |
| // Test selected label text style. |
| final RenderParagraph selectedLabel = _getText(tester, selectedValue); |
| expect(selectedLabel.text.style!.fontFamily, 'Roboto'); |
| expect(selectedLabel.text.style!.fontSize, 14.0); |
| expect(selectedLabel.text.style!.color, theme.colorScheme.onSurface); |
| |
| // Test unselected label text style. |
| final RenderParagraph unselectedLabel = _getText(tester, unselectedValue); |
| expect(unselectedLabel.text.style!.fontFamily, 'Roboto'); |
| expect(unselectedLabel.text.style!.fontSize, 14.0); |
| expect(unselectedLabel.text.style!.color, theme.colorScheme.onSurfaceVariant); |
| }); |
| |
| testWidgets('TabBar default tab indicator (primary)', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| final List<Widget> tabs = List<Widget>.generate(4, (int index) { |
| return Tab(text: 'Tab $index'); |
| }); |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: boilerplate( |
| useMaterial3: theme.useMaterial3, |
| child: Container( |
| alignment: Alignment.topLeft, |
| child: TabBar( |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); |
| expect(tabBarBox.size.height, 48.0); |
| |
| const double indicatorWeight = 3.0; |
| |
| |
| final RRect rrect = ui.ParagraphBuilder.shouldDisableRoundingHack |
| ? RRect.fromLTRBAndCorners( |
| 64.75, |
| tabBarBox.size.height - indicatorWeight, |
| 135.25, |
| tabBarBox.size.height, |
| topLeft: const Radius.circular(3.0), |
| topRight: const Radius.circular(3.0), |
| ) |
| : RRect.fromLTRBAndCorners( |
| 64.5, |
| tabBarBox.size.height - indicatorWeight, |
| 135.5, |
| tabBarBox.size.height, |
| topLeft: const Radius.circular(3.0), |
| topRight: const Radius.circular(3.0), |
| ); |
| |
| expect( |
| tabBarBox, |
| paints |
| ..rrect( |
| color: theme.colorScheme.primary, |
| rrect: rrect, |
| )); |
| }); |
| |
| testWidgets('TabBar default tab indicator (secondary)', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| final List<Widget> tabs = List<Widget>.generate(4, (int index) { |
| return Tab(text: 'Tab $index'); |
| }); |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| ); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: boilerplate( |
| useMaterial3: theme.useMaterial3, |
| child: Container( |
| alignment: Alignment.topLeft, |
| child: TabBar.secondary( |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); |
| expect(tabBarBox.size.height, 48.0); |
| |
| const double indicatorWeight = 2.0; |
| const double indicatorY = 48 - (indicatorWeight / 2.0); |
| const double indicatorLeft = indicatorWeight / 2.0; |
| const double indicatorRight = 200.0 - (indicatorWeight / 2.0); |
| |
| expect( |
| tabBarBox, |
| paints |
| // Divider |
| ..line( |
| color: theme.colorScheme.surfaceVariant, |
| ) |
| // Tab indicator |
| ..line( |
| color: theme.colorScheme.primary, |
| strokeWidth: indicatorWeight, |
| p1: const Offset(indicatorLeft, indicatorY), |
| p2: const Offset(indicatorRight, indicatorY), |
| ), |
| ); |
| }); |
| |
| testWidgets('TabBar default overlay (primary)', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| final List<String> tabs = <String>['A', 'B']; |
| |
| const String selectedValue = 'A'; |
| const String unselectedValue = 'B'; |
| await tester.pumpWidget( |
| buildFrame(tabs: tabs, value: selectedValue, useMaterial3: theme.useMaterial3), |
| ); |
| |
| RenderObject overlayColor() { |
| return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); |
| } |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(tester.getCenter(find.text(selectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect(color: theme.colorScheme.primary.withOpacity(0.08))); |
| |
| await gesture.down(tester.getCenter(find.text(selectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect()..rect(color: theme.colorScheme.primary.withOpacity(0.12))); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| await gesture.moveTo(tester.getCenter(find.text(unselectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08))); |
| |
| await gesture.down(tester.getCenter(find.text(selectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect()..rect(color: theme.colorScheme.primary.withOpacity(0.12))); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| }); |
| |
| testWidgets('TabBar default overlay (secondary)', (WidgetTester tester) async { |
| final ThemeData theme = ThemeData(useMaterial3: true); |
| final List<String> tabs = <String>['A', 'B']; |
| |
| const String selectedValue = 'A'; |
| const String unselectedValue = 'B'; |
| await tester.pumpWidget( |
| buildFrame(tabs: tabs, value: selectedValue, secondaryTabBar: true, useMaterial3: theme.useMaterial3), |
| ); |
| |
| RenderObject overlayColor() { |
| return tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures'); |
| } |
| |
| final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
| await gesture.addPointer(); |
| await gesture.moveTo(tester.getCenter(find.text(selectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08))); |
| |
| await gesture.down(tester.getCenter(find.text(selectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect()..rect(color: theme.colorScheme.onSurface.withOpacity(0.12))); |
| await gesture.up(); |
| await tester.pumpAndSettle(); |
| |
| await gesture.moveTo(tester.getCenter(find.text(unselectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect(color: theme.colorScheme.onSurface.withOpacity(0.08))); |
| |
| await gesture.down(tester.getCenter(find.text(selectedValue))); |
| await tester.pumpAndSettle(); |
| expect(overlayColor(), paints..rect()..rect(color: theme.colorScheme.onSurface.withOpacity(0.12))); |
| }); |
| |
| testWidgets('TabBar tap selects tab', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C')); |
| 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')); |
| 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')); |
| 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')); |
| 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('Material2 - 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']; |
| const Key tabBarKey = Key('TabBar'); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| useMaterial3: false, |
| )); |
| 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, moreOrLessEquals(400.0, epsilon: 1.0)); |
| }); |
| |
| testWidgets('Material3 - 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']; |
| const Key tabBarKey = Key('TabBar'); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| useMaterial3: true, |
| )); |
| 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, moreOrLessEquals(452.0, epsilon: 1.0)); |
| }); |
| |
| testWidgets('Material2 - Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/112776 |
| final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; |
| const Key tabBarKey = Key('TabBar'); |
| const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| padding: padding, |
| useMaterial3: false, |
| )); |
| 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, moreOrLessEquals(400.0, epsilon: 1.0)); |
| }); |
| |
| testWidgets('Material3 - Scrollable TabBar, with padding, tap centers selected tab', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/112776 |
| final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; |
| const Key tabBarKey = Key('TabBar'); |
| const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| padding: padding, |
| useMaterial3: true, |
| )); |
| 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, moreOrLessEquals(452.0, epsilon: 1.0)); |
| }); |
| |
| testWidgets('Material2 - Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/112776 |
| final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; |
| const Key tabBarKey = Key('TabBar'); |
| const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| padding: padding, |
| textDirection: TextDirection.rtl, |
| useMaterial3: false, |
| )); |
| 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 left of the TabBar's center |
| expect(tester.getCenter(find.text('FFFFFF')).dx, lessThan(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, moreOrLessEquals(400.0, epsilon: 1.0)); |
| }); |
| |
| testWidgets('Material3 - Scrollable TabBar, with padding and TextDirection.rtl, tap centers selected tab', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/112776 |
| final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL']; |
| const Key tabBarKey = Key('TabBar'); |
| const EdgeInsetsGeometry padding = EdgeInsets.only(right: 30, left: 60); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| padding: padding, |
| textDirection: TextDirection.rtl, |
| useMaterial3: true, |
| )); |
| 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 left of the TabBar's center |
| expect(tester.getCenter(find.text('FFFFFF')).dx, lessThan(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, moreOrLessEquals(348.0, epsilon: 1.0)); |
| }); |
| |
| testWidgets('Material2 - 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']; |
| const Key tabBarKey = Key('TabBar'); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| useMaterial3: false, |
| )); |
| 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('Material3 - 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']; |
| const Key tabBarKey = Key('TabBar'); |
| await tester.pumpWidget(buildFrame( |
| tabs: tabs, |
| value: 'AAAA', |
| isScrollable: true, |
| tabBarKey: tabBarKey, |
| useMaterial3: true, |
| )); |
| 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(720.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 boilerplate( |
| child: DefaultTabController( |
| initialIndex: tabs.indexOf(value), |
| length: tabs.length, |
| child: TabBarView( |
| children: tabs.map<Widget>((String name) { |
| return StateMarker( |
| child: Text(name), |
| ); |
| }).toList(), |
| ), |
| ), |
| ); |
| } |
| |
| StateMarkerState findStateMarkerState(String name) { |
| return tester.state(find.widgetWithText(StateMarker, name, skipOffstage: false)); |
| } |
| |
| 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 |
| // 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/pull/88878. |
| testWidgets('TabController notifies the index to change when left flinging', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| late TabController tabController; |
| |
| Widget buildTabControllerFrame(BuildContext context, TabController controller) { |
| tabController = controller; |
| return MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.iOS), |
| home: Scaffold( |
| appBar: AppBar( |
| title: const Text('tabs'), |
| bottom: TabBar( |
| controller: controller, |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| ), |
| ), |
| body: TabBarView( |
| controller: controller, |
| children: const <Widget>[ |
| Center(child: Text('CHILD A')), |
| Center(child: Text('CHILD B')), |
| Center(child: Text('CHILD C')), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(TabControllerFrame( |
| builder: buildTabControllerFrame, |
| length: tabs.length, |
| initialIndex: tabs.indexOf('C'), |
| )); |
| expect(tabController.index, tabs.indexOf('C')); |
| |
| tabController.addListener(() { |
| final int indexOfB = tabs.indexOf('B'); |
| expect(tabController.index, indexOfB); |
| }); |
| final Offset flingStart = tester.getCenter(find.text('CHILD C')); |
| await tester.flingFrom(flingStart, const Offset(600, 0.0), 10000.0); |
| await tester.pumpAndSettle(); |
| }); |
| |
| // 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( |
| MaterialApp( |
| home: Align( |
| alignment: Alignment.topLeft, |
| child: SizedBox( |
| width: 300.0, |
| height: 200.0, |
| child: DefaultTabController( |
| length: tabs.length, |
| child: Scaffold( |
| appBar: AppBar( |
| title: const Text('tabs'), |
| bottom: TabBar( |
| isScrollable: true, |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| ), |
| ), |
| body: TabBarView( |
| children: tabs.map<Widget>((String name) => 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); |
| |
| late 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']; |
| late TabController tabController; |
| |
| Widget buildTabControllerFrame(BuildContext context, TabController controller) { |
| tabController = controller; |
| return MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android), |
| home: Scaffold( |
| appBar: AppBar( |
| title: const Text('tabs'), |
| bottom: TabBar( |
| controller: controller, |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| ), |
| ), |
| body: TabBarView( |
| controller: controller, |
| children: const <Widget>[ |
| Center(child: Text('LEFT CHILD')), |
| Center(child: Text('RIGHT CHILD')), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(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.forward); |
| |
| 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']; |
| late TabController tabController; |
| |
| Widget buildTabControllerFrame(BuildContext context, TabController controller) { |
| tabController = controller; |
| return MaterialApp( |
| theme: ThemeData(platform: TargetPlatform.android), |
| home: Scaffold( |
| appBar: AppBar( |
| title: const Text('tabs'), |
| bottom: TabBar( |
| controller: controller, |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| ), |
| ), |
| body: TabBarView( |
| controller: controller, |
| children: const <Widget>[ |
| Center(child: Text('CHILD A')), |
| Center(child: Text('CHILD B')), |
| Center(child: Text('CHILD C')), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(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('TabBar unselectedLabelColor control test', (WidgetTester tester) async { |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: 2, |
| ); |
| |
| late Color firstColor; |
| late Color secondColor; |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TabBar( |
| controller: controller, |
| labelColor: Colors.green[500], |
| unselectedLabelColor: Colors.blue[500], |
| tabs: <Widget>[ |
| Builder( |
| builder: (BuildContext context) { |
| firstColor = IconTheme.of(context).color!; |
| return const Text('First'); |
| }, |
| ), |
| 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 = TabController( |
| vsync: const TestVSync(), |
| length: 2, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TabBarView( |
| controller: controller, |
| children: const <Widget>[ Text('First'), 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 animationDuration sets indicator animation duration', (WidgetTester tester) async { |
| const Duration animationDuration = Duration(milliseconds: 100); |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: animationDuration)); |
| final TabController controller = DefaultTabController.of(tester.element(find.text('A'))); |
| |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| expect(controller.indexIsChanging, true); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.pump(animationDuration); |
| expect(controller.index, 0); |
| expect(controller.previousIndex, 1); |
| expect(controller.indexIsChanging, false); |
| |
| //Test when index diff is greater than 1 |
| await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: animationDuration)); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| expect(controller.indexIsChanging, true); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.pump(animationDuration); |
| expect(controller.index, 2); |
| expect(controller.previousIndex, 0); |
| expect(controller.indexIsChanging, false); |
| }); |
| |
| testWidgets('TabBarView controller sets animation duration', (WidgetTester tester) async { |
| const Duration animationDuration = Duration(milliseconds: 100); |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| initialIndex: 1, |
| length: tabs.length, |
| animationDuration: animationDuration, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: 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); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| expect(position.pixels, 400); |
| await tester.pump(const Duration(milliseconds: 50)); |
| await tester.pump(animationDuration); |
| expect(position.pixels, 800); |
| }); |
| |
| testWidgets('TabBarView animation can be interrupted', (WidgetTester tester) async { |
| const Duration animationDuration = Duration(seconds: 2); |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| animationDuration: animationDuration, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: Text('2')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| )); |
| |
| expect(tabController.index, 0); |
| |
| final PageView pageView = tester.widget<PageView>(find.byType(PageView)); |
| final PageController pageController = pageView.controller; |
| final ScrollPosition position = pageController.position; |
| |
| expect(position.pixels, 0.0); |
| |
| await tester.tap(find.text('C')); |
| await tester.pump(const Duration(milliseconds: 10)); // TODO(bleroux): find why this is needed. |
| |
| // Runs the animation for half of the animation duration. |
| await tester.pump(const Duration(seconds: 1)); |
| |
| // The position should be between page 1 and page 2. |
| expect(position.pixels, greaterThan(400.0)); |
| expect(position.pixels, lessThan(800.0)); |
| |
| // Switch to another tab before the end of the animation. |
| await tester.tap(find.text('A')); |
| await tester.pump(const Duration(milliseconds: 10)); // TODO(bleroux): find why this is needed. |
| await tester.pump(animationDuration); |
| expect(position.pixels, 0.0); |
| |
| await tester.pumpAndSettle(); // Finish the animation. |
| }); |
| |
| testWidgets('TabBarView viewportFraction sets PageView viewport fraction', (WidgetTester tester) async { |
| const Duration animationDuration = Duration(milliseconds: 100); |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| initialIndex: 1, |
| length: tabs.length, |
| animationDuration: animationDuration, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| viewportFraction: 0.8, |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: Text('2')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| )); |
| |
| expect(tabController.index, 1); |
| |
| final PageView pageView = tester.widget(find.byType(PageView)); |
| final PageController pageController = pageView.controller; |
| |
| // The TabView was initialized with viewportFraction as 0.8 |
| // So it's expected the PageView inside would obtain the same viewportFraction |
| expect(pageController.viewportFraction, 0.8); |
| }); |
| |
| testWidgets('TabBarView viewportFraction is 1 by default', (WidgetTester tester) async { |
| const Duration animationDuration = Duration(milliseconds: 100); |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| initialIndex: 1, |
| length: tabs.length, |
| animationDuration: animationDuration, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: Text('2')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| )); |
| |
| expect(tabController.index, 1); |
| |
| final PageView pageView = tester.widget(find.byType(PageView)); |
| final PageController pageController = pageView.controller; |
| |
| // The TabView was initialized with default viewportFraction |
| // So it's expected the PageView inside would obtain the value 1 |
| expect(pageController.viewportFraction, 1); |
| }); |
| |
| testWidgets('TabBarView has clipBehavior Clip.hardEdge by default', (WidgetTester tester) async { |
| final List<Widget> tabs = <Widget>[const Text('First'), const Text('Second')]; |
| |
| Widget builder() { |
| return boilerplate( |
| child: DefaultTabController( |
| length: tabs.length, |
| child: TabBarView( |
| children: tabs, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(builder()); |
| final TabBarView tabBarView = tester.widget(find.byType(TabBarView)); |
| expect(tabBarView.clipBehavior, Clip.hardEdge); |
| }); |
| |
| testWidgets('TabBarView sets clipBehavior correctly', (WidgetTester tester) async { |
| final List<Widget> tabs = <Widget>[const Text('First'), const Text('Second')]; |
| |
| Widget builder() { |
| return boilerplate( |
| child: DefaultTabController( |
| length: tabs.length, |
| child: TabBarView( |
| clipBehavior: Clip.none, |
| children: tabs, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(builder()); |
| final PageView pageView = tester.widget(find.byType(PageView)); |
| expect(pageView.clipBehavior, Clip.none); |
| }); |
| |
| testWidgets('TabBar tap skips indicator animation when disabled in controller', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B']; |
| |
| const Color indicatorColor = Color(0xFFFF0000); |
| await tester.pumpWidget(buildFrame(useMaterial3: false, tabs: tabs, value: 'A', indicatorColor: indicatorColor, animationDuration: Duration.zero)); |
| |
| final RenderBox box = tester.renderObject(find.byType(TabBar)); |
| final TabIndicatorRecordingCanvas canvas = TabIndicatorRecordingCanvas(indicatorColor); |
| final TestRecordingPaintingContext context = 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(); |
| 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('TabBar tap changes index instantly when animation is disabled in controller', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: Duration.zero)); |
| final TabController controller = DefaultTabController.of(tester.element(find.text('A'))); |
| |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| expect(controller.index, 0); |
| expect(controller.previousIndex, 1); |
| expect(controller.indexIsChanging, false); |
| |
| //Test when index diff is greater than 1 |
| await tester.pumpWidget(buildFrame(tabs: tabs, value: 'B', animationDuration: Duration.zero)); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| expect(controller.index, 2); |
| expect(controller.previousIndex, 0); |
| expect(controller.indexIsChanging, false); |
| }); |
| |
| testWidgets('Scrollable TabBar does not have overscroll indicator', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| |
| await tester.pumpWidget(buildFrame(tabs: tabs, value: 'A', isScrollable: true)); |
| expect(find.byType(GlowingOverscrollIndicator), findsNothing); |
| }); |
| |
| testWidgets('TabBar should not throw when animation is disabled in controller', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/102600 |
| final List<String> tabs = <String>['A']; |
| |
| Widget buildWithTabBarView() { |
| return boilerplate( |
| child: DefaultTabController( |
| animationDuration: Duration.zero, |
| length: tabs.length, |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| isScrollable: true, |
| ), |
| Flexible( |
| child: TabBarView( |
| children: List<Widget>.generate( |
| tabs.length, |
| (int index) => Text('Tab $index'), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildWithTabBarView()); |
| TabController controller = DefaultTabController.of(tester.element(find.text('A'))); |
| expect(controller.index, 0); |
| |
| tabs.add('B'); |
| await tester.pumpWidget(buildWithTabBarView()); |
| tabs.add('C'); |
| await tester.pumpWidget(buildWithTabBarView()); |
| await tester.tap(find.text('C')); |
| await tester.pumpAndSettle(); |
| controller = DefaultTabController.of(tester.element(find.text('A'))); |
| expect(controller.index, 2); |
| |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('TabBarView skips animation when disabled in controller', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| initialIndex: 1, |
| length: tabs.length, |
| animationDuration: Duration.zero, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: 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); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| expect(position.pixels, 800); |
| }); |
| |
| testWidgets('TabBarView skips animation when disabled in controller - skip tabs', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| animationDuration: Duration.zero, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: Text('2')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| )); |
| |
| expect(tabController.index, 0); |
| |
| 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, 0); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| expect(position.pixels, 800); |
| }); |
| |
| testWidgets('TabBarView skips animation when disabled in controller - skip tabs twice', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/110970 |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| animationDuration: Duration.zero, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: Text('2')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| )); |
| |
| expect(tabController.index, 0); |
| |
| 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, 0); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| expect(position.pixels, 800); |
| |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| expect(position.pixels, 0); |
| }); |
| |
| testWidgets('TabBarView skips animation when disabled in controller - skip tabs followed by single tab navigation', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/110970 |
| final List<String> tabs = <String>['A', 'B', 'C']; |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| animationDuration: Duration.zero, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: Text('2')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| )); |
| |
| expect(tabController.index, 0); |
| |
| 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, 0); |
| await tester.tap(find.text('C')); |
| await tester.pump(); |
| expect(position.pixels, 800); |
| |
| await tester.tap(find.text('B')); |
| await tester.pump(); |
| expect(position.pixels, 400); |
| |
| await tester.tap(find.text('A')); |
| await tester.pump(); |
| expect(position.pixels, 0); |
| }); |
| |
| testWidgets('TabBarView skips animation when disabled in controller - two tabs', (WidgetTester tester) async { |
| final List<String> tabs = <String>['A', 'B']; |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| animationDuration: Duration.zero, |
| ); |
| await tester.pumpWidget(boilerplate( |
| child: Column( |
| children: <Widget>[ |
| TabBar( |
| tabs: tabs.map<Widget>((String tab) => Tab(text: tab)).toList(), |
| controller: tabController, |
| ), |
| SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| )); |
| |
| expect(tabController.index, 0); |
| |
| 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, 0); |
| await tester.tap(find.text('B')); |
| await tester.pump(); |
| expect(position.pixels, 400); |
| }); |
| |
| 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 = Color(0xFFFF0000); |
| await tester.pumpWidget(buildFrame(useMaterial3: false, tabs: tabs, value: 'A', indicatorColor: indicatorColor)); |
| |
| final RenderBox box = tester.renderObject(find.byType(TabBar)); |
| final TabIndicatorRecordingCanvas canvas = TabIndicatorRecordingCanvas(indicatorColor); |
| final TestRecordingPaintingContext context = 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 = TabController( |
| vsync: const TestVSync(), |
| length: 2, |
| ); |
| |
| Widget buildFrame() { |
| return boilerplate( |
| child: TabBar( |
| key: UniqueKey(), |
| controller: controller, |
| tabs: const <Widget>[ Text('A'), 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)); |
| }); |
| |
| |
| group('TabBarView children updated', () { |
| |
| Widget buildFrameWithMarker(List<String> log, String marker) { |
| return MaterialApp( |
| home: DefaultTabController( |
| animationDuration: const Duration(seconds: 1), |
| length: 3, |
| child: Scaffold( |
| appBar: AppBar( |
| bottom: const TabBar( |
| tabs: <Widget>[ |
| Tab(text: 'A'), |
| Tab(text: 'B'), |
| Tab(text: 'C'), |
| ], |
| ), |
| title: const Text('Tabs Test'), |
| ), |
| body: TabBarView( |
| children: <Widget>[ |
| TabBody(index: 0, log: log, marker: marker), |
| TabBody(index: 1, log: log, marker: marker), |
| TabBody(index: 2, log: log, marker: marker), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| testWidgets('TabBarView children can be updated during animation to an adjacent tab', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/107399 |
| final List<String> log = <String>[]; |
| |
| const String initialMarker = 'before'; |
| await tester.pumpWidget(buildFrameWithMarker(log, initialMarker)); |
| expect(log, <String>['init: 0']); |
| expect(find.text('0-$initialMarker'), findsOneWidget); |
| |
| // Select the second tab and wait until the transition starts |
| await tester.tap(find.text('B')); |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| // Check that both TabBody's are instantiated while the transition is animating |
| await tester.pump(const Duration(milliseconds: 400)); |
| expect(log, <String>['init: 0', 'init: 1']); |
| |
| // Update the TabBody's states while the transition is animating |
| const String updatedMarker = 'after'; |
| await tester.pumpWidget(buildFrameWithMarker(log, updatedMarker)); |
| |
| // Wait until the transition ends |
| await tester.pumpAndSettle(); |
| |
| // The TabBody state of the second TabBar should have been updated |
| expect(find.text('1-$initialMarker'), findsNothing); |
| expect(find.text('1-$updatedMarker'), findsOneWidget); |
| }); |
| |
| testWidgets('TabBarView children can be updated during animation to a non adjacent tab', (WidgetTester tester) async { |
| final List<String> log = <String>[]; |
| |
| const String initialMarker = 'before'; |
| await tester.pumpWidget(buildFrameWithMarker(log, initialMarker)); |
| expect(log, <String>['init: 0']); |
| expect(find.text('0-$initialMarker'), findsOneWidget); |
| |
| // Select the third tab and wait until the transition starts |
| await tester.tap(find.text('C')); |
| await tester.pump(const Duration(milliseconds: 100)); |
| |
| // Check that both TabBody's are instantiated while the transition is animating |
| await tester.pump(const Duration(milliseconds: 400)); |
| expect(log, <String>['init: 0', 'init: 2']); |
| |
| // Update the TabBody's states while the transition is animating |
| const String updatedMarker = 'after'; |
| await tester.pumpWidget(buildFrameWithMarker(log, updatedMarker)); |
| |
| // Wait until the transition ends |
| await tester.pumpAndSettle(); |
| |
| // The TabBody state of the third TabBar should have been updated |
| expect(find.text('2-$initialMarker'), findsNothing); |
| expect(find.text('2-$updatedMarker'), findsOneWidget); |
| }); |
| }); |
| |
| testWidgets('TabBarView scrolls end close to a new page', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/9375 |
| |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| initialIndex: 1, |
| length: 3, |
| ); |
| |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: SizedBox.expand( |
| child: Center( |
| child: SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: 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(500.0); |
| expect(tabController.index, 1); |
| |
| // Close enough to switch to page 2 |
| pageController.jumpTo(700.0); |
| expect(tabController.index, 2); |
| |
| // Same behavior going left: not left enough to get to page 0 |
| pageController.jumpTo(300.0); |
| expect(tabController.index, 1); |
| |
| // Left enough to get to page 0 |
| pageController.jumpTo(100.0); |
| expect(tabController.index, 0); |
| }); |
| |
| testWidgets('Can switch to non-neighboring tab in nested TabBarView without crashing', (WidgetTester tester) async { |
| // This is a regression test for https://github.com/flutter/flutter/issues/18756 |
| final TabController mainTabController = TabController(length: 4, vsync: const TestVSync()); |
| final TabController nestedTabController = TabController(length: 2, vsync: const TestVSync()); |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| appBar: AppBar( |
| title: const Text('Exception for Nested Tabs'), |
| bottom: TabBar( |
| controller: mainTabController, |
| tabs: const <Widget>[ |
| Tab(icon: Icon(Icons.add), text: 'A'), |
| Tab(icon: Icon(Icons.add), text: 'B'), |
| Tab(icon: Icon(Icons.add), text: 'C'), |
| Tab(icon: Icon(Icons.add), text: 'D'), |
| ], |
| ), |
| ), |
| body: TabBarView( |
| controller: mainTabController, |
| children: <Widget>[ |
| Container(color: Colors.red), |
| _NestedTabBarContainer(tabController: nestedTabController), |
| Container(color: Colors.green), |
| Container(color: Colors.indigo), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // expect first tab to be selected |
| expect(mainTabController.index, 0); |
| |
| // tap on third tab |
| await tester.tap(find.text('C')); |
| await tester.pumpAndSettle(); |
| |
| // expect third tab to be selected without exceptions |
| expect(mainTabController.index, 2); |
| }); |
| |
| testWidgets('TabBarView can warp when child is kept alive and contains ink', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/57662. |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: 3, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TabBarView( |
| controller: controller, |
| children: const <Widget>[ |
| Text('Page 1'), |
| Text('Page 2'), |
| KeepAliveInk('Page 3'), |
| ], |
| ), |
| ), |
| ); |
| |
| expect(controller.index, equals(0)); |
| expect(find.text('Page 1'), findsOneWidget); |
| expect(find.text('Page 3'), findsNothing); |
| |
| controller.index = 2; |
| await tester.pumpAndSettle(); |
| expect(find.text('Page 1'), findsNothing); |
| expect(find.text('Page 3'), findsOneWidget); |
| |
| controller.index = 0; |
| await tester.pumpAndSettle(); |
| expect(find.text('Page 1'), findsOneWidget); |
| expect(find.text('Page 3'), findsNothing); |
| |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('TabBarView scrolls end close to a new page with custom physics', (WidgetTester tester) async { |
| final TabController tabController = TabController( |
| vsync: const TestVSync(), |
| initialIndex: 1, |
| length: 3, |
| ); |
| |
| await tester.pumpWidget(Directionality( |
| textDirection: TextDirection.ltr, |
| child: SizedBox.expand( |
| child: Center( |
| child: SizedBox( |
| width: 400.0, |
| height: 400.0, |
| child: TabBarView( |
| controller: tabController, |
| physics: const TestScrollPhysics(), |
| children: const <Widget>[ |
| Center(child: Text('0')), |
| Center(child: Text('1')), |
| Center(child: 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(500.0); |
| expect(tabController.index, 1); |
| |
| // Close enough to switch to page 2 |
| pageController.jumpTo(700.0); |
| expect(tabController.index, 2); |
| |
| // Same behavior going left: not left enough to get to page 0 |
| pageController.jumpTo(300.0); |
| expect(tabController.index, 1); |
| |
| // Left enough to get to page 0 |
| pageController.jumpTo(100.0); |
| expect(tabController.index, 0); |
| }); |
| |
| testWidgets('TabBar accepts custom physics', (WidgetTester tester) async { |
| final List<Tab> tabs = List<Tab>.generate(20, (int index) { |
| return Tab(text: 'TAB #$index'); |
| }); |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| initialIndex: tabs.length - 1, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TabBar( |
| isScrollable: true, |
| controller: controller, |
| tabs: tabs, |
| physics: const TestScrollPhysics(), |
| ), |
| ), |
| ); |
| |
| final TabBar tabBar = tester.widget(find.byType(TabBar)); |
| final double position = tabBar.physics!.applyPhysicsToUserOffset(MockScrollMetrics(), 10); |
| |
| expect(position, equals(20)); |
| }); |
| |
| 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 = List<Tab>.generate(20, (int index) { |
| return Tab(text: 'TAB #$index'); |
| }); |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| initialIndex: tabs.length - 1, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| child: TabBar( |
| isScrollable: true, |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ); |
| |
| // The initialIndex tab should be visible and right justified |
| expect(find.text('TAB #19'), findsOneWidget); |
| |
| // Tabs have a minimum width of 72.0 and 'TAB #19' is wider than |
| // that. Tabs are padded horizontally with kTabLabelPadding. |
| final double tabRight = 800.0 - kTabLabelPadding.right; |
| |
| expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, tabRight); |
| }); |
| |
| testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async { |
| const Color indicatorColor = Color(0xFF00FF00); |
| const double indicatorWeight = 8.0; |
| const double padLeft = 8.0; |
| const double padRight = 4.0; |
| |
| final List<Widget> tabs = List<Widget>.generate(4, (int index) { |
| return Tab(text: 'Tab $index'); |
| }); |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| useMaterial3: false, |
| child: Container( |
| alignment: Alignment.topLeft, |
| child: TabBar( |
| indicatorWeight: indicatorWeight, |
| indicatorColor: indicatorColor, |
| indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight), |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); |
| expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0) |
| |
| const double indicatorY = 54.0 - indicatorWeight / 2.0; |
| double indicatorLeft = padLeft + indicatorWeight / 2.0; |
| double indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0); |
| |
| expect(tabBarBox, paints..line( |
| color: indicatorColor, |
| strokeWidth: indicatorWeight, |
| p1: Offset(indicatorLeft, indicatorY), |
| p2: Offset(indicatorRight, indicatorY), |
| )); |
| |
| // Select tab 3 |
| controller.index = 3; |
| await tester.pumpAndSettle(); |
| |
| indicatorLeft = 600.0 + padLeft + indicatorWeight / 2.0; |
| indicatorRight = 800.0 - (padRight + indicatorWeight / 2.0); |
| |
| expect(tabBarBox, paints..line( |
| color: indicatorColor, |
| strokeWidth: indicatorWeight, |
| p1: Offset(indicatorLeft, indicatorY), |
| p2: Offset(indicatorRight, indicatorY), |
| )); |
| }); |
| |
| testWidgets('TabBar with indicatorWeight, indicatorPadding (RTL)', (WidgetTester tester) async { |
| const Color indicatorColor = Color(0xFF00FF00); |
| const double indicatorWeight = 8.0; |
| const double padLeft = 8.0; |
| const double padRight = 4.0; |
| |
| final List<Widget> tabs = List<Widget>.generate(4, (int index) { |
| return Tab(text: 'Tab $index'); |
| }); |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| useMaterial3: false, |
| textDirection: TextDirection.rtl, |
| child: Container( |
| alignment: Alignment.topLeft, |
| child: TabBar( |
| indicatorWeight: indicatorWeight, |
| indicatorColor: indicatorColor, |
| indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight), |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); |
| expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0) |
| expect(tabBarBox.size.width, 800.0); |
| |
| const double indicatorY = 54.0 - indicatorWeight / 2.0; |
| double indicatorLeft = 600.0 + padLeft + indicatorWeight / 2.0; |
| double indicatorRight = 800.0 - padRight - indicatorWeight / 2.0; |
| |
| expect(tabBarBox, paints..line( |
| color: indicatorColor, |
| strokeWidth: indicatorWeight, |
| p1: Offset(indicatorLeft, indicatorY), |
| p2: Offset(indicatorRight, indicatorY), |
| )); |
| |
| // Select tab 3 |
| controller.index = 3; |
| await tester.pumpAndSettle(); |
| |
| indicatorLeft = padLeft + indicatorWeight / 2.0; |
| indicatorRight = 200.0 - padRight - indicatorWeight / 2.0; |
| |
| expect(tabBarBox, paints..line( |
| color: indicatorColor, |
| strokeWidth: indicatorWeight, |
| p1: Offset(indicatorLeft, indicatorY), |
| p2: Offset(indicatorRight, indicatorY), |
| )); |
| }); |
| |
| testWidgets('TabBar changes indicator attributes', (WidgetTester tester) async { |
| final List<Widget> tabs = List<Widget>.generate(4, (int index) { |
| return Tab(text: 'Tab $index'); |
| }); |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| ); |
| |
| Color indicatorColor = const Color(0xFF00FF00); |
| double indicatorWeight = 8.0; |
| double padLeft = 8.0; |
| double padRight = 4.0; |
| |
| Widget buildFrame() { |
| return boilerplate( |
| useMaterial3: false, |
| child: Container( |
| alignment: Alignment.topLeft, |
| child: TabBar( |
| indicatorWeight: indicatorWeight, |
| indicatorColor: indicatorColor, |
| indicatorPadding: EdgeInsets.only(left: padLeft, right: padRight), |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(buildFrame()); |
| |
| final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); |
| expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0) |
| |
| double indicatorY = 54.0 - indicatorWeight / 2.0; |
| double indicatorLeft = padLeft + indicatorWeight / 2.0; |
| double indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0); |
| |
| expect(tabBarBox, paints..line( |
| color: indicatorColor, |
| strokeWidth: indicatorWeight, |
| p1: Offset(indicatorLeft, indicatorY), |
| p2: Offset(indicatorRight, indicatorY), |
| )); |
| |
| indicatorColor = const Color(0xFF0000FF); |
| indicatorWeight = 4.0; |
| padLeft = 4.0; |
| padRight = 8.0; |
| |
| await tester.pumpWidget(buildFrame()); |
| |
| expect(tabBarBox.size.height, 50.0); // 54 = _kTabHeight(46) + indicatorWeight(4.0) |
| |
| indicatorY = 50.0 - indicatorWeight / 2.0; |
| indicatorLeft = padLeft + indicatorWeight / 2.0; |
| indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0); |
| |
| expect(tabBarBox, paints..line( |
| color: indicatorColor, |
| strokeWidth: indicatorWeight, |
| p1: Offset(indicatorLeft, indicatorY), |
| p2: Offset(indicatorRight, indicatorY), |
| )); |
| }); |
| |
| testWidgets('TabBar with directional indicatorPadding (LTR)', (WidgetTester tester) async { |
| final List<Widget> tabs = <Widget>[ |
| SizedBox(key: UniqueKey(), width: 130.0, height: 30.0), |
| SizedBox(key: UniqueKey(), width: 140.0, height: 40.0), |
| SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), |
| ]; |
| |
| const double indicatorWeight = 2.0; // the default |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| useMaterial3: false, |
| child: Container( |
| alignment: Alignment.topLeft, |
| child: TabBar( |
| indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0), |
| isScrollable: true, |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); |
| const double tabBarHeight = 50.0 + indicatorWeight; // 50 = max tab height |
| expect(tabBarBox.size.height, tabBarHeight); |
| |
| // Tab0 width = 130, height = 30 |
| double tabLeft = kTabLabelPadding.left; |
| double tabRight = tabLeft + 130.0; |
| double tabTop = (tabBarHeight - indicatorWeight - 30.0) / 2.0; |
| double tabBottom = tabTop + 30.0; |
| Rect tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); |
| expect(tester.getRect(find.byKey(tabs[0].key!)), tabRect); |
| |
| |
| // Tab1 width = 140, height = 40 |
| tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left; |
| tabRight = tabLeft + 140.0; |
| tabTop = (tabBarHeight - indicatorWeight - 40.0) / 2.0; |
| tabBottom = tabTop + 40.0; |
| tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); |
| expect(tester.getRect(find.byKey(tabs[1].key!)), tabRect); |
| |
| |
| // Tab2 width = 150, height = 50 |
| tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left; |
| tabRight = tabLeft + 150.0; |
| tabTop = (tabBarHeight - indicatorWeight - 50.0) / 2.0; |
| tabBottom = tabTop + 50.0; |
| tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); |
| expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect); |
| |
| // Tab 0 selected, indicator padding resolves to left: 100.0 |
| const double indicatorLeft = 100.0 + indicatorWeight / 2.0; |
| final double indicatorRight = 130.0 + kTabLabelPadding.horizontal - indicatorWeight / 2.0; |
| final double indicatorY = tabBottom + indicatorWeight / 2.0; |
| expect(tabBarBox, paints..line( |
| strokeWidth: indicatorWeight, |
| p1: Offset(indicatorLeft, indicatorY), |
| p2: Offset(indicatorRight, indicatorY), |
| )); |
| }); |
| |
| testWidgets('TabBar with directional indicatorPadding (RTL)', (WidgetTester tester) async { |
| final List<Widget> tabs = <Widget>[ |
| SizedBox(key: UniqueKey(), width: 130.0, height: 30.0), |
| SizedBox(key: UniqueKey(), width: 140.0, height: 40.0), |
| SizedBox(key: UniqueKey(), width: 150.0, height: 50.0), |
| ]; |
| |
| const double indicatorWeight = 2.0; // the default |
| |
| final TabController controller = TabController( |
| vsync: const TestVSync(), |
| length: tabs.length, |
| ); |
| |
| await tester.pumpWidget( |
| boilerplate( |
| useMaterial3: false, |
| textDirection: TextDirection.rtl, |
| child: Container( |
| alignment: Alignment.topLeft, |
| child: TabBar( |
| indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0), |
| isScrollable: true, |
| controller: controller, |
| tabs: tabs, |
| ), |
| ), |
| ), |
| ); |
| |
| final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar)); |
| const double tabBarHeight = 50.0 + indicatorWeight; // 50 = max tab height |
| expect(tabBarBox.size.height, tabBarHeight); |
| |
| // Tab2 width = 150, height = 50 |
| double tabLeft = kTabLabelPadding.left; |
| double tabRight = tabLeft + 150.0; |
| double tabTop = (tabBarHeight - indicatorWeight - 50.0) / 2.0; |
| double tabBottom = tabTop + 50.0; |
| Rect tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); |
| expect(tester.getRect(find.byKey(tabs[2].key!)), tabRect); |
| |
| // Tab1 width = 140, height = 40 |
| tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left; |
| tabRight = tabLeft + 140.0; |
| tabTop = (tabBarHeight - indicatorWeight - 40.0) / 2.0; |
| tabBottom = tabTop + 40.0; |
| tabRect = Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom); |
| expect(tester.getRect(find.byKey(tabs[1].key!)), tabRect); |
| |
| // Tab0 width = 130, height = 30 |
| |