tabs_test.dart 59.8 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.

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

11
import '../rendering/mock_canvas.dart';
12
import '../rendering/recording_canvas.dart';
13
import '../widgets/semantics_tester.dart';
14

15
Widget boilerplate({ Widget child, TextDirection textDirection = TextDirection.ltr }) {
16 17
  return new Localizations(
    locale: const Locale('en', 'US'),
18
    delegates: const <LocalizationsDelegate<dynamic>>[
19 20 21 22 23 24 25 26
      DefaultMaterialLocalizations.delegate,
      DefaultWidgetsLocalizations.delegate,
    ],
    child: new Directionality(
      textDirection: textDirection,
      child: new Material(
        child: child,
      ),
27 28 29 30
    ),
  );
}

Adam Barth's avatar
Adam Barth committed
31
class StateMarker extends StatefulWidget {
32
  const StateMarker({ Key key, this.child }) : super(key: key);
Adam Barth's avatar
Adam Barth committed
33 34 35 36 37 38 39 40 41 42 43 44

  final Widget child;

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

class StateMarkerState extends State<StateMarker> {
  String marker;

  @override
  Widget build(BuildContext context) {
45 46
    if (widget.child != null)
      return widget.child;
Adam Barth's avatar
Adam Barth committed
47 48 49 50
    return new Container();
  }
}

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

72
typedef Widget TabControllerFrameBuilder(BuildContext context, TabController controller);
Hans Muller's avatar
Hans Muller committed
73 74

class TabControllerFrame extends StatefulWidget {
75
  const TabControllerFrame({ this.length, this.initialIndex = 0, this.builder });
Hans Muller's avatar
Hans Muller committed
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

  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,
93 94
      length: widget.length,
      initialIndex: widget.initialIndex,
Hans Muller's avatar
Hans Muller committed
95 96 97 98 99 100 101 102 103 104 105
    );
  }

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

  @override
  Widget build(BuildContext context) {
106
    return widget.builder(context, _controller);
Hans Muller's avatar
Hans Muller committed
107 108
  }
}
109 110 111 112

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

134 135 136 137 138 139 140
class TabIndicatorRecordingCanvas extends TestRecordingCanvas {
  TabIndicatorRecordingCanvas(this.indicatorColor);

  final Color indicatorColor;
  Rect indicatorRect;

  @override
141 142 143
  void drawLine(Offset p1, Offset p2, Paint paint) {
    // Assuming that the indicatorWeight is 2.0, the default.
    const double indicatorWeight = 2.0;
144
    if (paint.color == indicatorColor)
145
      indicatorRect = new Rect.fromPoints(p1, p2).inflate(indicatorWeight / 2.0);
146 147 148
  }
}

149 150
class TestScrollPhysics extends ScrollPhysics {
  const TestScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
151

152 153 154 155
  @override
  TestScrollPhysics applyTo(ScrollPhysics ancestor) {
    return new TestScrollPhysics(parent: buildParent(ancestor));
  }
156

157 158
  static final SpringDescription _kDefaultSpring = new SpringDescription.withDampingRatio(
    mass: 0.5,
159
    stiffness: 500.0,
160 161
    ratio: 1.1,
  );
162

163 164 165 166
  @override
  SpringDescription get spring => _kDefaultSpring;
}

167
void main() {
168 169 170 171
  setUp(() {
    debugResetSemanticsIdCounter();
  });

172
  testWidgets('TabBar tap selects tab', (WidgetTester tester) async {
173
    final List<String> tabs = <String>['A', 'B', 'C'];
174

175
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
176 177 178
    expect(find.text('A'), findsOneWidget);
    expect(find.text('B'), findsOneWidget);
    expect(find.text('C'), findsOneWidget);
179
    final TabController controller = DefaultTabController.of(tester.element(find.text('A')));
Hans Muller's avatar
Hans Muller committed
180 181 182
    expect(controller, isNotNull);
    expect(controller.index, 2);
    expect(controller.previousIndex, 2);
183

Hans Muller's avatar
Hans Muller committed
184
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: false));
185 186
    await tester.tap(find.text('B'));
    await tester.pump();
Hans Muller's avatar
Hans Muller committed
187
    expect(controller.indexIsChanging, true);
188
    await tester.pump(const Duration(seconds: 1)); // finish the animation
Hans Muller's avatar
Hans Muller committed
189 190 191
    expect(controller.index, 1);
    expect(controller.previousIndex, 2);
    expect(controller.indexIsChanging, false);
192

193 194 195 196
    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
197 198
    expect(controller.index, 2);
    expect(controller.previousIndex, 1);
199

200 201 202 203
    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
204 205
    expect(controller.index, 0);
    expect(controller.previousIndex, 2);
206 207
  });

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

211
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'C', isScrollable: true));
212 213 214
    expect(find.text('A'), findsOneWidget);
    expect(find.text('B'), findsOneWidget);
    expect(find.text('C'), findsOneWidget);
215
    final TabController controller = DefaultTabController.of(tester.element(find.text('A')));
Hans Muller's avatar
Hans Muller committed
216 217
    expect(controller.index, 2);
    expect(controller.previousIndex, 2);
218

Hans Muller's avatar
Hans Muller committed
219
    await tester.tap(find.text('C'));
220
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
221
    expect(controller.index, 2);
222

Hans Muller's avatar
Hans Muller committed
223
    await tester.tap(find.text('B'));
224
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
225
    expect(controller.index, 1);
226

227
    await tester.tap(find.text('A'));
228
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
229
    expect(controller.index, 0);
230
  });
Hans Muller's avatar
Hans Muller committed
231

232
  testWidgets('Scrollable TabBar tap centers selected tab', (WidgetTester tester) async {
233
    final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
234
    const Key tabBarKey = Key('TabBar');
235
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAAAA', isScrollable: true, tabBarKey: tabBarKey));
236
    final TabController controller = DefaultTabController.of(tester.element(find.text('AAAAAA')));
Hans Muller's avatar
Hans Muller committed
237 238
    expect(controller, isNotNull);
    expect(controller.index, 0);
239 240 241

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

244
    await tester.tap(find.text('FFFFFF'));
245
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
246
    expect(controller.index, 5);
247
    // The center of the FFFFFF item is now at the TabBar's center
248
    expect(tester.getCenter(find.text('FFFFFF')).dx, closeTo(400.0, 1.0));
Hans Muller's avatar
Hans Muller committed
249 250 251
  });


252
  testWidgets('TabBar can be scrolled independent of the selection', (WidgetTester tester) async {
253
    final List<String> tabs = <String>['AAAA', 'BBBB', 'CCCC', 'DDDD', 'EEEE', 'FFFF', 'GGGG', 'HHHH', 'IIII', 'JJJJ', 'KKKK', 'LLLL'];
254
    const Key tabBarKey = Key('TabBar');
255
    await tester.pumpWidget(buildFrame(tabs: tabs, value: 'AAAA', isScrollable: true, tabBarKey: tabBarKey));
256
    final TabController controller = DefaultTabController.of(tester.element(find.text('AAAA')));
Hans Muller's avatar
Hans Muller committed
257 258
    expect(controller, isNotNull);
    expect(controller.index, 0);
259 260

    // Fling-scroll the TabBar to the left
261
    expect(tester.getCenter(find.text('HHHH')).dx, lessThan(700.0));
262
    await tester.fling(find.byKey(tabBarKey), const Offset(-200.0, 0.0), 10000.0);
263 264
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
265
    expect(tester.getCenter(find.text('HHHH')).dx, lessThan(500.0));
266 267

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

Hans Muller's avatar
Hans Muller committed
271
  testWidgets('TabBarView maintains state', (WidgetTester tester) async {
272
    final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE'];
273 274 275
    String value = tabs[0];

    Widget builder() {
276
      return boilerplate(
Hans Muller's avatar
Hans Muller committed
277 278 279 280
        child: new DefaultTabController(
          initialIndex: tabs.indexOf(value),
          length: tabs.length,
          child: new TabBarView(
281 282 283 284 285
            children: tabs.map((String name) {
              return new StateMarker(
                child: new Text(name)
              );
            }).toList()
Hans Muller's avatar
Hans Muller committed
286 287
          ),
        ),
288 289 290 291
      );
    }

    StateMarkerState findStateMarkerState(String name) {
292
      return tester.state(find.widgetWithText(StateMarker, name, skipOffstage: false));
293 294
    }

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

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

    // Move to the third tab.

313
    gesture = await tester.startGesture(tester.getCenter(find.text(tabs[1])));
314
    await gesture.moveBy(const Offset(-600.0, 0.0));
315 316
    await gesture.up();
    await tester.pump();
317
    expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
318
    await tester.pump(const Duration(seconds: 1));
Hans Muller's avatar
Hans Muller committed
319
    value = tabs[controller.index];
320
    expect(value, equals(tabs[2]));
321
    await tester.pumpWidget(builder());
322 323 324 325 326 327 328

    // The state is now gone.

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

    // Move back to the second tab.

329
    gesture = await tester.startGesture(tester.getCenter(find.text(tabs[2])));
330
    await gesture.moveBy(const Offset(600.0, 0.0));
331
    await tester.pump();
332
    final StateMarkerState markerState = findStateMarkerState(tabs[1]);
333 334
    expect(markerState.marker, isNull);
    markerState.marker = 'marked';
335 336 337
    await gesture.up();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
Hans Muller's avatar
Hans Muller committed
338
    value = tabs[controller.index];
339
    expect(value, equals(tabs[1]));
340
    await tester.pumpWidget(builder());
341
    expect(findStateMarkerState(tabs[1]).marker, equals('marked'));
Adam Barth's avatar
Adam Barth committed
342
  });
343 344

  testWidgets('TabBar left/right fling', (WidgetTester tester) async {
345
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
346 347 348 349 350 351 352

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

353
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
Hans Muller's avatar
Hans Muller committed
354
    expect(controller.index, 0);
355 356

    // Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
357
    Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
358
    await tester.flingFrom(flingStart, const Offset(-200.0, 0.0), 10000.0);
359
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
360
    expect(controller.index, 1);
361 362 363 364 365
    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'));
366
    await tester.flingFrom(flingStart, const Offset(200.0, 0.0), 10000.0);
367
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
368
    expect(controller.index, 0);
369 370 371 372
    expect(find.text('LEFT CHILD'), findsOneWidget);
    expect(find.text('RIGHT CHILD'), findsNothing);
  });

373
  testWidgets('TabBar left/right fling reverse (1)', (WidgetTester tester) async {
374
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
375 376 377 378 379 380 381

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

382
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
383 384
    expect(controller.index, 0);

385
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
386 387 388 389 390 391 392 393 394
    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 {
395
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
396 397 398 399 400 401 402

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

403
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
404 405
    expect(controller.index, 0);

406
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
407 408 409 410 411 412 413 414 415
    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);
  });

416
  // A regression test for https://github.com/flutter/flutter/issues/5095
417
  testWidgets('TabBar left/right fling reverse (2)', (WidgetTester tester) async {
418
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
419 420 421 422 423 424 425

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

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

429
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
430
    final TestGesture gesture = await tester.startGesture(flingStart);
431 432 433 434
    for (int index = 0; index > 50; index += 1) {
      await gesture.moveBy(const Offset(-10.0, 0.0));
      await tester.pump(const Duration(milliseconds: 1));
    }
435 436 437
    // 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.
438 439 440 441 442
    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();
443 444
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
Hans Muller's avatar
Hans Muller committed
445
    expect(controller.index, 0);
446 447 448 449
    expect(find.text('LEFT CHILD'), findsOneWidget);
    expect(find.text('RIGHT CHILD'), findsNothing);
  });

450 451
  // A regression test for https://github.com/flutter/flutter/issues/7133
  testWidgets('TabBar fling velocity', (WidgetTester tester) async {
452
    final List<String> tabs = <String>['AAAAAA', 'BBBBBB', 'CCCCCC', 'DDDDDD', 'EEEEEE', 'FFFFFF', 'GGGGGG', 'HHHHHH', 'IIIIII', 'JJJJJJ', 'KKKKKK', 'LLLLLL'];
453 454 455 456 457
    int index = 0;

    await tester.pumpWidget(
      new MaterialApp(
        home: new Align(
458
          alignment: Alignment.topLeft,
459 460 461
          child: new SizedBox(
            width: 300.0,
            height: 200.0,
Hans Muller's avatar
Hans Muller committed
462 463
            child: new DefaultTabController(
              length: tabs.length,
464 465
              child: new Scaffold(
                appBar: new AppBar(
466
                  title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
467
                  bottom: new TabBar(
468
                    isScrollable: true,
Hans Muller's avatar
Hans Muller committed
469
                    tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
470 471
                  ),
                ),
Hans Muller's avatar
Hans Muller committed
472
                body: new TabBarView(
473 474 475 476 477 478 479 480 481 482
                  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.
483
    await tester.fling(find.text('AAAAAA'), const Offset(-25.0, 0.0), 100.0);
484 485 486
    await tester.pump();
    await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
    final RenderBox box = tester.renderObject(find.text('BBBBBB'));
487
    expect(box.localToGlobal(Offset.zero).dx, greaterThan(0.0));
488
  });
Hans Muller's avatar
Hans Muller committed
489 490

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

    await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
494
    final TabController controller = DefaultTabController.of(tester.element(find.text('LEFT')));
Hans Muller's avatar
Hans Muller committed
495 496 497 498 499 500 501 502 503 504

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

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

    await tester.tap(find.text('RIGHT'));
505
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
506 507 508
    expect(value, 'RIGHT');

    await tester.tap(find.text('LEFT'));
509
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
510 511
    expect(value, 'LEFT');

512
    final Offset leftFlingStart = tester.getCenter(find.text('LEFT CHILD'));
Hans Muller's avatar
Hans Muller committed
513
    await tester.flingFrom(leftFlingStart, const Offset(-200.0, 0.0), 10000.0);
514
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
515 516
    expect(value, 'RIGHT');

517
    final Offset rightFlingStart = tester.getCenter(find.text('RIGHT CHILD'));
Hans Muller's avatar
Hans Muller committed
518
    await tester.flingFrom(rightFlingStart, const Offset(200.0, 0.0), 10000.0);
519
    await tester.pumpAndSettle();
Hans Muller's avatar
Hans Muller committed
520 521 522 523
    expect(value, 'LEFT');
  });

  testWidgets('Explicit TabController', (WidgetTester tester) async {
524
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
Hans Muller's avatar
Hans Muller committed
525 526 527 528 529 530 531 532
    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(
533
            title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
534 535 536 537 538 539 540
            bottom: new TabBar(
              controller: controller,
              tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
            ),
          ),
          body: new TabBarView(
            controller: controller,
541
            children: const <Widget>[
542 543
              Center(child: Text('LEFT CHILD')),
              Center(child: Text('RIGHT CHILD'))
Hans Muller's avatar
Hans Muller committed
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 572 573 574 575 576 577 578 579 580 581 582
            ]
          ),
        ),
      );
    }

    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

583
    final List<String> tabs = <String>['A', 'B', 'C'];
Hans Muller's avatar
Hans Muller committed
584 585 586 587 588 589 590 591
    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(
592
            title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
593 594 595 596 597 598 599
            bottom: new TabBar(
              controller: controller,
              tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
            ),
          ),
          body: new TabBarView(
            controller: controller,
600
            children: const <Widget>[
601 602 603
              Center(child: Text('CHILD A')),
              Center(child: Text('CHILD B')),
              Center(child: Text('CHILD C')),
Hans Muller's avatar
Hans Muller committed
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
            ]
          ),
        ),
      );
    }

    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

636
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
Hans Muller's avatar
Hans Muller committed
637 638 639
    await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));

    // Fling to the left, switch from the 'LEFT' tab to the 'RIGHT'
640
    final Offset flingStart = tester.getCenter(find.text('LEFT CHILD'));
Hans Muller's avatar
Hans Muller committed
641 642 643 644 645
    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
  });

646
  testWidgets('TabBar unselectedLabelColor control test', (WidgetTester tester) async {
647
    final TabController controller = new TabController(
648 649 650 651 652 653 654 655
      vsync: const TestVSync(),
      length: 2,
    );

    Color firstColor;
    Color secondColor;

    await tester.pumpWidget(
656
      boilerplate(
657 658 659 660 661 662 663 664
        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;
665
                return const Text('First');
666 667 668 669 670
              }
            ),
            new Builder(
              builder: (BuildContext context) {
                secondColor = IconTheme.of(context).color;
671
                return const Text('Second');
672 673 674 675 676 677 678 679 680 681 682
              }
            ),
          ],
        ),
      ),
    );

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

683
  testWidgets('TabBarView page left and right test', (WidgetTester tester) async {
684
    final TabController controller = new TabController(
685 686 687 688 689
      vsync: const TestVSync(),
      length: 2,
    );

    await tester.pumpWidget(
690
      boilerplate(
691 692
        child: new TabBarView(
          controller: controller,
693
          children: const <Widget>[ Text('First'), Text('Second') ],
694 695 696 697 698 699
        ),
      ),
    );

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

700
    TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0));
701 702
    expect(controller.index, equals(0));

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

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

721
    gesture = await tester.startGesture(const Offset(100.0, 100.0));
722 723
    expect(controller.index, equals(1));

724 725 726 727
    // 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));
728 729 730 731
    expect(controller.index, equals(1));
    expect(find.text('First'), findsNothing);
    expect(find.text('Second'), findsOneWidget);

732 733 734 735 736 737 738 739 740 741
    // 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);
  });
742 743 744 745 746 747

  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'];

748
    const Color indicatorColor = Color(0xFFFF0000);
749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
    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);
  });
777 778 779 780 781 782 783 784 785 786 787

  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() {
788
      return boilerplate(
789 790 791
        child: new TabBar(
          key: new UniqueKey(),
          controller: controller,
792
          tabs: const <Widget>[ Text('A'), Text('B') ],
793 794 795 796 797 798 799 800 801 802 803 804 805
        ),
      );
    }

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

807
  testWidgets('TabBarView scrolls end close to a new page', (WidgetTester tester) async {
808 809 810 811 812 813 814 815
    // 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,
    );

816 817 818
    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new SizedBox.expand(
819 820 821 822 823 824
        child: new Center(
          child: new SizedBox(
            width: 400.0,
            height: 400.0,
            child: new TabBarView(
              controller: tabController,
825
              children: const <Widget>[
826 827 828
                Center(child: Text('0')),
                Center(child: Text('1')),
                Center(child: Text('2')),
829 830 831 832 833
              ],
            ),
          ),
        ),
      ),
834
    ));
835 836 837 838 839 840

    expect(tabController.index, 1);

    final PageView pageView = tester.widget(find.byType(PageView));
    final PageController pageController = pageView.controller;
    final ScrollPosition position = pageController.position;
841 842 843 844 845 846 847

    // 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
848
    pageController.jumpTo(500.0);
849 850 851
    expect(tabController.index, 1);

    // Close enough to switch to page 2
852
    pageController.jumpTo(700.0);
853
    expect(tabController.index, 2);
854 855 856 857 858 859 860 861

    // 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);
862 863
  });

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

871 872 873
    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new SizedBox.expand(
874 875 876 877 878 879 880
        child: new Center(
          child: new SizedBox(
            width: 400.0,
            height: 400.0,
            child: new TabBarView(
              controller: tabController,
              physics: const TestScrollPhysics(),
881
              children: const <Widget>[
882 883 884
                Center(child: Text('0')),
                Center(child: Text('1')),
                Center(child: Text('2')),
885 886 887 888 889
              ],
            ),
          ),
        ),
      ),
890
    ));
891 892 893 894 895 896

    expect(tabController.index, 1);

    final PageView pageView = tester.widget(find.byType(PageView));
    final PageController pageController = pageView.controller;
    final ScrollPosition position = pageController.position;
897 898 899 900 901 902 903

    // 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
904
    pageController.jumpTo(500.0);
905 906 907
    expect(tabController.index, 1);

    // Close enough to switch to page 2
908
    pageController.jumpTo(700.0);
909
    expect(tabController.index, 2);
910 911 912 913 914 915 916 917

    // 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);
918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933
  });

  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(
934
      boilerplate(
935 936 937 938 939 940 941
        child: new TabBar(
          isScrollable: true,
          controller: controller,
          tabs: tabs,
        ),
      ),
    );
942

943 944
    // The initialIndex tab should be visible and right justified
    expect(find.text('TAB #19'), findsOneWidget);
945 946 947 948 949 950

    // 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);
951
  });
952

Ian Hickson's avatar
Ian Hickson committed
953
  testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async {
954
    const Color indicatorColor = Color(0xFF00FF00);
955
    const double indicatorWeight = 8.0;
956 957 958 959
    const double padLeft = 8.0;
    const double padRight = 4.0;

    final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
960
      return new Tab(text: 'Tab $index');
961 962 963 964 965 966 967 968
    });

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

    await tester.pumpWidget(
969
      boilerplate(
970 971 972 973 974 975 976 977 978
        child: new Container(
          alignment: Alignment.topLeft,
          child: new TabBar(
            indicatorWeight: indicatorWeight,
            indicatorColor: indicatorColor,
            indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight),
            controller: controller,
            tabs: tabs,
          ),
979 980 981 982 983
        ),
      ),
    );

    final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
984
    expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0)
985

986
    const double indicatorY = 54.0 - indicatorWeight / 2.0;
987 988
    double indicatorLeft = padLeft + indicatorWeight / 2.0;
    double indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0);
989

990 991 992 993 994
    expect(tabBarBox, paints..line(
      color: indicatorColor,
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
995 996 997 998 999 1000
    ));

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

1001 1002
    indicatorLeft = 600.0 + padLeft + indicatorWeight / 2.0;
    indicatorRight = 800.0 - (padRight + indicatorWeight / 2.0);
1003

1004 1005 1006 1007 1008
    expect(tabBarBox, paints..line(
      color: indicatorColor,
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
1009 1010
    ));
  });
1011

Ian Hickson's avatar
Ian Hickson committed
1012
  testWidgets('TabBar with indicatorWeight, indicatorPadding (RTL)', (WidgetTester tester) async {
1013
    const Color indicatorColor = Color(0xFF00FF00);
1014
    const double indicatorWeight = 8.0;
Ian Hickson's avatar
Ian Hickson committed
1015 1016 1017 1018
    const double padLeft = 8.0;
    const double padRight = 4.0;

    final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
1019
      return new Tab(text: 'Tab $index');
Ian Hickson's avatar
Ian Hickson committed
1020 1021 1022 1023 1024 1025 1026 1027 1028 1029
    });

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

    await tester.pumpWidget(
      boilerplate(
        textDirection: TextDirection.rtl,
1030 1031 1032 1033 1034 1035 1036 1037 1038
        child: new Container(
          alignment: Alignment.topLeft,
          child: new TabBar(
            indicatorWeight: indicatorWeight,
            indicatorColor: indicatorColor,
            indicatorPadding: const EdgeInsets.only(left: padLeft, right: padRight),
            controller: controller,
            tabs: tabs,
          ),
Ian Hickson's avatar
Ian Hickson committed
1039 1040 1041 1042 1043
        ),
      ),
    );

    final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
1044 1045 1046
    expect(tabBarBox.size.height, 54.0); // 54 = _kTabHeight(46) + indicatorWeight(8.0)
    expect(tabBarBox.size.width, 800.0);

1047
    const double indicatorY = 54.0 - indicatorWeight / 2.0;
1048 1049 1050 1051 1052 1053 1054 1055
    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: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1056 1057 1058 1059 1060 1061
    ));

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

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 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
    indicatorLeft = padLeft + indicatorWeight / 2.0;
    indicatorRight = 200.0 - padRight -  indicatorWeight / 2.0;

    expect(tabBarBox, paints..line(
      color: indicatorColor,
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
    ));
  });

  testWidgets('TabBar changes indicator attributes', (WidgetTester tester) async {
    final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
      return new Tab(text: 'Tab $index');
    });

    final TabController controller = new 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(
        child: new Container(
          alignment: Alignment.topLeft,
          child: new TabBar(
            indicatorWeight: indicatorWeight,
            indicatorColor: indicatorColor,
            indicatorPadding: new 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)
Ian Hickson's avatar
Ian Hickson committed
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 1135 1136
    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: new Offset(indicatorLeft, indicatorY),
      p2: new 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: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146
    ));
  });

  testWidgets('TabBar with directional indicatorPadding (LTR)', (WidgetTester tester) async {
    final List<Widget> tabs = <Widget>[
      new SizedBox(key: new UniqueKey(), width: 130.0, height: 30.0),
      new SizedBox(key: new UniqueKey(), width: 140.0, height: 40.0),
      new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0),
    ];

1147 1148
    const double indicatorWeight = 2.0; // the default

Ian Hickson's avatar
Ian Hickson committed
1149 1150 1151 1152 1153 1154 1155
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: tabs.length,
    );

    await tester.pumpWidget(
      boilerplate(
1156 1157 1158 1159 1160 1161 1162
        child: new Container(
          alignment: Alignment.topLeft,
          child: new TabBar(
            indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0),
            isScrollable: true,
            controller: controller,
            tabs: tabs,
Ian Hickson's avatar
Ian Hickson committed
1163 1164 1165 1166 1167
          ),
        ),
      ),
    );

1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198
    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 = new 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 = new 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 = new 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
1199
    const double indicatorLeft = 100.0 + indicatorWeight / 2.0;
1200 1201 1202 1203 1204 1205
    final double indicatorRight = 130.0 + kTabLabelPadding.horizontal - indicatorWeight / 2.0;
    final double indicatorY = tabBottom + indicatorWeight / 2.0;
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1206 1207 1208 1209 1210 1211 1212 1213 1214 1215
    ));
  });

  testWidgets('TabBar with directional indicatorPadding (RTL)', (WidgetTester tester) async {
    final List<Widget> tabs = <Widget>[
      new SizedBox(key: new UniqueKey(), width: 130.0, height: 30.0),
      new SizedBox(key: new UniqueKey(), width: 140.0, height: 40.0),
      new SizedBox(key: new UniqueKey(), width: 150.0, height: 50.0),
    ];

1216 1217
    const double indicatorWeight = 2.0; // the default

Ian Hickson's avatar
Ian Hickson committed
1218 1219 1220 1221 1222 1223 1224 1225
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: tabs.length,
    );

    await tester.pumpWidget(
      boilerplate(
        textDirection: TextDirection.rtl,
1226 1227 1228 1229 1230 1231 1232
        child: new Container(
          alignment: Alignment.topLeft,
          child: new TabBar(
            indicatorPadding: const EdgeInsetsDirectional.only(start: 100.0),
            isScrollable: true,
            controller: controller,
            tabs: tabs,
Ian Hickson's avatar
Ian Hickson committed
1233 1234 1235 1236 1237
          ),
        ),
      ),
    );

1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268
    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 = new 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 = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
    expect(tester.getRect(find.byKey(tabs[1].key)), tabRect);

    // Tab0 width = 130, height = 30
    tabLeft = tabRight + kTabLabelPadding.right + kTabLabelPadding.left;
    tabRight = tabLeft + 130.0;
    tabTop = (tabBarHeight - indicatorWeight - 30.0) / 2.0;
    tabBottom = tabTop + 30.0;
    tabRect = new Rect.fromLTRB(tabLeft, tabTop, tabRight, tabBottom);
    expect(tester.getRect(find.byKey(tabs[0].key)), tabRect);

    // Tab 0 selected, indicator padding resolves to right: 100.0
    final double indicatorLeft = tabLeft - kTabLabelPadding.left + indicatorWeight / 2.0;
    final double indicatorRight = tabRight + kTabLabelPadding.left - indicatorWeight / 2.0 - 100.0;
1269
    const double indicatorY = 50.0 + indicatorWeight / 2.0;
1270 1271 1272 1273
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1274 1275 1276 1277 1278
    ));
  });

  testWidgets('Overflowing RTL tab bar', (WidgetTester tester) async {
    final List<Widget> tabs = new List<Widget>.filled(100,
1279 1280 1281
      // For convenience padded width of each tab will equal 100:
      // 76 + kTabLabelPadding.horizontal(24)
      new SizedBox(key: new UniqueKey(), width: 76.0, height: 40.0),
Ian Hickson's avatar
Ian Hickson committed
1282 1283 1284 1285 1286 1287 1288
    );

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

1289 1290
    const double indicatorWeight = 2.0; // the default

Ian Hickson's avatar
Ian Hickson committed
1291 1292 1293
    await tester.pumpWidget(
      boilerplate(
        textDirection: TextDirection.rtl,
1294 1295
        child: new Container(
          alignment: Alignment.topLeft,
Ian Hickson's avatar
Ian Hickson committed
1296 1297 1298 1299 1300 1301 1302 1303 1304
          child: new TabBar(
            isScrollable: true,
            controller: controller,
            tabs: tabs,
          ),
        ),
      ),
    );

1305 1306 1307 1308 1309 1310 1311
    final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
    const double tabBarHeight = 40.0 + indicatorWeight;  // 40 = tab height
    expect(tabBarBox.size.height, tabBarHeight);

    // Tab 0 out of 100 selected
    double indicatorLeft = 99.0 * 100.0 + indicatorWeight / 2.0;
    double indicatorRight = 100.0 * 100.0 - indicatorWeight / 2.0;
1312
    const double indicatorY = 40.0 + indicatorWeight / 2.0;
1313 1314 1315 1316
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1317 1318 1319 1320 1321 1322
    ));

    controller.animateTo(tabs.length - 1, duration: const Duration(seconds: 1), curve: Curves.linear);
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 500));

1323 1324 1325
    // The x coordinates of p1 and p2 were derived empirically, not analytically.
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
1326 1327
      p1: const Offset(2476.0, indicatorY),
      p2: const Offset(2574.0, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1328 1329 1330 1331
    ));

    await tester.pump(const Duration(milliseconds: 501));

1332 1333 1334 1335 1336 1337 1338
    // Tab 99 out of 100 selected, appears on the far left because RTL
    indicatorLeft = indicatorWeight / 2.0;
    indicatorRight = 100.0 - indicatorWeight / 2.0;
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1339 1340 1341
    ));
  });

1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355
  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(
1356
      boilerplate(
1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370
        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(
1371
          id: 1,
1372 1373 1374
          rect: TestSemantics.fullScreen,
          children: <TestSemantics>[
            new TestSemantics(
1375
              id: 2,
1376 1377 1378
              rect: TestSemantics.fullScreen,
              children: <TestSemantics>[
                new TestSemantics(
1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400
                    id: 3,
                    rect: TestSemantics.fullScreen,
                    children: <TestSemantics>[
                      new TestSemantics(
                        id: 4,
                        actions: SemanticsAction.tap.index,
                        flags: SemanticsFlag.isSelected.index,
                        label: 'TAB #0\nTab 1 of 2',
                        rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
                        transform: new Matrix4.translationValues(0.0, 276.0, 0.0),
                      ),
                      new TestSemantics(
                        id: 5,
                        actions: SemanticsAction.tap.index,
                        label: 'TAB #1\nTab 2 of 2',
                        rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
                        transform: new Matrix4.translationValues(108.0, 276.0, 0.0),
                      ),
                    ]
                )
              ],
            ),
1401 1402
          ],
        ),
1403 1404 1405 1406 1407 1408 1409
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics));

    semantics.dispose();
  });
1410

1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436
  testWidgets('correct scrolling semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = new SemanticsTester(tester);

    final List<Tab> tabs = new List<Tab>.generate(20, (int index) {
      return new Tab(text: 'This is a very wide tab #$index');
    });

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

    await tester.pumpWidget(
      boilerplate(
        child: new Semantics(
          container: true,
          child: new TabBar(
            isScrollable: true,
            controller: controller,
            tabs: tabs,
          ),
        ),
      ),
    );

1437 1438 1439
    const String tab0title = 'This is a very wide tab #0\nTab 1 of 20';
    const String tab10title = 'This is a very wide tab #10\nTab 11 of 20';

1440
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
1441 1442
    expect(semantics, includesNodeWith(label: tab0title));
    expect(semantics, isNot(includesNodeWith(label: tab10title)));
1443 1444 1445 1446

    controller.index = 10;
    await tester.pumpAndSettle();

1447
    expect(semantics, isNot(includesNodeWith(label: tab0title)));
1448
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight]));
1449
    expect(semantics, includesNodeWith(label: tab10title));
1450 1451 1452 1453 1454 1455 1456 1457 1458 1459

    controller.index = 19;
    await tester.pumpAndSettle();

    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollRight]));

    controller.index = 0;
    await tester.pumpAndSettle();

    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
1460 1461
    expect(semantics, includesNodeWith(label: tab0title));
    expect(semantics, isNot(includesNodeWith(label: tab10title)));
1462 1463 1464 1465

    semantics.dispose();
  });

1466 1467 1468 1469 1470 1471 1472
  testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: 0,
    );

    await tester.pumpWidget(
1473
      boilerplate(
1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496
        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.

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

1500 1501
    await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0);
    await tester.pumpAndSettle();
1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512

    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(
1513
      boilerplate(
1514 1515 1516 1517
        child: new Column(
          children: <Widget>[
            new TabBar(
              controller: controller,
1518
              tabs: const <Widget>[Tab(text: 'TAB')],
1519 1520 1521 1522
            ),
            new Flexible(
              child: new TabBarView(
                controller: controller,
1523
                children: const <Widget>[Text('PAGE')],
1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536
              ),
            ),
          ],
        ),
      ),
    );

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

1537 1538 1539 1540
    // The one tab should be center vis the app's width (800).
    final double tabLeft = tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx;
    final double tabRight = tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx;
    expect(tabLeft + (tabRight - tabLeft) / 2.0, 400.0);
1541 1542 1543

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

1544 1545
    await tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0);
    await tester.pump(const Duration(milliseconds: 50));
1546 1547
    expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, tabLeft);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, tabRight);
1548
    await tester.pumpAndSettle();
1549

1550 1551
    await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0);
    await tester.pump(const Duration(milliseconds: 50));
1552 1553
    expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, tabLeft);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, tabRight);
1554
    await tester.pumpAndSettle();
1555 1556 1557 1558 1559 1560

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

1561 1562 1563 1564 1565 1566 1567 1568
  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(
1569
      boilerplate(
1570 1571 1572 1573 1574
        child: new Column(
          children: <Widget>[
            new TabBar(
              controller: controller,
              indicatorWeight: 30.0,
1575
              tabs: const <Widget>[Tab(text: 'TAB1'), Tab(text: 'TAB2')],
1576 1577 1578 1579
            ),
            new Flexible(
              child: new TabBarView(
                controller: controller,
1580
                children: const <Widget>[Text('PAGE1'), Text('PAGE2')],
1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596
              ),
            ),
          ],
        ),
      ),
    );

    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);
  });
1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633

  testWidgets('can override semantics of tabs', (WidgetTester tester) async {
    final SemanticsTester semantics = new SemanticsTester(tester);

    final List<Tab> tabs = new List<Tab>.generate(2, (int index) {
      return new Tab(
        child: new Semantics(
          label: 'Semantics override $index',
          child: new ExcludeSemantics(
            child: new Text('TAB #$index'),
          ),
        ),
      );
    });

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

    await tester.pumpWidget(
      boilerplate(
        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(
1634
          id: 1,
1635 1636 1637
          rect: TestSemantics.fullScreen,
          children: <TestSemantics>[
            new TestSemantics(
1638 1639 1640 1641
              id: 2,
              rect: TestSemantics.fullScreen,
              children: <TestSemantics>[
                new TestSemantics(
1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663
                    id: 3,
                    rect: TestSemantics.fullScreen,
                    children: <TestSemantics>[
                      new TestSemantics(
                        id: 4,
                        actions: SemanticsAction.tap.index,
                        flags: SemanticsFlag.isSelected.index,
                        label: 'Semantics override 0\nTab 1 of 2',
                        rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
                        transform: new Matrix4.translationValues(0.0, 276.0, 0.0),
                      ),
                      new TestSemantics(
                        id: 5,
                        actions: SemanticsAction.tap.index,
                        label: 'Semantics override 1\nTab 2 of 2',
                        rect: new Rect.fromLTRB(0.0, 0.0, 108.0, kTextTabBarHeight),
                        transform: new Matrix4.translationValues(108.0, 276.0, 0.0),
                      ),
                    ]
                )
              ],
            ),
1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674
          ],
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics));

    semantics.dispose();
  });

  test('illegal constructor combinations', () {
1675
    expect(() => new Tab(icon: nonconst(null)), throwsAssertionError);
1676 1677 1678
    expect(() => new Tab(icon: new Container(), text: 'foo', child: new Container()), throwsAssertionError);
    expect(() => new Tab(text: 'foo', child: new Container()), throwsAssertionError);
  });
1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689


  testWidgets('TabController changes', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/14812

    Widget buildFrame(TabController controller) {
      return boilerplate(
        child: new Container(
          alignment: Alignment.topLeft,
          child: new TabBar(
            controller: controller,
1690
            tabs: const <Tab>[
1691 1692
              Tab(text: 'LEFT'),
              Tab(text: 'RIGHT'),
1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719
            ],
          ),
        ),
      );
    }

    final TabController controller1 = new TabController(
      vsync: const TestVSync(),
      length: 2,
      initialIndex: 0,
    );

    final TabController controller2 = new TabController(
      vsync: const TestVSync(),
      length: 2,
      initialIndex: 0,
    );

    await tester.pumpWidget(buildFrame(controller1));
    await tester.pumpWidget(buildFrame(controller2));
    expect(controller1.index, 0);
    expect(controller2.index, 0);

    const double indicatorWeight = 2.0;
    final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
    expect(tabBarBox.size.height, 48.0); // 48 = _kTabHeight(46) + indicatorWeight(2.0)

1720
    const double indicatorY = 48.0 - indicatorWeight / 2.0;
1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745
    double indicatorLeft = indicatorWeight / 2.0;
    double indicatorRight = 400.0 - indicatorWeight / 2.0; // 400 = screen_width / 2
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
    ));

    await tester.tap(find.text('RIGHT'));
    await tester.pumpAndSettle();
    expect(controller1.index, 0);
    expect(controller2.index, 1);

    // Verify that the TabBar's _IndicatorPainter is now listening to
    // tabController2.

    indicatorLeft = 400.0 + indicatorWeight / 2.0;
    indicatorRight = 800.0 - indicatorWeight / 2.0;
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
    ));
  });

1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756
  testWidgets('Default tab indicator color is white', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/15958
    final List<String> tabs = <String>['LEFT', 'RIGHT'];
    await tester.pumpWidget(buildLeftRightApp(tabs: tabs, value: 'LEFT'));
    final RenderBox tabBarBox = tester.firstRenderObject<RenderBox>(find.byType(TabBar));
    expect(tabBarBox, paints..line(
      color: Colors.white,
    ));

  });

1757
}