Repository: flutter/flutter
File: packages/flutter/test/material/tabs_test.dart (Dart)

Code before:
      boilerplate(
        child: TabBar(isScrollable: true, controller: tabBarController, tabs: tabs),
      ),
    );

    final double oldPixels = tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels;

    // Trigger didUpdateWidget for the change to the external controller.
    await tester.pumpWidget(
      boilerplate(
        child: TabBar(
          isScrollable: true,
          controller: tabBarController,
          scrollController: tabScrollController,
          tabs: tabs,
        ),
      ),
    );

    // Creating the new scroll position should not throw.
    expect(tester.takeException(), isNull);
    final ScrollableState scrollableState = tester.state<ScrollableState>(find.byType(Scrollable));

    // The pixels of the new position should be the same as before the update.
    expect(scrollableState.position.pixels, oldPixels);
    expect(tabScrollController.debugHasTabBarState, isTrue);
  });

  // Regression test for https://github.com/flutter/flutter/issues/124608
  testWidgets('TabBar can be wrapped with RawScrollbar', (WidgetTester tester) async {
    final tabs = List<Tab>.generate(6, (int index) {
      return Tab(text: 'TAB #$index');
    });

    final TabController tabBarController = createTabController(
      vsync: const TestVSync(),
      length: tabs.length,
      initialIndex: tabs.length - 1,
    );
    final tabScrollController = TabBarScrollController();
    addTearDown(tabScrollController.dispose);

    await tester.pumpWidget(
      boilerplate(
        child: RawScrollbar(
          controller: tabScrollController,
          child: TabBar(
            isScrollable: true,
            controller: tabBarController,
            scrollController: tabScrollController,
            tabs: tabs,

Review comment (bug):
While I could verify that the internal controller is attached in the test above, I'm not sure how to test that the internal controller is _not_ attached to the TabBarState in this test. The idea is that a given `TabBarState` is only attached to one `TabBarScrollController`.

Do I need to verify this? Or maybe not, since it is the internal controller.

If I _do_ need to verify it, do you have ideas on how to expose the `debugHasTabBarState` for the internal controller from the `TabBar` widget? (The `_TabBarState` is private)

Code after:
      boilerplate(
        child: TabBar(isScrollable: true, controller: tabBarController, tabs: tabs),
      ),
    );

    final double oldPixels = tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels;

    // Trigger didUpdateWidget for the change to the external controller.
    await tester.pumpWidget(
      boilerplate(
        child: TabBar(
          isScrollable: true,
          controller: tabBarController,
          scrollController: tabScrollController,
          tabs: tabs,
        ),
      ),
    );

    // Creating the new scroll position should not throw.
    expect(tester.takeException(), isNull);
    final ScrollableState scrollableState = tester.state<ScrollableState>(find.byType(Scrollable));

    // The pixels of the new position should be the same as before the update.
    expect(scrollableState.position.pixels, oldPixels);

    // This should not throw, since the tab bar is attached to the external controller.
    expect(tabScrollController.debugCheckHasTabBarState(), isTrue);
    expect(tester.takeException(), isNull);
  });

  testWidgets(
    'TabBar correctly detaches old external TabBarScrollController when switched to a new one',
    (WidgetTester tester) async {
      final tabs = <Tab>[for (int i = 0; i < 10; i++) Tab(text: 'Tab $i')];

      final tabController = TabController(length: tabs.length, vsync: const TestVSync());
      addTearDown(tabController.dispose);

      final controllerA = TabBarScrollController();
      final controllerB = TabBarScrollController();
      addTearDown(controllerA.dispose);
      addTearDown(controllerB.dispose);

      await tester.pumpWidget(
        boilerplate(
          child: TabBar(
            isScrollable: true,
            controller: tabController,
            scrollController: controllerA,
            tabs: tabs,