flutter_flutter/packages/flutter/test/widgets/sliver_tree_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

980 lines
34 KiB
Dart

// 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(<String>['reduced-test-set'])
library;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
List<TreeSliverNode<String>> simpleNodeSet = <TreeSliverNode<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
void main() {
group('TreeSliverNode', () {
setUp(() {
// Reset node conditions for each test.
simpleNodeSet = <TreeSliverNode<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
});
test('getters, toString', () {
final children = <TreeSliverNode<String>>[TreeSliverNode<String>('child')];
final node = TreeSliverNode<String>('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<String>>[TreeSliverNode<String>('child')];
final node = TreeSliverNode<String>('parent', children: children, expanded: true);
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(tree: <TreeSliverNode<String>>[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<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
});
testWidgets('Can set controller on TreeSliver', (WidgetTester tester) async {
final controller = TreeSliverController();
TreeSliverController? returnedController;
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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: <Widget>[TreeSliver<String>(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: <Widget>[TreeSliver<String>(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: <Widget>[TreeSliver<String>(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: <Widget>[TreeSliver<String>(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: <Widget>[TreeSliver<String>(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: <Widget>[TreeSliver<String>(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<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
final controller = TreeSliverController();
// The default node builder wraps the leading icon with toggleNodeWith.
var toggled = false;
TreeSliverNode<String>? toggledNode;
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
onNodeToggle: (TreeSliverNode<Object?> node) {
toggled = true;
toggledNode = node as TreeSliverNode<String>;
},
),
],
),
),
);
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: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
controller: controller,
onNodeToggle: (TreeSliverNode<Object?> node) {
toggled = true;
toggledNode = node as TreeSliverNode<String>;
},
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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: <Widget>[
// 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<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
AnimationStyle? style;
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle toggleAnimationStyle,
) {
style ??= toggleAnimationStyle;
return Text(node.content.toString());
},
),
],
),
),
);
// Default
expect(style, TreeSliver.defaultToggleAnimationStyle);
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
toggleAnimationStyle: AnimationStyle.noAnimation,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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: <Widget>[
TreeSliver<String>(
tree: simpleNodeSet,
toggleAnimationStyle: const AnimationStyle(
curve: Curves.easeIn,
duration: Duration(milliseconds: 200),
),
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
final controller = TreeSliverController();
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
simpleNodeSet.add(TreeSliverNode<String>('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<String>>[
TreeSliverNode<String>('Root 0'),
TreeSliverNode<String>(
'Root 1',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 1:0'),
TreeSliverNode<String>('Child 1:1'),
],
),
TreeSliverNode<String>(
'Root 2',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('Child 2:0'),
TreeSliverNode<String>('Child 2:1'),
],
),
TreeSliverNode<String>('Root 3'),
];
final controller = TreeSliverController();
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[TreeSliver<String>(tree: simpleNodeSet, controller: controller)],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
simpleNodeSet[1].children.add(TreeSliverNode<String>('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<String>>[
TreeSliverNode<String>('First'),
TreeSliverNode<String>(
'Second',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'alpha',
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('uno'),
TreeSliverNode<String>('dos'),
TreeSliverNode<String>('tres'),
],
),
TreeSliverNode<String>('beta'),
TreeSliverNode<String>('kappa'),
],
),
TreeSliverNode<String>(
'Third',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('gamma'),
TreeSliverNode<String>('delta'),
TreeSliverNode<String>('epsilon'),
],
),
TreeSliverNode<String>('Fourth'),
];
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
TreeSliver<String>(
tree: tree,
controller: controller,
toggleAnimationStyle: AnimationStyle.noAnimation,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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<String>>[
TreeSliverNode<String>(
'First',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>(
'alpha',
expanded: true,
children: <TreeSliverNode<String>>[
TreeSliverNode<String>('uno'),
TreeSliverNode<String>('dos'),
TreeSliverNode<String>('tres'),
],
),
TreeSliverNode<String>('beta'),
TreeSliverNode<String>('kappa'),
],
),
];
Widget buildTreeSliver(TreeSliverController controller) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
shrinkWrap: true,
slivers: <Widget>[
TreeSliver<String>(
tree: tree,
controller: controller,
toggleAnimationStyle: const AnimationStyle(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 200),
),
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle animationStyle,
) {
final Widget child = GestureDetector(
key: ValueKey<String>(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<String>('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: <Widget>[
const PinnedHeaderSliver(child: SizedBox(height: 10)),
TreeSliver<Object>(
tree: <TreeSliverNode<Object>>[TreeSliverNode<Object>(Object())],
treeRowExtentBuilder: (_, _) => 10,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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: <Widget>[
TreeSliver<Object>(
indentation: TreeSliverIndentationType.custom(20),
tree: <TreeSliverNode<Key>>[
TreeSliverNode<Key>(
const ValueKey<int>(0),
expanded: true,
children: <TreeSliverNode<Key>>[TreeSliverNode<Key>(const ValueKey<int>(1))],
),
],
treeRowExtentBuilder: (_, _) => 20,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> node,
AnimationStyle animationStyle,
) {
return Container(key: node.content! as Key);
},
),
],
),
),
);
expect(tester.getTopLeft(find.byKey(const ValueKey<int>(1))), const Offset(20, 20));
});
testWidgets('TreeSliver renders correctly after scrolling.', (WidgetTester tester) async {
const key = ValueKey<String>('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: <Widget>[
TreeSliver<Object>(
tree: <TreeSliverNode<Object>>[TreeSliverNode<Object>(Object())],
treeRowExtentBuilder: (_, _) => 10,
treeNodeBuilder:
(
BuildContext context,
TreeSliverNode<Object?> 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));
});
}