// 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. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(['reduced-test-set']) library; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; List> simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( 'Root 1', expanded: true, children: >[ TreeSliverNode('Child 1:0'), TreeSliverNode('Child 1:1'), ], ), TreeSliverNode( 'Root 2', children: >[ TreeSliverNode('Child 2:0'), TreeSliverNode('Child 2:1'), ], ), TreeSliverNode('Root 3'), ]; void main() { group('TreeSliverNode', () { setUp(() { // Reset node conditions for each test. simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( 'Root 1', expanded: true, children: >[ TreeSliverNode('Child 1:0'), TreeSliverNode('Child 1:1'), ], ), TreeSliverNode( 'Root 2', children: >[ TreeSliverNode('Child 2:0'), TreeSliverNode('Child 2:1'), ], ), TreeSliverNode('Root 3'), ]; }); test('getters, toString', () { final children = >[TreeSliverNode('child')]; final node = TreeSliverNode('parent', children: children, expanded: true); expect(node.content, 'parent'); expect(node.children, children); expect(node.isExpanded, isTrue); expect(node.children.first.content, 'child'); expect(node.children.first.children.isEmpty, isTrue); expect(node.children.first.isExpanded, isFalse); // Set by TreeSliver when built for tree integrity expect(node.depth, isNull); expect(node.parent, isNull); expect(node.children.first.depth, isNull); expect(node.children.first.parent, isNull); expect(node.toString(), 'TreeSliverNode: parent, depth: null, parent, expanded: true'); expect(node.children.first.toString(), 'TreeSliverNode: child, depth: null, leaf'); }); testWidgets('TreeSliverNode sets ups parent and depth properties', (WidgetTester tester) async { final children = >[TreeSliverNode('child')]; final node = TreeSliverNode('parent', children: children, expanded: true); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver(tree: >[node]), ], ), ), ); expect(node.content, 'parent'); expect(node.children, children); expect(node.isExpanded, isTrue); expect(node.children.first.content, 'child'); expect(node.children.first.children.isEmpty, isTrue); expect(node.children.first.isExpanded, isFalse); // Set by TreeSliver when built for tree integrity expect(node.depth, 0); expect(node.parent, isNull); expect(node.children.first.depth, 1); expect(node.children.first.parent, node); expect(node.toString(), 'TreeSliverNode: parent, depth: root, parent, expanded: true'); expect(node.children.first.toString(), 'TreeSliverNode: child, depth: 1, leaf'); }); }); group('TreeController', () { setUp(() { // Reset node conditions for each test. simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( 'Root 1', expanded: true, children: >[ TreeSliverNode('Child 1:0'), TreeSliverNode('Child 1:1'), ], ), TreeSliverNode( 'Root 2', children: >[ TreeSliverNode('Child 2:0'), TreeSliverNode('Child 2:1'), ], ), TreeSliverNode('Root 3'), ]; }); testWidgets('Can set controller on TreeSliver', (WidgetTester tester) async { final controller = TreeSliverController(); TreeSliverController? returnedController; await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, controller: controller, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle toggleAnimationStyle, ) { returnedController ??= TreeSliverController.of(context); return TreeSliver.defaultTreeNodeBuilder(context, node, toggleAnimationStyle); }, ), ], ), ), ); expect(controller, returnedController); }); testWidgets('Can get default controller on TreeSliver', (WidgetTester tester) async { TreeSliverController? returnedController; await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle toggleAnimationStyle, ) { returnedController ??= TreeSliverController.maybeOf(context); return TreeSliver.defaultTreeNodeBuilder(context, node, toggleAnimationStyle); }, ), ], ), ), ); expect(returnedController, isNotNull); }); testWidgets('Can get node for TreeSliverNode.content', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), ); expect(controller.getNodeFor('Root 0'), simpleNodeSet[0]); }); testWidgets('Can get isExpanded for a node', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), ); expect(controller.isExpanded(simpleNodeSet[0]), isFalse); expect(controller.isExpanded(simpleNodeSet[1]), isTrue); }); testWidgets('Can get isActive for a node', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), ); expect(controller.isActive(simpleNodeSet[0]), isTrue); expect(controller.isActive(simpleNodeSet[1]), isTrue); // The parent 'Root 2' is not expanded, so its children are not active. expect(controller.isExpanded(simpleNodeSet[2]), isFalse); expect(controller.isActive(simpleNodeSet[2].children[0]), isFalse); }); testWidgets('Can toggleNode, to collapse or expand', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), ); // The parent 'Root 2' is not expanded, so its children are not active. expect(controller.isExpanded(simpleNodeSet[2]), isFalse); expect(controller.isActive(simpleNodeSet[2].children[0]), isFalse); // Toggle 'Root 2' to expand it controller.toggleNode(simpleNodeSet[2]); expect(controller.isExpanded(simpleNodeSet[2]), isTrue); expect(controller.isActive(simpleNodeSet[2].children[0]), isTrue); // The parent 'Root 1' is expanded, so its children are active. expect(controller.isExpanded(simpleNodeSet[1]), isTrue); expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue); // Collapse 'Root 1' controller.toggleNode(simpleNodeSet[1]); expect(controller.isExpanded(simpleNodeSet[1]), isFalse); expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue); // Nodes are not removed from the active list until the collapse animation // completes. The parent expansion state also updates. await tester.pumpAndSettle(); expect(controller.isExpanded(simpleNodeSet[1]), isFalse); expect(controller.isActive(simpleNodeSet[1].children[0]), isFalse); }); testWidgets('Can expandNode, then collapseAll', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), ); // The parent 'Root 2' is not expanded, so its children are not active. expect(controller.isExpanded(simpleNodeSet[2]), isFalse); expect(controller.isActive(simpleNodeSet[2].children[0]), isFalse); // Expand 'Root 2' controller.expandNode(simpleNodeSet[2]); expect(controller.isExpanded(simpleNodeSet[2]), isTrue); expect(controller.isActive(simpleNodeSet[2].children[0]), isTrue); // Both parents from our simple node set are expanded. // 'Root 1' expect(controller.isExpanded(simpleNodeSet[1]), isTrue); // 'Root 2' expect(controller.isExpanded(simpleNodeSet[2]), isTrue); // Collapse both. controller.collapseAll(); await tester.pumpAndSettle(); // Both parents from our simple node set have collapsed. // 'Root 1' expect(controller.isExpanded(simpleNodeSet[1]), isFalse); // 'Root 2' expect(controller.isExpanded(simpleNodeSet[2]), isFalse); }); testWidgets('Can collapseNode, then expandAll', (WidgetTester tester) async { final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), ), ); // The parent 'Root 1' is expanded, so its children are active. expect(controller.isExpanded(simpleNodeSet[1]), isTrue); expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue); // Collapse 'Root 1' controller.collapseNode(simpleNodeSet[1]); expect(controller.isExpanded(simpleNodeSet[1]), isFalse); expect(controller.isActive(simpleNodeSet[1].children[0]), isTrue); // Nodes are not removed from the active list until the collapse animation // completes. await tester.pumpAndSettle(); expect(controller.isActive(simpleNodeSet[1].children[0]), isFalse); // Both parents from our simple node set are collapsed. // 'Root 1' expect(controller.isExpanded(simpleNodeSet[1]), isFalse); // 'Root 2' expect(controller.isExpanded(simpleNodeSet[2]), isFalse); // Expand both. controller.expandAll(); // Both parents from our simple node set are expanded. // 'Root 1' expect(controller.isExpanded(simpleNodeSet[1]), isTrue); // 'Root 2' expect(controller.isExpanded(simpleNodeSet[2]), isTrue); }); }); test('TreeSliverIndentationType values are properly reflected', () { double value = TreeSliverIndentationType.standard.value; expect(value, 10.0); value = TreeSliverIndentationType.none.value; expect(value, 0.0); value = TreeSliverIndentationType.custom(50.0).value; expect(value, 50.0); }); testWidgets('.toggleNodeWith, onNodeToggle', (WidgetTester tester) async { simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( 'Root 1', expanded: true, children: >[ TreeSliverNode('Child 1:0'), TreeSliverNode('Child 1:1'), ], ), TreeSliverNode( 'Root 2', children: >[ TreeSliverNode('Child 2:0'), TreeSliverNode('Child 2:1'), ], ), TreeSliverNode('Root 3'), ]; final controller = TreeSliverController(); // The default node builder wraps the leading icon with toggleNodeWith. var toggled = false; TreeSliverNode? toggledNode; await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, controller: controller, onNodeToggle: (TreeSliverNode node) { toggled = true; toggledNode = node as TreeSliverNode; }, ), ], ), ), ); expect(controller.isExpanded(simpleNodeSet[1]), isTrue); await tester.tap(find.byType(Icon).first); await tester.pump(); expect(controller.isExpanded(simpleNodeSet[1]), isFalse); expect(toggled, isTrue); expect(toggledNode, simpleNodeSet[1]); await tester.pumpAndSettle(); expect(controller.isExpanded(simpleNodeSet[1]), isFalse); toggled = false; toggledNode = null; // Use toggleNodeWith to make the whole row trigger the node state. await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, controller: controller, onNodeToggle: (TreeSliverNode node) { toggled = true; toggledNode = node as TreeSliverNode; }, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle toggleAnimationStyle, ) { final Duration animationDuration = toggleAnimationStyle.duration ?? TreeSliver.defaultAnimationDuration; final Curve animationCurve = toggleAnimationStyle.curve ?? TreeSliver.defaultAnimationCurve; // This makes the whole row trigger toggling. return TreeSliver.wrapChildToToggleNode( node: node, child: Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ // Icon for parent nodes SizedBox.square( dimension: 30.0, child: node.children.isNotEmpty ? AnimatedRotation( turns: node.isExpanded ? 0.25 : 0.0, duration: animationDuration, curve: animationCurve, child: const Icon(IconData(0x25BA), size: 14), ) : null, ), // Spacer const SizedBox(width: 8.0), // Content Text(node.content.toString()), ], ), ), ); }, ), ], ), ), ); // Still collapsed from earlier expect(controller.isExpanded(simpleNodeSet[1]), isFalse); // Tapping on the text instead of the Icon. await tester.tap(find.text('Root 1')); await tester.pump(); expect(controller.isExpanded(simpleNodeSet[1]), isTrue); expect(toggled, isTrue); expect(toggledNode, simpleNodeSet[1]); }); testWidgets('AnimationStyle is piped through to node builder', (WidgetTester tester) async { simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( 'Root 1', expanded: true, children: >[ TreeSliverNode('Child 1:0'), TreeSliverNode('Child 1:1'), ], ), TreeSliverNode( 'Root 2', children: >[ TreeSliverNode('Child 2:0'), TreeSliverNode('Child 2:1'), ], ), TreeSliverNode('Root 3'), ]; AnimationStyle? style; await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle toggleAnimationStyle, ) { style ??= toggleAnimationStyle; return Text(node.content.toString()); }, ), ], ), ), ); // Default expect(style, TreeSliver.defaultToggleAnimationStyle); await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, toggleAnimationStyle: AnimationStyle.noAnimation, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle toggleAnimationStyle, ) { style = toggleAnimationStyle; return Text(node.content.toString()); }, ), ], ), ), ); expect(style, isNotNull); expect(style!.curve, isNull); expect(style!.duration, Duration.zero); style = null; await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: simpleNodeSet, toggleAnimationStyle: const AnimationStyle( curve: Curves.easeIn, duration: Duration(milliseconds: 200), ), treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle toggleAnimationStyle, ) { style ??= toggleAnimationStyle; return Text(node.content.toString()); }, ), ], ), ), ); expect(style, isNotNull); expect(style!.curve, Curves.easeIn); expect(style!.duration, const Duration(milliseconds: 200)); }); testWidgets('Adding more root TreeViewNodes are reflected in the tree', ( WidgetTester tester, ) async { simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( 'Root 1', expanded: true, children: >[ TreeSliverNode('Child 1:0'), TreeSliverNode('Child 1:1'), ], ), TreeSliverNode( 'Root 2', children: >[ TreeSliverNode('Child 2:0'), TreeSliverNode('Child 2:1'), ], ), TreeSliverNode('Root 3'), ]; final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { simpleNodeSet.add(TreeSliverNode('Added root')); }); }, ), ); }, ), ), ); await tester.pump(); expect(find.text('Root 0'), findsOneWidget); expect(find.text('Root 1'), findsOneWidget); expect(find.text('Child 1:0'), findsOneWidget); expect(find.text('Child 1:1'), findsOneWidget); expect(find.text('Root 2'), findsOneWidget); expect(find.text('Child 2:0'), findsNothing); expect(find.text('Child 2:1'), findsNothing); expect(find.text('Root 3'), findsOneWidget); expect(find.text('Added root'), findsNothing); await tester.tap(find.byType(FloatingActionButton)); await tester.pump(); expect(find.text('Root 0'), findsOneWidget); expect(find.text('Root 1'), findsOneWidget); expect(find.text('Child 1:0'), findsOneWidget); expect(find.text('Child 1:1'), findsOneWidget); expect(find.text('Root 2'), findsOneWidget); expect(find.text('Child 2:0'), findsNothing); expect(find.text('Child 2:1'), findsNothing); expect(find.text('Root 3'), findsOneWidget); // Node was added expect(find.text('Added root'), findsOneWidget); }); testWidgets('Adding more TreeViewNodes below the root are reflected in the tree', ( WidgetTester tester, ) async { simpleNodeSet = >[ TreeSliverNode('Root 0'), TreeSliverNode( 'Root 1', expanded: true, children: >[ TreeSliverNode('Child 1:0'), TreeSliverNode('Child 1:1'), ], ), TreeSliverNode( 'Root 2', children: >[ TreeSliverNode('Child 2:0'), TreeSliverNode('Child 2:1'), ], ), TreeSliverNode('Root 3'), ]; final controller = TreeSliverController(); await tester.pumpWidget( MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( body: CustomScrollView( slivers: [TreeSliver(tree: simpleNodeSet, controller: controller)], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { simpleNodeSet[1].children.add(TreeSliverNode('Added child')); }); }, ), ); }, ), ), ); await tester.pump(); expect(find.text('Root 0'), findsOneWidget); expect(find.text('Root 1'), findsOneWidget); expect(find.text('Child 1:0'), findsOneWidget); expect(find.text('Child 1:1'), findsOneWidget); expect(find.text('Added child'), findsNothing); expect(find.text('Root 2'), findsOneWidget); expect(find.text('Child 2:0'), findsNothing); expect(find.text('Child 2:1'), findsNothing); expect(find.text('Root 3'), findsOneWidget); await tester.tap(find.byType(FloatingActionButton)); await tester.pump(); expect(find.text('Root 0'), findsOneWidget); expect(find.text('Root 1'), findsOneWidget); expect(find.text('Child 1:0'), findsOneWidget); expect(find.text('Child 1:1'), findsOneWidget); // Child node was added expect(find.text('Added child'), findsOneWidget); expect(find.text('Root 2'), findsOneWidget); expect(find.text('Child 2:0'), findsNothing); expect(find.text('Child 2:1'), findsNothing); expect(find.text('Root 3'), findsOneWidget); }); testWidgets( 'TreeSliverNode should close all children when collapsed when animation is disabled', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/153889 final controller = TreeSliverController(); final tree = >[ TreeSliverNode('First'), TreeSliverNode( 'Second', children: >[ TreeSliverNode( 'alpha', children: >[ TreeSliverNode('uno'), TreeSliverNode('dos'), TreeSliverNode('tres'), ], ), TreeSliverNode('beta'), TreeSliverNode('kappa'), ], ), TreeSliverNode( 'Third', expanded: true, children: >[ TreeSliverNode('gamma'), TreeSliverNode('delta'), TreeSliverNode('epsilon'), ], ), TreeSliverNode('Fourth'), ]; await tester.pumpWidget( MaterialApp( home: CustomScrollView( slivers: [ TreeSliver( tree: tree, controller: controller, toggleAnimationStyle: AnimationStyle.noAnimation, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle animationStyle, ) { final Widget child = GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => controller.toggleNode(node), child: TreeSliver.defaultTreeNodeBuilder(context, node, animationStyle), ); return child; }, ), ], ), ), ); expect(find.text('First'), findsOneWidget); expect(find.text('Second'), findsOneWidget); expect(find.text('Third'), findsOneWidget); expect(find.text('Fourth'), findsOneWidget); expect(find.text('alpha'), findsNothing); expect(find.text('beta'), findsNothing); expect(find.text('kappa'), findsNothing); expect(find.text('gamma'), findsOneWidget); expect(find.text('delta'), findsOneWidget); expect(find.text('epsilon'), findsOneWidget); expect(find.text('uno'), findsNothing); expect(find.text('dos'), findsNothing); expect(find.text('tres'), findsNothing); await tester.tap(find.text('Second')); await tester.pumpAndSettle(); expect(find.text('alpha'), findsOneWidget); await tester.tap(find.text('alpha')); await tester.pumpAndSettle(); expect(find.text('uno'), findsOneWidget); expect(find.text('dos'), findsOneWidget); expect(find.text('tres'), findsOneWidget); await tester.tap(find.text('alpha')); await tester.pumpAndSettle(); expect(find.text('uno'), findsNothing); expect(find.text('dos'), findsNothing); expect(find.text('tres'), findsNothing); }, ); testWidgets( 'TreeSliverNode should close all children when collapsed when animation is completed', (WidgetTester tester) async { final controller = TreeSliverController(); final tree = >[ TreeSliverNode( 'First', expanded: true, children: >[ TreeSliverNode( 'alpha', expanded: true, children: >[ TreeSliverNode('uno'), TreeSliverNode('dos'), TreeSliverNode('tres'), ], ), TreeSliverNode('beta'), TreeSliverNode('kappa'), ], ), ]; Widget buildTreeSliver(TreeSliverController controller) { return MaterialApp( home: Scaffold( body: CustomScrollView( shrinkWrap: true, slivers: [ TreeSliver( tree: tree, controller: controller, toggleAnimationStyle: const AnimationStyle( curve: Curves.easeInOut, duration: Duration(milliseconds: 200), ), treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle animationStyle, ) { final Widget child = GestureDetector( key: ValueKey(node.content! as String), behavior: HitTestBehavior.translucent, onTap: () => controller.toggleNode(node), child: TreeSliver.defaultTreeNodeBuilder(context, node, animationStyle), ); return child; }, ), ], ), ), ); } await tester.pumpWidget(buildTreeSliver(controller)); expect(find.text('alpha'), findsOneWidget); expect(find.text('uno'), findsOneWidget); expect(find.text('dos'), findsOneWidget); expect(find.text('tres'), findsOneWidget); // Using runAsync to handle collapse and animations properly. await tester.runAsync(() async { await tester.tap(find.text('alpha')); await tester.pumpAndSettle(); expect(find.text('uno'), findsNothing); expect(find.text('dos'), findsNothing); expect(find.text('tres'), findsNothing); }); }, ); testWidgets('TreeSliver and PinnedHeaderSliver can render correctly when used together.', ( WidgetTester tester, ) async { const key = ValueKey('sliver_tree_pined_header'); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: RepaintBoundary( key: key, child: SizedBox( height: 20, width: 20, child: CustomScrollView( slivers: [ const PinnedHeaderSliver(child: SizedBox(height: 10)), TreeSliver( tree: >[TreeSliverNode(Object())], treeRowExtentBuilder: (_, _) => 10, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle animationStyle, ) { return const ColoredBox(color: Colors.red); }, ), ], ), ), ), ), ), ); await expectLater(find.byKey(key), matchesGoldenFile('sliver_tree.pined_header.0.png')); expect(tester.getTopLeft(find.byType(ColoredBox)), const Offset(0, 10)); }); testWidgets('The child node positions of TreeSliver are correct.', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: CustomScrollView( slivers: [ TreeSliver( indentation: TreeSliverIndentationType.custom(20), tree: >[ TreeSliverNode( const ValueKey(0), expanded: true, children: >[TreeSliverNode(const ValueKey(1))], ), ], treeRowExtentBuilder: (_, _) => 20, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle animationStyle, ) { return Container(key: node.content! as Key); }, ), ], ), ), ); expect(tester.getTopLeft(find.byKey(const ValueKey(1))), const Offset(20, 20)); }); testWidgets('TreeSliver renders correctly after scrolling.', (WidgetTester tester) async { const key = ValueKey('sliver_scrolling'); final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Align( alignment: Alignment.topLeft, child: RepaintBoundary( key: key, child: SizedBox( height: 20, width: 20, child: CustomScrollView( controller: scrollController, slivers: [ TreeSliver( tree: >[TreeSliverNode(Object())], treeRowExtentBuilder: (_, _) => 10, treeNodeBuilder: ( BuildContext context, TreeSliverNode node, AnimationStyle animationStyle, ) { return const ColoredBox(color: Colors.red); }, ), const SliverToBoxAdapter(child: SizedBox(height: 20)), ], ), ), ), ), ), ); scrollController.jumpTo(5); await tester.pumpAndSettle(); await expectLater(find.byKey(key), matchesGoldenFile('sliver_tree.scrolling.1.png')); expect(tester.getTopLeft(find.byType(ColoredBox)), const Offset(0, -5)); }); }