// 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/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('SliverAppBar - Stretch', () { testWidgets('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverScrollingPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100)); expect(header.child!.size.height, equals(200.0)); }); testWidgets('fills overscroll after reverse direction input - scrolling header', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( title: Text('Test'), stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverScrollingPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); expect(tester.getCenter(find.text('Test')).dy, 28.0); // First scroll the header away final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(anchor))); await gesture.moveBy(const Offset(0.0, -100.0)); await tester.pump(const Duration(milliseconds: 10)); expect(header.child!.size.height, equals(56.0)); expect(tester.getCenter(find.text('Test', skipOffstage: false)).dy, -28.0); // With the same gesture, scroll back and into overscroll await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pump(const Duration(milliseconds: 10)); // Header should stretch in overscroll expect(header.child!.size.height, equals(200.0)); expect(tester.getCenter(find.text('Test')).dy, 28.0); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('fills overscroll after reverse direction input - floating header', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( title: Text('Test'), stretch: true, floating: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverFloatingPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); expect(tester.getCenter(find.text('Test')).dy, 28.0); // First scroll the header away final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(anchor))); await gesture.moveBy(const Offset(0.0, -100.0)); await tester.pump(const Duration(milliseconds: 10)); expect(header.child!.size.height, equals(56.0)); expect(tester.getCenter(find.text('Test', skipOffstage: false)).dy, -28.0); // With the same gesture, scroll back and into overscroll await gesture.moveBy(const Offset(0.0, 200.0)); await tester.pump(const Duration(milliseconds: 10)); // Header should stretch in overscroll expect(header.child!.size.height, equals(200.0)); expect(tester.getCenter(find.text('Test')).dy, 28.0); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverScrollingPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100.0)); expect(header.child!.size.height, equals(100.0)); }); testWidgets('default trigger offset', (WidgetTester tester) async { bool didTrigger = false; const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ SliverAppBar( stretch: true, expandedHeight: 100.0, onStretchTrigger: () async { didTrigger = true; }, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); await slowDrag(tester, anchor, const Offset(0.0, 50.0)); expect(didTrigger, isFalse); await tester.pumpAndSettle(); await slowDrag(tester, anchor, const Offset(0.0, 150.0)); expect(didTrigger, isTrue); }); testWidgets('custom trigger offset', (WidgetTester tester) async { bool didTrigger = false; const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ SliverAppBar( stretch: true, expandedHeight: 100.0, stretchTriggerOffset: 150.0, onStretchTrigger: () async { didTrigger = true; }, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); await slowDrag(tester, anchor, const Offset(0.0, 100.0)); await tester.pumpAndSettle(); expect(didTrigger, isFalse); await slowDrag(tester, anchor, const Offset(0.0, 300.0)); expect(didTrigger, isTrue); }); testWidgets('stretch callback not triggered without overscroll physics', (WidgetTester tester) async { bool didTrigger = false; const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: <Widget>[ SliverAppBar( stretch: true, expandedHeight: 100.0, stretchTriggerOffset: 150.0, onStretchTrigger: () async { didTrigger = true; }, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); await slowDrag(tester, anchor, const Offset(0.0, 100.0)); await tester.pumpAndSettle(); expect(didTrigger, isFalse); await slowDrag(tester, anchor, const Offset(0.0, 300.0)); expect(didTrigger, isFalse); }); testWidgets('asserts reasonable trigger offset', (WidgetTester tester) async { expect( () { return MaterialApp( home: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: <Widget>[ SliverAppBar( stretch: true, expandedHeight: 100.0, stretchTriggerOffset: -150.0, ), SliverToBoxAdapter( child: Container( height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ); }, throwsAssertionError, ); }); }); group('SliverAppBar - Stretch, Pinned', () { testWidgets('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( pinned: true, stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverPinnedPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100)); expect(header.child!.size.height, equals(200.0)); }); testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( pinned: true, stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverPinnedPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100)); expect(header.child!.size.height, equals(100.0)); }); }); group('SliverAppBar - Stretch, Floating', () { testWidgets('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( floating: true, stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverFloatingPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100)); expect(header.child!.size.height, equals(200.0)); }); testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( floating: true, stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverFloatingPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100)); expect(header.child!.size.height, equals(100.0)); }); }); group('SliverAppBar - Stretch, Floating, Pinned', () { testWidgets('fills overscroll', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( floating: true, pinned: true, stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverFloatingPinnedPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100)); expect(header.child!.size.height, equals(200.0)); }); testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async { const Key anchor = Key('drag'); await tester.pumpWidget( MaterialApp( home: CustomScrollView( physics: const ClampingScrollPhysics(), slivers: <Widget>[ const SliverAppBar( pinned: true, floating: true, stretch: true, expandedHeight: 100.0, ), SliverToBoxAdapter( child: Container( key: anchor, height: 800, ), ), SliverToBoxAdapter( child: Container( height: 800, ), ), ], ), ), ); final RenderSliverFloatingPinnedPersistentHeader header = tester.renderObject( find.byType(SliverAppBar), ); expect(header.child!.size.height, equals(100.0)); await slowDrag(tester, anchor, const Offset(0.0, 100)); expect(header.child!.size.height, equals(100.0)); }); }); } Future<void> slowDrag(WidgetTester tester, Key widget, Offset offset) async { final Offset target = tester.getCenter(find.byKey(widget)); final TestGesture gesture = await tester.startGesture(target); await gesture.moveBy(offset); await tester.pump(const Duration(milliseconds: 10)); await gesture.up(); }