diff --git a/modules/ensemble/test/widget/list_view_test.dart b/modules/ensemble/test/widget/list_view_test.dart index 132f7b174..31f32fd3e 100644 --- a/modules/ensemble/test/widget/list_view_test.dart +++ b/modules/ensemble/test/widget/list_view_test.dart @@ -19,6 +19,69 @@ void main() { return widget; } + ensemble.ListView listViewWithManyChildren({int count = 20}) { + final widget = ensemble.ListView(); + widget.initChildren( + children: List.generate( + count, + (i) => ViewUtil.buildModel({ + 'Text': {'text': 'Item $i'} + }, null), + ), + ); + return widget; + } + + test('scrollToOffset is a no-op without attached scroll clients', () { + final controller = ensemble.ListViewController(); + final scrollController = ScrollController(); + controller.scrollController = scrollController; + + expect(() => controller.scrollToOffset(100), returnsNormally); + + scrollController.dispose(); + }); + + testWidgets('scrollToOffset jumps to pixel offset when not animated', + (tester) async { + final list = listViewWithManyChildren(); + + await tester.pumpWidget(TestUtils.wrapTestWidgetWithScope( + SizedBox( + height: 200, + width: 300, + child: list, + ), + )); + await tester.pumpAndSettle(); + + list.controller.scrollToOffset(80, animated: false); + await tester.pump(); + + expect(list.controller.scrollController!.offset, 80); + }); + + testWidgets('scrollToTop moves list to offset zero', (tester) async { + final list = listViewWithManyChildren(); + + await tester.pumpWidget(TestUtils.wrapTestWidgetWithScope( + SizedBox( + height: 200, + width: 300, + child: list, + ), + )); + await tester.pumpAndSettle(); + + list.controller.scrollToOffset(80, animated: false); + await tester.pump(); + expect(list.controller.scrollController!.offset, greaterThan(0)); + + list.controller.scrollToTop(animated: false); + await tester.pump(); + expect(list.controller.scrollController!.offset, 0); + }); + testWidgets('disposes internally-created scroll controller', (tester) async { final widget = ensemble.ListView(); diff --git a/modules/ensemble/test/widget/progress_indicator_test.dart b/modules/ensemble/test/widget/progress_indicator_test.dart new file mode 100644 index 000000000..7761a12b2 --- /dev/null +++ b/modules/ensemble/test/widget/progress_indicator_test.dart @@ -0,0 +1,34 @@ +import 'package:ensemble/widget/progress_indicator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'test_utils.dart'; + +void main() { + testWidgets('cancels countdown timers when removed from tree', (tester) async { + final widget = EnsembleProgressIndicator(); + widget.setProperty('countdown', 2); + + await tester.pumpWidget(TestUtils.wrapTestWidgetWithScope(widget)); + await tester.pump(); + + await tester.pumpWidget(TestUtils.wrapTestWidget(const SizedBox.shrink())); + await tester.pump(); + + // Advance well past the countdown; leaked timers would call setState off-tree. + await tester.pump(const Duration(seconds: 5)); + expect(tester.takeException(), isNull); + }); + + testWidgets('countdown updates progress after periodic tick', (tester) async { + final widget = EnsembleProgressIndicator(); + widget.setProperty('countdown', 2); + + await tester.pumpWidget(TestUtils.wrapTestWidgetWithScope(widget)); + await tester.pump(); + + await tester.pump(const Duration(milliseconds: 150)); + expect(widget.controller.value, isNotNull); + expect(widget.controller.value, greaterThan(0)); + }); +}