tabs_test.dart 59.4 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
import 'dart:ui' show SemanticsFlag, SemanticsAction;
6

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

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

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

  final Widget child;

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

class StateMarkerState extends State<StateMarker> {
  String marker;

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

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

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

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

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

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

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

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

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

  final Color indicatorColor;
  Rect indicatorRect;

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

151 152
class TestScrollPhysics extends ScrollPhysics {
  const TestScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
153

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

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

165 166 167 168
  @override
  SpringDescription get spring => _kDefaultSpring;
}

169
void main() {
170 171 172 173
  setUp(() {
    debugResetSemanticsIdCounter();
  });

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

    // Move to the third tab.

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

    // The state is now gone.

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

    // Move back to the second tab.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    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

585
    final List<String> tabs = <String>['A', 'B', 'C'];
Hans Muller's avatar
Hans Muller committed
586 587 588 589 590 591 592 593
    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(
594
            title: const Text('tabs'),
Hans Muller's avatar
Hans Muller committed
595 596 597 598 599 600 601
            bottom: new TabBar(
              controller: controller,
              tabs: tabs.map((String tab) => new Tab(text: tab)).toList(),
            ),
          ),
          body: new TabBarView(
            controller: controller,
602
            children: const <Widget>[
603 604 605
              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
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 636 637
            ]
          ),
        ),
      );
    }

    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

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

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

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

    Color firstColor;
    Color secondColor;

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

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

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

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

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

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

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

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

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

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

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

  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);
  });
779 780 781 782 783 784 785 786 787 788 789

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

    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));
  });
808 809 810 811 812 813 814 815 816 817

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

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

    expect(tabController.index, 1);

    final PageView pageView = tester.widget(find.byType(PageView));
    final PageController pageController = pageView.controller;
    final ScrollPosition position = pageController.position;
843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864

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

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

    expect(tabController.index, 1);

    final PageView pageView = tester.widget(find.byType(PageView));
    final PageController pageController = pageView.controller;
    final ScrollPosition position = pageController.position;
891 892 893 894 895 896 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
    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);
904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
  });

  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(
920
      boilerplate(
921 922 923 924 925 926 927
        child: new TabBar(
          isScrollable: true,
          controller: controller,
          tabs: tabs,
        ),
      ),
    );
928

929 930
    // The initialIndex tab should be visible and right justified
    expect(find.text('TAB #19'), findsOneWidget);
931 932 933 934 935 936

    // 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);
937
  });
938

Ian Hickson's avatar
Ian Hickson committed
939
  testWidgets('TabBar with indicatorWeight, indicatorPadding (LTR)', (WidgetTester tester) async {
940 941
    const Color indicatorColor = const Color(0xFF00FF00);
    const double indicatorWeight = 8.0;
942 943 944 945
    const double padLeft = 8.0;
    const double padRight = 4.0;

    final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
946
      return new Tab(text: 'Tab $index');
947 948 949 950 951 952 953 954
    });

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

    await tester.pumpWidget(
955
      boilerplate(
956 957 958 959 960 961 962 963 964
        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,
          ),
965 966 967 968 969
        ),
      ),
    );

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

972
    const double indicatorY = 54.0 - indicatorWeight / 2.0;
973 974
    double indicatorLeft = padLeft + indicatorWeight / 2.0;
    double indicatorRight = 200.0 - (padRight + indicatorWeight / 2.0);
975

976 977 978 979 980
    expect(tabBarBox, paints..line(
      color: indicatorColor,
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
981 982 983 984 985 986
    ));

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

987 988
    indicatorLeft = 600.0 + padLeft + indicatorWeight / 2.0;
    indicatorRight = 800.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

Ian Hickson's avatar
Ian Hickson committed
998
  testWidgets('TabBar with indicatorWeight, indicatorPadding (RTL)', (WidgetTester tester) async {
999 1000
    const Color indicatorColor = const Color(0xFF00FF00);
    const double indicatorWeight = 8.0;
Ian Hickson's avatar
Ian Hickson committed
1001 1002 1003 1004
    const double padLeft = 8.0;
    const double padRight = 4.0;

    final List<Widget> tabs = new List<Widget>.generate(4, (int index) {
1005
      return new Tab(text: 'Tab $index');
Ian Hickson's avatar
Ian Hickson committed
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015
    });

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

    await tester.pumpWidget(
      boilerplate(
        textDirection: TextDirection.rtl,
1016 1017 1018 1019 1020 1021 1022 1023 1024
        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
1025 1026 1027 1028 1029
        ),
      ),
    );

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

1033
    const double indicatorY = 54.0 - indicatorWeight / 2.0;
1034 1035 1036 1037 1038 1039 1040 1041
    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
1042 1043 1044 1045 1046 1047
    ));

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

1048 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 1088 1089 1090 1091 1092
    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
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
    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
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
    ));
  });

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

1133 1134
    const double indicatorWeight = 2.0; // the default

Ian Hickson's avatar
Ian Hickson committed
1135 1136 1137 1138 1139 1140 1141
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: tabs.length,
    );

    await tester.pumpWidget(
      boilerplate(
1142 1143 1144 1145 1146 1147 1148
        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
1149 1150 1151 1152 1153
          ),
        ),
      ),
    );

1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184
    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
1185
    const double indicatorLeft = 100.0 + indicatorWeight / 2.0;
1186 1187 1188 1189 1190 1191
    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
1192 1193 1194 1195 1196 1197 1198 1199 1200 1201
    ));
  });

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

1202 1203
    const double indicatorWeight = 2.0; // the default

Ian Hickson's avatar
Ian Hickson committed
1204 1205 1206 1207 1208 1209 1210 1211
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: tabs.length,
    );

    await tester.pumpWidget(
      boilerplate(
        textDirection: TextDirection.rtl,
1212 1213 1214 1215 1216 1217 1218
        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
1219 1220 1221 1222 1223
          ),
        ),
      ),
    );

1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254
    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;
1255
    const double indicatorY = 50.0 + indicatorWeight / 2.0;
1256 1257 1258 1259
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1260 1261 1262 1263 1264
    ));
  });

  testWidgets('Overflowing RTL tab bar', (WidgetTester tester) async {
    final List<Widget> tabs = new List<Widget>.filled(100,
1265 1266 1267
      // 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
1268 1269 1270 1271 1272 1273 1274
    );

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

1275 1276
    const double indicatorWeight = 2.0; // the default

Ian Hickson's avatar
Ian Hickson committed
1277 1278 1279
    await tester.pumpWidget(
      boilerplate(
        textDirection: TextDirection.rtl,
1280 1281
        child: new Container(
          alignment: Alignment.topLeft,
Ian Hickson's avatar
Ian Hickson committed
1282 1283 1284 1285 1286 1287 1288 1289 1290
          child: new TabBar(
            isScrollable: true,
            controller: controller,
            tabs: tabs,
          ),
        ),
      ),
    );

1291 1292 1293 1294 1295 1296 1297
    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;
1298
    const double indicatorY = 40.0 + indicatorWeight / 2.0;
1299 1300 1301 1302
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
      p1: new Offset(indicatorLeft, indicatorY),
      p2: new Offset(indicatorRight, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1303 1304 1305 1306 1307 1308
    ));

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

1309 1310 1311
    // The x coordinates of p1 and p2 were derived empirically, not analytically.
    expect(tabBarBox, paints..line(
      strokeWidth: indicatorWeight,
1312 1313
      p1: const Offset(2476.0, indicatorY),
      p2: const Offset(2574.0, indicatorY),
Ian Hickson's avatar
Ian Hickson committed
1314 1315 1316 1317
    ));

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

1318 1319 1320 1321 1322 1323 1324
    // 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
1325 1326 1327
    ));
  });

1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341
  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(
1342
      boilerplate(
1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356
        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(
1357
          id: 1,
1358 1359 1360
          rect: TestSemantics.fullScreen,
          children: <TestSemantics>[
            new TestSemantics(
1361
              id: 2,
1362 1363 1364 1365
              rect: TestSemantics.fullScreen,
              children: <TestSemantics>[
                new TestSemantics(
                  id: 3,
1366
                  nextNodeId: 4,
1367 1368 1369 1370 1371 1372 1373 1374
                  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: 4,
1375
                  previousNodeId: 3,
1376 1377 1378 1379 1380 1381 1382
                  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),
                ),
              ]
            )
1383 1384
          ],
        ),
1385 1386 1387 1388 1389 1390 1391
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics));

    semantics.dispose();
  });
1392

1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418
  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,
          ),
        ),
      ),
    );

1419 1420 1421
    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';

1422
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft]));
1423 1424
    expect(semantics, includesNodeWith(label: tab0title));
    expect(semantics, isNot(includesNodeWith(label: tab10title)));
1425 1426 1427 1428

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

1429
    expect(semantics, isNot(includesNodeWith(label: tab0title)));
1430
    expect(semantics, includesNodeWith(actions: <SemanticsAction>[SemanticsAction.scrollLeft, SemanticsAction.scrollRight]));
1431
    expect(semantics, includesNodeWith(label: tab10title));
1432 1433 1434 1435 1436 1437 1438 1439 1440 1441

    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]));
1442 1443
    expect(semantics, includesNodeWith(label: tab0title));
    expect(semantics, isNot(includesNodeWith(label: tab10title)));
1444 1445 1446 1447

    semantics.dispose();
  });

1448 1449 1450 1451 1452 1453 1454
  testWidgets('TabBar etc with zero tabs', (WidgetTester tester) async {
    final TabController controller = new TabController(
      vsync: const TestVSync(),
      length: 0,
    );

    await tester.pumpWidget(
1455
      boilerplate(
1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478
        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.

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

1482 1483
    await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0);
    await tester.pumpAndSettle();
1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494

    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(
1495
      boilerplate(
1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518
        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));

1519 1520 1521 1522
    // 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);
1523 1524 1525

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

1526 1527
    await tester.fling(find.byType(TabBar), const Offset(-100.0, 0.0), 5000.0);
    await tester.pump(const Duration(milliseconds: 50));
1528 1529
    expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, tabLeft);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, tabRight);
1530
    await tester.pumpAndSettle();
1531

1532 1533
    await tester.fling(find.byType(TabBarView), const Offset(100.0, 0.0), 5000.0);
    await tester.pump(const Duration(milliseconds: 50));
1534 1535
    expect(tester.getTopLeft(find.widgetWithText(Tab, 'TAB')).dx, tabLeft);
    expect(tester.getTopRight(find.widgetWithText(Tab, 'TAB')).dx, tabRight);
1536
    await tester.pumpAndSettle();
1537 1538 1539 1540 1541 1542

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

1543 1544 1545 1546 1547 1548 1549 1550
  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(
1551
      boilerplate(
1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578
        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);
  });
1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615

  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(
1616
          id: 1,
1617 1618 1619
          rect: TestSemantics.fullScreen,
          children: <TestSemantics>[
            new TestSemantics(
1620 1621 1622 1623 1624
              id: 2,
              rect: TestSemantics.fullScreen,
              children: <TestSemantics>[
                new TestSemantics(
                  id: 3,
1625
                  nextNodeId: 4,
1626 1627 1628 1629 1630 1631 1632 1633
                  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: 4,
1634
                  previousNodeId: 3,
1635 1636 1637 1638 1639 1640 1641
                  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),
                ),
              ]
            )
1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652
          ],
        ),
      ],
    );

    expect(semantics, hasSemantics(expectedSemantics));

    semantics.dispose();
  });

  test('illegal constructor combinations', () {
1653
    expect(() => new Tab(icon: nonconst(null)), throwsAssertionError);
1654 1655 1656
    expect(() => new Tab(icon: new Container(), text: 'foo', child: new Container()), throwsAssertionError);
    expect(() => new Tab(text: 'foo', child: new Container()), throwsAssertionError);
  });
1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667


  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,
1668
            tabs: const <Tab>[
1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697
              const Tab(text: 'LEFT'),
              const Tab(text: 'RIGHT'),
            ],
          ),
        ),
      );
    }

    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)

1698
    const double indicatorY = 48.0 - indicatorWeight / 2.0;
1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723
    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),
    ));
  });

1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734
  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,
    ));

  });

1735
}