Skip to content

Commit b8e4533

Browse files
mdebbarNoamDev
authored andcommitted
[web] Tests for browser history implementation (flutter#15324)
1 parent d3332ea commit b8e4533

3 files changed

Lines changed: 353 additions & 12 deletions

File tree

lib/web_ui/lib/src/engine/test_embedding.dart

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ part of engine;
66

77
const bool _debugLogHistoryActions = false;
88

9-
class _HistoryEntry {
9+
class TestHistoryEntry {
1010
final dynamic state;
1111
final String title;
1212
final String url;
1313

14-
const _HistoryEntry(this.state, this.title, this.url);
14+
const TestHistoryEntry(this.state, this.title, this.url);
1515

1616
@override
1717
String toString() {
@@ -25,24 +25,30 @@ class _HistoryEntry {
2525
/// It keeps a list of history entries and event listeners in memory and
2626
/// manipulates them in order to achieve the desired functionality.
2727
class TestLocationStrategy extends LocationStrategy {
28-
/// Passing a [defaultRouteName] will make the app start at that route. The
29-
/// way it does it is by using it as a path on the first history entry.
30-
TestLocationStrategy([String defaultRouteName = ''])
28+
/// Creates a instance of [TestLocationStrategy] with an empty string as the
29+
/// path.
30+
factory TestLocationStrategy() => TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, ''));
31+
32+
/// Creates an instance of [TestLocationStrategy] and populates it with a list
33+
/// that has [initialEntry] as the only item.
34+
TestLocationStrategy.fromEntry(TestHistoryEntry initialEntry)
3135
: _currentEntryIndex = 0,
32-
history = <_HistoryEntry>[_HistoryEntry(null, null, defaultRouteName)];
36+
history = <TestHistoryEntry>[initialEntry];
3337

3438
@override
3539
String get path => ensureLeading(currentEntry.url, '/');
3640

3741
int _currentEntryIndex;
38-
final List<_HistoryEntry> history;
42+
int get currentEntryIndex => _currentEntryIndex;
43+
44+
final List<TestHistoryEntry> history;
3945

40-
_HistoryEntry get currentEntry {
46+
TestHistoryEntry get currentEntry {
4147
assert(withinAppHistory);
4248
return history[_currentEntryIndex];
4349
}
4450

45-
set currentEntry(_HistoryEntry entry) {
51+
set currentEntry(TestHistoryEntry entry) {
4652
assert(withinAppHistory);
4753
history[_currentEntryIndex] = entry;
4854
}
@@ -62,7 +68,7 @@ class TestLocationStrategy extends LocationStrategy {
6268
// If the user goes A -> B -> C -> D, then goes back to B and pushes a new
6369
// entry called E, we should end up with: A -> B -> E in the history list.
6470
history.removeRange(_currentEntryIndex, history.length);
65-
history.add(_HistoryEntry(state, title, url));
71+
history.add(TestHistoryEntry(state, title, url));
6672

6773
if (_debugLogHistoryActions) {
6874
print('$runtimeType.pushState(...) -> $this');
@@ -72,7 +78,10 @@ class TestLocationStrategy extends LocationStrategy {
7278
@override
7379
void replaceState(dynamic state, String title, String url) {
7480
assert(withinAppHistory);
75-
currentEntry = _HistoryEntry(state, title, url);
81+
if (url == null || url == '') {
82+
url = currentEntry.url;
83+
}
84+
currentEntry = TestHistoryEntry(state, title, url);
7685

7786
if (_debugLogHistoryActions) {
7887
print('$runtimeType.replaceState(...) -> $this');
@@ -149,7 +158,7 @@ class TestLocationStrategy extends LocationStrategy {
149158
String toString() {
150159
final List<String> lines = List<String>(history.length);
151160
for (int i = 0; i < history.length; i++) {
152-
final _HistoryEntry entry = history[i];
161+
final TestHistoryEntry entry = history[i];
153162
lines[i] = _currentEntryIndex == i ? '* $entry' : ' $entry';
154163
}
155164
return '$runtimeType: [\n${lines.join('\n')}\n]';
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:typed_data';
7+
8+
import 'package:test/test.dart';
9+
import 'package:ui/src/engine.dart';
10+
11+
import '../spy.dart';
12+
13+
TestLocationStrategy _strategy;
14+
TestLocationStrategy get strategy => _strategy;
15+
set strategy(TestLocationStrategy newStrategy) {
16+
window.locationStrategy = _strategy = newStrategy;
17+
}
18+
19+
const Map<String, bool> originState = <String, bool>{'origin': true};
20+
const Map<String, bool> flutterState = <String, bool>{'flutter': true};
21+
22+
const MethodCodec codec = JSONMethodCodec();
23+
24+
void emptyCallback(ByteData date) {}
25+
26+
void main() {
27+
group('BrowserHistory', () {
28+
final PlatformMessagesSpy spy = PlatformMessagesSpy();
29+
30+
setUp(() {
31+
spy.setUp();
32+
});
33+
34+
tearDown(() {
35+
spy.tearDown();
36+
strategy = null;
37+
});
38+
39+
test('basic setup works', () {
40+
strategy = TestLocationStrategy.fromEntry(
41+
TestHistoryEntry('initial state', null, '/initial'));
42+
43+
// There should be two entries: origin and flutter.
44+
expect(strategy.history, hasLength(2));
45+
46+
// The origin entry is setup but its path should remain unchanged.
47+
final TestHistoryEntry originEntry = strategy.history[0];
48+
expect(originEntry.state, originState);
49+
expect(originEntry.url, '/initial');
50+
51+
// The flutter entry is pushed and its path should be derived from the
52+
// origin entry.
53+
final TestHistoryEntry flutterEntry = strategy.history[1];
54+
expect(flutterEntry.state, flutterState);
55+
expect(flutterEntry.url, '/initial');
56+
57+
// The flutter entry is the current entry.
58+
expect(strategy.currentEntry, flutterEntry);
59+
});
60+
61+
test('browser back button pops routes correctly', () async {
62+
strategy =
63+
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
64+
65+
// Initially, we should be on the flutter entry.
66+
expect(strategy.history, hasLength(2));
67+
expect(strategy.currentEntry.state, flutterState);
68+
expect(strategy.currentEntry.url, '/home');
69+
70+
pushRoute('/page1');
71+
// The number of entries shouldn't change.
72+
expect(strategy.history, hasLength(2));
73+
expect(strategy.currentEntryIndex, 1);
74+
// But the url of the current entry (flutter entry) should be updated.
75+
expect(strategy.currentEntry.state, flutterState);
76+
expect(strategy.currentEntry.url, '/page1');
77+
78+
// No platform messages have been sent so far.
79+
expect(spy.messages, isEmpty);
80+
// Clicking back should take us to page1.
81+
await strategy.back();
82+
// First, the framework should've received a `popRoute` platform message.
83+
expect(spy.messages, hasLength(1));
84+
expect(spy.messages[0].channel, 'flutter/navigation');
85+
expect(spy.messages[0].methodName, 'popRoute');
86+
expect(spy.messages[0].methodArguments, isNull);
87+
// We still have 2 entries.
88+
expect(strategy.history, hasLength(2));
89+
expect(strategy.currentEntryIndex, 1);
90+
// The url of the current entry (flutter entry) should go back to /home.
91+
expect(strategy.currentEntry.state, flutterState);
92+
expect(strategy.currentEntry.url, '/home');
93+
});
94+
95+
test('multiple browser back clicks', () async {
96+
strategy =
97+
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
98+
99+
pushRoute('/page1');
100+
pushRoute('/page2');
101+
102+
// Make sure we are on page2.
103+
expect(strategy.history, hasLength(2));
104+
expect(strategy.currentEntryIndex, 1);
105+
expect(strategy.currentEntry.state, flutterState);
106+
expect(strategy.currentEntry.url, '/page2');
107+
108+
// Back to page1.
109+
await strategy.back();
110+
// 1. The engine sends a `popRoute` platform message.
111+
expect(spy.messages, hasLength(1));
112+
expect(spy.messages[0].channel, 'flutter/navigation');
113+
expect(spy.messages[0].methodName, 'popRoute');
114+
expect(spy.messages[0].methodArguments, isNull);
115+
spy.messages.clear();
116+
// 2. The framework sends a `routePopped` platform message.
117+
popRoute('/page1');
118+
// 3. The history state should reflect that /page1 is currently active.
119+
expect(strategy.history, hasLength(2));
120+
expect(strategy.currentEntryIndex, 1);
121+
expect(strategy.currentEntry.state, flutterState);
122+
expect(strategy.currentEntry.url, '/page1');
123+
124+
// Back to home.
125+
await strategy.back();
126+
// 1. The engine sends a `popRoute` platform message.
127+
expect(spy.messages, hasLength(1));
128+
expect(spy.messages[0].channel, 'flutter/navigation');
129+
expect(spy.messages[0].methodName, 'popRoute');
130+
expect(spy.messages[0].methodArguments, isNull);
131+
spy.messages.clear();
132+
// 2. The framework sends a `routePopped` platform message.
133+
popRoute('/home');
134+
// 3. The history state should reflect that /page1 is currently active.
135+
expect(strategy.history, hasLength(2));
136+
expect(strategy.currentEntryIndex, 1);
137+
expect(strategy.currentEntry.state, flutterState);
138+
expect(strategy.currentEntry.url, '/home');
139+
140+
// The next browser back will exit the app.
141+
await strategy.back();
142+
// 1. The engine sends a `popRoute` platform message.
143+
expect(spy.messages, hasLength(1));
144+
expect(spy.messages[0].channel, 'flutter/navigation');
145+
expect(spy.messages[0].methodName, 'popRoute');
146+
expect(spy.messages[0].methodArguments, isNull);
147+
spy.messages.clear();
148+
// 2. The framework sends a `SystemNavigator.pop` platform message
149+
// because there are no more routes to pop.
150+
await systemNavigatorPop();
151+
// 3. The active entry doesn't belong to our history anymore because we
152+
// navigated past it.
153+
expect(strategy.currentEntryIndex, -1);
154+
});
155+
156+
test('handle user-provided url', () async {
157+
strategy =
158+
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
159+
160+
await _strategy.simulateUserTypingUrl('/page3');
161+
// This delay is necessary to wait for [BrowserHistory] because it
162+
// performs a `back` operation which results in a new event loop.
163+
await Future.delayed(Duration.zero);
164+
// 1. The engine sends a `pushRoute` platform message.
165+
expect(spy.messages, hasLength(1));
166+
expect(spy.messages[0].channel, 'flutter/navigation');
167+
expect(spy.messages[0].methodName, 'pushRoute');
168+
expect(spy.messages[0].methodArguments, '/page3');
169+
spy.messages.clear();
170+
// 2. The framework sends a `routePushed` platform message.
171+
pushRoute('/page3');
172+
// 3. The history state should reflect that /page3 is currently active.
173+
expect(strategy.history, hasLength(3));
174+
expect(strategy.currentEntryIndex, 1);
175+
expect(strategy.currentEntry.state, flutterState);
176+
expect(strategy.currentEntry.url, '/page3');
177+
178+
// Back to home.
179+
await strategy.back();
180+
// 1. The engine sends a `popRoute` platform message.
181+
expect(spy.messages, hasLength(1));
182+
expect(spy.messages[0].channel, 'flutter/navigation');
183+
expect(spy.messages[0].methodName, 'popRoute');
184+
expect(spy.messages[0].methodArguments, isNull);
185+
spy.messages.clear();
186+
// 2. The framework sends a `routePopped` platform message.
187+
popRoute('/home');
188+
// 3. The history state should reflect that /page1 is currently active.
189+
expect(strategy.history, hasLength(2));
190+
expect(strategy.currentEntryIndex, 1);
191+
expect(strategy.currentEntry.state, flutterState);
192+
expect(strategy.currentEntry.url, '/home');
193+
});
194+
195+
test('user types unknown url', () async {
196+
strategy =
197+
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
198+
199+
await _strategy.simulateUserTypingUrl('/unknown');
200+
// This delay is necessary to wait for [BrowserHistory] because it
201+
// performs a `back` operation which results in a new event loop.
202+
await Future.delayed(Duration.zero);
203+
// 1. The engine sends a `pushRoute` platform message.
204+
expect(spy.messages, hasLength(1));
205+
expect(spy.messages[0].channel, 'flutter/navigation');
206+
expect(spy.messages[0].methodName, 'pushRoute');
207+
expect(spy.messages[0].methodArguments, '/unknown');
208+
spy.messages.clear();
209+
// 2. The framework doesn't recognize the route name and ignores it.
210+
// 3. The history state should reflect that /home is currently active.
211+
expect(strategy.history, hasLength(3));
212+
expect(strategy.currentEntryIndex, 1);
213+
expect(strategy.currentEntry.state, flutterState);
214+
expect(strategy.currentEntry.url, '/home');
215+
});
216+
});
217+
}
218+
219+
void pushRoute(String routeName) {
220+
window.sendPlatformMessage(
221+
'flutter/navigation',
222+
codec.encodeMethodCall(MethodCall(
223+
'routePushed',
224+
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName},
225+
)),
226+
emptyCallback,
227+
);
228+
}
229+
230+
void replaceRoute(String routeName) {
231+
window.sendPlatformMessage(
232+
'flutter/navigation',
233+
codec.encodeMethodCall(MethodCall(
234+
'routeReplaced',
235+
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName},
236+
)),
237+
emptyCallback,
238+
);
239+
}
240+
241+
void popRoute(String previousRouteName) {
242+
window.sendPlatformMessage(
243+
'flutter/navigation',
244+
codec.encodeMethodCall(MethodCall(
245+
'routePopped',
246+
<String, dynamic>{
247+
'previousRouteName': previousRouteName,
248+
'routeName': '/foo'
249+
},
250+
)),
251+
emptyCallback,
252+
);
253+
}
254+
255+
Future<void> systemNavigatorPop() {
256+
final Completer<void> completer = Completer<void>();
257+
window.sendPlatformMessage(
258+
'flutter/platform',
259+
codec.encodeMethodCall(MethodCall('SystemNavigator.pop')),
260+
(_) => completer.complete(),
261+
);
262+
return completer.future;
263+
}

0 commit comments

Comments
 (0)