// 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/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show LogicalKeyboardKey; import 'package:flutter_test/flutter_test.dart'; import 'states.dart'; class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> { @override bool isSupported(Locale locale) => true; @override Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale); @override bool shouldReload(MaterialLocalizationsDelegate old) => false; } class WidgetsLocalizationsDelegate extends LocalizationsDelegate<WidgetsLocalizations> { @override bool isSupported(Locale locale) => true; @override Future<WidgetsLocalizations> load(Locale locale) => DefaultWidgetsLocalizations.load(locale); @override bool shouldReload(WidgetsLocalizationsDelegate old) => false; } Widget textFieldBoilerplate({ required Widget child }) { return MaterialApp( home: Localizations( locale: const Locale('en', 'US'), delegates: <LocalizationsDelegate<dynamic>>[ WidgetsLocalizationsDelegate(), MaterialLocalizationsDelegate(), ], child: Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(size: Size(800.0, 600.0)), child: Center( child: Material( child: child, ), ), ), ), ), ); } Widget primaryScrollControllerBoilerplate({ required Widget child, required ScrollController controller }) { return Directionality( textDirection: TextDirection.ltr, child: MediaQuery( data: const MediaQueryData(), child: PrimaryScrollController( controller: controller, child: child, ), ), ); } void main() { testWidgets('ListView control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: ListView( dragStartBehavior: DragStartBehavior.down, children: kStates.map<Widget>((String state) { return GestureDetector( onTap: () { log.add(state); }, dragStartBehavior: DragStartBehavior.down, child: Container( height: 200.0, color: const Color(0xFF0000FF), child: Text(state), ), ); }).toList(), ), ), ); await tester.tap(find.text('Alabama')); expect(log, equals(<String>['Alabama'])); log.clear(); expect(find.text('Nevada'), findsNothing); await tester.drag(find.text('Alabama'), const Offset(0.0, -4000.0)); await tester.pump(); expect(find.text('Alabama'), findsNothing); expect(tester.getCenter(find.text('Massachusetts')), equals(const Offset(400.0, 100.0))); await tester.tap(find.text('Massachusetts')); expect(log, equals(<String>['Massachusetts'])); log.clear(); }); testWidgets('ListView dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView( padding: EdgeInsets.zero, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('GridView.builder supports null items', (WidgetTester tester) async { await tester.pumpWidget(textFieldBoilerplate( child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 42, ), itemCount: 42, itemBuilder: (BuildContext context, int index) { if (index == 5) { return null; } return const Text('item'); }, ), )); expect(find.text('item'), findsNWidgets(5)); }); testWidgets('ListView.builder supports null items', (WidgetTester tester) async { await tester.pumpWidget(textFieldBoilerplate( child: ListView.builder( itemCount: 42, itemBuilder: (BuildContext context, int index) { if (index == 5) { return null; } return const Text('item'); }, ), )); expect(find.text('item'), findsNWidgets(5)); }); testWidgets('PageView supports null items in itemBuilder', (WidgetTester tester) async { final PageController controller = PageController(viewportFraction: 1 / 5); addTearDown(controller.dispose); await tester.pumpWidget(textFieldBoilerplate( child: PageView.builder( itemCount: 5, controller: controller, itemBuilder: (BuildContext context, int index) { if (index == 2) { return null; } return const Text('item'); }, ), )); expect(find.text('item'), findsNWidgets(2)); }); testWidgets('ListView.separated supports null items in itemBuilder', (WidgetTester tester) async { await tester.pumpWidget(textFieldBoilerplate( child: ListView.separated( itemCount: 42, separatorBuilder: (BuildContext context, int index) { return const Text('separator'); }, itemBuilder: (BuildContext context, int index) { if (index == 5) { return null; } return const Text('item'); }, ), )); expect(find.text('item'), findsNWidgets(5)); expect(find.text('separator'), findsNWidgets(5)); }); testWidgets('ListView.builder dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.builder( padding: EdgeInsets.zero, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, itemCount: focusNodes.length, itemBuilder: (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('ListView.custom dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.custom( padding: EdgeInsets.zero, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, childrenDelegate: SliverChildBuilderDelegate( (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, childCount: focusNodes.length, ), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('ListView.separated dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.separated( padding: EdgeInsets.zero, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, itemCount: focusNodes.length, separatorBuilder: (BuildContext context, int index) => const Divider(), itemBuilder: (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('GridView dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView( padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2), keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('GridView.builder dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.builder( padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2), keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, itemCount: focusNodes.length, itemBuilder: (BuildContext context, int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('GridView.count dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.count( padding: EdgeInsets.zero, crossAxisCount: 2, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('GridView.extent dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.extent( padding: EdgeInsets.zero, maxCrossAxisExtent: 300, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('GridView.custom dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.custom( padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2), keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, childrenDelegate: SliverChildBuilderDelegate( (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, childCount: focusNodes.length, ), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('ListView dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView( padding: EdgeInsets.zero, children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('ListView.builder dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.builder( padding: EdgeInsets.zero, itemCount: focusNodes.length, itemBuilder: (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('ListView.custom dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.custom( padding: EdgeInsets.zero, childrenDelegate: SliverChildBuilderDelegate( (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, childCount: focusNodes.length, ), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('ListView.separated dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: ListView.separated( padding: EdgeInsets.zero, itemCount: focusNodes.length, separatorBuilder: (BuildContext context, int index) => const Divider(), itemBuilder: (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('GridView dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView( padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2), children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('GridView.builder dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.builder( padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2), itemCount: focusNodes.length, itemBuilder: (BuildContext context, int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('GridView.count dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.count( padding: EdgeInsets.zero, crossAxisCount: 2, children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('GridView.extent dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.extent( padding: EdgeInsets.zero, maxCrossAxisExtent: 300, children: focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('GridView.custom dismiss keyboard manual test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: GridView.custom( padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount:2), childrenDelegate: SliverChildBuilderDelegate( (BuildContext context,int index) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNodes[index], style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }, childCount: focusNodes.length, ), ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isTrue); }); testWidgets('ListView restart ballistic activity out of range', (WidgetTester tester) async { Widget buildListView(int n) { return Directionality( textDirection: TextDirection.ltr, child: ListView( dragStartBehavior: DragStartBehavior.down, children: kStates.take(n).map<Widget>((String state) { return Container( height: 200.0, color: const Color(0xFF0000FF), child: Text(state), ); }).toList(), ), ); } await tester.pumpWidget(buildListView(30)); await tester.fling(find.byType(ListView), const Offset(0.0, -4000.0), 4000.0); await tester.pumpWidget(buildListView(15)); await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10)); await tester.pumpAndSettle(); final Viewport viewport = tester.widget(find.byType(Viewport)); expect(viewport.offset.pixels, equals(2400.0)); }); testWidgets('CustomScrollView control test', (WidgetTester tester) async { final List<String> log = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( dragStartBehavior: DragStartBehavior.down, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate( kStates.map<Widget>((String state) { return GestureDetector( dragStartBehavior: DragStartBehavior.down, onTap: () { log.add(state); }, child: Container( height: 200.0, color: const Color(0xFF0000FF), child: Text(state), ), ); }).toList(), ), ), ], ), ), ); await tester.tap(find.text('Alabama')); expect(log, equals(<String>['Alabama'])); log.clear(); expect(find.text('Nevada'), findsNothing); await tester.drag(find.text('Alabama'), const Offset(0.0, -4000.0)); await tester.pump(); expect(find.text('Alabama'), findsNothing); expect(tester.getCenter(find.text('Massachusetts')), equals(const Offset(400.0, 100.0))); await tester.tap(find.text('Massachusetts')); expect(log, equals(<String>['Massachusetts'])); log.clear(); }); testWidgets('CustomScrollView dismiss keyboard onDrag test', (WidgetTester tester) async { final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode()); addTearDown(() { for (final FocusNode node in focusNodes) { node.dispose(); } }); await tester.pumpWidget(textFieldBoilerplate( child: CustomScrollView( dragStartBehavior: DragStartBehavior.down, keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate( focusNodes.map((FocusNode focusNode) { return Container( height: 50, color: Colors.green, child: TextField( focusNode: focusNode, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, ), ), ); }).toList(), ), ), ], ), )); final Finder finder = find.byType(TextField).first; final TextField textField = tester.widget(finder); await tester.showKeyboard(finder); expect(textField.focusNode!.hasFocus, isTrue); await tester.drag(finder, const Offset(0.0, -40.0)); await tester.pumpAndSettle(); expect(textField.focusNode!.hasFocus, isFalse); }); testWidgets('Can jumpTo during drag', (WidgetTester tester) async { final List<Type> log = <Type>[]; final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: NotificationListener<ScrollNotification>( onNotification: (ScrollNotification notification) { log.add(notification.runtimeType); return false; }, child: ListView( controller: controller, children: kStates.map<Widget>((String state) { return SizedBox( height: 200.0, child: Text(state), ); }).toList(), ), ), ), ); expect(log, isEmpty); final TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0)); await gesture.moveBy(const Offset(0.0, -100.0)); expect(log, equals(<Type>[ ScrollStartNotification, UserScrollNotification, ScrollUpdateNotification, ])); log.clear(); await tester.pump(); controller.jumpTo(550.0); expect(controller.offset, equals(550.0)); expect(log, equals(<Type>[ ScrollEndNotification, UserScrollNotification, ScrollStartNotification, ScrollUpdateNotification, ScrollEndNotification, ])); log.clear(); await tester.pump(); await gesture.moveBy(const Offset(0.0, -100.0)); expect(controller.offset, equals(550.0)); expect(log, isEmpty); }); test('PrimaryScrollController.automaticallyInheritOnPlatforms defaults to all mobile platforms', (){ final ScrollController controller = ScrollController(); addTearDown(controller.dispose); final PrimaryScrollController primaryScrollController = PrimaryScrollController( controller: controller, child: const SizedBox(), ); expect( primaryScrollController.automaticallyInheritForPlatforms, TargetPlatformVariant.mobile().values, ); }); testWidgets('Vertical CustomScrollViews are not primary by default', (WidgetTester tester) async { const CustomScrollView view = CustomScrollView(); expect(view.primary, isNull); }); testWidgets('Vertical CustomScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: const CustomScrollView(), controller: controller, )); expect(controller.hasClients, isTrue); }, variant: TargetPlatformVariant.mobile()); testWidgets("Vertical CustomScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: const CustomScrollView(), controller: controller, )); expect(controller.hasClients, isFalse); }, variant: TargetPlatformVariant.desktop()); testWidgets('Vertical ListViews are not primary by default', (WidgetTester tester) async { final ListView view = ListView(); expect(view.primary, isNull); }); testWidgets('Vertical ListViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView(), controller: controller, )); expect(controller.hasClients, isTrue); }, variant: TargetPlatformVariant.mobile()); testWidgets("Vertical ListViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView(), controller: controller, )); expect(controller.hasClients, isFalse); }, variant: TargetPlatformVariant.desktop()); testWidgets('Vertical GridViews are not primary by default', (WidgetTester tester) async { final GridView view = GridView.count(crossAxisCount: 1); expect(view.primary, isNull); }); testWidgets('Vertical GridViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async { final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count(crossAxisCount: 1), controller: controller, )); expect(controller.hasClients, isTrue); }, variant: TargetPlatformVariant.mobile()); testWidgets("Vertical GridViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async { final ScrollController controller = ScrollController(); addTearDown(controller.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count(crossAxisCount: 1), controller: controller, )); expect(controller.hasClients, isFalse); }, variant: TargetPlatformVariant.desktop()); testWidgets('Horizontal CustomScrollViews are non-primary by default', (WidgetTester tester) async { final ScrollController controller1 = ScrollController(); addTearDown(controller1.dispose); final ScrollController controller2 = ScrollController(); addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: CustomScrollView( scrollDirection: Axis.horizontal, controller: controller2, ), controller: controller1, )); expect(controller1.hasClients, isFalse); }); testWidgets('Horizontal ListViews are non-primary by default', (WidgetTester tester) async { final ScrollController controller1 = ScrollController(); addTearDown(controller1.dispose); final ScrollController controller2 = ScrollController(); addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView( scrollDirection: Axis.horizontal, controller: controller2, ), controller: controller1, )); expect(controller1.hasClients, isFalse); }); testWidgets('Horizontal GridViews are non-primary by default', (WidgetTester tester) async { final ScrollController controller1 = ScrollController(); addTearDown(controller1.dispose); final ScrollController controller2 = ScrollController(); addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count( scrollDirection: Axis.horizontal, controller: controller2, crossAxisCount: 1, ), controller: controller1, )); expect(controller1.hasClients, isFalse); }); testWidgets('CustomScrollViews with controllers are non-primary by default', (WidgetTester tester) async { final ScrollController controller1 = ScrollController(); addTearDown(controller1.dispose); final ScrollController controller2 = ScrollController(); addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: CustomScrollView( controller: controller2, ), controller: controller1, )); expect(controller1.hasClients, isFalse); }); testWidgets('ListViews with controllers are non-primary by default', (WidgetTester tester) async { final ScrollController controller1 = ScrollController(); addTearDown(controller1.dispose); final ScrollController controller2 = ScrollController(); addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: ListView( controller: controller2, ), controller: controller1, )); expect(controller1.hasClients, isFalse); }); testWidgets('GridViews with controllers are non-primary by default', (WidgetTester tester) async { final ScrollController controller1 = ScrollController(); addTearDown(controller1.dispose); final ScrollController controller2 = ScrollController(); addTearDown(controller2.dispose); await tester.pumpWidget(primaryScrollControllerBoilerplate( child: GridView.count( controller: controller2, crossAxisCount: 1, ), controller: controller1, )); expect(controller1.hasClients, isFalse); }); testWidgets('CustomScrollView sets PrimaryScrollController when primary', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: PrimaryScrollController( controller: primaryScrollController, child: const CustomScrollView(primary: true), ), ), ); final Scrollable scrollable = tester.widget(find.byType(Scrollable)); expect(scrollable.controller, primaryScrollController); }); testWidgets('ListView sets PrimaryScrollController when primary', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: PrimaryScrollController( controller: primaryScrollController, child: ListView(primary: true), ), ), ); final Scrollable scrollable = tester.widget(find.byType(Scrollable)); expect(scrollable.controller, primaryScrollController); }); testWidgets('GridView sets PrimaryScrollController when primary', (WidgetTester tester) async { final ScrollController primaryScrollController = ScrollController(); addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: PrimaryScrollController( controller: primaryScrollController, child: GridView.count(primary: true, crossAxisCount: 1), ), ), ); final Scrollable scrollable = tester.widget(find.byType(Scrollable)); expect(scrollable.controller, primaryScrollController); }); testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async { const Key innerKey = Key('inner'); final ScrollController primaryScrollController = ScrollController(); addTearDown(primaryScrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: PrimaryScrollController( controller: primaryScrollController, child: ListView( primary: true, children: <Widget>[ Container( constraints: const BoxConstraints(maxHeight: 200.0), child: ListView(key: innerKey, primary: true), ), ], ), ), ), ); final Scrollable innerScrollable = tester.widget( find.descendant( of: find.byKey(innerKey), matching: find.byType(Scrollable), ), ); expect(innerScrollable.controller, isNull); }); testWidgets('Primary ListViews are always scrollable', (WidgetTester tester) async { final ListView view = ListView(primary: true); expect(view.physics, isA<AlwaysScrollableScrollPhysics>()); }); testWidgets('Non-primary ListViews are not always scrollable', (WidgetTester tester) async { final ListView view = ListView(primary: false); expect(view.physics, isNot(isA<AlwaysScrollableScrollPhysics>())); }); testWidgets('Defaulting-to-primary ListViews are always scrollable', (WidgetTester tester) async { final ListView view = ListView(); expect(view.physics, isA<AlwaysScrollableScrollPhysics>()); }); testWidgets('Defaulting-to-not-primary ListViews are not always scrollable', (WidgetTester tester) async { final ListView view = ListView(scrollDirection: Axis.horizontal); expect(view.physics, isNot(isA<AlwaysScrollableScrollPhysics>())); }); testWidgets('primary:true leads to scrolling', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: NotificationListener<OverscrollNotification>( onNotification: (OverscrollNotification message) { scrolled = true; return false; }, child: ListView( primary: true, ), ), ), ); await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); expect(scrolled, isTrue); }); testWidgets('primary:false leads to no scrolling', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: NotificationListener<OverscrollNotification>( onNotification: (OverscrollNotification message) { scrolled = true; return false; }, child: ListView( primary: false, ), ), ), ); await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); expect(scrolled, isFalse); }); testWidgets('physics:AlwaysScrollableScrollPhysics actually overrides primary:false default behavior', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: NotificationListener<OverscrollNotification>( onNotification: (OverscrollNotification message) { scrolled = true; return false; }, child: ListView( primary: false, physics: const AlwaysScrollableScrollPhysics(), ), ), ), ); await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); expect(scrolled, isTrue); }); testWidgets('physics:ScrollPhysics actually overrides primary:true default behavior', (WidgetTester tester) async { bool scrolled = false; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: NotificationListener<OverscrollNotification>( onNotification: (OverscrollNotification message) { scrolled = true; return false; }, child: ListView( primary: true, physics: const ScrollPhysics(), ), ), ), ); await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); expect(scrolled, isFalse); }); testWidgets('separatorBuilder must return something', (WidgetTester tester) async { const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA']; Widget buildFrame(Widget firstSeparator) { return MaterialApp( home: Material( child: ListView.separated( itemBuilder: (BuildContext context, int index) { return Text(listOfValues[index]); }, separatorBuilder: (BuildContext context, int index) { if (index == 0) { return firstSeparator; } else { return const Divider(); } }, itemCount: listOfValues.length, ), ), ); } // A separatorBuilder that always returns a Divider is fine await tester.pumpWidget(buildFrame(const Divider())); expect(tester.takeException(), isNull); }); testWidgets('when itemBuilder throws, creates Error Widget', (WidgetTester tester) async { const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA']; Widget buildFrame(bool throwOnFirstItem) { return MaterialApp( home: Material( child: ListView.builder( itemBuilder: (BuildContext context, int index) { if (index == 0 && throwOnFirstItem) { throw Exception('itemBuilder fail'); } return Text(listOfValues[index]); }, itemCount: listOfValues.length, ), ), ); } // When itemBuilder doesn't throw, no ErrorWidget await tester.pumpWidget(buildFrame(false)); expect(tester.takeException(), isNull); final Finder finder = find.byType(ErrorWidget); expect(find.byType(ErrorWidget), findsNothing); // When it does throw, one error widget is rendered in the item's place await tester.pumpWidget(buildFrame(true)); expect(tester.takeException(), isA<Exception>()); expect(finder, findsOneWidget); }); testWidgets('when separatorBuilder throws, creates ErrorWidget', (WidgetTester tester) async { const List<String> listOfValues = <String>['ALPHA', 'BETA', 'GAMMA', 'DELTA']; const Key key = Key('list'); Widget buildFrame(bool throwOnFirstSeparator) { return MaterialApp( home: Material( child: ListView.separated( key: key, itemBuilder: (BuildContext context, int index) { return Text(listOfValues[index]); }, separatorBuilder: (BuildContext context, int index) { if (index == 0 && throwOnFirstSeparator) { throw Exception('separatorBuilder fail'); } return const Divider(); }, itemCount: listOfValues.length, ), ), ); } // When separatorBuilder doesn't throw, no ErrorWidget await tester.pumpWidget(buildFrame(false)); expect(tester.takeException(), isNull); final Finder finder = find.byType(ErrorWidget); expect(find.byType(ErrorWidget), findsNothing); // When it does throw, one error widget is rendered in the separator's place await tester.pumpWidget(buildFrame(true)); expect(tester.takeException(), isA<Exception>()); expect(finder, findsOneWidget); }); testWidgets('ListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ListView( itemExtent: 100, prototypeItem: const SizedBox(), ), throwsAssertionError); }); testWidgets('ListView.builder asserts on negative childCount', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); }, itemCount: -1, ), throwsAssertionError); }); testWidgets('ListView.builder asserts on negative semanticChildCount', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); }, itemCount: 1, semanticChildCount: -1, ), throwsAssertionError); }); testWidgets('ListView.builder asserts on nonsensical childCount/semanticChildCount', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); }, itemCount: 1, semanticChildCount: 4, ), throwsAssertionError); }); testWidgets('ListView.builder asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ListView.builder( itemBuilder: (BuildContext context, int index) { return const SizedBox(); }, itemExtent: 100, prototypeItem: const SizedBox(), ), throwsAssertionError); }); testWidgets('ListView.custom asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async { expect(() => ListView.custom( childrenDelegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return const SizedBox(); }, ), itemExtent: 100, prototypeItem: const SizedBox(), ), throwsAssertionError); }); testWidgets('PrimaryScrollController provides fallback ScrollActions', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: CustomScrollView( primary: true, slivers: List<Widget>.generate( 20, (int index) { return SliverToBoxAdapter( child: Focus( autofocus: index == 0, child: SizedBox(key: ValueKey<String>('Box $index'), height: 50.0), ), ); }, ), ), ), ); final ScrollController controller = PrimaryScrollController.of( tester.element(find.byType(CustomScrollView)), ); await tester.pumpAndSettle(); expect(controller.position.pixels, equals(0.0)); expect( tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)), ); await tester.sendKeyEvent(LogicalKeyboardKey.pageDown); await tester.pumpAndSettle(); expect(controller.position.pixels, equals(400.0)); expect( tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, -400.0, 800.0, -350.0)), ); await tester.sendKeyEvent(LogicalKeyboardKey.pageUp); await tester.pumpAndSettle(); expect(controller.position.pixels, equals(0.0)); expect( tester.getRect(find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false)), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 50.0)), ); }); testWidgets('Fallback ScrollActions handle too many positions with error message', (WidgetTester tester) async { Widget getScrollView() { return SizedBox( width: 400.0, child: CustomScrollView( primary: true, slivers: List<Widget>.generate( 20, (int index) { return SliverToBoxAdapter( child: Focus( child: SizedBox(key: ValueKey<String>('Box $index'), height: 50.0), ), ); }, ), ), ); } await tester.pumpWidget( MaterialApp( home: Row( children: <Widget>[ getScrollView(), getScrollView(), ], ), ), ); await tester.pumpAndSettle(); expect( tester.getRect( find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false).first ), equals(const Rect.fromLTRB(0.0, 0.0, 400.0, 50.0)), ); await tester.sendKeyEvent(LogicalKeyboardKey.pageDown); final AssertionError exception = tester.takeException() as AssertionError; expect(exception, isAssertionError); expect( exception.message, contains( 'A ScrollAction was invoked with the PrimaryScrollController, but ' 'more than one ScrollPosition is attached.' ), ); }); testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return ListView.builder( itemBuilder: (BuildContext context, int index) { return SizedBox( key: ValueKey<int>(numbers[index]), // children with different heights height: 20 + numbers[index] * 10, child: ReorderableDragStartListener( index: index, child: Text(numbers[index].toString()), ) ); }, itemCount: numbers.length, itemExtent: 30, ); }, ), ), ) ); final double item0Height = tester.getSize(find.text('0').hitTestable()).height; final double item1Height = tester.getSize(find.text('1').hitTestable()).height; final double item2Height = tester.getSize(find.text('2').hitTestable()).height; expect(item0Height, 30.0); expect(item1Height, 30.0); expect(item2Height, 30.0); }); testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async { final List<int> numbers = <int>[0,1,2]; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return ListView.builder( itemBuilder: (BuildContext context, int index) { return SizedBox( key: ValueKey<int>(numbers[index]), // children with different heights height: 20 + numbers[index] * 10, child: ReorderableDragStartListener( index: index, child: Text(numbers[index].toString()), ) ); }, itemCount: numbers.length, prototypeItem: const SizedBox( height: 30, child: Text('3'), ), ); }, ), ), ) ); final double item0Height = tester.getSize(find.text('0').hitTestable()).height; final double item1Height = tester.getSize(find.text('1').hitTestable()).height; final double item2Height = tester.getSize(find.text('2').hitTestable()).height; expect(item0Height, 30.0); expect(item1Height, 30.0); expect(item2Height, 30.0); }); }