mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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
980 lines
34 KiB
Dart
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));
|
|
});
|
|
}
|