// 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. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(<String>['reduced-test-set']) import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; void main() { testWidgets('FlexibleSpaceBar centers title on iOS', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Scaffold( appBar: AppBar( flexibleSpace: const FlexibleSpaceBar( title: Text('X'), ), ), ), ), ); final Finder title = find.text('X'); Offset center = tester.getCenter(title); Size size = tester.getSize(title); expect(center.dx, lessThan(400.0 - size.width / 2.0)); for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) { // Clear the widget tree to avoid animating between platforms. await tester.pumpWidget(Container(key: UniqueKey())); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: platform), home: Scaffold( appBar: AppBar( flexibleSpace: const FlexibleSpaceBar( title: Text('X'), ), ), ), ), ); center = tester.getCenter(title); size = tester.getSize(title); expect(center.dx, greaterThan(400.0 - size.width / 2.0)); expect(center.dx, lessThan(400.0 + size.width / 2.0)); } }); testWidgets('FlexibleSpaceBarSettings provides settings to a FlexibleSpaceBar', (WidgetTester tester) async { const double minExtent = 100.0; const double initExtent = 200.0; const double maxExtent = 300.0; const double alpha = 0.5; final FlexibleSpaceBarSettings customSettings = FlexibleSpaceBar.createSettings( currentExtent: initExtent, minExtent: minExtent, maxExtent: maxExtent, toolbarOpacity: alpha, child: AppBar( flexibleSpace: const FlexibleSpaceBar( title: Text('title'), background: Text('X2'), collapseMode: CollapseMode.pin, ), ), ) as FlexibleSpaceBarSettings; const Key dragTarget = Key('orange box'); await tester.pumpWidget( MaterialApp( home: Scaffold( body: CustomScrollView( key: dragTarget, primary: true, slivers: <Widget>[ SliverPersistentHeader( floating: true, pinned: true, delegate: TestDelegate(settings: customSettings), ), SliverToBoxAdapter( child: Container( height: 1200.0, color: Colors.orange[400], ), ), ], ), ), ), ); final RenderBox clipRect = tester.renderObject(find.byType(ClipRect).first); final Transform transform = tester.firstWidget( find.descendant( of: find.byType(FlexibleSpaceBar), matching: find.byType(Transform), ), ); // The current (200) is half way between the min (100) and max (300) and the // lerp values used to calculate the scale are 1 and 1.5, so we check for 1.25. expect(transform.transform.getMaxScaleOnAxis(), 1.25); // The space bar rect always starts fully expanded. expect(clipRect.size.height, maxExtent); final Element actionTextBox = tester.element(find.text('title')); final Text textWidget = actionTextBox.widget as Text; final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(actionTextBox); final TextStyle effectiveStyle = defaultTextStyle.style.merge(textWidget.style); expect(effectiveStyle.color?.alpha, 128); // Which is alpha of .5 // We drag up to fully collapse the space bar. await tester.drag(find.byKey(dragTarget), const Offset(0, -400.0)); await tester.pumpAndSettle(); expect(clipRect.size.height, minExtent); }); testWidgets('FlexibleSpaceBar.background is visible when using height other than kToolbarHeight', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/80451 await tester.pumpWidget( MaterialApp( home: Scaffold( appBar: AppBar( toolbarHeight: 300, flexibleSpace: const FlexibleSpaceBar( title: Text('Title'), background: Text('Background'), collapseMode: CollapseMode.pin, ), ), body: CustomScrollView( primary: true, slivers: <Widget>[ SliverToBoxAdapter( child: Container( height: 1200.0, color: Colors.orange[400], ), ), ], ), ), ), ); final Opacity backgroundOpacity = tester.firstWidget(find.byType(Opacity)); expect(backgroundOpacity.opacity, 1.0); }); testWidgets('Collapsed FlexibleSpaceBar has correct semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const double expandedHeight = 200; await tester.pumpWidget( MaterialApp( home: Scaffold( body: CustomScrollView( slivers: <Widget>[ const SliverAppBar( pinned: true, expandedHeight: expandedHeight, title: Text('Title'), flexibleSpace: FlexibleSpaceBar( background: Text('Expanded title'), ), ), SliverList( delegate: SliverChildListDelegate( <Widget>[ for (int i = 0; i < 50; i++) SizedBox( height: 200, child: Center(child: Text('Item $i')), ), ], ), ), ], ), ), ), ); TestSemantics expectedSemantics = TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( id: 1, rect: TestSemantics.fullScreen, children: <TestSemantics>[ TestSemantics( id: 2, rect: TestSemantics.fullScreen, children: <TestSemantics>[ TestSemantics( id: 3, rect: TestSemantics.fullScreen, flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[ TestSemantics( id: 4, rect: TestSemantics.fullScreen, children: <TestSemantics>[ TestSemantics( id: 9, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, expandedHeight), children: <TestSemantics>[ TestSemantics( id: 12, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), children: <TestSemantics>[ TestSemantics( id: 13, rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 20.0), flags: <SemanticsFlag>[ SemanticsFlag.isHeader, SemanticsFlag.namesRoute, ], label: 'Title', textDirection: TextDirection.ltr, ), ], ), TestSemantics( id: 10, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), children: <TestSemantics>[ TestSemantics( id: 11, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, expandedHeight), label: 'Expanded title', textDirection: TextDirection.ltr, ), ], ), ], ), TestSemantics( id: 14, flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling], rect: TestSemantics.fullScreen, actions: <SemanticsAction>[SemanticsAction.scrollUp], children: <TestSemantics>[ TestSemantics( id: 5, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), label: 'Item 0', textDirection: TextDirection.ltr, ), TestSemantics( id: 6, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), label: 'Item 1', textDirection: TextDirection.ltr, ), TestSemantics( id: 7, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), flags: <SemanticsFlag>[SemanticsFlag.isHidden], label: 'Item 2', textDirection: TextDirection.ltr, ), TestSemantics( id: 8, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0), flags: <SemanticsFlag>[SemanticsFlag.isHidden], label: 'Item 3', textDirection: TextDirection.ltr, ), ], ), ], ), ], ), ], ), ], ), ], ); expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true)); // We drag up to fully collapse the space bar. await tester.drag(find.text('Item 1'), const Offset(0, -600.0)); await tester.pumpAndSettle(); expectedSemantics = TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( id: 1, rect: TestSemantics.fullScreen, children: <TestSemantics>[ TestSemantics( id: 2, rect: TestSemantics.fullScreen, children: <TestSemantics>[ TestSemantics( id: 3, rect: TestSemantics.fullScreen, flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[ TestSemantics( id: 4, rect: TestSemantics.fullScreen, children: <TestSemantics>[ TestSemantics( id: 9, // The app bar is collapsed. rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), children: <TestSemantics>[ TestSemantics( id: 12, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), children: <TestSemantics>[ TestSemantics( id: 13, rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 20.0), flags: <SemanticsFlag>[ SemanticsFlag.isHeader, SemanticsFlag.namesRoute, ], label: 'Title', textDirection: TextDirection.ltr, ), ], ), // The flexible space bar still persists in the // semantic tree even if it is collapsed. TestSemantics( id: 10, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 56.0), children: <TestSemantics>[ TestSemantics( id: 11, rect: const Rect.fromLTRB(0.0, 36.0, 800.0, 92.0), label: 'Expanded title', textDirection: TextDirection.ltr, ), ], ), ], ), TestSemantics( id: 14, flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling], rect: TestSemantics.fullScreen, actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown], children: <TestSemantics>[ TestSemantics( id: 5, rect: const Rect.fromLTRB(0.0, 150.0, 800.0, 200.0), flags: <SemanticsFlag>[SemanticsFlag.isHidden], label: 'Item 0', textDirection: TextDirection.ltr, ), TestSemantics( id: 6, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), flags: <SemanticsFlag>[SemanticsFlag.isHidden], label: 'Item 1', textDirection: TextDirection.ltr, ), TestSemantics( id: 7, rect: const Rect.fromLTRB(0.0, 56.0, 800.0, 200.0), label: 'Item 2', textDirection: TextDirection.ltr, ), TestSemantics( id: 8, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), label: 'Item 3', textDirection: TextDirection.ltr, ), TestSemantics( id: 15, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), label: 'Item 4', textDirection: TextDirection.ltr, ), TestSemantics( id: 16, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 200.0), flags: <SemanticsFlag>[SemanticsFlag.isHidden], label: 'Item 5', textDirection: TextDirection.ltr, ), TestSemantics( id: 17, rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0), flags: <SemanticsFlag>[SemanticsFlag.isHidden], label: 'Item 6', textDirection: TextDirection.ltr, ), ], ), ], ), ], ), ], ), ], ), ], ); expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true)); semantics.dispose(); }); // This is a regression test for https://github.com/flutter/flutter/issues/14227 testWidgets('FlexibleSpaceBar sets width constraints for the title', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; late double width; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (BuildContext context) { width = MediaQuery.of(context).size.width; return CustomScrollView( slivers: <Widget>[ SliverAppBar( expandedHeight: height, pinned: true, stretch: true, flexibleSpace: FlexibleSpaceBar( titlePadding: EdgeInsets.zero, title: Text( 'X' * 2000, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: titleFontSize), ), centerTitle: false, ), ), ], ); }, ), ), ), ); // The title is scaled and transformed to be 1.5 times bigger, when the // FlexibleSpaceBar is fully expanded, thus we expect the width to be // 1.5 times smaller than the full width. The height of the text is the same // as the font size, with 10 dps bottom margin. expect( tester.getRect(find.byType(Text)), Rect.fromLTRB( 0, height - titleFontSize - 10, (width / 1.5).floorToDouble() * 1.5, height, ), ); }); testWidgets('FlexibleSpaceBar sets constraints for the title - override expandedTitleScale', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; const double expandedTitleScale = 3.0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: CustomScrollView( slivers: <Widget>[ SliverAppBar( expandedHeight: height, pinned: true, stretch: true, flexibleSpace: FlexibleSpaceBar( expandedTitleScale: expandedTitleScale, titlePadding: EdgeInsets.zero, title: Text( 'X' * 41, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: titleFontSize,), ), centerTitle: false, ), ), SliverList( delegate: SliverChildListDelegate( <Widget>[ for (int i = 0; i < 3; i++) SizedBox( height: 200.0, child: Center(child: Text('Item $i')), ), ], ), ), ], ), ), ), ); // We drag up to fully collapse the space bar. await tester.drag(find.text('Item 0'), const Offset(0, -600.0)); await tester.pumpAndSettle(); final Finder title = find.byType(Text).first; final double collapsedWidth = tester.getRect(title).width; // We drag down to fully expand the space bar. await tester.drag(find.text('Item 2'), const Offset(0, 600.0)); await tester.pumpAndSettle(); // The title is shifted by this margin to maintain the position of the // bottom edge. const double bottomMargin = titleFontSize * (expandedTitleScale - 1); // The title is scaled and transformed to be 3 times bigger, when the // FlexibleSpaceBar is fully expanded, thus we expect the width to be // 3 times smaller than the full width. The height of the text is the same // as the font size, with 40 dps bottom margin to maintain its bottom position. expect( tester.getRect(title), Rect.fromLTRB( 0, height - titleFontSize - bottomMargin, (collapsedWidth / 3).floorToDouble() * 3, height, ), ); }); testWidgets('FlexibleSpaceBar scaled title', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: CustomScrollView( slivers: <Widget>[ const SliverAppBar( expandedHeight: height, pinned: true, stretch: true, flexibleSpace: RepaintBoundary( child: FlexibleSpaceBar( title: Text( 'X', style: TextStyle(fontSize: titleFontSize,), ), centerTitle: false, ), ), ), SliverList( delegate: SliverChildListDelegate( <Widget>[ for (int i = 0; i < 3; i += 1) SizedBox( height: 200.0, child: Center(child: Text('Item $i')), ), ], ), ), ], ), ), ), ); // We drag up to fully collapse the space bar. await tester.drag(find.text('Item 0'), const Offset(0, -600.0)); await tester.pumpAndSettle(); final Finder flexibleSpaceBar = find.ancestor(of: find.byType(FlexibleSpaceBar), matching: find.byType(RepaintBoundary).first); await expectLater( flexibleSpaceBar, matchesGoldenFile('flexible_space_bar.expanded_title_scale_default.collapsed.png') ); // We drag down to fully expand the space bar. await tester.drag(find.text('Item 2'), const Offset(0, 600.0)); await tester.pumpAndSettle(); await expectLater( flexibleSpaceBar, matchesGoldenFile('flexible_space_bar.expanded_title_scale_default.expanded.png') ); }); testWidgets('FlexibleSpaceBar scaled title - override expandedTitleScale', (WidgetTester tester) async { const double titleFontSize = 20.0; const double height = 300.0; const double expandedTitleScale = 3.0; await tester.pumpWidget( MaterialApp( home: Scaffold( body: CustomScrollView( slivers: <Widget>[ const SliverAppBar( expandedHeight: height, pinned: true, stretch: true, flexibleSpace: RepaintBoundary( child: FlexibleSpaceBar( title: Text( 'X', style: TextStyle(fontSize: titleFontSize,), ), centerTitle: false, expandedTitleScale: expandedTitleScale, ), ), ), SliverList( delegate: SliverChildListDelegate( <Widget>[ for (int i = 0; i < 3; i += 1) SizedBox( height: 200.0, child: Center(child: Text('Item $i')), ), ], ), ), ], ), ), ), ); // We drag up to fully collapse the space bar. await tester.drag(find.text('Item 0'), const Offset(0, -600.0)); await tester.pumpAndSettle(); final Finder flexibleSpaceBar = find.ancestor(of: find.byType(FlexibleSpaceBar), matching: find.byType(RepaintBoundary).first); // This should match the default behavior await expectLater( flexibleSpaceBar, matchesGoldenFile('flexible_space_bar.expanded_title_scale_default.collapsed.png') ); // We drag down to fully expand the space bar. await tester.drag(find.text('Item 2'), const Offset(0, 600.0)); await tester.pumpAndSettle(); await expectLater( flexibleSpaceBar, matchesGoldenFile('flexible_space_bar.expanded_title_scale_override.expanded.png') ); }); testWidgets('FlexibleSpaceBar test titlePadding defaults', (WidgetTester tester) async { Widget buildFrame(TargetPlatform platform, bool? centerTitle) { return MaterialApp( theme: ThemeData(platform: platform), home: Scaffold( appBar: AppBar( flexibleSpace: FlexibleSpaceBar( centerTitle: centerTitle, title: const Text('X'), ), ), ), ); } final Finder title = find.text('X'); final Finder flexibleSpaceBar = find.byType(FlexibleSpaceBar); Offset getTitleBottomLeft() { return Offset( tester.getTopLeft(title).dx, tester.getBottomRight(flexibleSpaceBar).dy - tester.getBottomRight(title).dy, ); } await tester.pumpWidget(buildFrame(TargetPlatform.android, null)); expect(getTitleBottomLeft(), const Offset(72.0, 16.0)); await tester.pumpWidget(buildFrame(TargetPlatform.android, true)); expect(getTitleBottomLeft(), const Offset(390.0, 16.0)); // Clear the widget tree to avoid animating between Android and iOS. await tester.pumpWidget(Container(key: UniqueKey())); await tester.pumpWidget(buildFrame(TargetPlatform.iOS, null)); expect(getTitleBottomLeft(), const Offset(390.0, 16.0)); await tester.pumpWidget(buildFrame(TargetPlatform.iOS, false)); expect(getTitleBottomLeft(), const Offset(72.0, 16.0)); // Clear the widget tree to avoid animating between iOS and macOS. await tester.pumpWidget(Container(key: UniqueKey())); await tester.pumpWidget(buildFrame(TargetPlatform.macOS, null)); expect(getTitleBottomLeft(), const Offset(390.0, 16.0)); await tester.pumpWidget(buildFrame(TargetPlatform.macOS, false)); expect(getTitleBottomLeft(), const Offset(72.0, 16.0)); }); testWidgets('FlexibleSpaceBar test titlePadding override', (WidgetTester tester) async { Widget buildFrame(TargetPlatform platform, bool? centerTitle) { return MaterialApp( theme: ThemeData(platform: platform), home: Scaffold( appBar: AppBar( flexibleSpace: FlexibleSpaceBar( titlePadding: EdgeInsets.zero, centerTitle: centerTitle, title: const Text('X'), ), ), ), ); } final Finder title = find.text('X'); final Finder flexibleSpaceBar = find.byType(FlexibleSpaceBar); Offset getTitleBottomLeft() { return Offset( tester.getTopLeft(title).dx, tester.getBottomRight(flexibleSpaceBar).dy - tester.getBottomRight(title).dy, ); } await tester.pumpWidget(buildFrame(TargetPlatform.android, null)); expect(getTitleBottomLeft(), Offset.zero); await tester.pumpWidget(buildFrame(TargetPlatform.android, true)); expect(getTitleBottomLeft(), const Offset(390.0, 0.0)); // Clear the widget tree to avoid animating between platforms. await tester.pumpWidget(Container(key: UniqueKey())); await tester.pumpWidget(buildFrame(TargetPlatform.iOS, null)); expect(getTitleBottomLeft(), const Offset(390.0, 0.0)); await tester.pumpWidget(buildFrame(TargetPlatform.iOS, false)); expect(getTitleBottomLeft(), Offset.zero); // Clear the widget tree to avoid animating between platforms. await tester.pumpWidget(Container(key: UniqueKey())); await tester.pumpWidget(buildFrame(TargetPlatform.macOS, null)); expect(getTitleBottomLeft(), const Offset(390.0, 0.0)); await tester.pumpWidget(buildFrame(TargetPlatform.macOS, false)); expect(getTitleBottomLeft(), Offset.zero); // Clear the widget tree to avoid animating between platforms. await tester.pumpWidget(Container(key: UniqueKey())); await tester.pumpWidget(buildFrame(TargetPlatform.windows, null)); expect(getTitleBottomLeft(), Offset.zero); await tester.pumpWidget(buildFrame(TargetPlatform.windows, true)); expect(getTitleBottomLeft(), const Offset(390.0, 0.0)); // Clear the widget tree to avoid animating between platforms. await tester.pumpWidget(Container(key: UniqueKey())); await tester.pumpWidget(buildFrame(TargetPlatform.linux, null)); expect(getTitleBottomLeft(), Offset.zero); await tester.pumpWidget(buildFrame(TargetPlatform.linux, true)); expect(getTitleBottomLeft(), const Offset(390.0, 0.0)); }); } class TestDelegate extends SliverPersistentHeaderDelegate { const TestDelegate({ required this.settings, }); final FlexibleSpaceBarSettings settings; @override double get maxExtent => settings.maxExtent; @override double get minExtent => settings.minExtent; @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return settings; } @override bool shouldRebuild(TestDelegate oldDelegate) => false; }