| // 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_test/flutter_test.dart'; |
| import 'package:flutter/src/foundation/diagnostics.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| void main() { |
| testWidgets('AnimatedList', (WidgetTester tester) async { |
| final AnimatedListItemBuilder builder = (BuildContext context, int index, Animation<double> animation) { |
| return SizedBox( |
| height: 100.0, |
| child: Center( |
| child: Text('item $index'), |
| ), |
| ); |
| }; |
| final GlobalKey<AnimatedListState> listKey = GlobalKey<AnimatedListState>(); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: AnimatedList( |
| key: listKey, |
| initialItemCount: 2, |
| itemBuilder: builder, |
| ), |
| ), |
| ); |
| |
| expect(find.byWidgetPredicate((Widget widget) { |
| return widget is SliverAnimatedList |
| && widget.initialItemCount == 2 |
| && widget.itemBuilder == builder; |
| }), findsOneWidget); |
| |
| listKey.currentState!.insertItem(0); |
| await tester.pump(); |
| expect(find.text('item 2'), findsOneWidget); |
| |
| listKey.currentState!.removeItem( |
| 2, |
| (BuildContext context, Animation<double> animation) { |
| return const SizedBox( |
| height: 100.0, |
| child: Center(child: Text('removing item')), |
| ); |
| }, |
| duration: const Duration(milliseconds: 100), |
| ); |
| |
| await tester.pump(); |
| expect(find.text('removing item'), findsOneWidget); |
| expect(find.text('item 2'), findsNothing); |
| |
| await tester.pumpAndSettle(const Duration(milliseconds: 100)); |
| expect(find.text('removing item'), findsNothing); |
| }); |
| |
| group('SliverAnimatedList', () { |
| testWidgets('initialItemCount', (WidgetTester tester) async { |
| final Map<int, Animation<double>> animations = <int, Animation<double>>{}; |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverAnimatedList( |
| initialItemCount: 2, |
| itemBuilder: (BuildContext context, int index, Animation<double> animation) { |
| animations[index] = animation; |
| return SizedBox( |
| height: 100.0, |
| child: Center( |
| child: Text('item $index'), |
| ), |
| ); |
| }, |
| ) |
| ], |
| ), |
| ), |
| ); |
| |
| expect(find.text('item 0'), findsOneWidget); |
| expect(find.text('item 1'), findsOneWidget); |
| expect(animations.containsKey(0), true); |
| expect(animations.containsKey(1), true); |
| expect(animations[0]!.value, 1.0); |
| expect(animations[1]!.value, 1.0); |
| }); |
| |
| testWidgets('insert', (WidgetTester tester) async { |
| final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverAnimatedList( |
| key: listKey, |
| itemBuilder: (BuildContext context, int index, Animation<double> animation) { |
| return SizeTransition( |
| key: ValueKey<int>(index), |
| axis: Axis.vertical, |
| sizeFactor: animation, |
| child: SizedBox( |
| height: 100.0, |
| child: Center(child: Text('item $index')), |
| ), |
| ); |
| }, |
| ) |
| ], |
| ), |
| ), |
| ); |
| |
| double itemHeight(int index) => tester.getSize(find.byKey(ValueKey<int>(index), skipOffstage: false)).height; |
| double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy; |
| double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index), skipOffstage: false)).dy; |
| |
| listKey.currentState!.insertItem( |
| 0, |
| duration: const Duration(milliseconds: 100), |
| ); |
| await tester.pump(); |
| |
| // Newly inserted item 0's height should animate from 0 to 100 |
| expect(itemHeight(0), 0.0); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(itemHeight(0), 50.0); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(itemHeight(0), 100.0); |
| |
| // The list now contains one fully expanded item at the top: |
| expect(find.text('item 0'), findsOneWidget); |
| expect(itemTop(0), 0.0); |
| expect(itemBottom(0), 100.0); |
| |
| listKey.currentState!.insertItem( |
| 0, |
| duration: const Duration(milliseconds: 100), |
| ); |
| listKey.currentState!.insertItem( |
| 0, |
| duration: const Duration(milliseconds: 100), |
| ); |
| await tester.pump(); |
| |
| // The height of the newly inserted items at index 0 and 1 should animate |
| // from 0 to 100. |
| // The height of the original item, now at index 2, should remain 100. |
| expect(itemHeight(0), 0.0); |
| expect(itemHeight(1), 0.0); |
| expect(itemHeight(2), 100.0); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(itemHeight(0), 50.0); |
| expect(itemHeight(1), 50.0); |
| expect(itemHeight(2), 100.0); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(itemHeight(0), 100.0); |
| expect(itemHeight(1), 100.0); |
| expect(itemHeight(2), 100.0); |
| |
| // The newly inserted "item 1" and "item 2" appear above "item 0" |
| expect(find.text('item 0'), findsOneWidget); |
| expect(find.text('item 1'), findsOneWidget); |
| expect(find.text('item 2'), findsOneWidget); |
| expect(itemTop(0), 0.0); |
| expect(itemBottom(0), 100.0); |
| expect(itemTop(1), 100.0); |
| expect(itemBottom(1), 200.0); |
| expect(itemTop(2), 200.0); |
| expect(itemBottom(2), 300.0); |
| }); |
| |
| testWidgets('remove', (WidgetTester tester) async { |
| final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); |
| final List<int> items = <int>[0, 1, 2]; |
| |
| Widget buildItem(BuildContext context, int item, Animation<double> animation) { |
| return SizeTransition( |
| key: ValueKey<int>(item), |
| axis: Axis.vertical, |
| sizeFactor: animation, |
| child: SizedBox( |
| height: 100.0, |
| child: Center( |
| child: Text('item $item', textDirection: TextDirection.ltr), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverAnimatedList( |
| key: listKey, |
| initialItemCount: 3, |
| itemBuilder: (BuildContext context, int index, Animation<double> animation) { |
| return buildItem(context, items[index], animation); |
| }, |
| ) |
| ], |
| ), |
| ), |
| ); |
| |
| double itemTop(int index) => tester.getTopLeft(find.byKey(ValueKey<int>(index))).dy; |
| double itemBottom(int index) => tester.getBottomLeft(find.byKey(ValueKey<int>(index))).dy; |
| |
| expect(find.text('item 0'), findsOneWidget); |
| expect(find.text('item 1'), findsOneWidget); |
| expect(find.text('item 2'), findsOneWidget); |
| |
| items.removeAt(0); |
| listKey.currentState!.removeItem( |
| 0, |
| (BuildContext context, Animation<double> animation) => buildItem(context, 0, animation), |
| duration: const Duration(milliseconds: 100), |
| ); |
| |
| // Items 0, 1, 2 at 0, 100, 200. All heights 100. |
| expect(itemTop(0), 0.0); |
| expect(itemBottom(0), 100.0); |
| expect(itemTop(1), 100.0); |
| expect(itemBottom(1), 200.0); |
| expect(itemTop(2), 200.0); |
| expect(itemBottom(2), 300.0); |
| |
| // Newly removed item 0's height should animate from 100 to 0 over 100ms |
| |
| // Items 0, 1, 2 at 0, 50, 150. Item 0's height is 50. |
| await tester.pump(); |
| await tester.pump(const Duration(milliseconds: 50)); |
| expect(itemTop(0), 0.0); |
| expect(itemBottom(0), 50.0); |
| expect(itemTop(1), 50.0); |
| expect(itemBottom(1), 150.0); |
| expect(itemTop(2), 150.0); |
| expect(itemBottom(2), 250.0); |
| |
| // Items 1, 2 at 0, 100. |
| await tester.pumpAndSettle(); |
| expect(itemTop(1), 0.0); |
| expect(itemBottom(1), 100.0); |
| expect(itemTop(2), 100.0); |
| expect(itemBottom(2), 200.0); |
| }); |
| |
| testWidgets('works in combination with other slivers', (WidgetTester tester) async { |
| final GlobalKey<SliverAnimatedListState> listKey = GlobalKey<SliverAnimatedListState>(); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: CustomScrollView( |
| slivers: <Widget>[ |
| SliverList( |
| delegate: SliverChildListDelegate(<Widget>[ |
| const SizedBox(height: 100), |
| const SizedBox(height: 100), |
| ]), |
| ), |
| SliverAnimatedList( |
| key: listKey, |
| initialItemCount: 3, |
| itemBuilder: (BuildContext context, int index, Animation<double> animation) { |
| return SizedBox( |
| height: 100, |
| child: Text('item $index'), |
| ); |
| }, |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| expect(tester.getTopLeft(find.text('item 0')).dy, 200); |
| expect(tester.getTopLeft(find.text('item 1')).dy, 300); |
| |
| listKey.currentState!.insertItem(3); |
| await tester.pumpAndSettle(); |
| expect(tester.getTopLeft(find.text('item 3')).dy, 500); |
| |
| listKey.currentState!.removeItem(0, |
| (BuildContext context, Animation<double> animation) { |
| return SizeTransition( |
| sizeFactor: animation, |
| key: const ObjectKey('removing'), |
| child: const SizedBox( |
| height: 100, |
| child: Text('removing'), |
| ), |
| ); |
| }, |
| duration: const Duration(seconds: 1), |
| ); |
| |
| await tester.pump(); |
| expect(find.text('item 3'), findsNothing); |
| |
| await tester.pump(const Duration(milliseconds: 500)); |
| expect( |
| tester.getSize(find.byKey(const ObjectKey('removing'))).height, |
| 50, |
| ); |
| expect(tester.getTopLeft(find.text('item 0')).dy, 250); |
| |
| await tester.pumpAndSettle(); |
| expect(find.text('removing'), findsNothing); |
| expect(tester.getTopLeft(find.text('item 0')).dy, 200); |
| }); |
| }); |
| |
| testWidgets('AnimatedList.of() called with a context that does not contain AnimatedList', |
| (WidgetTester tester) async { |
| final GlobalKey key = GlobalKey(); |
| await tester.pumpWidget(Container(key: key)); |
| late FlutterError error; |
| try { |
| AnimatedList.of(key.currentContext!); |
| } on FlutterError catch (e) { |
| error = e; |
| } |
| expect(error.diagnostics.length, 4); |
| expect(error.diagnostics[2].level, DiagnosticLevel.hint); |
| expect( |
| error.diagnostics[2].toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'This can happen when the context provided is from the same\n' |
| 'StatefulWidget that built the AnimatedList. Please see the\n' |
| 'AnimatedList documentation for examples of how to refer to an\n' |
| 'AnimatedListState object:\n' |
| 'https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html\n' |
| ), |
| ); |
| expect(error.diagnostics[3], isA<DiagnosticsProperty<Element>>()); |
| expect( |
| error.toStringDeep(), |
| equalsIgnoringHashCodes( |
| 'FlutterError\n' |
| ' AnimatedList.of() called with a context that does not contain an\n' |
| ' AnimatedList.\n' |
| ' No AnimatedList ancestor could be found starting from the context\n' |
| ' that was passed to AnimatedList.of().\n' |
| ' This can happen when the context provided is from the same\n' |
| ' StatefulWidget that built the AnimatedList. Please see the\n' |
| ' AnimatedList documentation for examples of how to refer to an\n' |
| ' AnimatedListState object:\n' |
| ' https://api.flutter.dev/flutter/widgets/AnimatedListState-class.html\n' |
| ' The context used was:\n' |
| ' Container-[GlobalKey#32cc6]\n' |
| ), |
| ); |
| }); |
| } |