Skip to content

Commit c4bd450

Browse files
authored
Let the framework toggle between single- and multi-entry histories (flutter#26164)
1 parent 401670d commit c4bd450

File tree

4 files changed

+62
-48
lines changed

4 files changed

+62
-48
lines changed

lib/web_ui/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
To run tests, use `dev/felt test`. See dev/README.md for details.

lib/web_ui/lib/src/engine/window.dart

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
5656
return urlStrategy;
5757
}
5858

59-
BrowserHistory? _browserHistory;
60-
bool _usingRouter = false;
59+
BrowserHistory? _browserHistory; // Must be either SingleEntryBrowserHistory or MultiEntriesBrowserHistory.
60+
6161
Future<void> _useSingleEntryBrowserHistory() async {
6262
if (_browserHistory is SingleEntryBrowserHistory) {
6363
return;
@@ -95,7 +95,6 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
9595
}) async {
9696
// Prevent any further customization of URL strategy.
9797
_isUrlStrategySet = true;
98-
_usingRouter = false;
9998
await _browserHistory?.tearDown();
10099
if (useSingle) {
101100
_browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy);
@@ -108,35 +107,25 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
108107
await _browserHistory?.tearDown();
109108
_browserHistory = null;
110109
// Reset the globals too.
111-
_usingRouter = false;
112110
_isUrlStrategySet = false;
113111
_customUrlStrategy = null;
114112
}
115113

116-
Future<bool> handleNavigationMessage(
117-
ByteData? data,
118-
) async {
114+
Future<bool> handleNavigationMessage(ByteData? data) async {
119115
final MethodCall decoded = JSONMethodCodec().decodeMethodCall(data);
120116
final Map<String, dynamic> arguments = decoded.arguments;
121-
122117
switch (decoded.method) {
123-
case 'routeUpdated':
124-
if (!_usingRouter) {
125-
await _useSingleEntryBrowserHistory();
126-
browserHistory.setRouteName(arguments['routeName']);
127-
} else {
128-
assert(
129-
false,
130-
'Receives old navigator update in a router application. '
131-
'This can happen if you use non-router versions of MaterialApp/'
132-
'CupertinoApp/WidgetsApp together with the router versions of them.'
133-
);
134-
return false;
135-
}
118+
case 'selectMultiEntryHistory':
119+
await _useMultiEntryBrowserHistory();
120+
return true;
121+
case 'selectSingleEntryHistory':
122+
await _useSingleEntryBrowserHistory();
123+
return true;
124+
case 'routeUpdated': // deprecated
125+
await _useSingleEntryBrowserHistory();
126+
browserHistory.setRouteName(arguments['routeName']);
136127
return true;
137128
case 'routeInformationUpdated':
138-
await _useMultiEntryBrowserHistory();
139-
_usingRouter = true;
140129
browserHistory.setRouteName(
141130
arguments['location'],
142131
state: arguments['state'],

lib/web_ui/test/engine/surface/scene_builder_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,6 @@ void testMain() {
495495
// Renders a `string` by breaking it up into individual characters and
496496
// rendering each character into its own layer.
497497
Future<void> testCase(String string, String description, { int deletions = 0, int additions = 0, int moves = 0 }) {
498-
print('Testing "$string" - $description');
499498
final Set<html.Node> actualDeletions = <html.Node>{};
500499
final Set<html.Node> actualAdditions = <html.Node>{};
501500

lib/web_ui/test/window_test.dart

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,15 @@ void testMain() {
4444
expect(window.defaultRouteName, '/initial');
4545
});
4646

47+
// window.defaultRouteName is now permanently decoupled from the history,
48+
// even in subsequent tests, because the PlatformDispatcher caches it.
49+
4750
test('window.defaultRouteName should reset after navigation platform message',
4851
() async {
4952
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
50-
TestHistoryEntry('initial state', null, '/initial'),
53+
// The URL here does not set the PlatformDispatcher's defaultRouteName,
54+
// since it got cached as soon as we read it above.
55+
TestHistoryEntry('initial state', null, '/not-really-inital/THIS_IS_IGNORED'),
5156
), useSingle: true);
5257
// Reading it multiple times should return the same value.
5358
expect(window.defaultRouteName, '/initial');
@@ -62,17 +67,48 @@ void testMain() {
6267
)),
6368
(_) { callback.complete(); },
6469
);
65-
// After a navigation platform message, [window.defaultRouteName] should
66-
// reset to "/".
70+
await callback.future;
71+
// After a navigation platform message, the PlatformDispatcher's
72+
// defaultRouteName resets to "/".
6773
expect(window.defaultRouteName, '/');
6874
});
6975

70-
test('should throw when using nav1 and nav2 together',
76+
// window.defaultRouteName is now '/'.
77+
78+
test('can switch history mode', () async {
79+
Completer<void> callback;
80+
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
81+
TestHistoryEntry('initial state', null, '/initial'),
82+
), useSingle: false);
83+
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
84+
85+
Future<void> check<T>(String method, Map<String, Object?> arguments) async {
86+
callback = Completer<void>();
87+
window.sendPlatformMessage(
88+
'flutter/navigation',
89+
JSONMethodCodec().encodeMethodCall(MethodCall(method, arguments)),
90+
(_) { callback.complete(); },
91+
);
92+
await callback.future;
93+
expect(window.browserHistory, isA<T>());
94+
}
95+
96+
await check<SingleEntryBrowserHistory>('selectSingleEntryHistory', <String, dynamic>{}); // -> single
97+
await check<MultiEntriesBrowserHistory>('selectMultiEntryHistory', <String, dynamic>{}); // -> multi
98+
await check<SingleEntryBrowserHistory>('routeUpdated', <String, dynamic>{'routeName': '/bar'}); // -> single
99+
await check<SingleEntryBrowserHistory>('routeInformationUpdated', <String, dynamic>{'location': '/bar'}); // does not change mode
100+
await check<MultiEntriesBrowserHistory>('selectMultiEntryHistory', <String, dynamic>{}); // -> multi
101+
await check<MultiEntriesBrowserHistory>('routeInformationUpdated', <String, dynamic>{'location': '/bar'}); // does not change mode
102+
});
103+
104+
test('should not throw when using nav1 and nav2 together',
71105
() async {
72106
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
73107
TestHistoryEntry('initial state', null, '/initial'),
74108
), useSingle: false);
75-
// Receive nav1 update first.
109+
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
110+
111+
// routeUpdated resets the history type
76112
Completer<void> callback = Completer<void>();
77113
window.sendPlatformMessage(
78114
'flutter/navigation',
@@ -83,10 +119,10 @@ void testMain() {
83119
(_) { callback.complete(); },
84120
);
85121
await callback.future;
86-
expect(window.browserHistory is SingleEntryBrowserHistory, true);
122+
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
87123
expect(window.browserHistory.urlStrategy!.getPath(), '/bar');
88124

89-
// We can still receive nav2 update.
125+
// routeInformationUpdated does not
90126
callback = Completer<void>();
91127
window.sendPlatformMessage(
92128
'flutter/navigation',
@@ -100,29 +136,18 @@ void testMain() {
100136
(_) { callback.complete(); },
101137
);
102138
await callback.future;
103-
expect(window.browserHistory is MultiEntriesBrowserHistory, true);
139+
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
104140
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
105141

106-
// Throws assertion error if it receives nav1 update after nav2 update.
107-
late AssertionError caughtAssertion;
142+
// they can be interleaved safely
108143
await window.handleNavigationMessage(
109144
JSONMethodCodec().encodeMethodCall(MethodCall(
110145
'routeUpdated',
111146
<String, dynamic>{'routeName': '/foo'},
112147
))
113-
).catchError((Object e) {
114-
caughtAssertion = e as AssertionError;
115-
});
116-
117-
expect(
118-
caughtAssertion.message,
119-
'Receives old navigator update in a router application. This can '
120-
'happen if you use non-router versions of '
121-
'MaterialApp/CupertinoApp/WidgetsApp together with the router versions of them.'
122148
);
123-
// The history does not change.
124-
expect(window.browserHistory is MultiEntriesBrowserHistory, true);
125-
expect(window.browserHistory.urlStrategy!.getPath(), '/baz');
149+
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
150+
expect(window.browserHistory.urlStrategy!.getPath(), '/foo');
126151
});
127152

128153
test('initialize browser history with default url strategy (single)', () async {
@@ -143,7 +168,7 @@ void testMain() {
143168
(_) { callback.complete(); },
144169
);
145170
await callback.future;
146-
expect(window.browserHistory is SingleEntryBrowserHistory, true);
171+
expect(window.browserHistory, isA<SingleEntryBrowserHistory>());
147172
// The url strategy should've been set to the default, and the path
148173
// should've been correctly set to "/bar".
149174
expect(window.browserHistory.urlStrategy, isNot(isNull));
@@ -171,7 +196,7 @@ void testMain() {
171196
(_) { callback.complete(); },
172197
);
173198
await callback.future;
174-
expect(window.browserHistory is MultiEntriesBrowserHistory, true);
199+
expect(window.browserHistory, isA<MultiEntriesBrowserHistory>());
175200
// The url strategy should've been set to the default, and the path
176201
// should've been correctly set to "/baz".
177202
expect(window.browserHistory.urlStrategy, isNot(isNull));

0 commit comments

Comments
 (0)