tabs_test.dart 39.9 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui' show SemanticsFlags, SemanticsAction;

Adam Barth's avatar
Adam Barth committed
7
import 'package:flutter_test/flutter_test.dart';
8 9
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/physics.dart';
12

13
import '../rendering/mock_canvas.dart';
14
import '../rendering/recording_canvas.dart';
15
import '../widgets/semantics_tester.dart';
16

17 18 19 20 21 22 23 24 25
Widget boilerplate({ Widget child }) {
  return new Directionality(
    textDirection: TextDirection.ltr,
    child: new Material(
      child: child,
    ),
  );
}

Adam Barth's avatar
Adam Barth committed
26
class StateMarker extends StatefulWidget {
27
  const StateMarker({ Key key, this.child }) : super(key: key);
Adam Barth's avatar
Adam Barth committed
28 29 30 31 32 33 34 35 36 37 38 39

  final Widget child;

  @override
  StateMarkerState createState() => new StateMarkerState();
}

class StateMarkerState extends State<StateMarker> {
  String marker;

  @override
  Widget build(BuildContext context) {
40 41
    if (widget.child != null)
      return widget.child;
Adam Barth's avatar
Adam Barth committed
42 43 44 45
    return new Container();
  }
}

46 47 48 49 50 51 52
Widget buildFrame({
    Key tabBarKey,
    List<String> tabs,
    String value,
    bool isScrollable: false,
    Color indicatorColor,
  }) {
53
  return boilerplate(
Hans Muller's avatar
Hans Muller committed
54 55 56 57
    child: new DefaultTabController(
      initialIndex: tabs.indexOf(value),
      length: tabs.length,
      child: new TabBar(
Hans Muller's avatar
Hans Muller committed
58
        key: tabBarKey,
Hans Muller's avatar
Hans Muller committed
59 60
        tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
        isScrollable: isScrollable,
61
        indicatorColor: indicatorColor,
Hans Muller's avatar
Hans Muller committed
62 63
      ),
    ),
64 65 66
  );
}

Hans Muller's avatar
Hans Muller committed
67 68 69
typedef Widget TabControllerFrameBuilder(BuildContext context, TabController controller);

class TabControllerFrame extends StatefulWidget {
70
  const TabControllerFrame({ this.length, this.initialIndex: 0, this.builder });
Hans Muller's avatar
Hans Muller committed
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87

  final int length;
  final int initialIndex;
  final TabControllerFrameBuilder builder;

  @override
  TabControllerFrameState createState() => new TabControllerFrameState();
}

class TabControllerFrameState extends State<TabControllerFrame> with SingleTickerProviderStateMixin {
  TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = new TabController(
      vsync: this,
88 89
      length: widget.length,
      initialIndex: widget.initialIndex,
Hans Muller's avatar
Hans Muller committed
90 91 92 93 94 95 96 97 98 99 100
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
101
    return widget.builder(context, _controller);
Hans Muller's avatar
Hans Muller committed
102 103
  }
}
104 105 106 107

Widget buildLeftRightApp({ List<String> tabs, String value }) {
  return new MaterialApp(
    theme: new ThemeData(platform: TargetPlatform.android),
Hans Muller's avatar
Hans Muller committed
108 109 110
    home: new DefaultTabController(
      initialIndex: tabs.indexOf(value),
      length: tabs.length,
111 112
      child: new Scaffold(
        appBar: new AppBar(
113
          title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
114 115 116
          bottom: new TabBar(
            tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
          ),
117
        ),
Hans Muller's avatar
Hans Muller committed
118
        body: new TabBarView(
119
          children: <Widget>[
120 121
            const Center(child: const Text('LEFT CHILD')),
            const Center(child: const Text('RIGHT CHILD'))
122 123 124 125 126 127 128
          ]
        )
      )
    )
  );
}

129 130 131 132 133 134 135 136 137 138 139 140 141
class TabIndicatorRecordingCanvas extends TestRecordingCanvas {
  TabIndicatorRecordingCanvas(this.indicatorColor);

  final Color indicatorColor;
  Rect indicatorRect;

  @override
  void drawRect(Rect rect, Paint paint) {
    if (paint.color == indicatorColor)
      indicatorRect = rect;
  }
}

142 143
class TestScrollPhysics extends ScrollPhysics {
  const TestScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
144

145 146 147 148
  @override
  TestScrollPhysics applyTo(ScrollPhysics ancestor) {
    return new TestScrollPhysics(parent: buildParent(ancestor));
  }
149

150 151
  static final SpringDescription _kDefaultSpring = new SpringDescription.withDampingRatio(
    mass: 0.5,
152
    stiffness: 500.0,
153 154
    ratio: 1.1,
  );
155

156 157 158 159
  @override
  SpringDescription get spring => _kDefaultSpring;
}

160
void main() {
161
  testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
162
    final List<String> tabs = <String>['A', 'B', 'C'];
163

164
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
165 166 167
    expect(find.text('A'), findsOneWidget);
    expect(find.text('B'), findsOneWidget);
    expect(find.text('C'), findsOneWidget);
168
    final TabController controller = DefaultTabController.of(tester.element(find.text('A')));
Hans Muller's avatar
Hans Muller committed
169 170 171
    expect(controller, isNotNull);
    expect(controller.index, 2);
    expect(controller.previousIndex, 2);
172

Hans Muller's avatar
Hans Muller committed
173
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
174 175
    await tester.tap(find.text('B'));
    await tester.pump();
Hans Muller's avatar
Hans Muller committed
176
    expect(controller.indexIsChanging, true);
177
    await tester.pump(const Duration(seconds: 1)); // finish the animation
Hans Muller's avatar
Hans Muller committed
178 179 180
    expect(controller.index, 1);
    expect(controller.previousIndex, 2);
    expect(controller.indexIsChanging, false);
181

182 183 184 185
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
    await tester.tap(find.text('C'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
Hans Muller's avatar
Hans Muller committed
186 187
    expect(controller.index, 2);
    expect(controller.previousIndex, 1);
188

189 190 191 192
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
    await tester.tap(find.text('A'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
Hans Muller's avatar
Hans Muller committed
193 194
    expect(controller.index, 0);
    expect(controller.previousIndex, 2);
195 196
  });

197
  testWidgets('Scrollable TabBar tap selects tab', (WidgetTester tester) async {
198
    final List<String> tabs = <String>['A', 'B', 'C'];
199

200
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true));
201 202 203
    expect(find.text('A'), findsOneWidget);
    expect(find.text('B'), findsOneWidget);
    expect(find.text('C'), findsOneWidget);
204
    final TabController controller = DefaultTabController.of(tester.element(find.text('A')));
Hans Muller's avatar
Hans Muller committed
205 206
    expect(controller.index, 2);
    expect(controller.previousIndex, 2);
207

Hans Muller's avatar
Hans Muller committed
208
    await tester.tap(find.text('C'));
209
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
210
    expect(controller.index, 2);
211

Hans Muller's avatar
Hans Muller committed
212
    await tester.tap(find.text('B'));
213
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
214
    expect(controller.index, 1);
215

216
    await tester.tap(find.text('A'));
217
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
218
    expect(controller.index, 0);
219
  });
Hans Muller's avatar
Hans Muller committed
220

221
  testWidgets('Scrollable TabBar tap centers selected tab', (WidgetTester tester) async {
222 223
    final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
    final Key tabBarKey = const Key('TabBar');
224
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
225
    final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
Hans Muller's avatar
Hans Muller committed
226 227
    expect(controller, isNotNull);
    expect(controller.index, 0);
228 229 230

    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
231
    expect(tester.getCenter(find.text('FFFFFF')).dx, greaterThan(401.0));
232

233
    await tester.tap(find.text('FFFFFF'));
234
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
235
    expect(controller.index, 5);
236
    // The center of the FFFFFF item is now at the TabBar's center
237
    expect(tester.getCenter(find.text('FFFFFF')).dx, closeTo(400.0, 1.0));
Hans Muller's avatar
Hans Muller committed
238 239 240
  });


241
  testWidgets('TabBar can be scrolled independent of the selection', (WidgetTester tester) async {
242 243
    final List<String> tabs = <String>['AAAA', 'BBBB', 'CCCC', 'DDDD', 'EEEE', 'FFFF', 'GGGG', 'HHHH', 'IIII', 'JJJJ', 'KKKK', 'LLLL'];
    final Key tabBarKey = const Key('TabBar');
244
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAA', isScrollable: true, tabBarKey: tabBarKey));
245
    final TabController controller = DefaultTabController.of(tester.element(find.text('AAAA')));
Hans Muller's avatar
Hans Muller committed
246 247
    expect(controller, isNotNull);
    expect(controller.index, 0);
248 249

    // Fling-scroll the TabBar to the left
250
    expect(tester.getCenter(find.text('HHHH')).dx, lessThan(700.0));
251
    await tester.fling(find.byKey(tabBarKey), const Offset(-200.0, 0.0), 10000.0);
252 253
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
254
    expect(tester.getCenter(find.text('HHHH')).dx, lessThan(500.0));
255 256

    // Scrolling the TabBar doesn't change the selection
Hans Muller's avatar
Hans Muller committed
257
    expect(controller.index, 0);
Hans Muller's avatar
Hans Muller committed
258
  });
Adam Barth's avatar
Adam Barth committed
259

Hans Muller's avatar
Hans Muller committed
260
  testWidgets('TabBarView maintains state', (WidgetTester tester) async {
261
    final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE'];
262 263 264
    String value = tabs[0];

    Widget builder() {
265
      return boilerplate(
Hans Muller's avatar
Hans Muller committed
266 267 268 269
        child: new DefaultTabController(
          initialIndex: tabs.indexOf(value),
          length: tabs.length,
          child: new TabBarView(
270 271 272 273 274
            children: tabs.map((String name) {
              return new StateMarker(
                child: new Text(name)
              );
            }).toList()
Hans Muller's avatar
Hans Muller committed
275 276
          ),
        ),
277 278 279 280 281 282 283
      );
    }

    StateMarkerState findStateMarkerState(String name) {
      return tester.state(find.widgetWithText(StateMarker, name));
    }

284
    await tester.pumpWidget(builder());
285
    final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
Hans Muller's avatar
Hans Muller committed
286

287
    TestGesture gesture = await tester.startGesture(tester.getCenter(find.text(tabs[0])));
288
    await gesture.moveBy(const Offset(-600.0, 0.0));
289
    await tester.pump();
290 291
    expect(value, equals(tabs[0]));
    findStateMarkerState(tabs[1]).marker = 'marked';
292 293 294
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
Hans Muller's avatar
Hans Muller committed
295
    value = tabs[controller.index];
296
    expect(value, equals(tabs[1]));
297
    await tester.pumpWidget(builder());
298 299 300 301
    expect(findStateMarkerState(tabs[1]).marker, equals('marked'));

    // Move to the third tab.

302
    gesture = await tester.startGesture(tester.getCenter(find.text(tabs[1])));
303
    await gesture.moveBy(const Offset(-600.0, 0.0));
304 305
    await gesture.up();
    await tester.pump();
306
    expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
307
    await tester.pump(const Duration(seconds: 1));
Hans Muller's avatar
Hans Muller committed
308
    value = tabs[controller.index];
309
    expect(value, equals(tabs[2]));
310
    await tester.pumpWidget(builder());
311 312 313 314 315 316 317

    // The state is now gone.

    expect(find.text(tabs[1]), findsNothing);

    // Move back to the second tab.

318
    gesture = await tester.startGesture(tester.getCenter(find.text(tabs[2])));
319
    await gesture.moveBy(const Offset(600.0, 0.0));
320
    await tester.pump();
321
    final StateMarkerState markerState = findStateMarkerState(tabs[1]);
322 323
    expect(markerState.marker, isNull);
    markerState.marker = 'marked';
324 325 326
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
Hans Muller's avatar
Hans Muller committed
327
    value = tabs[controller.index];
328
    expect(value, equals(tabs[1]));
329
    await tester.pumpWidget(builder());
330
    expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
Adam Barth's avatar
Adam Barth committed
331
  });
332 333

  testWidgets('TabBar left/right fling', (WidgetTester tester) async {
334
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
335 336 337 338 339 340 341

    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);

342
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
Hans Muller's avatar
Hans Muller committed
343
    expect(controller.index, 0);
344 345

    // Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
346
    Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
347
    await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
348
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
349
    expect(controller.index, 1);
350 351 352 353 354
    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'));
355
    await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
356
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
357
    expect(controller.index, 0);
358 359 360 361
    expect(find.text('LEFT CHILD'), findsOneWidget);
    expect(find.text('RIGHT CHILD'), findsNothing);
  });

362
  testWidgets('TabBar left/right fling reverse (1)', (WidgetTester tester) async {
363
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
364 365 366 367 368 369 370

    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);

371
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
372 373
    expect(controller.index, 0);

374
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
375 376 377 378 379 380 381 382 383
    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 {
384
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
385 386 387 388 389 390 391

    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);

392
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
393 394
    expect(controller.index, 0);

395
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
396 397 398 399 400 401 402 403 404
    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);
  });

405
  // A regression test for https://github.com/flutter/flutter/issues/5095
406
  testWidgets('TabBar left/right fling reverse (2)', (WidgetTester tester) async {
407
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
408 409 410 411 412 413 414

    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);

415
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
Hans Muller's avatar
Hans Muller committed
416
    expect(controller.index, 0);
417

418
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
419
    final TestGesture gesture = await tester.startGesture(flingStart);
420 421 422 423
    for (int index = 0; index > 50; index += 1) {
      await gesture.moveBy(const Offset(-10.0, 0.0));
      await tester.pump(const Duration(milliseconds: 1));
    }
424 425 426
    // End the fling by reversing direction. This should cause not cause
    // a change to the selected tab, everything should just settle back to
    // to where it started.
427 428 429 430 431
    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();
432 433
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
Hans Muller's avatar
Hans Muller committed
434
    expect(controller.index, 0);
435 436 437 438
    expect(find.text('LEFT CHILD'), findsOneWidget);
    expect(find.text('RIGHT CHILD'), findsNothing);
  });

439 440
  // A regression test for https://github.com/flutter/flutter/issues/7133
  testWidgets('TabBar fling velocity', (WidgetTester tester) async {
441
    final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
442 443 444 445 446 447 448 449 450
    int index = 0;

    await tester.pumpWidget(
      new MaterialApp(
        home: new Align(
          alignment: FractionalOffset.topLeft,
          child: new SizedBox(
            width: 300.0,
            height: 200.0,
Hans Muller's avatar
Hans Muller committed
451 452
            child: new DefaultTabController(
              length: tabs.length,
453 454
              child: new Scaffold(
                appBar: new AppBar(
455
                  title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
456
                  bottom: new TabBar(
457
                    isScrollable: true,
Hans Muller's avatar
Hans Muller committed
458
                    tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
459 460
                  ),
                ),
Hans Muller's avatar
Hans Muller committed
461
                body: new TabBarView(
462 463 464 465 466 467 468 469 470 471
                  children: tabs.map((String name) => new Text('${index++}')).toList(),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    // After a small slow fling to the left, we expect the second item to still be visible.
472
    await tester.fling(find.text('AAAAAA'), const Offset(-25.0, 0.0), 100.0);
473 474 475
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    final RenderBox box = tester.renderObject(find.text('BBBBBB'));
476
    expect(box.localToGlobal(Offset.zero).dx, greaterThan(0.0));
477
  });
Hans Muller's avatar
Hans Muller committed
478 479

  testWidgets('TabController change notification', (WidgetTester tester) async {
480
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
Hans Muller's avatar
Hans Muller committed
481 482

    await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
483
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
Hans Muller's avatar
Hans Muller committed
484 485 486 487 488 489 490 491 492 493

    expect(controller, isNotNull);
    expect(controller.index, 0);

    String value;
    controller.addListener(() {
      value = tabs[controller.index];
    });

    await tester.tap(find.text('RIGHT'));
494
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
495 496 497
    expect(value, 'RIGHT');

    await tester.tap(find.text('LEFT'));
498
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
499 500
    expect(value, 'LEFT');

501
    final Offset leftFlingStart = tester.getCenter(find.text('LEFT CHILD'));
Hans Muller's avatar
Hans Muller committed
502
    await tester.flingFrom(leftFlingStart, const Offset(-200.0, 0.0), 10000.0);
503
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
504 505
    expect(value, 'RIGHT');

506
    final Offset rightFlingStart = tester.getCenter(find.text('RIGHT CHILD'));
Hans Muller's avatar
Hans Muller committed
507
    await tester.flingFrom(rightFlingStart, const Offset(200.0, 0.0), 10000.0);
508
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
509 510 511 512
    expect(value, 'LEFT');
  });

  testWidgets('Explicit TabController', (WidgetTester tester) async {
513
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
Hans Muller's avatar
Hans Muller committed
514 515 516 517 518 519 520 521
    TabController tabController;

    Widget buildTabControllerFrame(BuildContext context, TabController controller) {
      tabController = controller;
      return new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.android),
        home: new Scaffold(
          appBar: new AppBar(
522
            title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
523 524 525 526 527 528 529 530
            bottom: new TabBar(
              controller: controller,
              tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
            ),
          ),
          body: new TabBarView(
            controller: controller,
            children: <Widget>[
531 532
              const Center(child: const Text('LEFT CHILD')),
              const Center(child: const Text('RIGHT CHILD'))
Hans Muller's avatar
Hans Muller committed
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571
            ]
          ),
        ),
      );
    }

    await tester.pumpWidget(new TabControllerFrame(
      builder: buildTabControllerFrame,
      length: tabs.length,
      initialIndex: 1,
    ));

    expect(find.text('LEFT'), findsOneWidget);
    expect(find.text('RIGHT'), findsOneWidget);
    expect(find.text('LEFT CHILD'), findsNothing);
    expect(find.text('RIGHT CHILD'), findsOneWidget);
    expect(tabController.index, 1);
    expect(tabController.previousIndex, 1);
    expect(tabController.indexIsChanging, false);
    expect(tabController.animation.value, 1.0);
    expect(tabController.animation.status, AnimationStatus.completed);

    tabController.index = 0;
    await tester.pump(const Duration(milliseconds: 500));
    await tester.pump(const Duration(milliseconds: 500));
    expect(find.text('LEFT CHILD'), findsOneWidget);
    expect(find.text('RIGHT CHILD'), findsNothing);

    tabController.index = 1;
    await tester.pump(const Duration(milliseconds: 500));
    await tester.pump(const Duration(milliseconds: 500));
    expect(find.text('LEFT CHILD'), findsNothing);
    expect(find.text('RIGHT CHILD'), findsOneWidget);
  });

  testWidgets('TabController listener resets index', (WidgetTester tester) async {
    // This is a regression test for the scenario brought up here
    // https://github.com/flutter/flutter/pull/7387#pullrequestreview-15630946

572
    final List<String> tabs = <String>['A', 'B', 'C'];
Hans Muller's avatar
Hans Muller committed
573 574 575 576 577 578 579 580
    TabController tabController;

    Widget buildTabControllerFrame(BuildContext context, TabController controller) {
      tabController = controller;
      return new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.android),
        home: new Scaffold(
          appBar: new AppBar(
581
            title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
582 583 584 585 586 587 588 589
            bottom: new TabBar(
              controller: controller,
              tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
            ),
          ),
          body: new TabBarView(
            controller: controller,
            children: <Widget>[
590 591 592
              const Center(child: const Text('CHILD A')),
              const Center(child: const Text('CHILD B')),
              const Center(child: const Text('CHILD C')),
Hans Muller's avatar
Hans Muller committed
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
            ]
          ),
        ),
      );
    }

    await tester.pumpWidget(new TabControllerFrame(
      builder: buildTabControllerFrame,
      length: tabs.length,
    ));

    tabController.animation.addListener(() {
      if (tabController.animation.status == AnimationStatus.forward)
        tabController.index = 2;
      expect(tabController.indexIsChanging, true);
    });

    expect(tabController.index, 0);
    expect(tabController.indexIsChanging, false);

    tabController.animateTo(1, duration: const Duration(milliseconds: 200), curve: Curves.linear);
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 300));

    expect(tabController.index, 2);
    expect(tabController.indexIsChanging, false);
  });

  testWidgets('TabBarView child disposed during animation', (WidgetTester tester) async {
    // This is a regression test for the scenario brought up here
    // https://github.com/flutter/flutter/pull/7387#discussion_r95089191x

625
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
Hans Muller's avatar
Hans Muller committed
626 627 628
    await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));

    // Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
629
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
Hans Muller's avatar
Hans Muller committed
630 631 632 633 634
    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
  });

635
  testWidgets('TabBar unselectedLabelColor control test', (WidgetTester tester) async {
636
    final TabController controller = new TabController(
637 638 639 640 641 642 643 644
      vsync: const TestVSync(),
      length: 2,
    );

    Color firstColor;
    Color secondColor;

    await tester.pumpWidget(
645
      boilerplate(
646 647 648 649 650 651 652 653
        child: new TabBar(
          controller: controller,
          labelColor: Colors.green[500],
          unselectedLabelColor: Colors.blue[500],
          tabs: <Widget>[
            new Builder(
              builder: (BuildContext context) {
                firstColor = IconTheme.of(context).color;
654
                return const Text('First');
655 656 657 658 659
              }
            ),
            new Builder(
              builder: (BuildContext context) {
                secondColor = IconTheme.of(context).color;
660
                return const Text('Second');
661 662 663 664 665 666 667 668 669 670 671
              }
            ),
          ],
        ),
      ),
    );

    expect(firstColor, equals(Colors.green[500]));
    expect(secondColor, equals(Colors.blue[500]));
  });

672
  testWidgets('TabBarView page left and right test', (WidgetTester tester) async {
673
    final TabController controller = new TabController(
674 675 676 677 678
      vsync: const TestVSync(),
      length: 2,
    );

    await tester.pumpWidget(
679
      boilerplate(
680 681
        child: new TabBarView(
          controller: controller,
682
          children: <Widget>[ const Text('First'), const Text('Second') ],
683 684 685 686 687 688
        ),
      ),
    );

    expect(controller.index, equals(0));

689
    TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
690 691
    expect(controller.index, equals(0));

692 693 694 695
    // 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));
696
    expect(controller.index, equals(0));
697 698
    expect(find.text('First'), findsOneWidget);
    expect(find.text('Second'), findsNothing);
699

700 701 702 703 704 705
    // 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
706
    expect(controller.index, equals(1));
707 708
    expect(find.text('First'), findsNothing);
    expect(find.text('Second'), findsOneWidget);
709

710
    gesture = await tester.startGesture(const Offset(100.0, 100.0));
711 712
    expect(controller.index, equals(1));

713 714 715 716
    // 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));
717 718 719 720
    expect(controller.index, equals(1));
    expect(find.text('First'), findsNothing);
    expect(find.text('Second'), findsOneWidget);

721 722 723 724 725 726 727 728 729 730
    // 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);
  });
731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765

  testWidgets('TabBar tap animates the selection indicator', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/7479

    final List<String> tabs = <String>['A', 'B'];

    const Color indicatorColor = const Color(0xFFFF0000);
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'A', indicatorColor: indicatorColor));

    final RenderBox box = tester.renderObject(find.byType(TabBar));
    final TabIndicatorRecordingCanvas canvas = new TabIndicatorRecordingCanvas(indicatorColor);
    final TestRecordingPaintingContext context = new TestRecordingPaintingContext(canvas);

    box.paint(context, Offset.zero);
    final Rect indicatorRect0 = canvas.indicatorRect;
    expect(indicatorRect0.left, 0.0);
    expect(indicatorRect0.width, 400.0);
    expect(indicatorRect0.height, 2.0);

    await tester.tap(find.text('B'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    box.paint(context, Offset.zero);
    final Rect indicatorRect1 = canvas.indicatorRect;
    expect(indicatorRect1.left, greaterThan(indicatorRect0.left));
    expect(indicatorRect1.right, lessThan(800.0));
    expect(indicatorRect1.height, 2.0);

    await tester.pump(const Duration(milliseconds: 300));
    box.paint(context, Offset.zero);
    final Rect indicatorRect2 = canvas.indicatorRect;
    expect(indicatorRect2.left, 400.0);
    expect(indicatorRect2.width, 400.0);
    expect(indicatorRect2.height, 2.0);
  });
766 767 768 769 770 771 772 773 774 775 776

  testWidgets('TabBarView child disposed during animation', (WidgetTester tester) async {
    // This is a regression test for this patch:
    // https://github.com/flutter/flutter/pull/9015

    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: 2,
    );

    Widget buildFrame() {
777
      return boilerplate(
778 779 780
        child: new TabBar(
          key: new UniqueKey(),
          controller: controller,
781
          tabs: <Widget>[ const Text('A'), const Text('B') ],
782 783 784 785 786 787 788 789 790 791 792 793 794
        ),
      );
    }

    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));
  });
795 796 797 798 799 800 801 802 803 804

  testWidgets('TabBarView scrolls end very VERY close to a new page', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/9375

    final TabController tabController = new TabController(
      vsync: const TestVSync(),
      initialIndex: 1,
      length: 3,
    );

805 806 807
    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new SizedBox.expand(
808 809 810 811 812 813 814 815 816 817 818 819 820 821 822
        child: new Center(
          child: new SizedBox(
            width: 400.0,
            height: 400.0,
            child: new TabBarView(
              controller: tabController,
              children: <Widget>[
                const Center(child: const Text('0')),
                const Center(child: const Text('1')),
                const Center(child: const Text('2')),
              ],
            ),
          ),
        ),
      ),
823
    ));
824 825 826 827 828 829

    expect(tabController.index, 1);

    final PageView pageView = tester.widget(find.byType(PageView));
    final PageController pageController = pageView.controller;
    final ScrollPosition position = pageController.position;
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851

    // The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
    // page 1 is at 400.0, page 2 is at 800.0.

    expect(position.pixels, 400.0);

    // Not close enough to switch to page 2
    pageController.jumpTo(800.0 - 1.25 * position.physics.tolerance.distance);
    expect(tabController.index, 1);

    // Close enough to switch to page 2
    pageController.jumpTo(800.0 - 0.75 * position.physics.tolerance.distance);
    expect(tabController.index, 2);
  });

  testWidgets('TabBarView scrolls end very close to a new page with custom physics', (WidgetTester tester) async {
    final TabController tabController = new TabController(
      vsync: const TestVSync(),
      initialIndex: 1,
      length: 3,
    );

852 853 854
    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new SizedBox.expand(
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870
        child: new Center(
          child: new SizedBox(
            width: 400.0,
            height: 400.0,
            child: new TabBarView(
              controller: tabController,
              physics: const TestScrollPhysics(),
              children: <Widget>[
                const Center(child: const Text('0')),
                const Center(child: const Text('1')),
                const Center(child: const Text('2')),
              ],
            ),
          ),
        ),
      ),
871
    ));
872 873 874 875 876 877

    expect(tabController.index, 1);

    final PageView pageView = tester.widget(find.byType(PageView));
    final PageController pageController = pageView.controller;
    final ScrollPosition position = pageController.position;
878 879 880 881 882 883 884 885 886 887 888 889 890

    // The TabBarView's page width is 400, so page 0 is at scroll offset 0.0,
    // page 1 is at 400.0, page 2 is at 800.0.

    expect(position.pixels, 400.0);

    // Not close enough to switch to page 2
    pageController.jumpTo(800.0 - 1.25 * position.physics.tolerance.distance);
    expect(tabController.index, 1);

    // Close enough to switch to page 2
    pageController.jumpTo(800.0 - 0.75 * position.physics.tolerance.distance);
    expect(tabController.index, 2);
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906
  });

  testWidgets('Scrollable TabBar with a non-zero TabController initialIndex', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/9374

    final List<Tab> tabs = new List<Tab>.generate(20, (int index) {
      return new Tab(text: 'TAB #$index');
    });

    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: tabs.length,
      initialIndex: tabs.length - 1,
    );

    await tester.pumpWidget(
907
      boilerplate(
908 909 910 911 912 913 914
        child: new TabBar(
          isScrollable: true,
          controller: controller,
          tabs: tabs,
        ),
      ),
    );
915

916 917 918
    // The initialIndex tab should be visible and right justified
    expect(find.text('TAB #19'), findsOneWidget);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB #19')).dx, 800.0);
919
  });
920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940

  testWidgets('TabBar with indicatorWeight, indicatorPadding', (WidgetTester tester) async {
    const Color color = const Color(0xFF00FF00);
    const double height = 100.0;
    const double weight = 8.0;
    const double padLeft = 8.0;
    const double padRight = 4.0;

    final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
      return new Container(
        key: new ValueKey<int>(index),
        height: height,
      );
    });

    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: tabs.length,
    );

    await tester.pumpWidget(
941
      boilerplate(
942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983
        child: new Column(
          children: <Widget>[
            new TabBar(
              indicatorWeight: 8.0,
              indicatorColor: color,
              indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight),
              controller: controller,
              tabs: tabs,
            ),
            new Flexible(child: new Container()),
          ],
        ),
      ),
    );

    final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));

    // Selected tab dimensions
    double tabWidth = tester.getSize(find.byKey(const ValueKey<int>(0))).width;
    double tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(0))).dx;
    double tabRight = tabLeft + tabWidth;

    expect(tabBarBox, paints..rect(
      style: PaintingStyle.fill,
      color: color,
      rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight)
    ));

    // Select tab 3
    controller.index = 3;
    await tester.pumpAndSettle();

    tabWidth = tester.getSize(find.byKey(const ValueKey<int>(3))).width;
    tabLeft = tester.getTopLeft(find.byKey(const ValueKey<int>(3))).dx;
    tabRight = tabLeft + tabWidth;

    expect(tabBarBox, paints..rect(
      style: PaintingStyle.fill,
      color: color,
      rect: new Rect.fromLTRB(tabLeft + padLeft, height, tabRight - padRight, height + weight)
    ));
  });
984

985 986 987 988 989 990 991 992 993 994 995 996 997 998
  testWidgets('correct semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = new SemanticsTester(tester);

    final List<Tab> tabs = new List<Tab>.generate(2, (int index) {
      return new Tab(text: 'TAB #$index');
    });

    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: tabs.length,
      initialIndex: 0,
    );

    await tester.pumpWidget(
999
      boilerplate(
1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
        child: new Semantics(
          container: true,
          child: new TabBar(
            isScrollable: true,
            controller: controller,
            tabs: tabs,
          ),
        ),
      ),
    );

    final TestSemantics expectedSemantics = new TestSemantics.root(
      children: <TestSemantics>[
        new TestSemantics.rootChild(
          id: 1,
          rect: TestSemantics.fullScreen,
          children: <TestSemantics>[
            new TestSemantics(
              id: 2,
              actions: SemanticsAction.tap.index,
              flags: SemanticsFlags.isSelected.index,
1021
              label: 'TAB #0\nTab 1 of 2',
1022
              rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
1023 1024 1025
              transform: new Matrix4.translationValues(0.0, 276.0, 0.0),
            ),
            new TestSemantics(
1026
              id: 5,
1027
              actions: SemanticsAction.tap.index,
1028
              label: 'TAB #1\nTab 2 of 2',
1029
              rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
              transform: new Matrix4.translationValues(108.0, 276.0, 0.0),
            ),
          ]),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics));

    semantics.dispose();
  });
1040 1041 1042 1043 1044 1045 1046 1047

  testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: 0,
    );

    await tester.pumpWidget(
1048
      boilerplate(
1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087
        child: new Column(
          children: <Widget>[
            new TabBar(
              controller: controller,
              tabs: const <Widget>[],
            ),
            new Flexible(
              child: new TabBarView(
                controller: controller,
                children: const <Widget>[],
              ),
            ),
          ],
        ),
      ),
    );

    expect(controller.index, 0);
    expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0));
    expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0));

    // A fling in the TabBar or TabBarView, shouldn't do anything.

    await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0));
    await(tester.pumpAndSettle());

    await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0));
    await(tester.pumpAndSettle());

    expect(controller.index, 0);
  });

  testWidgets('TabBar etc with one tab', (WidgetTester tester) async {
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: 1,
    );

    await tester.pumpWidget(
1088
      boilerplate(
1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134
        child: new Column(
          children: <Widget>[
            new TabBar(
              controller: controller,
              tabs: const <Widget>[const Tab(text: 'TAB')],
            ),
            new Flexible(
              child: new TabBarView(
                controller: controller,
                children: const <Widget>[const Text('PAGE')],
              ),
            ),
          ],
        ),
      ),
    );

    expect(controller.index, 0);
    expect(find.text('TAB'), findsOneWidget);
    expect(find.text('PAGE'), findsOneWidget);
    expect(tester.getSize(find.byType(TabBar)), const Size(800.0, 48.0));
    expect(tester.getSize(find.byType(TabBarView)), const Size(800.0, 600.0 - 48.0));

    // The one tab spans the app's width
    expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);

    // A fling in the TabBar or TabBarView, shouldn't move the tab.

    await(tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0));
    await(tester.pump(const Duration(milliseconds: 50)));
    expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
    await(tester.pumpAndSettle());

    await(tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0));
    await(tester.pump(const Duration(milliseconds: 50)));
    expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, 0);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, 800);
    await(tester.pumpAndSettle());

    expect(controller.index, 0);
    expect(find.text('TAB'), findsOneWidget);
    expect(find.text('PAGE'), findsOneWidget);
  });

1135 1136 1137 1138 1139 1140 1141 1142
  testWidgets('can tap on indicator at very bottom of TabBar to switch tabs', (WidgetTester tester) async {
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: 2,
      initialIndex: 0,
    );

    await tester.pumpWidget(
1143
      boilerplate(
1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
        child: new Column(
          children: <Widget>[
            new TabBar(
              controller: controller,
              indicatorWeight: 30.0,
              tabs: const <Widget>[const Tab(text: 'TAB1'), const Tab(text: 'TAB2')],
            ),
            new Flexible(
              child: new TabBarView(
                controller: controller,
                children: const <Widget>[const Text('PAGE1'), const Text('PAGE2')],
              ),
            ),
          ],
        ),
      ),
    );

    expect(controller.index, 0);

    final Offset bottomRight = tester.getBottomRight(find.byType(TabBar)) - const Offset(1.0, 1.0);
    final TestGesture gesture = await tester.startGesture(bottomRight);
    await gesture.up();
    await tester.pumpAndSettle();

    expect(controller.index, 1);
  });
1171
}