| // 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 '../rendering/mock_canvas.dart'; |
| import 'semantics_tester.dart'; |
| |
| Future<void> test(WidgetTester tester, double offset, { double anchor = 0.0 }) { |
| return tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Viewport( |
| anchor: anchor / 600.0, |
| offset: ViewportOffset.fixed(offset), |
| slivers: const <Widget>[ |
| SliverToBoxAdapter(child: SizedBox(height: 400.0)), |
| SliverToBoxAdapter(child: SizedBox(height: 400.0)), |
| SliverToBoxAdapter(child: SizedBox(height: 400.0)), |
| SliverToBoxAdapter(child: SizedBox(height: 400.0)), |
| SliverToBoxAdapter(child: SizedBox(height: 400.0)), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| Future<void> testSliverFixedExtentList(WidgetTester tester, List<String> items) { |
| return tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverFixedExtentList( |
| itemExtent: 900, |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| return Center( |
| key: ValueKey<String>(items[index]), |
| child: KeepAlive( |
| items[index], |
| ), |
| ); |
| }, |
| childCount : items.length, |
| findChildIndexCallback: (Key key) { |
| final ValueKey<String> valueKey = key as ValueKey<String>; |
| return items.indexOf(valueKey.value); |
| }, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| } |
| |
| void verify(WidgetTester tester, List<Offset> idealPositions, List<bool> idealVisibles) { |
| final List<Offset> actualPositions = tester.renderObjectList<RenderBox>(find.byType(SizedBox, skipOffstage: false)).map<Offset>( |
| (RenderBox target) => target.localToGlobal(Offset.zero), |
| ).toList(); |
| final List<bool> actualVisibles = tester.renderObjectList<RenderSliverToBoxAdapter>(find.byType(SliverToBoxAdapter, skipOffstage: false)).map<bool>( |
| (RenderSliverToBoxAdapter target) => target.geometry!.visible, |
| ).toList(); |
| expect(actualPositions, equals(idealPositions)); |
| expect(actualVisibles, equals(idealVisibles)); |
| } |
| |
| void main() { |
| testWidgets('Viewport basic test', (WidgetTester tester) async { |
| await test(tester, 0.0); |
| expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); |
| verify(tester, <Offset>[ |
| Offset.zero, |
| const Offset(0.0, 400.0), |
| const Offset(0.0, 800.0), |
| const Offset(0.0, 1200.0), |
| const Offset(0.0, 1600.0), |
| ], <bool>[true, true, false, false, false]); |
| |
| await test(tester, 200.0); |
| verify(tester, <Offset>[ |
| const Offset(0.0, -200.0), |
| const Offset(0.0, 200.0), |
| const Offset(0.0, 600.0), |
| const Offset(0.0, 1000.0), |
| const Offset(0.0, 1400.0), |
| ], <bool>[true, true, false, false, false]); |
| |
| await test(tester, 600.0); |
| verify(tester, <Offset>[ |
| const Offset(0.0, -600.0), |
| const Offset(0.0, -200.0), |
| const Offset(0.0, 200.0), |
| const Offset(0.0, 600.0), |
| const Offset(0.0, 1000.0), |
| ], <bool>[false, true, true, false, false]); |
| |
| await test(tester, 900.0); |
| verify(tester, <Offset>[ |
| const Offset(0.0, -900.0), |
| const Offset(0.0, -500.0), |
| const Offset(0.0, -100.0), |
| const Offset(0.0, 300.0), |
| const Offset(0.0, 700.0), |
| ], <bool>[false, false, true, true, false]); |
| }); |
| |
| testWidgets('Viewport anchor test', (WidgetTester tester) async { |
| await test(tester, 0.0, anchor: 100.0); |
| expect(tester.renderObject<RenderBox>(find.byType(Viewport)).size, equals(const Size(800.0, 600.0))); |
| verify(tester, <Offset>[ |
| const Offset(0.0, 100.0), |
| const Offset(0.0, 500.0), |
| const Offset(0.0, 900.0), |
| const Offset(0.0, 1300.0), |
| const Offset(0.0, 1700.0), |
| ], <bool>[true, true, false, false, false]); |
| |
| await test(tester, 200.0, anchor: 100.0); |
| verify(tester, <Offset>[ |
| const Offset(0.0, -100.0), |
| const Offset(0.0, 300.0), |
| const Offset(0.0, 700.0), |
| const Offset(0.0, 1100.0), |
| const Offset(0.0, 1500.0), |
| ], <bool>[true, true, false, false, false]); |
| |
| await test(tester, 600.0, anchor: 100.0); |
| verify(tester, <Offset>[ |
| const Offset(0.0, -500.0), |
| const Offset(0.0, -100.0), |
| const Offset(0.0, 300.0), |
| const Offset(0.0, 700.0), |
| const Offset(0.0, 1100.0), |
| ], <bool>[false, true, true, false, false]); |
| |
| await test(tester, 900.0, anchor: 100.0); |
| verify(tester, <Offset>[ |
| const Offset(0.0, -800.0), |
| const Offset(0.0, -400.0), |
| Offset.zero, |
| const Offset(0.0, 400.0), |
| const Offset(0.0, 800.0), |
| ], <bool>[false, false, true, true, false]); |
| }); |
| |
| testWidgets('Multiple grids and lists', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Center( |
| child: SizedBox( |
| width: 44.4, |
| height: 60.0, |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverList( |
| delegate: SliverChildListDelegate( |
| const <Widget>[ |
| SizedBox(height: 22.2, child: Text('TOP')), |
| SizedBox(height: 22.2), |
| SizedBox(height: 22.2), |
| ], |
| ), |
| ), |
| SliverFixedExtentList( |
| itemExtent: 22.2, |
| delegate: SliverChildListDelegate( |
| const <Widget>[ |
| SizedBox(), |
| Text('A'), |
| SizedBox(), |
| ], |
| ), |
| ), |
| SliverGrid( |
| gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( |
| crossAxisCount: 2, |
| ), |
| delegate: SliverChildListDelegate( |
| const <Widget>[ |
| SizedBox(), |
| Text('B'), |
| SizedBox(), |
| ], |
| ), |
| ), |
| SliverList( |
| delegate: SliverChildListDelegate( |
| const <Widget>[ |
| SizedBox(height: 22.2), |
| SizedBox(height: 22.2), |
| SizedBox(height: 22.2, child: Text('BOTTOM')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| final TestGesture gesture = await tester.startGesture(const Offset(400.0, 300.0)); |
| expect(find.text('TOP'), findsOneWidget); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsNothing); |
| expect(find.text('BOTTOM'), findsNothing); |
| await gesture.moveBy(const Offset(0.0, -70.0)); |
| await tester.pump(); |
| expect(find.text('TOP'), findsNothing); |
| expect(find.text('A'), findsOneWidget); |
| expect(find.text('B'), findsNothing); |
| expect(find.text('BOTTOM'), findsNothing); |
| await gesture.moveBy(const Offset(0.0, -70.0)); |
| await tester.pump(); |
| expect(find.text('TOP'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsOneWidget); |
| expect(find.text('BOTTOM'), findsNothing); |
| await gesture.moveBy(const Offset(0.0, -70.0)); |
| await tester.pump(); |
| expect(find.text('TOP'), findsNothing); |
| expect(find.text('A'), findsNothing); |
| expect(find.text('B'), findsNothing); |
| expect(find.text('BOTTOM'), findsOneWidget); |
| }); |
| |
| testWidgets('SliverFixedExtentList correctly clears garbage', (WidgetTester tester) async { |
| final List<String> items = <String>['1', '2', '3', '4', '5', '6']; |
| await testSliverFixedExtentList(tester, items); |
| // Keep alive widgets require 1 frame to notify their parents. Pumps in between |
| // drags to ensure widgets are kept alive. |
| await tester.drag(find.byType(CustomScrollView),const Offset(0.0, -1200.0)); |
| await tester.pump(); |
| await tester.drag(find.byType(CustomScrollView),const Offset(0.0, -1200.0)); |
| await tester.pump(); |
| await tester.drag(find.byType(CustomScrollView),const Offset(0.0, -800.0)); |
| await tester.pump(); |
| expect(find.text('1'), findsNothing); |
| expect(find.text('2'), findsNothing); |
| expect(find.text('3'), findsNothing); |
| expect(find.text('4'), findsOneWidget); |
| expect(find.text('5'), findsOneWidget); |
| // Indexes [0, 1, 2] are kept alive and [3, 4] are in viewport, thus the sliver |
| // will need to keep updating the elements at these indexes whenever a rebuild is |
| // triggered. The current child list in RenderSliverFixedExtentList is |
| // '4' -> '5' -> null. |
| // |
| // With the insertion below, all items will get shifted back 1 position. The sliver |
| // will have to update indexes [0, 1, 2, 3, 4, 5]. Since this is the first time |
| // item '0' gets initialized, mounting the element will cause it to attach to |
| // child list in RenderSliverFixedExtentList. This will create a gap. |
| // '0' -> '4' -> '5' -> null. |
| items.insert(0, '0'); |
| await testSliverFixedExtentList(tester, items); |
| // Sliver should collect leading and trailing garbage correctly. |
| // |
| // The child list update should occur in following order. |
| // '0' -> '4' -> '5' -> null Started with Original list. |
| // '4' -> null Removed 1 leading garbage and 1 trailing garbage. |
| // '3' -> '4' -> null Prepended '3' because viewport is still at [3, 4]. |
| expect(find.text('0'), findsNothing); |
| expect(find.text('1'), findsNothing); |
| expect(find.text('2'), findsNothing); |
| expect(find.text('3'), findsOneWidget); |
| expect(find.text('4'), findsOneWidget); |
| }); |
| |
| testWidgets('SliverFixedExtentList handles underflow when its children changes', (WidgetTester tester) async { |
| final List<String> items = <String>['1', '2', '3', '4', '5', '6']; |
| final List<String> initializedChild = <String>[]; |
| List<Widget> children = <Widget>[]; |
| for (final String item in items) { |
| children.add( |
| StateInitSpy( |
| item, () => initializedChild.add(item), key: ValueKey<String>(item), |
| ), |
| ); |
| } |
| final ScrollController controller = ScrollController(initialScrollOffset: 5400); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| controller: controller, |
| slivers: <Widget>[ |
| SliverFixedExtentList( |
| itemExtent: 900, |
| delegate: SliverChildListDelegate(children), |
| ), |
| ], |
| ), |
| ), |
| ); |
| await tester.pumpAndSettle(); |
| expect(find.text('1'), findsNothing); |
| expect(find.text('2'), findsNothing); |
| expect(find.text('3'), findsNothing); |
| expect(find.text('4'), findsNothing); |
| expect(find.text('5'), findsNothing); |
| expect(find.text('6'), findsOneWidget); |
| expect(listEquals<String>(initializedChild, <String>['6']), isTrue); |
| |
| // move to item 1 and swap the children at the same time |
| controller.jumpTo(0); |
| final Widget temp = children[5]; |
| children[5] = children[0]; |
| children[0] = temp; |
| children = List<Widget>.from(children); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| controller: controller, |
| slivers: <Widget>[ |
| SliverFixedExtentList( |
| itemExtent: 900, |
| delegate: SliverChildListDelegate(children), |
| ), |
| ], |
| ), |
| ), |
| ); |
| expect(find.text('1'), findsNothing); |
| expect(find.text('2'), findsNothing); |
| expect(find.text('3'), findsNothing); |
| expect(find.text('4'), findsNothing); |
| expect(find.text('5'), findsNothing); |
| expect(find.text('6'), findsOneWidget); |
| // None of the children should be built. |
| expect(listEquals<String>(initializedChild, <String>['6']), isTrue); |
| }); |
| |
| testWidgets( |
| 'SliverGrid Correctly layout children after rearranging', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const TestSliverGrid( |
| <Widget>[ |
| Text('item0', key: Key('0')), |
| Text('item1', key: Key('1')), |
| ], |
| )); |
| await tester.pumpWidget(const TestSliverGrid( |
| <Widget>[ |
| Text('item0', key: Key('0')), |
| Text('item3', key: Key('3')), |
| Text('item4', key: Key('4')), |
| Text('item1', key: Key('1')), |
| ], |
| )); |
| expect(find.text('item0'), findsOneWidget); |
| expect(find.text('item3'), findsOneWidget); |
| expect(find.text('item4'), findsOneWidget); |
| expect(find.text('item1'), findsOneWidget); |
| |
| final Offset item0Location = tester.getCenter(find.text('item0')); |
| final Offset item3Location = tester.getCenter(find.text('item3')); |
| final Offset item4Location = tester.getCenter(find.text('item4')); |
| final Offset item1Location = tester.getCenter(find.text('item1')); |
| |
| expect(isRight(item0Location, item3Location) && sameHorizontal(item0Location, item3Location), true); |
| expect(isBelow(item0Location, item4Location) && sameVertical(item0Location, item4Location), true); |
| expect(isBelow(item0Location, item1Location) && isRight(item0Location, item1Location), true); |
| }, |
| ); |
| |
| testWidgets( |
| 'SliverGrid negative usableCrossAxisExtent', |
| (WidgetTester tester) async { |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: SizedBox( |
| width: 4, |
| height: 4, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverGrid( |
| gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( |
| crossAxisCount: 2, |
| crossAxisSpacing: 8, |
| mainAxisSpacing: 8, |
| ), |
| delegate: SliverChildListDelegate( |
| <Widget>[ |
| const Center(child: Text('A')), |
| const Center(child: Text('B')), |
| const Center(child: Text('C')), |
| const Center(child: Text('D')), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect(tester.takeException(), isNull); |
| }, |
| ); |
| |
| testWidgets( |
| 'SliverList can handle inaccurate scroll offset due to changes in children list', |
| (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/pull/59888. |
| bool skip = true; |
| Widget _buildItem(BuildContext context, int index) { |
| return !skip || index.isEven |
| ? Card( |
| child: ListTile( |
| title: Text( |
| 'item$index', |
| style: const TextStyle(fontSize: 80), |
| ), |
| ), |
| ) |
| : Container(); |
| } |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: CustomScrollView( |
| slivers: <Widget> [ |
| SliverList( |
| delegate: SliverChildBuilderDelegate( |
| _buildItem, |
| childCount: 30, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| // Only even items 0~12 are on the screen. |
| expect(find.text('item0'), findsOneWidget); |
| expect(find.text('item12'), findsOneWidget); |
| expect(find.text('item14'), findsNothing); |
| |
| await tester.drag(find.byType(CustomScrollView), const Offset(0.0, -750.0)); |
| await tester.pump(); |
| // Only even items 16~28 are on the screen. |
| expect(find.text('item15'), findsNothing); |
| expect(find.text('item16'), findsOneWidget); |
| expect(find.text('item28'), findsOneWidget); |
| |
| skip = false; |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold( |
| body: CustomScrollView( |
| slivers: <Widget> [ |
| SliverList( |
| delegate: SliverChildBuilderDelegate( |
| _buildItem, |
| childCount: 30, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| // Only items 12~19 are on the screen. |
| expect(find.text('item11'), findsNothing); |
| expect(find.text('item12'), findsOneWidget); |
| expect(find.text('item19'), findsOneWidget); |
| expect(find.text('item20'), findsNothing); |
| |
| await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0)); |
| await tester.pump(); |
| |
| // Only items 10~16 are on the screen. |
| expect(find.text('item9'), findsNothing); |
| expect(find.text('item10'), findsOneWidget); |
| expect(find.text('item16'), findsOneWidget); |
| expect(find.text('item17'), findsNothing); |
| |
| // The inaccurate scroll offset should reach zero at this point |
| await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0)); |
| await tester.pump(); |
| |
| // Only items 7~13 are on the screen. |
| expect(find.text('item6'), findsNothing); |
| expect(find.text('item7'), findsOneWidget); |
| expect(find.text('item13'), findsOneWidget); |
| expect(find.text('item14'), findsNothing); |
| |
| // It will be corrected as we scroll, so we have to drag multiple times. |
| await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0)); |
| await tester.pump(); |
| await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0)); |
| await tester.pump(); |
| await tester.drag(find.byType(CustomScrollView), const Offset(0.0, 250.0)); |
| await tester.pump(); |
| |
| // Only items 0~6 are on the screen. |
| expect(find.text('item0'), findsOneWidget); |
| expect(find.text('item6'), findsOneWidget); |
| expect(find.text('item7'), findsNothing); |
| }, |
| ); |
| |
| testWidgets( |
| 'SliverFixedExtentList Correctly layout children after rearranging', |
| (WidgetTester tester) async { |
| await tester.pumpWidget(const TestSliverFixedExtentList( |
| <Widget>[ |
| Text('item0', key: Key('0')), |
| Text('item2', key: Key('2')), |
| Text('item1', key: Key('1')), |
| ], |
| )); |
| await tester.pumpWidget(const TestSliverFixedExtentList( |
| <Widget>[ |
| Text('item0', key: Key('0')), |
| Text('item3', key: Key('3')), |
| Text('item1', key: Key('1')), |
| Text('item4', key: Key('4')), |
| Text('item2', key: Key('2')), |
| ], |
| )); |
| expect(find.text('item0'), findsOneWidget); |
| expect(find.text('item3'), findsOneWidget); |
| expect(find.text('item1'), findsOneWidget); |
| expect(find.text('item4'), findsOneWidget); |
| expect(find.text('item2'), findsOneWidget); |
| |
| final Offset item0Location = tester.getCenter(find.text('item0')); |
| final Offset item3Location = tester.getCenter(find.text('item3')); |
| final Offset item1Location = tester.getCenter(find.text('item1')); |
| final Offset item4Location = tester.getCenter(find.text('item4')); |
| final Offset item2Location = tester.getCenter(find.text('item2')); |
| |
| expect(isBelow(item0Location, item3Location) && sameVertical(item0Location, item3Location), true); |
| expect(isBelow(item3Location, item1Location) && sameVertical(item3Location, item1Location), true); |
| expect(isBelow(item1Location, item4Location) && sameVertical(item1Location, item4Location), true); |
| expect(isBelow(item4Location, item2Location) && sameVertical(item4Location, item2Location), true); |
| }, |
| ); |
| |
| testWidgets('Can override ErrorWidget.build', (WidgetTester tester) async { |
| const Text errorText = Text('error'); |
| final ErrorWidgetBuilder oldBuilder = ErrorWidget.builder; |
| ErrorWidget.builder = (FlutterErrorDetails details) => errorText; |
| final SliverChildBuilderDelegate builderThrowsDelegate = SliverChildBuilderDelegate( |
| (_, __) => throw 'builder', |
| addAutomaticKeepAlives: false, |
| addRepaintBoundaries: false, |
| addSemanticIndexes: false, |
| ); |
| final KeyedSubtree wrapped = builderThrowsDelegate.build(_NullBuildContext(), 0)! as KeyedSubtree; |
| expect(wrapped.child, errorText); |
| expect(tester.takeException(), 'builder'); |
| ErrorWidget.builder = oldBuilder; |
| }); |
| |
| testWidgets('SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - super fast', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(initialScrollOffset: 600); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| controller: controller, |
| cacheExtent: 0, |
| slivers: <Widget>[ |
| SliverFixedExtentList( |
| itemExtent: 200, |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| if (index <= 6) { |
| return Center(child: Text('Page $index')); |
| } |
| return null; |
| }, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| expect(find.text('Page 0'), findsNothing); |
| expect(find.text('Page 6'), findsNothing); |
| |
| await tester.drag(find.text('Page 5'), const Offset(0, -1000)); |
| // Controller will be temporarily over-scrolled (before the frame triggered by the drag) because |
| // SliverFixedExtentList doesn't report its size until it has built its last child, so the |
| // maxScrollExtent is infinite, so when we move by 1000 pixels in one go, we go all the way. |
| // |
| // This never actually gets rendered, it's just the controller state before we lay out. |
| expect(controller.offset, 1600.0); |
| |
| // However, once we pump, the scroll offset gets clamped to the newly discovered maximum, which |
| // is the itemExtent (200) times the number of items (7) minus the height of the viewport (600). |
| // This adds up to 800.0. |
| await tester.pump(); |
| expect(find.text('Page 0'), findsNothing); |
| expect(find.text('Page 6'), findsOneWidget); |
| expect(controller.offset, 800.0); |
| |
| expect(await tester.pumpAndSettle(), 1); // there should be no animation here |
| expect(controller.offset, 800.0); |
| }); |
| |
| testWidgets('SliverFixedExtentList with SliverChildBuilderDelegate auto-correct scroll offset - reasonable', (WidgetTester tester) async { |
| final ScrollController controller = ScrollController(initialScrollOffset: 600); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| controller: controller, |
| cacheExtent: 0, |
| slivers: <Widget>[ |
| SliverFixedExtentList( |
| itemExtent: 200, |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| if (index <= 6) { |
| return Center(child: Text('Page $index')); |
| } |
| return null; |
| }, |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| await tester.drag(find.text('Page 5'), const Offset(0, -210)); |
| // Controller will be temporarily over-scrolled. |
| expect(controller.offset, 810.0); |
| await tester.pumpAndSettle(); |
| // It will be corrected after a auto scroll animation. |
| expect(controller.offset, 800.0); |
| }); |
| |
| Widget _boilerPlate(Widget sliver) { |
| return Localizations( |
| locale: const Locale('en', 'us'), |
| delegates: const <LocalizationsDelegate<dynamic>>[ |
| DefaultWidgetsLocalizations.delegate, |
| DefaultMaterialLocalizations.delegate, |
| ], |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: MediaQuery( |
| data: const MediaQueryData(), |
| child: CustomScrollView(slivers: <Widget>[sliver]), |
| ), |
| ), |
| ); |
| } |
| |
| group('SliverOffstage - ', () { |
| testWidgets('offstage true', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOffstage( |
| sliver: SliverToBoxAdapter( |
| child: Text('a'), |
| ), |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(0)); |
| expect(find.byType(Text), findsNothing); |
| final RenderViewport renderViewport = tester.renderObject(find.byType(Viewport)); |
| final RenderSliver renderSliver = renderViewport.lastChild!; |
| expect(renderSliver.geometry!.scrollExtent, 0.0); |
| expect(find.byType(SliverOffstage), findsNothing); |
| }); |
| |
| testWidgets('offstage false', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOffstage( |
| offstage: false, |
| sliver: SliverToBoxAdapter( |
| child: Text('a'), |
| ), |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| expect(find.byType(Text), findsOneWidget); |
| final RenderViewport renderViewport = tester.renderObject(find.byType(Viewport)); |
| final RenderSliver renderSliver = renderViewport.lastChild!; |
| expect(renderSliver.geometry!.scrollExtent, 14.0); |
| expect(find.byType(SliverOffstage), paints..paragraph()); |
| }); |
| }); |
| |
| group('SliverOpacity - ', () { |
| testWidgets('painting & semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| |
| // Opacity 1.0: Semantics and painting |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOpacity( |
| sliver: SliverToBoxAdapter( |
| child: Text( |
| 'a', |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| opacity: 1.0, |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| expect(find.byType(SliverOpacity), paints..paragraph()); |
| |
| // Opacity 0.0: Nothing |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOpacity( |
| sliver: SliverToBoxAdapter( |
| child: Text( |
| 'a', |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| opacity: 0.0, |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(0)); |
| expect(find.byType(SliverOpacity), paintsNothing); |
| |
| // Opacity 0.0 with semantics: Just semantics |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOpacity( |
| sliver: SliverToBoxAdapter( |
| child: Text( |
| 'a', |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| opacity: 0.0, |
| alwaysIncludeSemantics: true, |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| expect(find.byType(SliverOpacity), paintsNothing); |
| |
| // Opacity 0.0 without semantics: Nothing |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOpacity( |
| sliver: SliverToBoxAdapter( |
| child: Text( |
| 'a', |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| opacity: 0.0, |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(0)); |
| expect(find.byType(SliverOpacity), paintsNothing); |
| |
| // Opacity 0.1: Semantics and painting |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOpacity( |
| sliver: SliverToBoxAdapter( |
| child: Text( |
| 'a', |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| opacity: 0.1, |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| expect(find.byType(SliverOpacity), paints..paragraph()); |
| |
| // Opacity 0.1 without semantics: Still has semantics and painting |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOpacity( |
| sliver: SliverToBoxAdapter( |
| child: Text( |
| 'a', |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| opacity: 0.1, |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| expect(find.byType(SliverOpacity), paints..paragraph()); |
| |
| // Opacity 0.1 with semantics: Semantics and painting |
| await tester.pumpWidget(_boilerPlate( |
| const SliverOpacity( |
| sliver: SliverToBoxAdapter( |
| child: Text( |
| 'a', |
| textDirection: TextDirection.rtl, |
| ), |
| ), |
| opacity: 0.1, |
| alwaysIncludeSemantics: true, |
| ), |
| )); |
| |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| expect(find.byType(SliverOpacity), paints..paragraph()); |
| |
| semantics.dispose(); |
| }); |
| }); |
| |
| group('SliverIgnorePointer - ', () { |
| testWidgets('ignores pointer events', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final List<String> events = <String>[]; |
| await tester.pumpWidget(_boilerPlate( |
| SliverIgnorePointer( |
| ignoringSemantics: false, |
| sliver: SliverToBoxAdapter( |
| child: GestureDetector( |
| child: const Text('a'), |
| onTap: () { |
| events.add('tap'); |
| }, |
| ), |
| ), |
| ), |
| )); |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| await tester.tap(find.byType(GestureDetector), warnIfMissed: false); |
| expect(events, equals(<String>[])); |
| }); |
| |
| testWidgets('ignores semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final List<String> events = <String>[]; |
| await tester.pumpWidget(_boilerPlate( |
| SliverIgnorePointer( |
| ignoring: false, |
| ignoringSemantics: true, |
| sliver: SliverToBoxAdapter( |
| child: GestureDetector( |
| child: const Text('a'), |
| onTap: () { |
| events.add('tap'); |
| }, |
| ), |
| ), |
| ), |
| )); |
| expect(semantics.nodesWith(label: 'a'), hasLength(0)); |
| await tester.tap(find.byType(GestureDetector)); |
| expect(events, equals(<String>['tap'])); |
| }); |
| |
| testWidgets('ignores pointer events & semantics', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final List<String> events = <String>[]; |
| await tester.pumpWidget(_boilerPlate( |
| SliverIgnorePointer( |
| ignoringSemantics: true, |
| sliver: SliverToBoxAdapter( |
| child: GestureDetector( |
| child: const Text('a'), |
| onTap: () { |
| events.add('tap'); |
| }, |
| ), |
| ), |
| ), |
| )); |
| expect(semantics.nodesWith(label: 'a'), hasLength(0)); |
| await tester.tap(find.byType(GestureDetector), warnIfMissed: false); |
| expect(events, equals(<String>[])); |
| }); |
| |
| testWidgets('ignores nothing', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final List<String> events = <String>[]; |
| await tester.pumpWidget(_boilerPlate( |
| SliverIgnorePointer( |
| ignoring: false, |
| ignoringSemantics: false, |
| sliver: SliverToBoxAdapter( |
| child: GestureDetector( |
| child: const Text('a'), |
| onTap: () { |
| events.add('tap'); |
| }, |
| ), |
| ), |
| ), |
| )); |
| expect(semantics.nodesWith(label: 'a'), hasLength(1)); |
| await tester.tap(find.byType(GestureDetector)); |
| expect(events, equals(<String>['tap'])); |
| }); |
| }); |
| |
| testWidgets('SliverList handles 0 scrollOffsetCorrection', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/62198 |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| body: CustomScrollView( |
| physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), |
| slivers: <Widget>[ |
| SliverList( |
| delegate: SliverChildListDelegate( |
| const <Widget>[ |
| SizedBox.shrink(), |
| Text('index 1'), |
| Text('index 2'), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| )); |
| await tester.fling(find.byType(Scrollable), const Offset(0.0, -500.0), 10000.0); |
| await tester.pumpAndSettle(); |
| expect(tester.takeException(), isNull); |
| }); |
| |
| testWidgets('SliverGrid children can be arbitrarily placed', (WidgetTester tester) async { |
| // Regression test for https://github.com/flutter/flutter/issues/64006 |
| int firstTapped = 0; |
| int secondTapped = 0; |
| final Key key = UniqueKey(); |
| await tester.pumpWidget(MaterialApp( |
| home: Scaffold( |
| key: key, |
| body: CustomScrollView( |
| slivers: <Widget>[ |
| SliverGrid( |
| delegate: SliverChildBuilderDelegate( |
| (BuildContext context, int index) { |
| return Material( |
| color: index.isEven ? Colors.yellow : Colors.red, |
| child: InkWell( |
| onTap: () { |
| index.isEven ? firstTapped++ : secondTapped++; |
| }, |
| child: Text('Index $index'), |
| ), |
| ); |
| }, |
| childCount: 2, |
| ), |
| gridDelegate: _TestArbitrarySliverGridDelegate(), |
| ), |
| ], |
| ), |
| ), |
| )); |
| // Assertion not triggered by arbitrary placement |
| expect(tester.takeException(), isNull); |
| |
| // Verify correct hit testing |
| await tester.tap(find.text('Index 0')); |
| expect(firstTapped, 1); |
| expect(secondTapped, 0); |
| await tester.tap(find.text('Index 1')); |
| expect(firstTapped, 1); |
| expect(secondTapped, 1); |
| // Check other places too |
| final Offset bottomLeft = tester.getBottomLeft(find.byKey(key)); |
| await tester.tapAt(bottomLeft); |
| expect(firstTapped, 1); |
| expect(secondTapped, 1); |
| final Offset topRight = tester.getTopRight(find.byKey(key)); |
| await tester.tapAt(topRight); |
| expect(firstTapped, 1); |
| expect(secondTapped, 1); |
| await tester.tapAt(const Offset(100.0, 100.0)); |
| expect(firstTapped, 1); |
| expect(secondTapped, 1); |
| await tester.tapAt(const Offset(700.0, 500.0)); |
| expect(firstTapped, 1); |
| expect(secondTapped, 1); |
| }); |
| } |
| |
| bool isRight(Offset a, Offset b) => b.dx > a.dx; |
| bool isBelow(Offset a, Offset b) => b.dy > a.dy; |
| bool sameHorizontal(Offset a, Offset b) => b.dy == a.dy; |
| bool sameVertical(Offset a, Offset b) => b.dx == a.dx; |
| |
| class TestSliverGrid extends StatelessWidget { |
| const TestSliverGrid(this.children, { Key? key }) : super(key: key); |
| |
| final List<Widget> children; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget> [ |
| SliverGrid( |
| delegate: SliverChildListDelegate( |
| children, |
| ), |
| gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( |
| crossAxisCount: 2, |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| class _TestArbitrarySliverGridDelegate implements SliverGridDelegate { |
| @override |
| SliverGridLayout getLayout(SliverConstraints constraints) { |
| return _TestArbitrarySliverGridLayout(); |
| } |
| |
| @override |
| bool shouldRelayout(SliverGridDelegate oldDelegate) { |
| return false; |
| } |
| } |
| |
| class _TestArbitrarySliverGridLayout implements SliverGridLayout { |
| @override |
| double computeMaxScrollOffset(int childCount) => 1000; |
| |
| @override |
| int getMinChildIndexForScrollOffset(double scrollOffset) => 0; |
| |
| @override |
| int getMaxChildIndexForScrollOffset(double scrollOffset) => 2; |
| |
| @override |
| SliverGridGeometry getGeometryForChildIndex(int index) { |
| return SliverGridGeometry( |
| scrollOffset: index * 100.0 + 300.0, |
| crossAxisOffset: 200.0, |
| mainAxisExtent: 100.0, |
| crossAxisExtent: 100.0, |
| ); |
| } |
| } |
| |
| class TestSliverFixedExtentList extends StatelessWidget { |
| const TestSliverFixedExtentList(this.children, { Key? key }) : super(key: key); |
| |
| final List<Widget> children; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget> [ |
| SliverFixedExtentList( |
| itemExtent: 10.0, |
| delegate: SliverChildListDelegate( |
| children, |
| ), |
| ), |
| ], |
| ), |
| ); |
| } |
| } |
| |
| class StateInitSpy extends StatefulWidget { |
| const StateInitSpy(this.data, this.onStateInit, { Key? key }) : super(key: key); |
| |
| final String data; |
| final VoidCallback onStateInit; |
| |
| @override |
| StateInitSpyState createState() => StateInitSpyState(); |
| } |
| |
| class StateInitSpyState extends State<StateInitSpy> { |
| @override |
| void initState() { |
| super.initState(); |
| widget.onStateInit(); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Text(widget.data); |
| } |
| } |
| |
| class KeepAlive extends StatefulWidget { |
| const KeepAlive(this.data, { Key? key }) : super(key: key); |
| |
| final String data; |
| |
| @override |
| KeepAliveState createState() => KeepAliveState(); |
| } |
| |
| class KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin { |
| @override |
| bool get wantKeepAlive => true; |
| |
| @override |
| Widget build(BuildContext context) { |
| super.build(context); |
| return Text(widget.data); |
| } |
| } |
| |
| class _NullBuildContext implements BuildContext { |
| @override |
| dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError(); |
| } |