From 42add0f8fa15e65cd2b1892ca00221e0edbfa8b6 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 31 Jan 2024 10:13:17 -0600 Subject: [PATCH] Split out AppBar/SliverAppBar material tests (#142560) While looking into resolving https://github.com/flutter/flutter/issues/117903, I found the massive test file `app_bar_test.dart` and found it unwieldy to work with. So before proposing a solution to #117903, which would touch many of these tests, I figured a clean up would be best first. This splits up `app_bar_test.dart` with a new file `app_bar_sliver_test.dart`, and adds `app_bar_utils.dart` for shared test methods. It basically moves all SliverAppBar tests into their own file, leaving just AppBar tests in the original file. --- .../test/material/app_bar_sliver_test.dart | 2446 +++++++++++ .../flutter/test/material/app_bar_test.dart | 3604 +++-------------- .../flutter/test/material/app_bar_utils.dart | 50 + 3 files changed, 3071 insertions(+), 3029 deletions(-) create mode 100644 packages/flutter/test/material/app_bar_sliver_test.dart create mode 100644 packages/flutter/test/material/app_bar_utils.dart diff --git a/packages/flutter/test/material/app_bar_sliver_test.dart b/packages/flutter/test/material/app_bar_sliver_test.dart new file mode 100644 index 0000000000..1c05386309 --- /dev/null +++ b/packages/flutter/test/material/app_bar_sliver_test.dart @@ -0,0 +1,2446 @@ +// 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: [ + SliverAppBar( + title: const Text('AppBar Title'), + floating: floating, + pinned: pinned, + collapsedHeight: collapsedHeight, + expandedHeight: expandedHeight, + toolbarHeight: toolbarHeight, + snap: snap, + bottom: TabBar( + tabs: ['A','B','C'].map((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( + builder: (BuildContext context) { + return Scaffold( + body: CustomScrollView( + primary: true, + slivers: [ + 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: [ + SliverAppBar.medium( + leading: IconButton( + onPressed: () {}, + icon: const Icon(Icons.menu), + ), + title: const Text(title), + bottom: const TabBar( + tabs: [ + 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: [ + SliverAppBar.large( + leading: IconButton( + onPressed: () {}, + icon: const Icon(Icons.menu), + ), + title: const Text(title), + bottom: const TabBar( + tabs: [ + 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: [ + 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: [ + 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: [ + 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: [ + 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: [ + 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 [ + 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: [ + 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 [ + 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: [ + 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: [ + 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: [ + 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: [ + 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: [ + 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: [ + 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: [ + SliverAppBar( + elevation: 0, + backgroundColor: MaterialStateColor.resolveWith((Set states) { + return states.contains(MaterialState.scrolledUnder) + ? scrolledColor + : defaultColor; + }), + expandedHeight: expandedHeight, + pinned: true, + flexibleSpace: includeFlexibleSpace + ? const FlexibleSpaceBar(title: Text('SliverAppBar')) + : null, + ), + SliverList( + delegate: SliverChildListDelegate( + [ + 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(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(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: [ + 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(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(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(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(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(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: [ + 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(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(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(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(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(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: [ + 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(find.byType(AppBar)); + Material getMaterial() => tester.widget(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(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(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: [ + 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 >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Directionality( + textDirection: TextDirection.rtl, + child: MediaQuery( + data: topPadding100, + child: CustomScrollView( + primary: true, + slivers: [ + SliverAppBar( + leading: Placeholder(key: leadingKey), + title: Placeholder(key: titleKey, fallbackHeight: kToolbarHeight), + actions: [ 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 >[ + DefaultMaterialLocalizations.delegate, + DefaultWidgetsLocalizations.delegate, + ], + child: Directionality( + textDirection: TextDirection.rtl, + child: MediaQuery( + data: topPadding100, + child: CustomScrollView( + primary: true, + slivers: [ + SliverAppBar( + leading: Placeholder(key: leadingKey), + title: Placeholder(key: titleKey), + actions: [ 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 [ + 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( + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + children: [ + TestSemantics( + label: 'Leading', + textDirection: TextDirection.ltr, + ), + TestSemantics( + flags: [ + 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 [ + 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( + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + textDirection: TextDirection.rtl, + children: [ + TestSemantics( + children: [ + TestSemantics( + label: 'Leading', + textDirection: TextDirection.rtl, + ), + TestSemantics( + flags: [ + 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: [ + SliverAppBar( + leading: Text('Leading'), + flexibleSpace: ExcludeSemantics(child: Text('Title')), + actions: [Text('Action 1')], + excludeHeaderSemantics: true, + ), + ], + ), + ), + ); + + expect(semantics, hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + children: [ + TestSemantics( + children: [ + TestSemantics( + children: [ + TestSemantics( + label: 'Leading', + textDirection: TextDirection.ltr, + ), + TestSemantics( + label: 'Action 1', + textDirection: TextDirection.ltr, + ), + ], + ), + TestSemantics(), + ], + ), + TestSemantics( + flags: [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: [ + SliverAppBar( + leading: Text('Leading'), + flexibleSpace: Text('Flexible space'), + actions: [Text('Action 1')], + ), + ], + ), + ), + ); + + expect(semantics, hasSemantics( + TestSemantics.root( + children: [ + TestSemantics( + textDirection: TextDirection.ltr, + children: [ + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.scopesRoute], + children: [ + TestSemantics( + children: [ + TestSemantics( + children: [ + TestSemantics( + children: [ + TestSemantics( + label: 'Leading', + textDirection: TextDirection.ltr, + ), + TestSemantics( + label: 'Action 1', + textDirection: TextDirection.ltr, + ), + ], + ), + TestSemantics( + children: [ + TestSemantics( + flags: [SemanticsFlag.isHeader], + label: 'Flexible space', + textDirection: TextDirection.ltr, + ), + ], + ), + ], + ), + TestSemantics( + flags: [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: [ + SliverAppBar( + expandedHeight: appBarHeight, + floating: true, + snap: snap, + actions: [ + TextButton( + child: const Text('snap=false'), + onPressed: () { + setState(() { + snap = false; + }); + }, + ), + ], + flexibleSpace: FlexibleSpaceBar( + background: Container( + height: appBarHeight, + color: Colors.orange, + ), + ), + ), + SliverList( + delegate: SliverChildListDelegate( + [ + 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: [ + SliverAppBar( + leading: Text('L'), + title: Text('No Scaffold'), + actions: [Text('A1'), Text('A2')], + ), + ], + ), + ), + ); + + final Finder sliverAppBarFinder = find.byType(SliverAppBar); + SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget(finder); + expect(getSliverAppBarWidget(sliverAppBarFinder).shape, null); + + final Finder materialFinder = find.byType(Material); + Material getMaterialWidget(Finder finder) => tester.widget(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: [ + SliverAppBar( + leading: Text('L'), + title: Text('No Scaffold'), + actions: [Text('A1'), Text('A2')], + shape: roundedRectangleBorder, + ), + ], + ), + ), + ); + + final Finder sliverAppBarFinder = find.byType(SliverAppBar); + SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget(finder); + expect(getSliverAppBarWidget(sliverAppBarFinder).shape, roundedRectangleBorder); + + final Finder materialFinder = find.byType(Material); + Material getMaterialWidget(Finder finder) => tester.widget(finder); + expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder); + }); + + testWidgets('SliverAppBar configures the delegate properly', (WidgetTester tester) async { + Future buildAndVerifyDelegate({ required bool pinned, required bool floating, required bool snap }) async { + await tester.pumpWidget( + MaterialApp( + home: CustomScrollView( + slivers: [ + SliverAppBar( + title: const Text('Jumbo'), + pinned: pinned, + floating: floating, + snap: snap, + ), + ], + ), + ), + ); + + final SliverPersistentHeaderDelegate delegate = tester + .widget(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: [ + 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: [ + 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(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(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(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(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(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: [ + 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(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(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(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(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(collapsedTitleOpacity).opacity, 0); + expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + }); + }); +} diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 441e2c966c..bee6c74cc6 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -2,57 +2,13 @@ // 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/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.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: [ - SliverAppBar( - title: const Text('AppBar Title'), - floating: floating, - pinned: pinned, - collapsedHeight: collapsedHeight, - expandedHeight: expandedHeight, - toolbarHeight: toolbarHeight, - snap: snap, - bottom: TabBar( - tabs: ['A','B','C'].map((String t) => Tab(text: 'TAB $t')).toList(), - ), - ), - SliverToBoxAdapter( - child: Container( - height: 1200.0, - color: Colors.orange[400], - ), - ), - ], - ), - ), - ), - ); -} - -ScrollController primaryScrollController(WidgetTester tester) { - return PrimaryScrollController.of(tester.element(find.byType(CustomScrollView))); -} +import 'app_bar_utils.dart'; TextStyle? _iconStyle(WidgetTester tester, IconData icon) { final RichText iconRichText = tester.widget( @@ -60,22 +16,6 @@ TextStyle? _iconStyle(WidgetTester tester, IconData icon) { ); return iconRichText.text.style; } - -void _verifyTextNotClipped(Finder textFinder, WidgetTester tester) { - final Rect clipRect = tester.getRect(find.ancestor(of: textFinder, matching: find.byType(ClipRect)).first); - final Rect textRect = tester.getRect(textFinder); - expect(textRect.top, inInclusiveRange(clipRect.top, clipRect.bottom)); - expect(textRect.bottom, inInclusiveRange(clipRect.top, clipRect.bottom)); - expect(textRect.left, inInclusiveRange(clipRect.left, clipRect.right)); - expect(textRect.right, inInclusiveRange(clipRect.left, clipRect.right)); -} - -double appBarHeight(WidgetTester tester) => tester.getSize(find.byType(AppBar, skipOffstage: false)).height; -double appBarTop(WidgetTester tester) => tester.getTopLeft(find.byType(AppBar, skipOffstage: false)).dy; -double appBarBottom(WidgetTester tester) => tester.getBottomLeft(find.byType(AppBar, skipOffstage: false)).dy; - -double tabBarHeight(WidgetTester tester) => tester.getSize(find.byType(TabBar, skipOffstage: false)).height; - void main() { setUp(() { debugResetSemanticsIdCounter(); @@ -558,26 +498,6 @@ void main() { ); }); - testWidgets('Material2 - AppBar drawer icon has default color', (WidgetTester tester) async { - final ThemeData themeData = ThemeData.from( - colorScheme: const ColorScheme.light(), - useMaterial3: false, - ); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: Scaffold( - appBar: AppBar( - title: const Text('Howdy!'), - ), - drawer: const Drawer(), - ), - ), - ); - - expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary); - }); - testWidgets('Material3 - AppBar drawer icon has default color', (WidgetTester tester) async { final ThemeData themeData = ThemeData.from( colorScheme: const ColorScheme.light(), @@ -655,26 +575,6 @@ void main() { ); }); - testWidgets('Material2 - AppBar endDrawer icon has default color', (WidgetTester tester) async { - final ThemeData themeData = ThemeData.from( - colorScheme: const ColorScheme.light(), - useMaterial3: false, - ); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: Scaffold( - appBar: AppBar( - title: const Text('Howdy!'), - ), - endDrawer: const Drawer(), - ), - ), - ); - - expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary); - }); - testWidgets('Material3 - AppBar endDrawer icon has default color', (WidgetTester tester) async { final ThemeData themeData = ThemeData.from( colorScheme: const ColorScheme.light(), @@ -733,64 +633,6 @@ void main() { expect(_iconStyle(tester, Icons.menu)?.color, color); }); - testWidgets('Material2 - leading widget extends to edge and is square', (WidgetTester tester) async { - final ThemeData themeData = ThemeData( - platform: TargetPlatform.android, - useMaterial3: false, - ); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: Scaffold( - appBar: AppBar( - leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), - title: const Text('X'), - ), - drawer: const Column(), // Doesn't really matter. Triggers a hamburger regardless. - ), - ), - ); - - // Default IconButton has a size of (56x56). - final Finder hamburger = find.byType(IconButton); - expect(tester.getTopLeft(hamburger), Offset.zero); - expect(tester.getSize(hamburger), const Size(56.0, 56.0)); - - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: Scaffold( - appBar: AppBar( - leading: Container(), - title: const Text('X'), - ), - ), - ), - ); - - // Default leading widget has a size of (56x56). - final Finder leadingBox = find.byType(Container); - expect(tester.getTopLeft(leadingBox), Offset.zero); - expect(tester.getSize(leadingBox), const Size(56.0, 56.0)); - - // The custom leading widget should still be 56x56 even if its size is smaller. - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: Scaffold( - appBar: AppBar( - leading: const SizedBox(height: 36, width: 36,), - title: const Text('X'), - ), // Doesn't really matter. Triggers a hamburger regardless. - ), - ), - ); - - final Finder leading = find.byType(SizedBox); - expect(tester.getTopLeft(leading), Offset.zero); - expect(tester.getSize(leading), const Size(56.0, 56.0)); - }); - testWidgets('Material3 - leading widget extends to edge and is square', (WidgetTester tester) async { final ThemeData themeData = ThemeData( platform: TargetPlatform.android, @@ -849,46 +691,6 @@ void main() { expect(tester.getSize(leading), const Size(56.0, 56.0)); }); - testWidgets('Material2 - Action is 4dp from edge and 48dp min', (WidgetTester tester) async { - final ThemeData theme = ThemeData( - platform: TargetPlatform.android, - useMaterial3: false, - ); - await tester.pumpWidget( - MaterialApp( - theme: theme, - home: Scaffold( - appBar: AppBar( - title: const Text('X'), - actions: const [ - IconButton( - icon: Icon(Icons.share), - onPressed: null, - tooltip: 'Share', - iconSize: 20.0, - ), - IconButton( - icon: Icon(Icons.add), - onPressed: null, - tooltip: 'Add', - iconSize: 60.0, - ), - ], - ), - ), - ), - ); - - final Finder addButton = find.widgetWithIcon(IconButton, Icons.add); - expect(tester.getTopRight(addButton), const Offset(800.0, 0.0)); - // It's still the size it was plus the 2 * 8dp padding from IconButton. - expect(tester.getSize(addButton), const Size(60.0 + 2 * 8.0, 56.0)); - - final Finder shareButton = find.widgetWithIcon(IconButton, Icons.share); - // The 20dp icon is expanded to fill the IconButton's touch target to 48dp. - expect(tester.getSize(shareButton), const Size(48.0, 56.0)); - }); - testWidgets('Material3 - Action is 4dp from edge and 48dp min', (WidgetTester tester) async { final ThemeData theme = ThemeData( platform: TargetPlatform.android, @@ -929,511 +731,6 @@ void main() { expect(tester.getSize(shareButton), const Size(48.0, 48.0)); }); - 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(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(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: [ - 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(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(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(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(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(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: [ - 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(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(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(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(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(collapsedTitleOpacity).opacity, 0); - expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); - }); - - testWidgets('Material2 - AppBar uses the specified elevation or defaults to 4.0', (WidgetTester tester) async { - Widget buildAppBar([double? elevation]) { - return MaterialApp( - theme: ThemeData(useMaterial3: false), - home: Scaffold( - appBar: AppBar(title: const Text('Title'), elevation: elevation), - ), - ); - } - - Material getMaterial() => tester.widget(find.descendant( - of: find.byType(AppBar), - matching: find.byType(Material), - )); - - // Default elevation should be used for the material. - await tester.pumpWidget(buildAppBar()); - expect(getMaterial().elevation, 4); - - // AppBar should use the specified elevation. - await tester.pumpWidget(buildAppBar(8.0)); - expect(getMaterial().elevation, 8.0); - }); - testWidgets('Material3 - AppBar uses the specified elevation or defaults to 0', (WidgetTester tester) async { Widget buildAppBar([double? elevation]) { return MaterialApp( @@ -1538,164 +835,6 @@ void main() { expect(getMaterial().elevation, 10); }); - group('SliverAppBar elevation', () { - Widget buildSliverAppBar(bool forceElevated, {double? elevation, double? themeElevation}) { - return MaterialApp( - theme: ThemeData( - appBarTheme: AppBarTheme( - elevation: themeElevation, - scrolledUnderElevation: themeElevation, - ), - ), - home: CustomScrollView( - slivers: [ - 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(find.byType(AppBar)); - Material getMaterial() => tester.widget(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(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(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: [ - 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('AppBar dimensions, with and without bottom, primary', (WidgetTester tester) async { const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0)); @@ -2006,53 +1145,6 @@ void main() { ); }); - testWidgets('Material2 - AppBar ink splash draw on the correct canvas', (WidgetTester tester) async { - // This is a regression test for https://github.com/flutter/flutter/issues/58665 - final Key key = UniqueKey(); - await tester.pumpWidget( - MaterialApp( - // Test was designed against InkSplash so need to make sure that is used. - theme: ThemeData( - useMaterial3: false, - splashFactory: InkSplash.splashFactory - ), - home: Center( - child: AppBar( - title: const Text('Abc'), - actions: [ - IconButton( - key: key, - icon: const Icon(Icons.add_circle), - tooltip: 'First button', - onPressed: () {}, - ), - ], - flexibleSpace: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: const Alignment(-0.04, 1.0), - colors: [Colors.blue.shade500, Colors.blue.shade800], - ), - ), - ), - ), - ), - ), - ); - final RenderObject painter = tester.renderObject( - find.descendant( - of: find.descendant( - of: find.byType(AppBar), - matching: find.byType(Stack), - ), - matching: find.byType(Material), - ), - ); - await tester.tap(find.byKey(key)); - expect(painter, paints..save()..translate()..save()..translate()..circle(x: 24.0, y: 28.0)); - }); - testWidgets('Material3 - AppBar ink splash draw on the correct canvas', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/58665 final Key key = UniqueKey(); @@ -2265,252 +1357,6 @@ void main() { expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(10 + NavigationToolbar.kMiddleSpacing, 72)); }); - 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 >[ - DefaultMaterialLocalizations.delegate, - DefaultWidgetsLocalizations.delegate, - ], - child: Directionality( - textDirection: TextDirection.rtl, - child: MediaQuery( - data: topPadding100, - child: CustomScrollView( - primary: true, - slivers: [ - SliverAppBar( - leading: Placeholder(key: leadingKey), - title: Placeholder(key: titleKey, fallbackHeight: kToolbarHeight), - actions: [ 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 >[ - DefaultMaterialLocalizations.delegate, - DefaultWidgetsLocalizations.delegate, - ], - child: Directionality( - textDirection: TextDirection.rtl, - child: MediaQuery( - data: topPadding100, - child: CustomScrollView( - primary: true, - slivers: [ - SliverAppBar( - leading: Placeholder(key: leadingKey), - title: Placeholder(key: titleKey), - actions: [ 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 [ - 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( - children: [ - TestSemantics( - children: [ - TestSemantics( - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics( - children: [ - TestSemantics( - label: 'Leading', - textDirection: TextDirection.ltr, - ), - TestSemantics( - flags: [ - 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 [ - 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( - children: [ - TestSemantics( - children: [ - TestSemantics( - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics( - textDirection: TextDirection.rtl, - children: [ - TestSemantics( - children: [ - TestSemantics( - label: 'Leading', - textDirection: TextDirection.rtl, - ), - TestSemantics( - flags: [ - 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('AppBar excludes header semantics correctly', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -2567,170 +1413,6 @@ void main() { semantics.dispose(); }); - testWidgets('SliverAppBar excludes header semantics correctly', (WidgetTester tester) async { - final SemanticsTester semantics = SemanticsTester(tester); - - await tester.pumpWidget( - const MaterialApp( - home: CustomScrollView( - slivers: [ - SliverAppBar( - leading: Text('Leading'), - flexibleSpace: ExcludeSemantics(child: Text('Title')), - actions: [Text('Action 1')], - excludeHeaderSemantics: true, - ), - ], - ), - ), - ); - - expect(semantics, hasSemantics( - TestSemantics.root( - children: [ - TestSemantics( - textDirection: TextDirection.ltr, - children: [ - TestSemantics( - children: [ - TestSemantics( - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics( - children: [ - TestSemantics( - children: [ - TestSemantics( - children: [ - TestSemantics( - label: 'Leading', - textDirection: TextDirection.ltr, - ), - TestSemantics( - label: 'Action 1', - textDirection: TextDirection.ltr, - ), - ], - ), - TestSemantics(), - ], - ), - TestSemantics( - flags: [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: [ - SliverAppBar( - leading: Text('Leading'), - flexibleSpace: Text('Flexible space'), - actions: [Text('Action 1')], - ), - ], - ), - ), - ); - - expect(semantics, hasSemantics( - TestSemantics.root( - children: [ - TestSemantics( - textDirection: TextDirection.ltr, - children: [ - TestSemantics( - children: [ - TestSemantics( - flags: [SemanticsFlag.scopesRoute], - children: [ - TestSemantics( - children: [ - TestSemantics( - children: [ - TestSemantics( - children: [ - TestSemantics( - label: 'Leading', - textDirection: TextDirection.ltr, - ), - TestSemantics( - label: 'Action 1', - textDirection: TextDirection.ltr, - ), - ], - ), - TestSemantics( - children: [ - TestSemantics( - flags: [SemanticsFlag.isHeader], - label: 'Flexible space', - textDirection: TextDirection.ltr, - ), - ], - ), - ], - ), - TestSemantics( - flags: [SemanticsFlag.hasImplicitScrolling], - ), - ], - ), - ], - ), - ], - ), - ], - ), - ], - ), - ignoreRect: true, - ignoreTransform: true, - ignoreId: true, - )); - - semantics.dispose(); - }); - - testWidgets('Material2 - AppBar draws a light system bar for a dark background', (WidgetTester tester) async { - final ThemeData darkTheme = ThemeData.dark(useMaterial3: false); - await tester.pumpWidget(MaterialApp( - theme: darkTheme, - home: Scaffold( - appBar: AppBar( - title: const Text('test'), - ), - ), - )); - - expect(darkTheme.colorScheme.brightness, Brightness.dark); - expect(SystemChrome.latestStyle, const SystemUiOverlayStyle( - statusBarBrightness: Brightness.dark, - statusBarIconBrightness: Brightness.light, - )); - }); - testWidgets('Material3 - AppBar draws a light system bar for a dark background', (WidgetTester tester) async { final ThemeData darkTheme = ThemeData.dark(useMaterial3: true); await tester.pumpWidget(MaterialApp( @@ -2750,26 +1432,6 @@ void main() { )); }); - testWidgets('Material2 - AppBar draws a dark system bar for a light background', (WidgetTester tester) async { - final ThemeData lightTheme = ThemeData(primarySwatch: Colors.lightBlue, useMaterial3: false); - await tester.pumpWidget( - MaterialApp( - theme: lightTheme, - home: Scaffold( - appBar: AppBar( - title: const Text('test'), - ), - ), - ), - ); - - expect(lightTheme.colorScheme.brightness, Brightness.light); - expect(SystemChrome.latestStyle, const SystemUiOverlayStyle( - statusBarBrightness: Brightness.light, - statusBarIconBrightness: Brightness.dark, - )); - }); - testWidgets('Material3 - AppBar draws a dark system bar for a light background', (WidgetTester tester) async { final ThemeData lightTheme = ThemeData(useMaterial3: true); await tester.pumpWidget( @@ -2791,57 +1453,6 @@ void main() { )); }); - testWidgets('Material2 - Default system bar brightness based on AppBar background color brightness.', (WidgetTester tester) async { - Widget buildAppBar(ThemeData theme) { - return MaterialApp( - theme: theme, - home: Scaffold( - appBar: AppBar(title: const Text('Title')), - ), - ); - } - - // Using a light theme. - { - await tester.pumpWidget(buildAppBar(ThemeData(useMaterial3: false))); - final Material appBarMaterial = tester.widget( - find.descendant( - of: find.byType(AppBar), - matching: find.byType(Material), - ), - ); - final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!); - final Brightness onAppBarBrightness = appBarBrightness == Brightness.light - ? Brightness.dark - : Brightness.light; - - expect(SystemChrome.latestStyle, SystemUiOverlayStyle( - statusBarBrightness: appBarBrightness, - statusBarIconBrightness: onAppBarBrightness, - )); - } - - // Using a dark theme. - { - await tester.pumpWidget(buildAppBar(ThemeData.dark(useMaterial3: false))); - final Material appBarMaterial = tester.widget( - find.descendant( - of: find.byType(AppBar), - matching: find.byType(Material), - ), - ); - final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!); - final Brightness onAppBarBrightness = appBarBrightness == Brightness.light - ? Brightness.dark - : Brightness.light; - - expect(SystemChrome.latestStyle, SystemUiOverlayStyle( - statusBarBrightness: appBarBrightness, - statusBarIconBrightness: onAppBarBrightness, - )); - } - }); - testWidgets('Material3 - Default system bar brightness based on AppBar background color brightness.', (WidgetTester tester) async { Widget buildAppBar(ThemeData theme) { return MaterialApp( @@ -2895,25 +1506,6 @@ void main() { } }); - testWidgets('Material2 - Default status bar color', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - key: GlobalKey(), - theme: ThemeData.light().copyWith( - useMaterial3: false, - appBarTheme: const AppBarTheme(), - ), - home: Scaffold( - appBar: AppBar( - title: const Text('title'), - ), - ), - ), - ); - - expect(SystemChrome.latestStyle!.statusBarColor, null); - }); - testWidgets('Material3 - Default status bar color', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -2953,67 +1545,6 @@ void main() { expect(SystemChrome.latestStyle!.systemNavigationBarColor, Colors.green); }); - 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: [ - SliverAppBar( - expandedHeight: appBarHeight, - floating: true, - snap: snap, - actions: [ - TextButton( - child: const Text('snap=false'), - onPressed: () { - setState(() { - snap = false; - }); - }, - ), - ], - flexibleSpace: FlexibleSpaceBar( - background: Container( - height: appBarHeight, - color: Colors.orange, - ), - ), - ), - SliverList( - delegate: SliverChildListDelegate( - [ - 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('AppBar shape default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -3058,58 +1589,6 @@ void main() { expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder); }); - testWidgets('SliverAppBar shape default', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: CustomScrollView( - slivers: [ - SliverAppBar( - leading: Text('L'), - title: Text('No Scaffold'), - actions: [Text('A1'), Text('A2')], - ), - ], - ), - ), - ); - - final Finder sliverAppBarFinder = find.byType(SliverAppBar); - SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget(finder); - expect(getSliverAppBarWidget(sliverAppBarFinder).shape, null); - - final Finder materialFinder = find.byType(Material); - Material getMaterialWidget(Finder finder) => tester.widget(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: [ - SliverAppBar( - leading: Text('L'), - title: Text('No Scaffold'), - actions: [Text('A1'), Text('A2')], - shape: roundedRectangleBorder, - ), - ], - ), - ), - ); - - final Finder sliverAppBarFinder = find.byType(SliverAppBar); - SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget(finder); - expect(getSliverAppBarWidget(sliverAppBarFinder).shape, roundedRectangleBorder); - - final Finder materialFinder = find.byType(Material); - Material getMaterialWidget(Finder finder) => tester.widget(finder); - expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder); - }); - testWidgets('AppBars title has upper limit on text scaling, textScaleFactor = 1, 1.34, 2', (WidgetTester tester) async { late double textScaleFactor; @@ -3220,42 +1699,6 @@ void main() { expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy); }); - testWidgets('SliverAppBar configures the delegate properly', (WidgetTester tester) async { - Future buildAndVerifyDelegate({ required bool pinned, required bool floating, required bool snap }) async { - await tester.pumpWidget( - MaterialApp( - home: CustomScrollView( - slivers: [ - SliverAppBar( - title: const Text('Jumbo'), - pinned: pinned, - floating: floating, - snap: snap, - ), - ], - ), - ), - ); - - final SliverPersistentHeaderDelegate delegate = tester - .widget(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('AppBar respects toolbarHeight', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -3272,61 +1715,6 @@ void main() { expect(appBarHeight(tester), 48); }); - 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('AppBar respects leadingWidth', (WidgetTester tester) async { const Key key = Key('leading'); await tester.pumpWidget(MaterialApp( @@ -3343,24 +1731,6 @@ void main() { expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0, 0, 100, 56)); }); - testWidgets('SliverAppBar respects leadingWidth', (WidgetTester tester) async { - const Key key = Key('leading'); - await tester.pumpWidget(const MaterialApp( - home: CustomScrollView( - slivers: [ - 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("AppBar with EndDrawer doesn't have leading", (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( @@ -3391,13 +1761,6 @@ void main() { expect(navToolBar.middleSpacing, NavigationToolbar.kMiddleSpacing); }); - 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); - }); - testWidgets('AppBar foregroundColor and backgroundColor', (WidgetTester tester) async { const Color foregroundColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff00ffff); @@ -3886,210 +2249,41 @@ void main() { }); group('MaterialStateColor scrolledUnder', () { - const double collapsedHeight = kToolbarHeight; - const double expandedHeight = 200.0; const Color scrolledColor = Color(0xff00ff00); const Color defaultColor = Color(0xff0000ff); - Finder findAppBarMaterial() { - return find.descendant(of: find.byType(AppBar), matching: find.byType(Material)).first; - } - - Color? getAppBarBackgroundColor(WidgetTester tester) { - return tester.widget(findAppBarMaterial()).color; - } - - group('SliverAppBar', () { - Widget buildSliverApp({ - required double contentHeight, - bool reverse = false, - bool includeFlexibleSpace = false, - }) { - return MaterialApp( - home: Scaffold( - body: CustomScrollView( - reverse: reverse, - slivers: [ - SliverAppBar( - elevation: 0, - backgroundColor: MaterialStateColor.resolveWith((Set states) { - return states.contains(MaterialState.scrolledUnder) - ? scrolledColor - : defaultColor; - }), - expandedHeight: expandedHeight, - pinned: true, - flexibleSpace: includeFlexibleSpace - ? const FlexibleSpaceBar(title: Text('SliverAppBar')) - : null, - ), - SliverList( - delegate: SliverChildListDelegate( - [ - Container(height: contentHeight, color: Colors.teal), - ], - ), - ), - ], - ), + Widget buildAppBar({ + required double contentHeight, + bool reverse = false, + bool includeFlexibleSpace = false + }) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: MaterialStateColor.resolveWith((Set states) { + return states.contains(MaterialState.scrolledUnder) + ? scrolledColor + : defaultColor; + }), + title: const Text('AppBar'), + flexibleSpace: includeFlexibleSpace + ? const FlexibleSpaceBar(title: Text('FlexibleSpace')) + : null, ), - ); - } + body: ListView( + reverse: reverse, + children: [ + 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); - }); - }); - - group('AppBar', () { - Widget buildAppBar({ - required double contentHeight, - bool reverse = false, - bool includeFlexibleSpace = false - }) { - return MaterialApp( + testWidgets('backgroundColor for horizontal scrolling', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( home: Scaffold( appBar: AppBar( elevation: 0, @@ -4099,302 +2293,275 @@ void main() { : defaultColor; }), title: const Text('AppBar'), - flexibleSpace: includeFlexibleSpace - ? const FlexibleSpaceBar(title: Text('FlexibleSpace')) - : null, + notificationPredicate: (ScrollNotification notification) { + // Represents both scroll views below being treated as a + // single viewport. + return notification.depth <= 1; + }, ), - body: ListView( - reverse: reverse, - children: [ - Container(height: contentHeight, color: Colors.teal), - ], - ), - ), - ); - } - - testWidgets('backgroundColor for horizontal scrolling', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - appBar: AppBar( - elevation: 0, - backgroundColor: MaterialStateColor.resolveWith((Set states) { - return states.contains(MaterialState.scrolledUnder) - ? scrolledColor - : defaultColor; - }), - title: const Text('AppBar'), - notificationPredicate: (ScrollNotification notification) { - // Represents both scroll views below being treated as a - // single viewport. - return notification.depth <= 1; - }, - ), - body: SingleChildScrollView( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Container( - height: 1200, - width: 1200, - color: Colors.teal, - ), - ), - ), - ), - ) - ); - - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); - await tester.pump(); - await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - gesture = await tester.startGesture(const Offset(50.0, 300.0)); - // Scroll horizontally - await gesture.moveBy(const Offset(-kToolbarHeight, 0.0)); - await tester.pump(); - await gesture.moveBy(const Offset(-kToolbarHeight, 0.0)); - await gesture.up(); - await tester.pumpAndSettle(); - // The app bar is still scrolled under vertically, so it should not have - // changed back in response to horizontal scrolling. - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - }); - - testWidgets('backgroundColor', (WidgetTester tester) async { - await tester.pumpWidget( - buildAppBar(contentHeight: 1200.0) - ); - - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - gesture = await tester.startGesture(const Offset(50.0, 300.0)); - await gesture.moveBy(const Offset(0.0, kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - }); - - testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async { - await tester.pumpWidget( - buildAppBar(contentHeight: 1200.0, includeFlexibleSpace: true) - ); - - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - gesture = await tester.startGesture(const Offset(50.0, 300.0)); - await gesture.moveBy(const Offset(0.0, kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - }); - - testWidgets('backgroundColor - reverse', (WidgetTester tester) async { - await tester.pumpWidget( - buildAppBar(contentHeight: 1200.0, reverse: true) - ); - await tester.pump(); - - // In this test case, the content always extends under the AppBar, so it - // should always be the scrolledColor. - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(0.0, kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - gesture = await tester.startGesture(const Offset(50.0, 300.0)); - await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - }); - - testWidgets('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async { - await tester.pumpWidget( - buildAppBar( - contentHeight: 1200.0, - reverse: true, - includeFlexibleSpace: true, - ) - ); - await tester.pump(); - - // In this test case, the content always extends under the AppBar, so it - // should always be the scrolledColor. - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(0.0, kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - - gesture = await tester.startGesture(const Offset(50.0, 300.0)); - await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); - - expect(getAppBarBackgroundColor(tester), scrolledColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - }); - - testWidgets('_handleScrollNotification safely calls setState()', (WidgetTester tester) async { - // Regression test for failures found in Google internal issue b/185192049. - final ScrollController controller = ScrollController(initialScrollOffset: 400); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('AppBar'), - ), - body: Scrollbar( - thumbVisibility: true, - controller: controller, - child: ListView( - controller: controller, - children: [ - Container(height: 1200.0, color: Colors.teal), - ], - ), - ), - ), - ), - ); - - expect(tester.takeException(), isNull); - - controller.dispose(); - }); - - testWidgets('does not trigger on horizontal scroll', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - appBar: AppBar( - elevation: 0, - backgroundColor: MaterialStateColor.resolveWith((Set states) { - return states.contains(MaterialState.scrolledUnder) - ? scrolledColor - : defaultColor; - }), - title: const Text('AppBar'), - ), - body: ListView( + body: SingleChildScrollView( + child: SingleChildScrollView( scrollDirection: Axis.horizontal, + child: Container( + height: 1200, + width: 1200, + color: Colors.teal, + ), + ), + ), + ), + ) + ); + + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); + await tester.pump(); + await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + gesture = await tester.startGesture(const Offset(50.0, 300.0)); + // Scroll horizontally + await gesture.moveBy(const Offset(-kToolbarHeight, 0.0)); + await tester.pump(); + await gesture.moveBy(const Offset(-kToolbarHeight, 0.0)); + await gesture.up(); + await tester.pumpAndSettle(); + // The app bar is still scrolled under vertically, so it should not have + // changed back in response to horizontal scrolling. + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + }); + + testWidgets('backgroundColor', (WidgetTester tester) async { + await tester.pumpWidget( + buildAppBar(contentHeight: 1200.0) + ); + + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + gesture = await tester.startGesture(const Offset(50.0, 300.0)); + await gesture.moveBy(const Offset(0.0, kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + }); + + testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async { + await tester.pumpWidget( + buildAppBar(contentHeight: 1200.0, includeFlexibleSpace: true) + ); + + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + gesture = await tester.startGesture(const Offset(50.0, 300.0)); + await gesture.moveBy(const Offset(0.0, kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + }); + + testWidgets('backgroundColor - reverse', (WidgetTester tester) async { + await tester.pumpWidget( + buildAppBar(contentHeight: 1200.0, reverse: true) + ); + await tester.pump(); + + // In this test case, the content always extends under the AppBar, so it + // should always be the scrolledColor. + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + gesture = await tester.startGesture(const Offset(50.0, 300.0)); + await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + }); + + testWidgets('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async { + await tester.pumpWidget( + buildAppBar( + contentHeight: 1200.0, + reverse: true, + includeFlexibleSpace: true, + ) + ); + await tester.pump(); + + // In this test case, the content always extends under the AppBar, so it + // should always be the scrolledColor. + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + gesture = await tester.startGesture(const Offset(50.0, 300.0)); + await gesture.moveBy(const Offset(0.0, -kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), scrolledColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + }); + + testWidgets('_handleScrollNotification safely calls setState()', (WidgetTester tester) async { + // Regression test for failures found in Google internal issue b/185192049. + final ScrollController controller = ScrollController(initialScrollOffset: 400); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('AppBar'), + ), + body: Scrollbar( + thumbVisibility: true, + controller: controller, + child: ListView( + controller: controller, children: [ - Container(height: 600.0, width: 1200.0, color: Colors.teal), + Container(height: 1200.0, color: Colors.teal), ], ), ), ), - ); + ), + ); - expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.takeException(), isNull); - TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(-100.0, 0.0)); - await gesture.up(); - await tester.pumpAndSettle(); + controller.dispose(); + }); - expect(getAppBarBackgroundColor(tester), defaultColor); + testWidgets('does not trigger on horizontal scroll', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: MaterialStateColor.resolveWith((Set states) { + return states.contains(MaterialState.scrolledUnder) + ? scrolledColor + : defaultColor; + }), + title: const Text('AppBar'), + ), + body: ListView( + scrollDirection: Axis.horizontal, + children: [ + Container(height: 600.0, width: 1200.0, color: Colors.teal), + ], + ), + ), + ), + ); - gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(100.0, 0.0)); - await gesture.up(); - await tester.pumpAndSettle(); + expect(getAppBarBackgroundColor(tester), defaultColor); - expect(getAppBarBackgroundColor(tester), defaultColor); - }); + TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(-100.0, 0.0)); + await gesture.up(); + await tester.pumpAndSettle(); - testWidgets('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async { - await tester.pumpWidget( - buildAppBar( - contentHeight: 200.0, - reverse: true, - ) - ); - await tester.pump(); + expect(getAppBarBackgroundColor(tester), defaultColor); - // In reverse, the content here is not long enough to scroll under the app - // bar. - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(100.0, 0.0)); + await gesture.up(); + await tester.pumpAndSettle(); - final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(0.0, kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); + expect(getAppBarBackgroundColor(tester), defaultColor); + }); - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - }); + testWidgets('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async { + await tester.pumpWidget( + buildAppBar( + contentHeight: 200.0, + reverse: true, + ) + ); + await tester.pump(); - testWidgets('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async { - await tester.pumpWidget( - buildAppBar( - contentHeight: 200.0, - reverse: true, - includeFlexibleSpace: true, - ) - ); - await tester.pump(); + // In reverse, the content here is not long enough to scroll under the app + // bar. + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - // In reverse, the content here is not long enough to scroll under the app - // bar. - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); - final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); - await gesture.moveBy(const Offset(0.0, kToolbarHeight)); - await gesture.up(); - await tester.pumpAndSettle(); + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + }); - expect(getAppBarBackgroundColor(tester), defaultColor); - expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); - }); + testWidgets('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async { + await tester.pumpWidget( + buildAppBar( + contentHeight: 200.0, + reverse: true, + includeFlexibleSpace: true, + ) + ); + await tester.pump(); + + // In reverse, the content here is not long enough to scroll under the app + // bar. + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); + + final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0)); + await gesture.moveBy(const Offset(0.0, kToolbarHeight)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(getAppBarBackgroundColor(tester), defaultColor); + expect(tester.getSize(findAppBarMaterial()).height, kToolbarHeight); }); }); @@ -4576,772 +2743,6 @@ void main() { expect(tester.getSize(find.byKey(leadingKey)).width, leadingWidth); }); - 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: [ - 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 [ - 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: [ - 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 [ - 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: [ - 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: [ - 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: [ - 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: [ - 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: [ - 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: [ - 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); - }); - - 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( - builder: (BuildContext context) { - return Scaffold( - body: CustomScrollView( - primary: true, - slivers: [ - 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: [ - SliverAppBar.medium( - leading: IconButton( - onPressed: () {}, - icon: const Icon(Icons.menu), - ), - title: const Text(title), - bottom: const TabBar( - tabs: [ - 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: [ - SliverAppBar.large( - leading: IconButton( - onPressed: () {}, - icon: const Icon(Icons.menu), - ), - title: const Text(title), - bottom: const TabBar( - tabs: [ - 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: [ - 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: [ - 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: [ - 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: [ - 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); - }); - group('AppBar.forceMaterialTransparency', () { Material getAppBarMaterial(WidgetTester tester) { return tester.widget(find @@ -5418,176 +2819,321 @@ void main() { }); 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; - + testWidgets('Material2 - AppBar draws a light system bar for a dark background', (WidgetTester tester) async { + final ThemeData darkTheme = ThemeData.dark(useMaterial3: false); await tester.pumpWidget(MaterialApp( - theme: theme, + theme: darkTheme, home: Scaffold( - body: CustomScrollView( - primary: true, - slivers: [ - const SliverAppBar.medium( - title: Text('AppBar Title'), - ), - SliverToBoxAdapter( - child: Container( - height: 1200, - color: Colors.orange[400], - ), - ), - ], + appBar: AppBar( + title: const Text('test'), ), ), )); - 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(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(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(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(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(collapsedTitleOpacity).opacity, 0); - expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + expect(darkTheme.colorScheme.brightness, Brightness.dark); + expect(SystemChrome.latestStyle, const SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + )); }); - 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: [ - const SliverAppBar.large( - title: Text('AppBar Title'), - ), - SliverToBoxAdapter( - child: Container( - height: 1200, - color: Colors.orange[400], - ), - ), - ], + testWidgets('Material2 - AppBar drawer icon has default color', (WidgetTester tester) async { + final ThemeData themeData = ThemeData.from( + colorScheme: const ColorScheme.light(), + useMaterial3: false, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + title: const Text('Howdy!'), + ), + drawer: const Drawer(), ), ), + ); + + expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary); + }); + + testWidgets('Material2 - AppBar endDrawer icon has default color', (WidgetTester tester) async { + final ThemeData themeData = ThemeData.from( + colorScheme: const ColorScheme.light(), + useMaterial3: false, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + title: const Text('Howdy!'), + ), + endDrawer: const Drawer(), + ), + ), + ); + + expect(_iconStyle(tester, Icons.menu)?.color, themeData.colorScheme.onPrimary); + }); + + testWidgets('Material2 - leading widget extends to edge and is square', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + platform: TargetPlatform.android, + useMaterial3: false, + ); + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + title: const Text('X'), + ), + drawer: const Column(), // Doesn't really matter. Triggers a hamburger regardless. + ), + ), + ); + + // Default IconButton has a size of (56x56). + final Finder hamburger = find.byType(IconButton); + expect(tester.getTopLeft(hamburger), Offset.zero); + expect(tester.getSize(hamburger), const Size(56.0, 56.0)); + + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + leading: Container(), + title: const Text('X'), + ), + ), + ), + ); + + // Default leading widget has a size of (56x56). + final Finder leadingBox = find.byType(Container); + expect(tester.getTopLeft(leadingBox), Offset.zero); + expect(tester.getSize(leadingBox), const Size(56.0, 56.0)); + + // The custom leading widget should still be 56x56 even if its size is smaller. + await tester.pumpWidget( + MaterialApp( + theme: themeData, + home: Scaffold( + appBar: AppBar( + leading: const SizedBox(height: 36, width: 36,), + title: const Text('X'), + ), // Doesn't really matter. Triggers a hamburger regardless. + ), + ), + ); + + final Finder leading = find.byType(SizedBox); + expect(tester.getTopLeft(leading), Offset.zero); + expect(tester.getSize(leading), const Size(56.0, 56.0)); + }); + + testWidgets('Material2 - Action is 4dp from edge and 48dp min', (WidgetTester tester) async { + final ThemeData theme = ThemeData( + platform: TargetPlatform.android, + useMaterial3: false, + ); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Scaffold( + appBar: AppBar( + title: const Text('X'), + actions: const [ + IconButton( + icon: Icon(Icons.share), + onPressed: null, + tooltip: 'Share', + iconSize: 20.0, + ), + IconButton( + icon: Icon(Icons.add), + onPressed: null, + tooltip: 'Add', + iconSize: 60.0, + ), + ], + ), + ), + ), + ); + + final Finder addButton = find.widgetWithIcon(IconButton, Icons.add); + expect(tester.getTopRight(addButton), const Offset(800.0, 0.0)); + // It's still the size it was plus the 2 * 8dp padding from IconButton. + expect(tester.getSize(addButton), const Size(60.0 + 2 * 8.0, 56.0)); + + final Finder shareButton = find.widgetWithIcon(IconButton, Icons.share); + // The 20dp icon is expanded to fill the IconButton's touch target to 48dp. + expect(tester.getSize(shareButton), const Size(48.0, 56.0)); + }); + + testWidgets('Material2 - AppBar uses the specified elevation or defaults to 4.0', (WidgetTester tester) async { + Widget buildAppBar([double? elevation]) { + return MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + appBar: AppBar(title: const Text('Title'), elevation: elevation), + ), + ); + } + + Material getMaterial() => tester.widget(find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), )); - 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), + // Default elevation should be used for the material. + await tester.pumpWidget(buildAppBar()); + expect(getMaterial().elevation, 4); + + // AppBar should use the specified elevation. + await tester.pumpWidget(buildAppBar(8.0)); + expect(getMaterial().elevation, 8.0); + }); + + testWidgets('Material2 - AppBar ink splash draw on the correct canvas', (WidgetTester tester) async { + // This is a regression test for https://github.com/flutter/flutter/issues/58665 + final Key key = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + // Test was designed against InkSplash so need to make sure that is used. + theme: ThemeData( + useMaterial3: false, + splashFactory: InkSplash.splashFactory + ), + home: Center( + child: AppBar( + title: const Text('Abc'), + actions: [ + IconButton( + key: key, + icon: const Icon(Icons.add_circle), + tooltip: 'First button', + onPressed: () {}, + ), + ], + flexibleSpace: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: const Alignment(-0.04, 1.0), + colors: [Colors.blue.shade500, Colors.blue.shade800], + ), + ), + ), + ), + ), + ), ); - final Finder collapsedTitle = find.text('AppBar Title').last; - final Finder collapsedTitleOpacity = find.ancestor( - of: collapsedTitle, - matching: find.byType(AnimatedOpacity), + final RenderObject painter = tester.renderObject( + find.descendant( + of: find.descendant( + of: find.byType(AppBar), + matching: find.byType(Stack), + ), + matching: find.byType(Material), + ), + ); + await tester.tap(find.byKey(key)); + expect(painter, paints..save()..translate()..save()..translate()..circle(x: 24.0, y: 28.0)); + }); + + testWidgets('Material2 - Default status bar color', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + key: GlobalKey(), + theme: ThemeData.light().copyWith( + useMaterial3: false, + appBarTheme: const AppBarTheme(), + ), + home: Scaffold( + appBar: AppBar( + title: const Text('title'), + ), + ), + ), ); - // Default, fully expanded app bar. - expect(controller.offset, 0); - expect(find.byType(SliverAppBar), findsOneWidget); - expect(appBarHeight(tester), expandedAppBarHeight); - expect(tester.widget(collapsedTitleOpacity).opacity, 0); - expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + expect(SystemChrome.latestStyle!.statusBarColor, null); + }); - // 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(expandedTitle).text.style!.color, - theme.colorScheme.onPrimary, + testWidgets('Material2 - AppBar draws a dark system bar for a light background', (WidgetTester tester) async { + final ThemeData lightTheme = ThemeData(primarySwatch: Colors.lightBlue, useMaterial3: false); + await tester.pumpWidget( + MaterialApp( + theme: lightTheme, + home: Scaffold( + appBar: AppBar( + title: const Text('test'), + ), + ), + ), ); - // 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(collapsedTitleOpacity).opacity, 0); - expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45); + expect(lightTheme.colorScheme.brightness, Brightness.light); + expect(SystemChrome.latestStyle, const SystemUiOverlayStyle( + statusBarBrightness: Brightness.light, + statusBarIconBrightness: Brightness.dark, + )); + }); - // 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(collapsedTitleOpacity).opacity, 1); - expect(tester.getSize(expandedTitleClip).height, 0); + testWidgets('Material2 - Default system bar brightness based on AppBar background color brightness.', (WidgetTester tester) async { + Widget buildAppBar(ThemeData theme) { + return MaterialApp( + theme: theme, + home: Scaffold( + appBar: AppBar(title: const Text('Title')), + ), + ); + } - // Scroll back to fully expanded. - controller.jumpTo(0); - await tester.pumpAndSettle(); - expect(find.byType(SliverAppBar), findsOneWidget); - expect(appBarHeight(tester), expandedAppBarHeight); - expect(tester.widget(collapsedTitleOpacity).opacity, 0); - expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); + // Using a light theme. + { + await tester.pumpWidget(buildAppBar(ThemeData(useMaterial3: false))); + final Material appBarMaterial = tester.widget( + find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + ), + ); + final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!); + final Brightness onAppBarBrightness = appBarBrightness == Brightness.light + ? Brightness.dark + : Brightness.light; + + expect(SystemChrome.latestStyle, SystemUiOverlayStyle( + statusBarBrightness: appBarBrightness, + statusBarIconBrightness: onAppBarBrightness, + )); + } + + // Using a dark theme. + { + await tester.pumpWidget(buildAppBar(ThemeData.dark(useMaterial3: false))); + final Material appBarMaterial = tester.widget( + find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + ), + ); + final Brightness appBarBrightness = ThemeData.estimateBrightnessForColor(appBarMaterial.color!); + final Brightness onAppBarBrightness = appBarBrightness == Brightness.light + ? Brightness.dark + : Brightness.light; + + expect(SystemChrome.latestStyle, SystemUiOverlayStyle( + statusBarBrightness: appBarBrightness, + statusBarIconBrightness: onAppBarBrightness, + )); + } }); }); } diff --git a/packages/flutter/test/material/app_bar_utils.dart b/packages/flutter/test/material/app_bar_utils.dart new file mode 100644 index 0000000000..d312df315e --- /dev/null +++ b/packages/flutter/test/material/app_bar_utils.dart @@ -0,0 +1,50 @@ +// 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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Finder findAppBarMaterial() { + return find.descendant( + of: find.byType(AppBar), + matching: find.byType(Material), + ).first; +} + +Color? getAppBarBackgroundColor(WidgetTester tester) { + return tester.widget(findAppBarMaterial()).color; +} + +double appBarHeight(WidgetTester tester) { + return tester.getSize(find.byType(AppBar, skipOffstage: false)).height; +} + +double appBarTop(WidgetTester tester) { + return tester.getTopLeft(find.byType(AppBar, skipOffstage: false)).dy; +} + +double appBarBottom(WidgetTester tester) { + return tester.getBottomLeft(find.byType(AppBar, skipOffstage: false)).dy; +} + +double tabBarHeight(WidgetTester tester) { + return tester.getSize(find.byType(TabBar, skipOffstage: false)).height; +} + +ScrollController primaryScrollController(WidgetTester tester) { + return PrimaryScrollController.of( + tester.element(find.byType(CustomScrollView)) + ); +} + +void verifyTextNotClipped(Finder textFinder, WidgetTester tester) { + final Rect clipRect = tester.getRect( + find.ancestor(of: textFinder, matching: find.byType(ClipRect)).first, + ); + final Rect textRect = tester.getRect(textFinder); + expect(textRect.top, inInclusiveRange(clipRect.top, clipRect.bottom)); + expect(textRect.bottom, inInclusiveRange(clipRect.top, clipRect.bottom)); + expect(textRect.left, inInclusiveRange(clipRect.left, clipRect.right)); + expect(textRect.right, inInclusiveRange(clipRect.left, clipRect.right)); +}