Unverified Commit ff73448f authored by chunhtai's avatar chunhtai Committed by GitHub

Reland "Adds a parent scope TraversalEdgeBehavior and fixes modal rou… (#134554)

…… (#134550)"

fixes https://github.com/flutter/flutter/issues/112567

This reverts commit 5900c4ba.

The internal test needs migration. cl/564746935

This is the same of original pr, no new change

## 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].
- [ ] All existing and new tests are passing.

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

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#overview
[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene
[test-exempt]:
https://github.com/flutter/flutter/wiki/Tree-hygiene#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#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/wiki/Tree-hygiene#handling-breaking-changes
[Discord]: https://github.com/flutter/flutter/wiki/Chat
parent 51f1a464
...@@ -1683,6 +1683,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1683,6 +1683,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
}, },
onUnknownRoute: _onUnknownRoute, onUnknownRoute: _onUnknownRoute,
observers: widget.navigatorObservers!, observers: widget.navigatorObservers!,
routeTraversalEdgeBehavior: kIsWeb ? TraversalEdgeBehavior.leaveFlutterView : TraversalEdgeBehavior.parentScope,
reportsRouteUpdateToEngine: true, reportsRouteUpdateToEngine: true,
), ),
); );
......
...@@ -120,6 +120,16 @@ enum TraversalEdgeBehavior { ...@@ -120,6 +120,16 @@ enum TraversalEdgeBehavior {
/// address bar, escape an `iframe`, or focus on HTML elements other than /// address bar, escape an `iframe`, or focus on HTML elements other than
/// those managed by Flutter. /// those managed by Flutter.
leaveFlutterView, leaveFlutterView,
/// Allows focus to traverse up to parent scope.
///
/// When reaching the edge of the current scope, requesting the next focus
/// will look up to the parent scope of the current scope and focus the focus
/// node next to the current scope.
///
/// If there is no parent scope above the current scope, fallback to
/// [closedLoop] behavior.
parentScope,
} }
/// Determines how focusable widgets are traversed within a [FocusTraversalGroup]. /// Determines how focusable widgets are traversed within a [FocusTraversalGroup].
...@@ -186,6 +196,60 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -186,6 +196,60 @@ abstract class FocusTraversalPolicy with Diagnosticable {
); );
} }
/// Request focus on a focus node as a result of a tab traversal.
///
/// If the `node` is a [FocusScopeNode], this method will recursively find
/// the next focus from its descendants until it find a regular [FocusNode].
///
/// Returns true if this method focused a new focus node.
bool _requestTabTraversalFocus(
FocusNode node, {
ScrollPositionAlignmentPolicy? alignmentPolicy,
double? alignment,
Duration? duration,
Curve? curve,
required bool forward,
}) {
if (node is FocusScopeNode) {
if (node.focusedChild != null) {
// Can't stop here as the `focusedChild` may be a focus scope node
// without a first focus. The first focus will be picked in the
// next iteration.
return _requestTabTraversalFocus(
node.focusedChild!,
alignmentPolicy: alignmentPolicy,
alignment: alignment,
duration: duration,
curve: curve,
forward: forward,
);
}
final List<FocusNode> sortedChildren = _sortAllDescendants(node, node);
if (sortedChildren.isNotEmpty) {
_requestTabTraversalFocus(
forward ? sortedChildren.first : sortedChildren.last,
alignmentPolicy: alignmentPolicy,
alignment: alignment,
duration: duration,
curve: curve,
forward: forward,
);
// Regardless if _requestTabTraversalFocus return true or false, a first
// focus has been picked.
return true;
}
}
final bool nodeHadPrimaryFocus = node.hasPrimaryFocus;
requestFocusCallback(
node,
alignmentPolicy: alignmentPolicy,
alignment: alignment,
duration: duration,
curve: curve,
);
return !nodeHadPrimaryFocus;
}
/// Returns the node that should receive focus if focus is traversing /// Returns the node that should receive focus if focus is traversing
/// forwards, and there is no current focus. /// forwards, and there is no current focus.
/// ///
...@@ -340,10 +404,21 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -340,10 +404,21 @@ abstract class FocusTraversalPolicy with Diagnosticable {
@protected @protected
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode); Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) { static Iterable<FocusNode> _getDescendantsWithoutExpandingScope(FocusNode node) {
final List<FocusNode> result = <FocusNode>[];
for (final FocusNode child in node.children) {
if (child is! FocusScopeNode) {
result.addAll(_getDescendantsWithoutExpandingScope(child));
}
result.add(child);
}
return result;
}
static Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) {
final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy(); final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{}; final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
for (final FocusNode node in scope.descendants) { for (final FocusNode node in _getDescendantsWithoutExpandingScope(scope)) {
final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node); final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node);
// Group nodes need to be added to their parent's node, or to the "null" // Group nodes need to be added to their parent's node, or to the "null"
// node if no parent is found. This creates the hierarchy of group nodes // node if no parent is found. This creates the hierarchy of group nodes
...@@ -376,7 +451,7 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -376,7 +451,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
// Sort all descendants, taking into account the FocusTraversalGroup // Sort all descendants, taking into account the FocusTraversalGroup
// that they are each in, and filtering out non-traversable/focusable nodes. // that they are each in, and filtering out non-traversable/focusable nodes.
List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) { static List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope); final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope);
// Build the sorting data structure, separating descendants into groups. // Build the sorting data structure, separating descendants into groups.
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode, currentNode); final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode, currentNode);
...@@ -463,30 +538,42 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -463,30 +538,42 @@ abstract class FocusTraversalPolicy with Diagnosticable {
if (focusedChild == null) { if (focusedChild == null) {
final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode); final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode);
if (firstFocus != null) { if (firstFocus != null) {
requestFocusCallback( return _requestTabTraversalFocus(
firstFocus, firstFocus,
alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart, alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
forward: forward,
); );
return true;
} }
} }
focusedChild ??= nearestScope; focusedChild ??= nearestScope;
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, focusedChild); final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, focusedChild);
assert(sortedNodes.contains(focusedChild)); assert(sortedNodes.contains(focusedChild));
if (sortedNodes.length < 2) {
// If there are no nodes to traverse to, like when descendantsAreTraversable
// is false or skipTraversal for all the nodes is true.
return false;
}
if (forward && focusedChild == sortedNodes.last) { if (forward && focusedChild == sortedNodes.last) {
switch (nearestScope.traversalEdgeBehavior) { switch (nearestScope.traversalEdgeBehavior) {
case TraversalEdgeBehavior.leaveFlutterView: case TraversalEdgeBehavior.leaveFlutterView:
focusedChild.unfocus(); focusedChild.unfocus();
return false; return false;
case TraversalEdgeBehavior.parentScope:
final FocusScopeNode? parentScope = nearestScope.enclosingScope;
if (parentScope != null && parentScope != FocusManager.instance.rootScope) {
focusedChild.unfocus();
parentScope.nextFocus();
// Verify the focus really has changed.
return focusedChild.enclosingScope?.focusedChild != focusedChild;
}
// No valid parent scope. Fallback to closed loop behavior.
return _requestTabTraversalFocus(
sortedNodes.first,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
forward: forward,
);
case TraversalEdgeBehavior.closedLoop: case TraversalEdgeBehavior.closedLoop:
requestFocusCallback(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd); return _requestTabTraversalFocus(
return true; sortedNodes.first,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
forward: forward,
);
} }
} }
if (!forward && focusedChild == sortedNodes.first) { if (!forward && focusedChild == sortedNodes.first) {
...@@ -494,9 +581,26 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -494,9 +581,26 @@ abstract class FocusTraversalPolicy with Diagnosticable {
case TraversalEdgeBehavior.leaveFlutterView: case TraversalEdgeBehavior.leaveFlutterView:
focusedChild.unfocus(); focusedChild.unfocus();
return false; return false;
case TraversalEdgeBehavior.parentScope:
final FocusScopeNode? parentScope = nearestScope.enclosingScope;
if (parentScope != null && parentScope != FocusManager.instance.rootScope) {
focusedChild.unfocus();
parentScope.previousFocus();
// Verify the focus really has changed.
return focusedChild.enclosingScope?.focusedChild != focusedChild;
}
// No valid parent scope. Fallback to closed loop behavior.
return _requestTabTraversalFocus(
sortedNodes.last,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
forward: forward,
);
case TraversalEdgeBehavior.closedLoop: case TraversalEdgeBehavior.closedLoop:
requestFocusCallback(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart); return _requestTabTraversalFocus(
return true; sortedNodes.last,
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
forward: forward,
);
} }
} }
...@@ -504,11 +608,11 @@ abstract class FocusTraversalPolicy with Diagnosticable { ...@@ -504,11 +608,11 @@ abstract class FocusTraversalPolicy with Diagnosticable {
FocusNode? previousNode; FocusNode? previousNode;
for (final FocusNode node in maybeFlipped) { for (final FocusNode node in maybeFlipped) {
if (previousNode == focusedChild) { if (previousNode == focusedChild) {
requestFocusCallback( return _requestTabTraversalFocus(
node, node,
alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart, alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
forward: forward,
); );
return true;
} }
previousNode = node; previousNode = node;
} }
......
...@@ -1144,9 +1144,7 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> { ...@@ -1144,9 +1144,7 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
/// The default value of [Navigator.routeTraversalEdgeBehavior]. /// The default value of [Navigator.routeTraversalEdgeBehavior].
/// ///
/// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior} /// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior}
const TraversalEdgeBehavior kDefaultRouteTraversalEdgeBehavior = kIsWeb const TraversalEdgeBehavior kDefaultRouteTraversalEdgeBehavior = TraversalEdgeBehavior.parentScope;
? TraversalEdgeBehavior.leaveFlutterView
: TraversalEdgeBehavior.closedLoop;
/// A widget that manages a set of child widgets with a stack discipline. /// A widget that manages a set of child widgets with a stack discipline.
/// ///
......
...@@ -834,7 +834,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -834,7 +834,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
late Listenable _listenable; late Listenable _listenable;
/// The node this scope will use for its root [FocusScope] widget. /// The node this scope will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope'); final FocusScopeNode focusScopeNode = FocusScopeNode(
debugLabel: '$_ModalScopeState Focus Scope',
);
final ScrollController primaryScrollController = ScrollController(); final ScrollController primaryScrollController = ScrollController();
@override @override
...@@ -936,6 +938,8 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -936,6 +938,8 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
controller: primaryScrollController, controller: primaryScrollController,
child: FocusScope( child: FocusScope(
node: focusScopeNode, // immutable node: focusScopeNode, // immutable
// Only top most route can participate in focus traversal.
skipTraversal: !widget.route.isCurrent,
child: RepaintBoundary( child: RepaintBoundary(
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _listenable, // immutable animation: _listenable, // immutable
...@@ -1704,11 +1708,26 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1704,11 +1708,26 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
changedInternalState(); changedInternalState();
} }
@override
void didChangeNext(Route<dynamic>? nextRoute) {
super.didChangeNext(nextRoute);
changedInternalState();
}
@override
void didPopNext(Route<dynamic> nextRoute) {
super.didPopNext(nextRoute);
changedInternalState();
}
@override @override
void changedInternalState() { void changedInternalState() {
super.changedInternalState(); super.changedInternalState();
setState(() { /* internal state already changed */ }); // No need to mark dirty if this method is called during build phase.
_modalBarrier.markNeedsBuild(); if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
setState(() { /* internal state already changed */ });
_modalBarrier.markNeedsBuild();
}
_modalScope.maintainState = maintainState; _modalScope.maintainState = maintainState;
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -792,6 +793,10 @@ void main() { ...@@ -792,6 +793,10 @@ void main() {
testWidgetsWithLeakTracking("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async { testWidgetsWithLeakTracking("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'IconButton 1'); final FocusNode focusNode1 = FocusNode(debugLabel: 'IconButton 1');
final FocusNode focusNode2 = FocusNode(debugLabel: 'IconButton 2'); final FocusNode focusNode2 = FocusNode(debugLabel: 'IconButton 2');
addTearDown(() {
focusNode1.dispose();
focusNode2.dispose();
});
await tester.pumpWidget( await tester.pumpWidget(
wrap( wrap(
...@@ -821,11 +826,8 @@ void main() { ...@@ -821,11 +826,8 @@ void main() {
expect(focusNode1.nextFocus(), isFalse); expect(focusNode1.nextFocus(), isFalse);
await tester.pump(); await tester.pump();
expect(focusNode1.hasPrimaryFocus, isTrue); expect(focusNode1.hasPrimaryFocus, !kIsWeb);
expect(focusNode2.hasPrimaryFocus, isFalse); expect(focusNode2.hasPrimaryFocus, isFalse);
focusNode1.dispose();
focusNode2.dispose();
}); });
group('feedback', () { group('feedback', () {
......
...@@ -981,7 +981,7 @@ void main() { ...@@ -981,7 +981,7 @@ void main() {
expect(buttonNode2.hasFocus, isFalse); expect(buttonNode2.hasFocus, isFalse);
primaryFocus!.nextFocus(); primaryFocus!.nextFocus();
await tester.pump(); await tester.pump();
expect(buttonNode1.hasFocus, isTrue); expect(buttonNode1.hasFocus, isFalse);
expect(buttonNode2.hasFocus, isFalse); expect(buttonNode2.hasFocus, isFalse);
}, },
); );
......
...@@ -441,6 +441,96 @@ void main() { ...@@ -441,6 +441,96 @@ void main() {
}); });
testWidgetsWithLeakTracking('Nested navigator does not trap focus', (WidgetTester tester) async {
final FocusNode node1 = FocusNode();
addTearDown(node1.dispose);
final FocusNode node2 = FocusNode();
addTearDown(node2.dispose);
final FocusNode node3 = FocusNode();
addTearDown(node3.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: FocusScope(
child: Column(
children: <Widget>[
Focus(
focusNode: node1,
child: const SizedBox(width: 100, height: 100),
),
SizedBox(
width: 100,
height: 100,
child: Navigator(
pages: <Page<void>>[
MaterialPage<void>(
child: Focus(
focusNode: node2,
child: const SizedBox(width: 100, height: 100),
),
),
],
onPopPage: (_, __) => false,
),
),
Focus(
focusNode: node3,
child: const SizedBox(width: 100, height: 100),
),
],
),
),
),
),
);
node1.requestFocus();
await tester.pump();
expect(node1.hasFocus, isTrue);
expect(node2.hasFocus, isFalse);
expect(node3.hasFocus, isFalse);
node1.nextFocus();
await tester.pump();
expect(node1.hasFocus, isFalse);
expect(node2.hasFocus, isTrue);
expect(node3.hasFocus, isFalse);
node2.nextFocus();
await tester.pump();
expect(node1.hasFocus, isFalse);
expect(node2.hasFocus, isFalse);
expect(node3.hasFocus, isTrue);
node3.nextFocus();
await tester.pump();
expect(node1.hasFocus, isTrue);
expect(node2.hasFocus, isFalse);
expect(node3.hasFocus, isFalse);
node1.previousFocus();
await tester.pump();
expect(node1.hasFocus, isFalse);
expect(node2.hasFocus, isFalse);
expect(node3.hasFocus, isTrue);
node3.previousFocus();
await tester.pump();
expect(node1.hasFocus, isFalse);
expect(node2.hasFocus, isTrue);
expect(node3.hasFocus, isFalse);
node2.previousFocus();
await tester.pump();
expect(node1.hasFocus, isTrue);
expect(node2.hasFocus, isFalse);
expect(node3.hasFocus, isFalse);
});
group(ReadingOrderTraversalPolicy, () { group(ReadingOrderTraversalPolicy, () {
testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async { testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
......
...@@ -1489,29 +1489,34 @@ void main() { ...@@ -1489,29 +1489,34 @@ void main() {
return result; return result;
}, },
)); ));
expect(log, <String>['building page 1 - false']); final List<String> expected = <String>['building page 1 - false'];
expect(log, expected);
key.currentState!.pushReplacement(PageRouteBuilder<int>( key.currentState!.pushReplacement(PageRouteBuilder<int>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
log.add('building page 2 - ${ModalRoute.of(context)!.canPop}'); log.add('building page 2 - ${ModalRoute.of(context)!.canPop}');
return const Placeholder(); return const Placeholder();
}, },
)); ));
expect(log, <String>['building page 1 - false']); expect(log, expected);
await tester.pump(); await tester.pump();
expect(log, <String>['building page 1 - false', 'building page 2 - false']); expected.add('building page 2 - false');
expected.add('building page 1 - false'); // page 1 is rebuilt again because isCurrent changed.
expect(log, expected);
await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150));
expect(log, <String>['building page 1 - false', 'building page 2 - false']); expect(log, expected);
key.currentState!.pushReplacement(PageRouteBuilder<int>( key.currentState!.pushReplacement(PageRouteBuilder<int>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
log.add('building page 3 - ${ModalRoute.of(context)!.canPop}'); log.add('building page 3 - ${ModalRoute.of(context)!.canPop}');
return const Placeholder(); return const Placeholder();
}, },
)); ));
expect(log, <String>['building page 1 - false', 'building page 2 - false']); expect(log, expected);
await tester.pump(); await tester.pump();
expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']); expected.add('building page 3 - false');
expected.add('building page 2 - false'); // page 2 is rebuilt again because isCurrent changed.
expect(log, expected);
await tester.pump(const Duration(milliseconds: 200)); await tester.pump(const Duration(milliseconds: 200));
expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']); expect(log, expected);
}); });
testWidgetsWithLeakTracking('route semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('route semantics', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment