// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

import '../widgets/semantics_tester.dart';
import 'app_bar_utils.dart';

Widget buildSliverAppBarApp({
  bool floating = false,
  bool pinned = false,
  double? collapsedHeight,
  double? expandedHeight,
  bool snap = false,
  double toolbarHeight = kToolbarHeight,
}) {
  return MaterialApp(
    home: Scaffold(
      body: DefaultTabController(
        length: 3,
        child: CustomScrollView(
          primary: true,
          slivers: <Widget>[
            SliverAppBar(
              title: const Text('AppBar Title'),
              floating: floating,
              pinned: pinned,
              collapsedHeight: collapsedHeight,
              expandedHeight: expandedHeight,
              toolbarHeight: toolbarHeight,
              snap: snap,
              bottom: TabBar(
                tabs: <String>['A','B','C'].map<Widget>((String t) => Tab(text: 'TAB $t')).toList(),
              ),
            ),
            SliverToBoxAdapter(
              child: Container(
                height: 1200.0,
                color: Colors.orange[400],
              ),
            ),
          ],
        ),
      ),
    ),
  );
}

void main() {
  setUp(() {
    debugResetSemanticsIdCounter();
  });

  testWidgets(
    'SliverAppBar large & medium title respects automaticallyImplyLeading',
    (WidgetTester tester) async {
      // This is a regression test for https://github.com/flutter/flutter/issues/121511
      const String title = 'AppBar Title';
      const double titleSpacing = 16.0;

      Widget buildWidget() {
        return MaterialApp(
          home: Scaffold(
            body: Builder(
              builder: (BuildContext context) {
                return Center(
                  child: FilledButton(
                    onPressed: () {
                      Navigator.push(context, MaterialPageRoute<void>(
                        builder: (BuildContext context) {
                          return Scaffold(
                            body: CustomScrollView(
                              primary: true,
                              slivers: <Widget>[
                                const SliverAppBar.large(
                                  title: Text(title),
                                ),
                                SliverToBoxAdapter(
                                  child: Container(
                                    height: 1200,
                                    color: Colors.orange[400],
                                  ),
                                ),
                              ],
                            ),
                          );
                        },
                      ));
                    },
                    child: const Text('Go to page'),
                  ),
                );
              }
            ),
          ),
        );
      }

      await tester.pumpWidget(buildWidget());

      expect(find.byType(BackButton), findsNothing);

      await tester.tap(find.byType(FilledButton));
      await tester.pumpAndSettle();

      final Finder collapsedTitle = find.text(title).last;
      final Offset backButtonOffset = tester.getTopRight(find.byType(BackButton));
      final Offset titleOffset = tester.getTopLeft(collapsedTitle);
      expect(titleOffset.dx, backButtonOffset.dx + titleSpacing);
  });

  testWidgets('SliverAppBar.medium with bottom widget', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/115091
    const double collapsedAppBarHeight = 64;
    const double expandedAppBarHeight = 112;
    const double bottomHeight = 48;
    const String title = 'Medium App Bar';

    Widget buildWidget() {
      return MaterialApp(
        home: DefaultTabController(
          length: 3,
          child: Scaffold(
            body: CustomScrollView(
              primary: true,
              slivers: <Widget>[
                SliverAppBar.medium(
                  leading: IconButton(
                    onPressed: () {},
                    icon: const Icon(Icons.menu),
                  ),
                  title: const Text(title),
                  bottom: const TabBar(
                    tabs: <Widget>[
                      Tab(text: 'Tab 1'),
                      Tab(text: 'Tab 2'),
                      Tab(text: 'Tab 3'),
                    ],
                  ),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildWidget());

    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight + bottomHeight);

    final Finder expandedTitle = find.text(title).first;
    final Offset expandedTitleOffset = tester.getBottomLeft(expandedTitle);
    final Offset tabOffset = tester.getTopLeft(find.byType(TabBar));
    expect(expandedTitleOffset.dy, tabOffset.dy);

    // Scroll CustomScrollView to collapse SliverAppBar.
    final ScrollController controller = primaryScrollController(tester);
    controller.jumpTo(160);
    await tester.pumpAndSettle();

    expect(appBarHeight(tester), collapsedAppBarHeight + bottomHeight);
  });

  testWidgets('SliverAppBar.large with bottom widget', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/115091
    const double collapsedAppBarHeight = 64;
    const double expandedAppBarHeight = 152;
    const double bottomHeight = 48;
    const String title = 'Large App Bar';

    Widget buildWidget() {
      return MaterialApp(
        home: DefaultTabController(
          length: 3,
          child: Scaffold(
            body: CustomScrollView(
              primary: true,
              slivers: <Widget>[
                SliverAppBar.large(
                  leading: IconButton(
                    onPressed: () {},
                    icon: const Icon(Icons.menu),
                  ),
                  title: const Text(title),
                  bottom: const TabBar(
                    tabs: <Widget>[
                      Tab(text: 'Tab 1'),
                      Tab(text: 'Tab 2'),
                      Tab(text: 'Tab 3'),
                    ],
                  ),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildWidget());

    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight + bottomHeight);

    final Finder expandedTitle = find.text(title).first;
    final Offset expandedTitleOffset = tester.getBottomLeft(expandedTitle);
    final Offset tabOffset = tester.getTopLeft(find.byType(TabBar));
    expect(expandedTitleOffset.dy, tabOffset.dy);

    // Scroll CustomScrollView to collapse SliverAppBar.
    final ScrollController controller = primaryScrollController(tester);
    controller.jumpTo(200);
    await tester.pumpAndSettle();

    expect(appBarHeight(tester), collapsedAppBarHeight + bottomHeight);
  });

  testWidgets('SliverAppBar.medium expanded title has upper limit on text scaling', (WidgetTester tester) async {
    const String title = 'Medium AppBar';
    Widget buildAppBar({double textScaleFactor = 1.0}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: MediaQuery.withClampedTextScaling(
          minScaleFactor: textScaleFactor,
          maxScaleFactor: textScaleFactor,
          child: Material(
            child: CustomScrollView(
              slivers: <Widget>[
                const SliverAppBar.medium(
                  title: Text(title),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildAppBar());

    final Finder expandedTitle = find.text(title).first;
    expect(tester.getRect(expandedTitle).height, 32.0);
    verifyTextNotClipped(expandedTitle, tester);

    await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
    expect(tester.getRect(expandedTitle).height, 43.0);
    verifyTextNotClipped(expandedTitle, tester);

    await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
    expect(tester.getRect(expandedTitle).height, 43.0);
    verifyTextNotClipped(expandedTitle, tester);
  }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933

  testWidgets('SliverAppBar.large expanded title has upper limit on text scaling', (WidgetTester tester) async {
    const String title = 'Large AppBar';
    Widget buildAppBar({double textScaleFactor = 1.0}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: MediaQuery.withClampedTextScaling(
          minScaleFactor: textScaleFactor,
          maxScaleFactor: textScaleFactor,
          child: Material(
            child: CustomScrollView(
              slivers: <Widget>[
                const SliverAppBar.large(
                  title: Text(title, maxLines: 1),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildAppBar());

    final Finder expandedTitle = find.text(title).first;
    expect(tester.getRect(expandedTitle).height, 36.0);

    await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
    expect(tester.getRect(expandedTitle).height, closeTo(48.0, 0.1));

    await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
    expect(tester.getRect(expandedTitle).height, closeTo(48.0, 0.1));
  }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933

  testWidgets('SliverAppBar.medium expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async {
    const String title = 'Medium AppBar';
    Widget buildAppBar({double textScaleFactor = 1.0}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: MediaQuery.withClampedTextScaling(
          minScaleFactor: textScaleFactor,
          maxScaleFactor: textScaleFactor,
          child: Material(
            child: CustomScrollView(
              slivers: <Widget>[
                const SliverAppBar.medium(
                  title: Text(title, maxLines: 1),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildAppBar());

    final Finder expandedTitle = find.text(title).first;
    expect(tester.getBottomLeft(expandedTitle).dy, 96.0);
    verifyTextNotClipped(expandedTitle, tester);

    await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
    expect(tester.getBottomLeft(expandedTitle).dy, 107.0);
    verifyTextNotClipped(expandedTitle, tester);

    await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
    expect(tester.getBottomLeft(expandedTitle).dy, 107.0);
    verifyTextNotClipped(expandedTitle, tester);
  }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/99933

  testWidgets('SliverAppBar.large expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async {
    const String title = 'Large AppBar';
    Widget buildAppBar({double textScaleFactor = 1.0}) {
      return MaterialApp(
        theme: ThemeData(useMaterial3: true),
        home: MediaQuery.withClampedTextScaling(
          minScaleFactor: textScaleFactor,
          maxScaleFactor: textScaleFactor,
          child: Material(
            child: CustomScrollView(
              slivers: <Widget>[
                const SliverAppBar.large(
                  title: Text(title, maxLines: 1),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildAppBar());
    final Finder expandedTitle = find.text(title).first;
    final RenderSliver renderSliverAppBar = tester.renderObject(find.byType(SliverAppBar));
    expect(
      tester.getBottomLeft(expandedTitle).dy,
      renderSliverAppBar.geometry!.scrollExtent - 28.0,
      reason: 'bottom padding of a large expanded title should be 28.',
    );
    verifyTextNotClipped(expandedTitle, tester);

    await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
    expect(
      tester.getBottomLeft(expandedTitle).dy,
      renderSliverAppBar.geometry!.scrollExtent - 28.0,
      reason: 'bottom padding of a large expanded title should be 28.',
    );
    verifyTextNotClipped(expandedTitle, tester);

    // The bottom padding of the expanded title needs to be reduced for it to be
    // fully visible.
    await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
    expect(tester.getBottomLeft(expandedTitle).dy, 124.0);
    verifyTextNotClipped(expandedTitle, tester);
  });

  testWidgets(
    'SliverAppBar.medium collapsed title does not overlap with leading/actions widgets',
    (WidgetTester tester) async {
      const String title = 'Medium SliverAppBar Very Long Title';
      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: CustomScrollView(
            primary: true,
            slivers: <Widget>[
              SliverPadding(
                padding: const EdgeInsets.symmetric(horizontal: 200),
                sliver: SliverAppBar.medium(
                  leading: IconButton(
                    icon: const Icon(Icons.menu),
                    onPressed: () {},
                  ),
                  title: const Text(title, maxLines: 1),
                  centerTitle: true,
                  actions: const <Widget>[
                    Icon(Icons.search),
                    Icon(Icons.sort),
                    Icon(Icons.more_vert),
                  ],
                ),
              ),
              SliverToBoxAdapter(
                child: Container(
                  height: 1200,
                  color: Colors.orange[400],
                ),
              ),
            ],
          ),
        ),
      ));

      // Scroll to collapse the SliverAppBar.
      final ScrollController controller = primaryScrollController(tester);
      controller.jumpTo(45);
      await tester.pumpAndSettle();

      final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu));
      Offset titleOffset = tester.getTopLeft(find.text(title).last);
      // The title widget should be to the right of the leading widget.
      expect(titleOffset.dx, greaterThan(leadingOffset.dx));

      titleOffset = tester.getTopRight(find.text(title).last);
      final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
      // The title widget should be to the left of the search icon.
      expect(titleOffset.dx, lessThan(searchOffset.dx));
  });

  testWidgets(
    'SliverAppBar.large collapsed title does not overlap with leading/actions widgets',
    (WidgetTester tester) async {
      const String title = 'Large SliverAppBar Very Long Title';
      await tester.pumpWidget(MaterialApp(
        home: Scaffold(
          body: CustomScrollView(
            primary: true,
            slivers: <Widget>[
              SliverPadding(
                padding: const EdgeInsets.symmetric(horizontal: 200),
                sliver: SliverAppBar.large(
                  leading: IconButton(
                    icon: const Icon(Icons.menu),
                    onPressed: () {},
                  ),
                  title: const Text(title, maxLines: 1),
                  centerTitle: true,
                  actions: const <Widget>[
                    Icon(Icons.search),
                    Icon(Icons.sort),
                    Icon(Icons.more_vert),
                  ],
                ),
              ),
              SliverToBoxAdapter(
                child: Container(
                  height: 1200,
                  color: Colors.orange[400],
                ),
              ),
            ],
          ),
        ),
      ));

      // Scroll to collapse the SliverAppBar.
      final ScrollController controller = primaryScrollController(tester);
      controller.jumpTo(45);
      await tester.pumpAndSettle();

      final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu));
      Offset titleOffset = tester.getTopLeft(find.text(title).last);
      // The title widget should be to the right of the leading widget.
      expect(titleOffset.dx, greaterThan(leadingOffset.dx));

      titleOffset = tester.getTopRight(find.text(title).last);
      final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
      // The title widget should be to the left of the search icon.
      expect(titleOffset.dx, lessThan(searchOffset.dx));
  });

  testWidgets('SliverAppBar.medium respects title spacing', (WidgetTester tester) async {
    const String title = 'Medium SliverAppBar Very Long Title';
    const double titleSpacing = 16.0;

    Widget buildWidget({double? titleSpacing, bool? centerTitle}) {
      return MaterialApp(
        home: Scaffold(
          body: CustomScrollView(
            primary: true,
            slivers: <Widget>[
              SliverPadding(
                padding: const EdgeInsets.symmetric(horizontal: 200),
                sliver: SliverAppBar.medium(
                  leading: IconButton(
                    onPressed: () {},
                    icon: const Icon(Icons.menu),
                  ),
                  title: const Text(title, maxLines: 1),
                  centerTitle: centerTitle,
                  titleSpacing: titleSpacing,
                  actions: <Widget>[
                    IconButton(onPressed: () {}, icon: const Icon(Icons.sort)),
                    IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
                  ],
                ),
              ),
              SliverToBoxAdapter(
                child: Container(
                  height: 1200,
                  color: Colors.orange[400],
                ),
              ),
            ],
          ),
        ),
      );
    }

    await tester.pumpWidget(buildWidget());

    final Finder collapsedTitle = find.text(title).last;

    // Scroll to collapse the SliverAppBar.
    ScrollController controller = primaryScrollController(tester);
    controller.jumpTo(120);
    await tester.pumpAndSettle();

    // By default, title widget should be to the right of the
    // leading widget and title spacing should be respected.
    Offset titleOffset = tester.getTopLeft(collapsedTitle);
    Offset iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
    expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);

    await tester.pumpWidget(buildWidget(centerTitle: true));
    // Scroll to collapse the SliverAppBar.
    controller = primaryScrollController(tester);
    controller.jumpTo(120);
    await tester.pumpAndSettle();

    // By default, title widget should be to the left of the first
    // trailing widget and title spacing should be respected.
    titleOffset = tester.getTopRight(collapsedTitle);
    iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
    expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);

    // Test custom title spacing, set to 0.0.
    await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
    // Scroll to collapse the SliverAppBar.
    controller = primaryScrollController(tester);
    controller.jumpTo(120);
    await tester.pumpAndSettle();

    // The title widget should be to the right of the leading
    // widget with no spacing.
    titleOffset = tester.getTopLeft(collapsedTitle);
    iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
    expect(titleOffset.dx, iconButtonOffset.dx);

    // Set centerTitle to true so the end of the title can reach
    // the action widgets.
    await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
    // Scroll to collapse the SliverAppBar.
    controller = primaryScrollController(tester);
    controller.jumpTo(120);
    await tester.pumpAndSettle();

    // The title widget should be to the left of the first
    // leading widget with no spacing.
    titleOffset = tester.getTopRight(collapsedTitle);
    iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
    expect(titleOffset.dx, iconButtonOffset.dx);
  });

  testWidgets('SliverAppBar.large respects title spacing', (WidgetTester tester) async {
    const String title = 'Large SliverAppBar Very Long Title';
    const double titleSpacing = 16.0;

    Widget buildWidget({double? titleSpacing, bool? centerTitle}) {
      return MaterialApp(
        home: Scaffold(
          body: CustomScrollView(
            primary: true,
            slivers: <Widget>[
              SliverPadding(
                padding: const EdgeInsets.symmetric(horizontal: 200),
                sliver: SliverAppBar.large(
                  leading: IconButton(
                    onPressed: () {},
                    icon: const Icon(Icons.menu),
                  ),
                  title: const Text(title, maxLines: 1),
                  centerTitle: centerTitle,
                  titleSpacing: titleSpacing,
                  actions: <Widget>[
                    IconButton(onPressed: () {}, icon: const Icon(Icons.sort)),
                    IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
                  ],
                ),
              ),
              SliverToBoxAdapter(
                child: Container(
                  height: 1200,
                  color: Colors.orange[400],
                ),
              ),
            ],
          ),
        ),
      );
    }

    await tester.pumpWidget(buildWidget());

    final Finder collapsedTitle = find.text(title).last;

    // Scroll to collapse the SliverAppBar.
    ScrollController controller = primaryScrollController(tester);
    controller.jumpTo(160);
    await tester.pumpAndSettle();

    // By default, title widget should be to the right of the leading
    // widget and title spacing should be respected.
    Offset titleOffset = tester.getTopLeft(collapsedTitle);
    Offset iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
    expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);

    await tester.pumpWidget(buildWidget(centerTitle: true));
    // Scroll to collapse the SliverAppBar.
    controller = primaryScrollController(tester);
    controller.jumpTo(160);
    await tester.pumpAndSettle();

    // By default, title widget should be to the left of the
    // leading widget and title spacing should be respected.
    titleOffset = tester.getTopRight(collapsedTitle);
    iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
    expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);

    // Test custom title spacing, set to 0.0.
    await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
    controller = primaryScrollController(tester);
    controller.jumpTo(160);
    await tester.pumpAndSettle();

    // The title widget should be to the right of the leading
    // widget with no spacing.
    titleOffset = tester.getTopLeft(collapsedTitle);
    iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
    expect(titleOffset.dx, iconButtonOffset.dx);

    // Set centerTitle to true so the end of the title can reach
    // the action widgets.
    await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
    // Scroll to collapse the SliverAppBar.
    controller = primaryScrollController(tester);
    controller.jumpTo(160);
    await tester.pumpAndSettle();

    // The title widget should be to the left of the first
    // leading widget with no spacing.
    titleOffset = tester.getTopRight(collapsedTitle);
    iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
    expect(titleOffset.dx, iconButtonOffset.dx);
  });

  testWidgets(
    'SliverAppBar.medium without the leading widget updates collapsed title padding',
    (WidgetTester tester) async {
      const String title = 'Medium SliverAppBar Title';
      const double leadingPadding = 56.0;
      const double titleSpacing = 16.0;

      Widget buildWidget({ bool showLeading = true }) {
        return MaterialApp(
          home: Scaffold(
            body: CustomScrollView(
              primary: true,
              slivers: <Widget>[
                SliverAppBar.medium(
                  automaticallyImplyLeading: false,
                  leading: showLeading
                    ? IconButton(
                        icon: const Icon(Icons.menu),
                        onPressed: () {},
                      )
                    : null,
                  title: const Text(title),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        );
      }

      await tester.pumpWidget(buildWidget());

      final Finder collapsedTitle = find.text(title).last;

      // Scroll to collapse the SliverAppBar.
      ScrollController controller = primaryScrollController(tester);
      controller.jumpTo(45);
      await tester.pumpAndSettle();

      // If the leading widget is present, the title widget should be to the
      // right of the leading widget and title spacing should be respected.
      Offset titleOffset = tester.getTopLeft(collapsedTitle);
      expect(titleOffset.dx, leadingPadding + titleSpacing);

      // Hide the leading widget.
      await tester.pumpWidget(buildWidget(showLeading: false));
      // Scroll to collapse the SliverAppBar.
      controller = primaryScrollController(tester);
      controller.jumpTo(45);
      await tester.pumpAndSettle();

      // If the leading widget is not present, the title widget will
      // only have the default title spacing.
      titleOffset = tester.getTopLeft(collapsedTitle);
      expect(titleOffset.dx, titleSpacing);
  });

  testWidgets(
    'SliverAppBar.large without the leading widget updates collapsed title padding',
    (WidgetTester tester) async {
      const String title = 'Large SliverAppBar Title';
      const double leadingPadding = 56.0;
      const double titleSpacing = 16.0;

      Widget buildWidget({ bool showLeading = true }) {
        return MaterialApp(
          home: Scaffold(
            body: CustomScrollView(
              primary: true,
              slivers: <Widget>[
                SliverAppBar.large(
                  automaticallyImplyLeading: false,
                  leading: showLeading
                    ? IconButton(
                        icon: const Icon(Icons.menu),
                        onPressed: () {},
                      )
                    : null,
                  title: const Text(title),
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 1200,
                    color: Colors.orange[400],
                  ),
                ),
              ],
            ),
          ),
        );
      }

      await tester.pumpWidget(buildWidget());

      final Finder collapsedTitle = find.text(title).last;

      // Scroll CustomScrollView to collapse SliverAppBar.
      ScrollController controller = primaryScrollController(tester);
      controller.jumpTo(45);
      await tester.pumpAndSettle();

      // If the leading widget is present, the title widget should be to the
      // right of the leading widget and title spacing should be respected.
      Offset titleOffset = tester.getTopLeft(collapsedTitle);
      expect(titleOffset.dx, leadingPadding + titleSpacing);

      // Hide the leading widget.
      await tester.pumpWidget(buildWidget(showLeading: false));
      // Scroll to collapse the SliverAppBar.
      controller = primaryScrollController(tester);
      controller.jumpTo(45);
      await tester.pumpAndSettle();

      // If the leading widget is not present, the title widget will
      // only have the default title spacing.
      titleOffset = tester.getTopLeft(collapsedTitle);
      expect(titleOffset.dx, titleSpacing);
  });

  group('MaterialStateColor scrolledUnder', () {
    const double collapsedHeight = kToolbarHeight;
    const double expandedHeight = 200.0;
    const Color scrolledColor = Color(0xff00ff00);
    const Color defaultColor = Color(0xff0000ff);

      Widget buildSliverApp({
        required double contentHeight,
        bool reverse = false,
        bool includeFlexibleSpace = false,
      }) {
        return MaterialApp(
          home: Scaffold(
            body: CustomScrollView(
              reverse: reverse,
              slivers: <Widget>[
                SliverAppBar(
                  elevation: 0,
                  backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
                    return states.contains(MaterialState.scrolledUnder)
                      ? scrolledColor
                      : defaultColor;
                  }),
                  expandedHeight: expandedHeight,
                  pinned: true,
                  flexibleSpace: includeFlexibleSpace
                      ? const FlexibleSpaceBar(title: Text('SliverAppBar'))
                      : null,
                ),
                SliverList(
                  delegate: SliverChildListDelegate(
                    <Widget>[
                      Container(height: contentHeight, color: Colors.teal),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      }

      testWidgets('backgroundColor', (WidgetTester tester) async {
        await tester.pumpWidget(
          buildSliverApp(contentHeight: 1200.0)
        );

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);

        TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
        await gesture.moveBy(const Offset(0.0, -expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), scrolledColor);
        expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);

        gesture = await tester.startGesture(const Offset(50.0, 300.0));
        await gesture.moveBy(const Offset(0.0, expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
      });

      testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async {
        await tester.pumpWidget(
          buildSliverApp(contentHeight: 1200.0, includeFlexibleSpace: true)
        );

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);

        TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
        await gesture.moveBy(const Offset(0.0, -expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), scrolledColor);
        expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);

        gesture = await tester.startGesture(const Offset(50.0, 300.0));
        await gesture.moveBy(const Offset(0.0, expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
      });

      testWidgets('backgroundColor - reverse', (WidgetTester tester) async {
        await tester.pumpWidget(
          buildSliverApp(contentHeight: 1200.0, reverse: true)
        );

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);

        TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
        await gesture.moveBy(const Offset(0.0, expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), scrolledColor);
        expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);

        gesture = await tester.startGesture(const Offset(50.0, 300.0));
        await gesture.moveBy(const Offset(0.0, -expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
      });

      testWidgets('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async {
        await tester.pumpWidget(
          buildSliverApp(
            contentHeight: 1200.0,
            reverse: true,
            includeFlexibleSpace: true,
          )
        );

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);

        TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
        await gesture.moveBy(const Offset(0.0, expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), scrolledColor);
        expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);

        gesture = await tester.startGesture(const Offset(50.0, 300.0));
        await gesture.moveBy(const Offset(0.0, -expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
      });

      testWidgets('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async {
        await tester.pumpWidget(
          buildSliverApp(contentHeight: 200, reverse: true)
        );

        // In reverse, the content here is not long enough to scroll under the app
        // bar.
        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);

        final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
        await gesture.moveBy(const Offset(0.0, expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
      });

      testWidgets('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async {
        await tester.pumpWidget(
          buildSliverApp(
            contentHeight: 200,
            reverse: true,
            includeFlexibleSpace: true,
          )
        );

        // In reverse, the content here is not long enough to scroll under the app
        // bar.
        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);

        final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
        await gesture.moveBy(const Offset(0.0, expandedHeight));
        await gesture.up();
        await tester.pumpAndSettle();

        expect(getAppBarBackgroundColor(tester), defaultColor);
        expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
      });
    });

  testWidgets('SliverAppBar default configuration', (WidgetTester tester) async {
    await tester.pumpWidget(buildSliverAppBarApp());

    final ScrollController controller = primaryScrollController(tester);
    expect(controller.offset, 0.0);
    expect(find.byType(SliverAppBar), findsOneWidget);

    final double initialAppBarHeight = appBarHeight(tester);
    final double initialTabBarHeight = tabBarHeight(tester);

    // Scroll the not-pinned appbar partially out of view
    controller.jumpTo(50.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), initialAppBarHeight);
    expect(tabBarHeight(tester), initialTabBarHeight);

    // Scroll the not-pinned appbar out of view
    controller.jumpTo(600.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsNothing);
    expect(appBarHeight(tester), initialAppBarHeight);
    expect(tabBarHeight(tester), initialTabBarHeight);

    // Scroll the not-pinned appbar back into view
    controller.jumpTo(0.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), initialAppBarHeight);
    expect(tabBarHeight(tester), initialTabBarHeight);
  });

  testWidgets('SliverAppBar expandedHeight, pinned', (WidgetTester tester) async {
    await tester.pumpWidget(buildSliverAppBarApp(
      pinned: true,
      expandedHeight: 128.0,
    ));

    final ScrollController controller = primaryScrollController(tester);
    expect(controller.offset, 0.0);
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), 128.0);

    const double initialAppBarHeight = 128.0;
    final double initialTabBarHeight = tabBarHeight(tester);

    // Scroll the not-pinned appbar, collapsing the expanded height. At this
    // point both the toolbar and the tabbar are visible.
    controller.jumpTo(600.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(tabBarHeight(tester), initialTabBarHeight);
    expect(appBarHeight(tester), lessThan(initialAppBarHeight));
    expect(appBarHeight(tester), greaterThan(initialTabBarHeight));

    // Scroll the not-pinned appbar back into view
    controller.jumpTo(0.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), initialAppBarHeight);
    expect(tabBarHeight(tester), initialTabBarHeight);
  });

  testWidgets('SliverAppBar expandedHeight, pinned and floating', (WidgetTester tester) async {
    await tester.pumpWidget(buildSliverAppBarApp(
      floating: true,
      pinned: true,
      expandedHeight: 128.0,
    ));

    final ScrollController controller = primaryScrollController(tester);
    expect(controller.offset, 0.0);
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), 128.0);

    const double initialAppBarHeight = 128.0;
    final double initialTabBarHeight = tabBarHeight(tester);

    // Scroll the floating-pinned appbar, collapsing the expanded height. At this
    // point only the tabBar is visible.
    controller.jumpTo(600.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(tabBarHeight(tester), initialTabBarHeight);
    expect(appBarHeight(tester), lessThan(initialAppBarHeight));
    expect(appBarHeight(tester), initialTabBarHeight);

    // Scroll the floating-pinned appbar back into view
    controller.jumpTo(0.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), initialAppBarHeight);
    expect(tabBarHeight(tester), initialTabBarHeight);
  });

  testWidgets('SliverAppBar expandedHeight, floating with snap:true', (WidgetTester tester) async {
    await tester.pumpWidget(buildSliverAppBarApp(
      floating: true,
      snap: true,
      expandedHeight: 128.0,
    ));
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarTop(tester), 0.0);
    expect(appBarHeight(tester), 128.0);
    expect(appBarBottom(tester), 128.0);

    // Scroll to the middle of the list. The (floating) appbar is no longer visible.
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    position.jumpTo(256.00);
    await tester.pumpAndSettle();
    expect(find.byType(SliverAppBar), findsNothing);
    expect(appBarTop(tester), lessThanOrEqualTo(-128.0));

    // Drag the scrollable up and down. The app bar should not snap open, its
    // height should just track the drag offset.
    TestGesture gesture = await tester.startGesture(const Offset(50.0, 256.0));
    await gesture.moveBy(const Offset(0.0, 128.0)); // drag the appbar all the way open
    await tester.pump();
    expect(appBarTop(tester), 0.0);
    expect(appBarHeight(tester), 128.0);

    await gesture.moveBy(const Offset(0.0, -50.0));
    await tester.pump();
    expect(appBarBottom(tester), 78.0); // 78 == 128 - 50

    // Trigger the snap open animation: drag down and release
    await gesture.moveBy(const Offset(0.0, 10.0));
    await gesture.up();

    // Now verify that the appbar is animating open
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    double bottom = appBarBottom(tester);
    expect(bottom, greaterThan(88.0)); // 88 = 78 + 10

    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    expect(appBarBottom(tester), greaterThan(bottom));

    // The animation finishes when the appbar is full height.
    await tester.pumpAndSettle();
    expect(appBarHeight(tester), 128.0);

    // Now that the app bar is open, perform the same drag scenario
    // in reverse: drag the appbar up and down and then trigger the
    // snap closed animation.
    gesture = await tester.startGesture(const Offset(50.0, 256.0));
    await gesture.moveBy(const Offset(0.0, -128.0)); // drag the appbar closed
    await tester.pump();
    expect(appBarBottom(tester), 0.0);

    await gesture.moveBy(const Offset(0.0, 100.0));
    await tester.pump();
    expect(appBarBottom(tester), 100.0);

    // Trigger the snap close animation: drag upwards and release
    await gesture.moveBy(const Offset(0.0, -10.0));
    await gesture.up();

    // Now verify that the appbar is animating closed
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    bottom = appBarBottom(tester);
    expect(bottom, lessThan(90.0));

    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    expect(appBarBottom(tester), lessThan(bottom));

    // The animation finishes when the appbar is off screen.
    await tester.pumpAndSettle();
    expect(appBarTop(tester), lessThanOrEqualTo(0.0));
    expect(appBarBottom(tester), lessThanOrEqualTo(0.0));
  });

  testWidgets('SliverAppBar expandedHeight, floating and pinned with snap:true', (WidgetTester tester) async {
    await tester.pumpWidget(buildSliverAppBarApp(
      floating: true,
      pinned: true,
      snap: true,
      expandedHeight: 128.0,
    ));
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarTop(tester), 0.0);
    expect(appBarHeight(tester), 128.0);
    expect(appBarBottom(tester), 128.0);

    // Scroll to the middle of the list. The only the tab bar is visible
    // because this is a pinned appbar.
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    position.jumpTo(256.0);
    await tester.pumpAndSettle();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarTop(tester), 0.0);
    expect(appBarHeight(tester), kTextTabBarHeight);

    // Drag the scrollable up and down. The app bar should not snap open, the
    // bottom of the appbar should just track the drag offset.
    TestGesture gesture = await tester.startGesture(const Offset(50.0, 200.0));
    await gesture.moveBy(const Offset(0.0, 100.0));
    await tester.pump();
    expect(appBarHeight(tester), 100.0);

    await gesture.moveBy(const Offset(0.0, -25.0));
    await tester.pump();
    expect(appBarHeight(tester), 75.0);

    // Trigger the snap animation: drag down and release
    await gesture.moveBy(const Offset(0.0, 10.0));
    await gesture.up();

    // Now verify that the appbar is animating open
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    final double height = appBarHeight(tester);
    expect(height, greaterThan(85.0));
    expect(height, lessThan(128.0));

    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    expect(appBarHeight(tester), greaterThan(height));
    expect(appBarHeight(tester), lessThan(128.0));

    // The animation finishes when the appbar is fully expanded
    await tester.pumpAndSettle();
    expect(appBarTop(tester), 0.0);
    expect(appBarHeight(tester), 128.0);
    expect(appBarBottom(tester), 128.0);

    // Now that the appbar is fully expanded, Perform the same drag
    // scenario in reverse: drag the appbar up and down and then trigger
    // the snap closed animation.
    gesture = await tester.startGesture(const Offset(50.0, 256.0));
    await gesture.moveBy(const Offset(0.0, -128.0));
    await tester.pump();
    expect(appBarBottom(tester), kTextTabBarHeight);

    await gesture.moveBy(const Offset(0.0, 100.0));
    await tester.pump();
    expect(appBarBottom(tester), 100.0);

    // Trigger the snap close animation: drag upwards and release
    await gesture.moveBy(const Offset(0.0, -10.0));
    await gesture.up();

    // Now verify that the appbar is animating closed
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    final double bottom = appBarBottom(tester);
    expect(bottom, lessThan(90.0));

    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
    expect(appBarBottom(tester), lessThan(bottom));

    // The animation finishes when the appbar shrinks back to its pinned height
    await tester.pumpAndSettle();
    expect(appBarTop(tester), lessThanOrEqualTo(0.0));
    expect(appBarBottom(tester), kTextTabBarHeight);
  });

  testWidgets('SliverAppBar expandedHeight, collapsedHeight', (WidgetTester tester) async {
    const double expandedAppBarHeight = 400.0;
    const double collapsedAppBarHeight = 200.0;

    await tester.pumpWidget(buildSliverAppBarApp(
      collapsedHeight: collapsedAppBarHeight,
      expandedHeight: expandedAppBarHeight,
    ));

    final ScrollController controller = primaryScrollController(tester);
    expect(controller.offset, 0.0);
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight);

    final double initialTabBarHeight = tabBarHeight(tester);

    // Scroll the not-pinned appbar partially out of view.
    controller.jumpTo(50.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight - 50.0);
    expect(tabBarHeight(tester), initialTabBarHeight);

    // Scroll the not-pinned appbar out of view, to its collapsed height.
    controller.jumpTo(600.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsNothing);
    expect(appBarHeight(tester), collapsedAppBarHeight + initialTabBarHeight);
    expect(tabBarHeight(tester), initialTabBarHeight);

    // Scroll the not-pinned appbar back into view.
    controller.jumpTo(0.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight);
    expect(tabBarHeight(tester), initialTabBarHeight);
  });

  testWidgets('Material3 - SliverAppBar.medium defaults', (WidgetTester tester) async {
    final ThemeData theme = ThemeData(useMaterial3: true);
    const double collapsedAppBarHeight = 64;
    const double expandedAppBarHeight = 112;

    await tester.pumpWidget(MaterialApp(
      theme: theme,
      home: Scaffold(
        body: CustomScrollView(
          primary: true,
          slivers: <Widget>[
            const SliverAppBar.medium(
              title: Text('AppBar Title'),
            ),
            SliverToBoxAdapter(
              child: Container(
                height: 1200,
                color: Colors.orange[400],
              ),
            ),
          ],
        ),
      ),
    ));

    final ScrollController controller = primaryScrollController(tester);
    // There are two widgets for the title. The first title is a larger version
    // that is shown at the bottom when the app bar is expanded. It scrolls under
    // the main row until it is completely hidden and then the first title is
    // faded in. The last is the title on the mainrow with the icons. It is
    // transparent when the app bar is expanded, and opaque when it is collapsed.
    final Finder expandedTitle = find.text('AppBar Title').first;
    final Finder expandedTitleClip = find.ancestor(
      of: expandedTitle,
      matching: find.byType(ClipRect),
    ).first;
    final Finder collapsedTitle = find.text('AppBar Title').last;
    final Finder collapsedTitleOpacity = find.ancestor(
      of: collapsedTitle,
      matching: find.byType(AnimatedOpacity),
    );

    // Default, fully expanded app bar.
    expect(controller.offset, 0);
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
    expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);

    // Test the expanded title is positioned correctly.
    final Offset titleOffset = tester.getBottomLeft(expandedTitle);
    expect(titleOffset.dx, 16.0);
    if (!kIsWeb || isCanvasKit) {
      expect(titleOffset.dy, 96.0);
    }

    verifyTextNotClipped(expandedTitle, tester);

    // Test the expanded title default color.
    expect(
      tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
      theme.colorScheme.onSurface,
    );

    // Scroll the expanded app bar partially out of view.
    controller.jumpTo(45);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight - 45);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
    expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45);

    // Scroll so that it is completely collapsed.
    controller.jumpTo(600);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), collapsedAppBarHeight);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 1);
    expect(tester.getSize(expandedTitleClip).height, 0);

    // Scroll back to fully expanded.
    controller.jumpTo(0);
    await tester.pumpAndSettle();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
    expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
  });

  testWidgets('Material3 - SliverAppBar.large defaults', (WidgetTester tester) async {
    final ThemeData theme = ThemeData(useMaterial3: true);
    const double collapsedAppBarHeight = 64;
    const double expandedAppBarHeight = 152;

    await tester.pumpWidget(MaterialApp(
      theme: theme,
      home: Scaffold(
        body: CustomScrollView(
          primary: true,
          slivers: <Widget>[
            const SliverAppBar.large(
              title: Text('AppBar Title'),
            ),
            SliverToBoxAdapter(
              child: Container(
                height: 1200,
                color: Colors.orange[400],
              ),
            ),
          ],
        ),
      ),
    ));

    final ScrollController controller = primaryScrollController(tester);
    // There are two widgets for the title. The first title is a larger version
    // that is shown at the bottom when the app bar is expanded. It scrolls under
    // the main row until it is completely hidden and then the first title is
    // faded in. The last is the title on the mainrow with the icons. It is
    // transparent when the app bar is expanded, and opaque when it is collapsed.
    final Finder expandedTitle = find.text('AppBar Title').first;
    final Finder expandedTitleClip = find.ancestor(
      of: expandedTitle,
      matching: find.byType(ClipRect),
    ).first;
    final Finder collapsedTitle = find.text('AppBar Title').last;
    final Finder collapsedTitleOpacity = find.ancestor(
      of: collapsedTitle,
      matching: find.byType(AnimatedOpacity),
    );

    // Default, fully expanded app bar.
    expect(controller.offset, 0);
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
    expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);

    // Test the expanded title is positioned correctly.
    final Offset titleOffset = tester.getBottomLeft(expandedTitle);
    expect(titleOffset.dx, 16.0);
    final RenderSliver renderSliverAppBar = tester.renderObject(find.byType(SliverAppBar));
    // The expanded title and the bottom padding fits in the flexible space.
    expect(
      titleOffset.dy,
      renderSliverAppBar.geometry!.scrollExtent - 28.0,
      reason: 'bottom padding of a large expanded title should be 28.',
    );
    verifyTextNotClipped(expandedTitle, tester);

    // Test the expanded title default color.
    expect(
      tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
      theme.colorScheme.onSurface,
    );


    // Scroll the expanded app bar partially out of view.
    controller.jumpTo(45);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight - 45);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
    expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45);

    // Scroll so that it is completely collapsed.
    controller.jumpTo(600);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), collapsedAppBarHeight);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 1);
    expect(tester.getSize(expandedTitleClip).height, 0);

    // Scroll back to fully expanded.
    controller.jumpTo(0);
    await tester.pumpAndSettle();
    expect(find.byType(SliverAppBar), findsOneWidget);
    expect(appBarHeight(tester), expandedAppBarHeight);
    expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
    expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
  });

  group('SliverAppBar elevation', () {
    Widget buildSliverAppBar(bool forceElevated, {double? elevation, double? themeElevation}) {
      return MaterialApp(
        theme: ThemeData(
          appBarTheme: AppBarTheme(
            elevation: themeElevation,
            scrolledUnderElevation: themeElevation,
          ),
        ),
        home: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              title: const Text('Title'),
              forceElevated: forceElevated,
              elevation: elevation,
              scrolledUnderElevation: elevation,
            ),
          ],
        ),
      );
    }

    testWidgets('Respects forceElevated parameter', (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/59158.
      AppBar getAppBar() => tester.widget<AppBar>(find.byType(AppBar));
      Material getMaterial() => tester.widget<Material>(find.byType(Material));
      final bool useMaterial3 = ThemeData().useMaterial3;

      // When forceElevated is off, SliverAppBar should not be elevated.
      await tester.pumpWidget(buildSliverAppBar(false));
      expect(getMaterial().elevation, 0.0);

      // Default elevation should be used by the material, but
      // the AppBar's elevation should not be specified by SliverAppBar.
      // When useMaterial3 is true, and forceElevated is true, the default elevation
      // should be the value of `scrolledUnderElevation` which is 3.0
      await tester.pumpWidget(buildSliverAppBar(true));
      expect(getMaterial().elevation, useMaterial3 ? 3.0 : 4.0);
      expect(getAppBar().elevation, null);

      // SliverAppBar should use the specified elevation.
      await tester.pumpWidget(buildSliverAppBar(true, elevation: 8.0));
      expect(getMaterial().elevation, 8.0);
    });

    testWidgets('Uses elevation of AppBarTheme by default', (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/73525.
      Material getMaterial() => tester.widget<Material>(find.byType(Material));

      await tester.pumpWidget(buildSliverAppBar(false, themeElevation: 12.0));
      expect(getMaterial().elevation, 0.0);

      await tester.pumpWidget(buildSliverAppBar(true, themeElevation: 12.0));
      expect(getMaterial().elevation, 12.0);

      await tester.pumpWidget(buildSliverAppBar(true, elevation: 8.0, themeElevation: 12.0));
      expect(getMaterial().elevation, 8.0);
    });
  });

  group('SliverAppBar.forceMaterialTransparency', () {
    Material getSliverAppBarMaterial(WidgetTester tester) {
      return tester.widget<Material>(find
          .descendant(of: find.byType(SliverAppBar), matching: find.byType(Material))
          .first);
    }

    // Generates a MaterialApp with a SliverAppBar in a CustomScrollView.
    // The first cell of the scroll view contains a button at its top, and is
    // initially scrolled so that it is beneath the SliverAppBar.
    (ScrollController, Widget) buildWidget({
      required bool forceMaterialTransparency,
      required VoidCallback onPressed
    }) {
      const double appBarHeight = 120;
      final ScrollController controller = ScrollController(initialScrollOffset: appBarHeight);

      return (
      controller,
      MaterialApp(
        home: Scaffold(
          body: CustomScrollView(
              controller: controller,
              slivers: <Widget>[
                SliverAppBar(
                  collapsedHeight: appBarHeight,
                  expandedHeight: appBarHeight,
                  pinned: true,
                  elevation: 0,
                  backgroundColor: Colors.transparent,
                  forceMaterialTransparency: forceMaterialTransparency,
                  title: const Text('AppBar'),
                ),
                SliverList(
                  delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
                    return SizedBox(
                      height: appBarHeight,
                      child: index == 0
                          ? Align(
                          alignment: Alignment.topCenter,
                          child: TextButton(onPressed: onPressed, child: const Text('press')))
                          : const SizedBox(),
                    );
                  },
                    childCount: 20,
                  ),
                ),
              ]),
        ),
      ),
      );
    }

    testWidgets(
        'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async {
      bool buttonWasPressed = false;
      final (ScrollController controller, Widget widget) = buildWidget(
        forceMaterialTransparency:true,
        onPressed:() { buttonWasPressed = true; },
      );
      await tester.pumpWidget(widget);

      final Material material = getSliverAppBarMaterial(tester);
      expect(material.type, MaterialType.transparency);

      final Finder buttonFinder = find.byType(TextButton);
      await tester.tap(buttonFinder);
      await tester.pump();
      expect(buttonWasPressed, isTrue);

      controller.dispose();
    });

    testWidgets(
        'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async {
      // Set this, and tester.tap(warnIfMissed:false), to suppress
      // errors/warning that the button is not hittable (which is expected).
      WidgetController.hitTestWarningShouldBeFatal = false;

      bool buttonWasPressed = false;
      final (ScrollController controller, Widget widget) = buildWidget(
        forceMaterialTransparency:false,
        onPressed:() { buttonWasPressed = true; },
      );
      await tester.pumpWidget(widget);

      final Material material = getSliverAppBarMaterial(tester);
      expect(material.type, MaterialType.canvas);

      final Finder buttonFinder = find.byType(TextButton);
      await tester.tap(buttonFinder, warnIfMissed:false);
      await tester.pump();
      expect(buttonWasPressed, isFalse);

      controller.dispose();
    });
  });

  testWidgets('SliverAppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async {
    const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0));
    final Key leadingKey = UniqueKey();
    final Key titleKey = UniqueKey();
    final Key trailingKey = UniqueKey();

    await tester.pumpWidget(
      Localizations(
        locale: const Locale('en', 'US'),
        delegates: const <LocalizationsDelegate<dynamic>>[
          DefaultMaterialLocalizations.delegate,
          DefaultWidgetsLocalizations.delegate,
        ],
        child: Directionality(
          textDirection: TextDirection.rtl,
          child: MediaQuery(
            data: topPadding100,
            child: CustomScrollView(
              primary: true,
              slivers: <Widget>[
                SliverAppBar(
                  leading: Placeholder(key: leadingKey),
                  title: Placeholder(key: titleKey, fallbackHeight: kToolbarHeight),
                  actions: <Widget>[ Placeholder(key: trailingKey) ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
    expect(tester.getTopLeft(find.byType(AppBar)), Offset.zero);
    expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 100.0));
    expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(416.0, 100.0));
    expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(0.0, 100.0));
  });

  testWidgets('SliverAppBar positioning of leading and trailing widgets with bottom padding', (WidgetTester tester) async {
    const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0, bottom: 50.0));
    final Key leadingKey = UniqueKey();
    final Key titleKey = UniqueKey();
    final Key trailingKey = UniqueKey();

    await tester.pumpWidget(
      Localizations(
        locale: const Locale('en', 'US'),
        delegates: const <LocalizationsDelegate<dynamic>>[
          DefaultMaterialLocalizations.delegate,
          DefaultWidgetsLocalizations.delegate,
        ],
        child: Directionality(
          textDirection: TextDirection.rtl,
          child: MediaQuery(
            data: topPadding100,
            child: CustomScrollView(
              primary: true,
              slivers: <Widget>[
                SliverAppBar(
                  leading: Placeholder(key: leadingKey),
                  title: Placeholder(key: titleKey),
                  actions: <Widget>[ Placeholder(key: trailingKey) ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
    expect(tester.getRect(find.byType(AppBar)), const Rect.fromLTRB(0.0, 0.0, 800.00, 100.0 + 56.0));
    expect(tester.getRect(find.byKey(leadingKey)), const Rect.fromLTRB(800.0 - 56.0, 100.0, 800.0, 100.0 + 56.0));
    expect(tester.getRect(find.byKey(trailingKey)), const Rect.fromLTRB(0.0, 100.0, 400.0, 100.0 + 56.0));
  });

  testWidgets('SliverAppBar provides correct semantics in LTR', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: AppBar(
            leading: const Text('Leading'),
            title: const Text('Title'),
            actions: const <Widget>[
              Text('Action 1'),
              Text('Action 2'),
              Text('Action 3'),
            ],
            bottom: const PreferredSize(
              preferredSize: Size(0.0, kToolbarHeight),
              child: Text('Bottom'),
            ),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            children: <TestSemantics>[
              TestSemantics(
                children: <TestSemantics> [
                  TestSemantics(
                    flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                    children: <TestSemantics>[
                      TestSemantics(
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'Leading',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[
                              SemanticsFlag.namesRoute,
                              SemanticsFlag.isHeader,
                            ],
                            label: 'Title',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            label: 'Action 1',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            label: 'Action 2',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            label: 'Action 3',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            label: 'Bottom',
                            textDirection: TextDirection.ltr,
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true,
      ignoreTransform: true,
      ignoreId: true,
    ));

    semantics.dispose();
  });

  testWidgets('SliverAppBar provides correct semantics in RTL', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      MaterialApp(
        home: Semantics(
          textDirection: TextDirection.rtl,
          child: Directionality(
            textDirection: TextDirection.rtl,
            child: Center(
              child: AppBar(
                leading: const Text('Leading'),
                title: const Text('Title'),
                actions: const <Widget>[
                  Text('Action 1'),
                  Text('Action 2'),
                  Text('Action 3'),
                ],
                bottom: const PreferredSize(
                  preferredSize: Size(0.0, kToolbarHeight),
                  child: Text('Bottom'),
                ),
              ),
            ),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            children: <TestSemantics>[
              TestSemantics(
                children: <TestSemantics>[
                  TestSemantics(
                    flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                    children: <TestSemantics>[
                      TestSemantics(
                        textDirection: TextDirection.rtl,
                        children: <TestSemantics>[
                          TestSemantics(
                            children: <TestSemantics>[
                              TestSemantics(
                                label: 'Leading',
                                textDirection: TextDirection.rtl,
                              ),
                              TestSemantics(
                                flags: <SemanticsFlag>[
                                  SemanticsFlag.namesRoute,
                                  SemanticsFlag.isHeader,
                                ],
                                label: 'Title',
                                textDirection: TextDirection.rtl,
                              ),
                              TestSemantics(
                                label: 'Action 1',
                                textDirection: TextDirection.rtl,
                              ),
                              TestSemantics(
                                label: 'Action 2',
                                textDirection: TextDirection.rtl,
                              ),
                              TestSemantics(
                                label: 'Action 3',
                                textDirection: TextDirection.rtl,
                              ),
                              TestSemantics(
                                label: 'Bottom',
                                textDirection: TextDirection.rtl,
                              ),
                            ],
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true,
      ignoreTransform: true,
      ignoreId: true,
    ));

    semantics.dispose();
  });

  testWidgets('SliverAppBar excludes header semantics correctly', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      const MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              leading: Text('Leading'),
              flexibleSpace: ExcludeSemantics(child: Text('Title')),
              actions: <Widget>[Text('Action 1')],
              excludeHeaderSemantics: true,
            ),
          ],
        ),
      ),
    );

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            textDirection: TextDirection.ltr,
            children: <TestSemantics>[
              TestSemantics(
                children: <TestSemantics>[
                  TestSemantics(
                    flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                    children: <TestSemantics>[
                      TestSemantics(
                        children: <TestSemantics>[
                          TestSemantics(
                            children: <TestSemantics>[
                              TestSemantics(
                                children: <TestSemantics>[
                                  TestSemantics(
                                    label: 'Leading',
                                    textDirection: TextDirection.ltr,
                                  ),
                                  TestSemantics(
                                    label: 'Action 1',
                                    textDirection: TextDirection.ltr,
                                  ),
                                ],
                              ),
                              TestSemantics(),
                            ],
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true,
      ignoreTransform: true,
      ignoreId: true,
    ));

    semantics.dispose();
  });

  testWidgets('SliverAppBar with flexible space has correct semantics order', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/64922.
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      const MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              leading: Text('Leading'),
              flexibleSpace: Text('Flexible space'),
              actions: <Widget>[Text('Action 1')],
            ),
          ],
        ),
      ),
    );

    expect(semantics, hasSemantics(
      TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics(
            textDirection: TextDirection.ltr,
            children: <TestSemantics>[
              TestSemantics(
                children: <TestSemantics>[
                  TestSemantics(
                    flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                    children: <TestSemantics>[
                      TestSemantics(
                        children: <TestSemantics>[
                          TestSemantics(
                            children: <TestSemantics>[
                              TestSemantics(
                                children: <TestSemantics>[
                                  TestSemantics(
                                    label: 'Leading',
                                    textDirection: TextDirection.ltr,
                                  ),
                                  TestSemantics(
                                    label: 'Action 1',
                                    textDirection: TextDirection.ltr,
                                  ),
                                ],
                              ),
                              TestSemantics(
                                children: <TestSemantics>[
                                  TestSemantics(
                                    flags: <SemanticsFlag>[SemanticsFlag.isHeader],
                                    label: 'Flexible space',
                                    textDirection: TextDirection.ltr,
                                  ),
                                ],
                              ),
                            ],
                          ),
                          TestSemantics(
                            flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      ignoreRect: true,
      ignoreTransform: true,
      ignoreId: true,
    ));

    semantics.dispose();
  });

  testWidgets('Changing SliverAppBar snap from true to false', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/17598
    const double appBarHeight = 256.0;
    bool snap = true;

    await tester.pumpWidget(
      MaterialApp(
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Scaffold(
              body: CustomScrollView(
                slivers: <Widget>[
                  SliverAppBar(
                    expandedHeight: appBarHeight,
                    floating: true,
                    snap: snap,
                    actions: <Widget>[
                      TextButton(
                        child: const Text('snap=false'),
                        onPressed: () {
                          setState(() {
                            snap = false;
                          });
                        },
                      ),
                    ],
                    flexibleSpace: FlexibleSpaceBar(
                      background: Container(
                        height: appBarHeight,
                        color: Colors.orange,
                      ),
                    ),
                  ),
                  SliverList(
                    delegate: SliverChildListDelegate(
                      <Widget>[
                        Container(height: 1200.0, color: Colors.teal),
                      ],
                    ),
                  ),
                ],
              ),
            );
          },
        ),
      ),
    );

    TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
    await gesture.moveBy(const Offset(0.0, -100.0));
    await gesture.up();

    await tester.tap(find.text('snap=false'));
    await tester.pumpAndSettle();

    gesture = await tester.startGesture(const Offset(50.0, 400.0));
    await gesture.moveBy(const Offset(0.0, -100.0));
    await gesture.up();
    await tester.pump();
  });

  testWidgets('SliverAppBar shape default', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              leading: Text('L'),
              title: Text('No Scaffold'),
              actions: <Widget>[Text('A1'), Text('A2')],
            ),
          ],
        ),
      ),
    );

    final Finder sliverAppBarFinder = find.byType(SliverAppBar);
    SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget<SliverAppBar>(finder);
    expect(getSliverAppBarWidget(sliverAppBarFinder).shape, null);

    final Finder materialFinder = find.byType(Material);
    Material getMaterialWidget(Finder finder) => tester.widget<Material>(finder);
    expect(getMaterialWidget(materialFinder).shape, null);
  });

  testWidgets('SliverAppBar with shape', (WidgetTester tester) async {
    const RoundedRectangleBorder roundedRectangleBorder = RoundedRectangleBorder(
      borderRadius: BorderRadius.all(Radius.circular(15.0)),
    );
    await tester.pumpWidget(
      const MaterialApp(
        home: CustomScrollView(
          slivers: <Widget>[
            SliverAppBar(
              leading: Text('L'),
              title: Text('No Scaffold'),
              actions: <Widget>[Text('A1'), Text('A2')],
              shape: roundedRectangleBorder,
            ),
          ],
        ),
      ),
    );

    final Finder sliverAppBarFinder = find.byType(SliverAppBar);
    SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget<SliverAppBar>(finder);
    expect(getSliverAppBarWidget(sliverAppBarFinder).shape, roundedRectangleBorder);

    final Finder materialFinder = find.byType(Material);
    Material getMaterialWidget(Finder finder) => tester.widget<Material>(finder);
    expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder);
  });

  testWidgets('SliverAppBar configures the delegate properly', (WidgetTester tester) async {
    Future<void> buildAndVerifyDelegate({ required bool pinned, required bool floating, required bool snap }) async {
      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            slivers: <Widget>[
              SliverAppBar(
                title: const Text('Jumbo'),
                pinned: pinned,
                floating: floating,
                snap: snap,
              ),
            ],
          ),
        ),
      );

      final SliverPersistentHeaderDelegate delegate = tester
        .widget<SliverPersistentHeader>(find.byType(SliverPersistentHeader))
        .delegate;

      // Ensure we have a non-null vsync when it's needed.
      if (!floating || (delegate.snapConfiguration == null && delegate.showOnScreenConfiguration == null)) {
        expect(delegate.vsync, isNotNull);
      }

      expect(delegate.showOnScreenConfiguration != null, snap && floating);
    }

    await buildAndVerifyDelegate(pinned: false, floating: true, snap: false);
    await buildAndVerifyDelegate(pinned: false, floating: true, snap: true);

    await buildAndVerifyDelegate(pinned: true, floating: true, snap: false);
    await buildAndVerifyDelegate(pinned: true, floating: true, snap: true);
  });

  testWidgets('SliverAppBar default collapsedHeight with respect to toolbarHeight', (WidgetTester tester) async {
    const double toolbarHeight = 100.0;

    await tester.pumpWidget(buildSliverAppBarApp(
      toolbarHeight: toolbarHeight,
    ));

    final ScrollController controller = primaryScrollController(tester);
    final double initialTabBarHeight = tabBarHeight(tester);

    // Scroll the not-pinned appbar out of view, to its collapsed height.
    controller.jumpTo(300.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsNothing);
    // By default, the collapsedHeight is toolbarHeight + bottom.preferredSize.height,
    // in this case initialTabBarHeight.
    expect(appBarHeight(tester), toolbarHeight + initialTabBarHeight);
  });

  testWidgets('SliverAppBar collapsedHeight with toolbarHeight', (WidgetTester tester) async {
    const double toolbarHeight = 100.0;
    const double collapsedHeight = 150.0;

    await tester.pumpWidget(buildSliverAppBarApp(
      toolbarHeight: toolbarHeight,
      collapsedHeight: collapsedHeight,
    ));

    final ScrollController controller = primaryScrollController(tester);
    final double initialTabBarHeight = tabBarHeight(tester);

    // Scroll the not-pinned appbar out of view, to its collapsed height.
    controller.jumpTo(300.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsNothing);
    expect(appBarHeight(tester), collapsedHeight + initialTabBarHeight);
  });

  testWidgets('SliverAppBar collapsedHeight', (WidgetTester tester) async {
    const double collapsedHeight = 56.0;

    await tester.pumpWidget(buildSliverAppBarApp(
      collapsedHeight: collapsedHeight,
    ));

    final ScrollController controller = primaryScrollController(tester);
    final double initialTabBarHeight = tabBarHeight(tester);

    // Scroll the not-pinned appbar out of view, to its collapsed height.
    controller.jumpTo(300.0);
    await tester.pump();
    expect(find.byType(SliverAppBar), findsNothing);
    expect(appBarHeight(tester), collapsedHeight + initialTabBarHeight);
  });

  testWidgets('SliverAppBar respects leadingWidth', (WidgetTester tester) async {
    const Key key = Key('leading');
    await tester.pumpWidget(const MaterialApp(
      home: CustomScrollView(
        slivers: <Widget>[
          SliverAppBar(
            leading: Placeholder(key: key),
            leadingWidth: 100,
            title: Text('Title'),
          ),
        ],
      ),
    ));

    // By default toolbarHeight is 56.0.
    expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0, 0, 100, 56));
  });

  testWidgets('SliverAppBar.titleSpacing defaults to NavigationToolbar.kMiddleSpacing', (WidgetTester tester) async {
    await tester.pumpWidget(buildSliverAppBarApp());

    final NavigationToolbar navToolBar = tester.widget(find.byType(NavigationToolbar));
    expect(navToolBar.middleSpacing, NavigationToolbar.kMiddleSpacing);
  });

  group('Material 2', () {
    // These tests are only relevant for Material 2. Once Material 2
    // support is deprecated and the APIs are removed, these tests
    // can be deleted.

    testWidgets('Material2 - SliverAppBar.medium defaults', (WidgetTester tester) async {
      final ThemeData theme = ThemeData(useMaterial3: false);
      const double collapsedAppBarHeight = 64;
      const double expandedAppBarHeight = 112;

      await tester.pumpWidget(MaterialApp(
        theme: theme,
        home: Scaffold(
          body: CustomScrollView(
            primary: true,
            slivers: <Widget>[
              const SliverAppBar.medium(
                title: Text('AppBar Title'),
              ),
              SliverToBoxAdapter(
                child: Container(
                  height: 1200,
                  color: Colors.orange[400],
                ),
              ),
            ],
          ),
        ),
      ));

      final ScrollController controller = primaryScrollController(tester);
      // There are two widgets for the title. The first title is a larger version
      // that is shown at the bottom when the app bar is expanded. It scrolls under
      // the main row until it is completely hidden and then the first title is
      // faded in. The last is the title on the mainrow with the icons. It is
      // transparent when the app bar is expanded, and opaque when it is collapsed.
      final Finder expandedTitle = find.text('AppBar Title').first;
      final Finder expandedTitleClip = find.ancestor(
        of: expandedTitle,
        matching: find.byType(ClipRect),
      );
      final Finder collapsedTitle = find.text('AppBar Title').last;
      final Finder collapsedTitleOpacity = find.ancestor(
        of: collapsedTitle,
        matching: find.byType(AnimatedOpacity),
      );

      // Default, fully expanded app bar.
      expect(controller.offset, 0);
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), expandedAppBarHeight);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
      expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);

      // Test the expanded title is positioned correctly.
      final Offset titleOffset = tester.getBottomLeft(expandedTitle);
      expect(titleOffset, const Offset(16.0, 92.0));

      // Test the expanded title default color.
      expect(
        tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
        theme.colorScheme.onPrimary,
      );

      // Scroll the expanded app bar partially out of view.
      controller.jumpTo(45);
      await tester.pump();
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), expandedAppBarHeight - 45);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
      expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45);

      // Scroll so that it is completely collapsed.
      controller.jumpTo(600);
      await tester.pump();
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), collapsedAppBarHeight);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 1);
      expect(tester.getSize(expandedTitleClip).height, 0);

      // Scroll back to fully expanded.
      controller.jumpTo(0);
      await tester.pumpAndSettle();
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), expandedAppBarHeight);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
      expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
    });

    testWidgets('Material2 - SliverAppBar.large defaults', (WidgetTester tester) async {
      final ThemeData theme = ThemeData(useMaterial3: false);
      const double collapsedAppBarHeight = 64;
      const double expandedAppBarHeight = 152;

      await tester.pumpWidget(MaterialApp(
        theme: theme,
        home: Scaffold(
          body: CustomScrollView(
            primary: true,
            slivers: <Widget>[
              const SliverAppBar.large(
                title: Text('AppBar Title'),
              ),
              SliverToBoxAdapter(
                child: Container(
                  height: 1200,
                  color: Colors.orange[400],
                ),
              ),
            ],
          ),
        ),
      ));

      final ScrollController controller = primaryScrollController(tester);
      // There are two widgets for the title. The first title is a larger version
      // that is shown at the bottom when the app bar is expanded. It scrolls under
      // the main row until it is completely hidden and then the first title is
      // faded in. The last is the title on the mainrow with the icons. It is
      // transparent when the app bar is expanded, and opaque when it is collapsed.
      final Finder expandedTitle = find.text('AppBar Title').first;
      final Finder expandedTitleClip = find.ancestor(
        of: expandedTitle,
        matching: find.byType(ClipRect),
      );
      final Finder collapsedTitle = find.text('AppBar Title').last;
      final Finder collapsedTitleOpacity = find.ancestor(
        of: collapsedTitle,
        matching: find.byType(AnimatedOpacity),
      );

      // Default, fully expanded app bar.
      expect(controller.offset, 0);
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), expandedAppBarHeight);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
      expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);

      // Test the expanded title is positioned correctly.
      final Offset titleOffset = tester.getBottomLeft(expandedTitle);
      expect(titleOffset, const Offset(16.0, 124.0));

      // Test the expanded title default color.
      expect(
        tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
        theme.colorScheme.onPrimary,
      );

      // Scroll the expanded app bar partially out of view.
      controller.jumpTo(45);
      await tester.pump();
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), expandedAppBarHeight - 45);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
      expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45);

      // Scroll so that it is completely collapsed.
      controller.jumpTo(600);
      await tester.pump();
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), collapsedAppBarHeight);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 1);
      expect(tester.getSize(expandedTitleClip).height, 0);

      // Scroll back to fully expanded.
      controller.jumpTo(0);
      await tester.pumpAndSettle();
      expect(find.byType(SliverAppBar), findsOneWidget);
      expect(appBarHeight(tester), expandedAppBarHeight);
      expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
      expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
    });
  });
}
