未验证 提交 833c6a2e 编写于 作者: C chunhtai 提交者: GitHub

Implement browser history class for router widget (#20794)

上级 0c6c265a
...@@ -25,6 +25,9 @@ abstract class LocationStrategy { ...@@ -25,6 +25,9 @@ abstract class LocationStrategy {
/// The active path in the browser history. /// The active path in the browser history.
String get path; String get path;
/// The state of the current browser history entry.
dynamic get state;
/// Given a path that's internal to the app, create the external url that /// Given a path that's internal to the app, create the external url that
/// will be used in the browser. /// will be used in the browser.
String prepareExternalUrl(String internalUrl); String prepareExternalUrl(String internalUrl);
...@@ -36,7 +39,7 @@ abstract class LocationStrategy { ...@@ -36,7 +39,7 @@ abstract class LocationStrategy {
void replaceState(dynamic state, String title, String url); void replaceState(dynamic state, String title, String url);
/// Go to the previous history entry. /// Go to the previous history entry.
Future<void> back(); Future<void> back({int count = 1});
} }
/// This is an implementation of [LocationStrategy] that uses the browser URL's /// This is an implementation of [LocationStrategy] that uses the browser URL's
...@@ -82,6 +85,9 @@ class HashLocationStrategy extends LocationStrategy { ...@@ -82,6 +85,9 @@ class HashLocationStrategy extends LocationStrategy {
return path.substring(1); return path.substring(1);
} }
@override
dynamic get state => _platformLocation.state;
@override @override
String prepareExternalUrl(String internalUrl) { String prepareExternalUrl(String internalUrl) {
// It's convention that if the hash path is empty, we omit the `#`; however, // It's convention that if the hash path is empty, we omit the `#`; however,
...@@ -104,8 +110,8 @@ class HashLocationStrategy extends LocationStrategy { ...@@ -104,8 +110,8 @@ class HashLocationStrategy extends LocationStrategy {
} }
@override @override
Future<void> back() { Future<void> back({int count = 1}) {
_platformLocation.back(); _platformLocation.back(count);
return _waitForPopState(); return _waitForPopState();
} }
...@@ -142,10 +148,11 @@ abstract class PlatformLocation { ...@@ -142,10 +148,11 @@ abstract class PlatformLocation {
String get pathname; String get pathname;
String get search; String get search;
String? get hash; String? get hash;
dynamic get state;
void pushState(dynamic state, String title, String url); void pushState(dynamic state, String title, String url);
void replaceState(dynamic state, String title, String url); void replaceState(dynamic state, String title, String url);
void back(); void back(int count);
} }
/// An implementation of [PlatformLocation] for the browser. /// An implementation of [PlatformLocation] for the browser.
...@@ -184,6 +191,9 @@ class BrowserPlatformLocation extends PlatformLocation { ...@@ -184,6 +191,9 @@ class BrowserPlatformLocation extends PlatformLocation {
@override @override
String get hash => _location.hash; String get hash => _location.hash;
@override
dynamic get state => _history.state;
@override @override
void pushState(dynamic state, String title, String url) { void pushState(dynamic state, String title, String url) {
_history.pushState(state, title, url); _history.pushState(state, title, url);
...@@ -195,7 +205,7 @@ class BrowserPlatformLocation extends PlatformLocation { ...@@ -195,7 +205,7 @@ class BrowserPlatformLocation extends PlatformLocation {
} }
@override @override
void back() { void back(int count) {
_history.back(); _history.go(-count);
} }
} }
...@@ -5,96 +5,264 @@ ...@@ -5,96 +5,264 @@
// @dart = 2.10 // @dart = 2.10
part of engine; part of engine;
const MethodCall _popRouteMethodCall = MethodCall('popRoute'); /// An abstract class that provides the API for [EngineWindow] to delegate its
/// navigating events.
Map<String, bool> _originState = <String, bool>{'origin': true};
Map<String, bool> _flutterState = <String, bool>{'flutter': true};
/// The origin entry is the history entry that the Flutter app landed on. It's
/// created by the browser when the user navigates to the url of the app.
bool _isOriginEntry(dynamic state) {
return state is Map && state['origin'] == true;
}
/// The flutter entry is a history entry that we maintain on top of the origin
/// entry. It allows us to catch popstate events when the user hits the back
/// button.
bool _isFlutterEntry(dynamic state) {
return state is Map && state['flutter'] == true;
}
/// The [BrowserHistory] class is responsible for integrating Flutter Web apps
/// with the browser history so that the back button works as expected.
/// ///
/// It does that by always keeping a single entry (conventionally called the /// Subclasses will have access to [BrowserHistory.locationStrategy] to
/// "flutter" entry) at the top of the browser history. That way, the browser's /// interact with the html browser history and should come up with their own
/// back button always triggers a `popstate` event and never closes the app (we /// ways to manage the states in the browser history.
/// close the app programmatically by calling [SystemNavigator.pop] when there
/// are no more app routes to be popped).
/// ///
/// There should only be one global instance of this class. /// There should only be one global instance among all all subclasses.
class BrowserHistory { ///
LocationStrategy? _locationStrategy; /// See also:
ui.VoidCallback? _unsubscribe; ///
/// * [SingleEntryBrowserHistory]: which creates a single fake browser history
/// entry and delegates all browser navigating events to the flutter
/// framework.
/// * [MultiEntriesBrowserHistory]: which creates a set of states that records
/// the navigating events happened in the framework.
abstract class BrowserHistory {
late ui.VoidCallback _unsubscribe;
/// Changing the location strategy will unsubscribe from the old strategy's /// The strategy to interact with html browser history.
/// event listeners, and subscribe to the new one. LocationStrategy? get locationStrategy => _locationStrategy;
/// LocationStrategy? _locationStrategy;
/// If the given [strategy] is the same as the existing one, nothing will /// Updates the strategy.
/// happen.
/// ///
/// If the given strategy is null, it will render this [BrowserHistory] /// This method will also remove any previous modifications to the html
/// instance inactive. /// browser history and start anew.
set locationStrategy(LocationStrategy? strategy) { Future<void> setLocationStrategy(LocationStrategy? strategy) async {
if (strategy != _locationStrategy) { if (strategy != _locationStrategy) {
_tearoffStrategy(_locationStrategy); await _tearoffStrategy(_locationStrategy);
_locationStrategy = strategy; _locationStrategy = strategy;
_setupStrategy(_locationStrategy); await _setupStrategy(_locationStrategy);
} }
} }
/// Returns the currently active location strategy. Future<void> _setupStrategy(LocationStrategy? strategy) async {
@visibleForTesting if (strategy == null) {
LocationStrategy? get locationStrategy => _locationStrategy; return;
}
_unsubscribe = strategy.onPopState(onPopState as dynamic Function(html.Event));
await setup();
}
/// The path of the current location of the user's browser. Future<void> _tearoffStrategy(LocationStrategy? strategy) async {
String get currentPath => _locationStrategy?.path ?? '/'; if (strategy == null) {
return;
}
_unsubscribe();
/// Update the url with the given [routeName]. await tearDown();
void setRouteName(String? routeName) { }
/// Exit this application and return to the previous page.
Future<void> exit() async {
if (_locationStrategy != null) { if (_locationStrategy != null) {
_setupFlutterEntry(_locationStrategy!, replace: true, path: routeName); await _tearoffStrategy(_locationStrategy);
// Now the history should be in the original state, back one more time to
// exit the application.
await _locationStrategy!.back();
_locationStrategy = null;
} }
} }
/// This method does the same thing as the browser back button. /// This method does the same thing as the browser back button.
Future<void> back() { Future<void> back() {
if (_locationStrategy != null) { if (locationStrategy != null) {
return _locationStrategy!.back(); return locationStrategy!.back();
} }
return Future<void>.value(); return Future<void>.value();
} }
/// This method exits the app and goes to whatever website was active before. /// The path of the current location of the user's browser.
Future<void> exit() { String get currentPath => locationStrategy?.path ?? '/';
if (_locationStrategy != null) {
_tearoffStrategy(_locationStrategy); /// The state of the current location of the user's browser.
// After tearing off the location strategy, we should be on the "origin" dynamic get currentState => locationStrategy?.state;
// entry. So we need to go back one more time to exit the app.
final Future<void> backFuture = _locationStrategy!.back(); /// Update the url with the given [routeName] and [state].
_locationStrategy = null; void setRouteName(String? routeName, {dynamic? state});
return backFuture;
/// A callback method to handle browser backward or forward buttons.
///
/// Subclasses should send appropriate system messages to update the flutter
/// applications accordingly.
@protected
void onPopState(covariant html.PopStateEvent event);
/// Sets up any prerequisites to use this browser history class.
@protected
Future<void> setup() => Future<void>.value();
/// Restore any modifications to the html browser history during the lifetime
/// of this class.
@protected
Future<void> tearDown() => Future<void>.value();
}
/// A browser history class that creates a set of browser history entries to
/// support browser backward and forward button natively.
///
/// This class pushes a browser history entry every time the framework reports
/// a route change and sends a `pushRouteInformation` method call to the
/// framework when the browser jumps to a specific browser history entry.
///
/// The web engine uses this class to manage its browser history when the
/// framework uses a Router for routing.
///
/// See also:
///
/// * [SingleEntryBrowserHistory], which is used when the framework does not use
/// a Router for routing.
class MultiEntriesBrowserHistory extends BrowserHistory {
late int _lastSeenSerialCount;
int get _currentSerialCount {
if (_hasSerialCount(currentState)) {
return currentState['serialCount'] as int;
} }
return 0;
}
dynamic _tagWithSerialCount(dynamic originialState, int count) {
return <dynamic, dynamic> {
'serialCount': count,
'state': originialState,
};
}
bool _hasSerialCount(dynamic state) {
return state is Map && state['serialCount'] != null;
}
@override
void setRouteName(String? routeName, {dynamic? state}) {
if (locationStrategy != null) {
assert(routeName != null);
_lastSeenSerialCount += 1;
locationStrategy!.pushState(
_tagWithSerialCount(state, _lastSeenSerialCount),
'flutter',
routeName!,
);
}
}
@override
void onPopState(covariant html.PopStateEvent event) {
assert(locationStrategy != null);
// May be a result of direct url access while the flutter application is
// already running.
if (!_hasSerialCount(event.state)) {
// In this case we assume this will be the next history entry from the
// last seen entry.
locationStrategy!.replaceState(
_tagWithSerialCount(event.state, _lastSeenSerialCount + 1),
'flutter',
currentPath);
}
_lastSeenSerialCount = _currentSerialCount;
if (window._onPlatformMessage != null) {
window.invokeOnPlatformMessage(
'flutter/navigation',
const JSONMethodCodec().encodeMethodCall(
MethodCall('pushRouteInformation', <dynamic, dynamic>{
'location': currentPath,
'state': event.state?['state'],
})
),
(_) {},
);
}
}
@override
Future<void> setup() {
if (!_hasSerialCount(currentState)) {
locationStrategy!.replaceState(
_tagWithSerialCount(currentState, 0),
'flutter',
currentPath
);
}
// If we retore from a page refresh, the _currentSerialCount may not be 0.
_lastSeenSerialCount = _currentSerialCount;
return Future<void>.value(); return Future<void>.value();
} }
@override
Future<void> tearDown() async {
// Restores the html browser history.
assert(_hasSerialCount(currentState));
int backCount = _currentSerialCount;
if (backCount > 0) {
await locationStrategy!.back(count: backCount);
}
// Unwrap state.
assert(_hasSerialCount(currentState) && _currentSerialCount == 0);
locationStrategy!.replaceState(
currentState['state'],
'flutter',
currentPath,
);
}
}
/// The browser history class is responsible for integrating Flutter Web apps
/// with the browser history so that the back button works as expected.
///
/// It does that by always keeping a single entry (conventionally called the
/// "flutter" entry) at the top of the browser history. That way, the browser's
/// back button always triggers a `popstate` event and never closes the app (we
/// close the app programmatically by calling [SystemNavigator.pop] when there
/// are no more app routes to be popped).
///
/// The web engine uses this class when the framework does not use Router for
/// routing, and it does not support browser forward button.
///
/// See also:
///
/// * [MultiEntriesBrowserHistory], which is used when the framework uses a
/// Router for routing.
class SingleEntryBrowserHistory extends BrowserHistory {
static const MethodCall _popRouteMethodCall = MethodCall('popRoute');
static const String _kFlutterTag = 'flutter';
static const String _kOriginTag = 'origin';
Map<String, dynamic> _wrapOriginState(dynamic state) {
return <String, dynamic>{_kOriginTag: true, 'state': state};
}
dynamic _unwrapOriginState(dynamic state) {
assert(_isOriginEntry(state));
final Map<dynamic, dynamic> originState = state as Map<dynamic, dynamic>;
return originState['state'];
}
Map<String, bool> _flutterState = <String, bool>{_kFlutterTag: true};
/// The origin entry is the history entry that the Flutter app landed on. It's
/// created by the browser when the user navigates to the url of the app.
bool _isOriginEntry(dynamic state) {
return state is Map && state[_kOriginTag] == true;
}
/// The flutter entry is a history entry that we maintain on top of the origin
/// entry. It allows us to catch popstate events when the user hits the back
/// button.
bool _isFlutterEntry(dynamic state) {
return state is Map && state[_kFlutterTag] == true;
}
@override
void setRouteName(String? routeName, {dynamic? state}) {
if (locationStrategy != null) {
_setupFlutterEntry(locationStrategy!, replace: true, path: routeName);
}
}
String? _userProvidedRouteName; String? _userProvidedRouteName;
void _popStateListener(covariant html.PopStateEvent event) { @override
void onPopState(covariant html.PopStateEvent event) {
if (_isOriginEntry(event.state)) { if (_isOriginEntry(event.state)) {
// If we find ourselves in the origin entry, it means that the user
// clicked the back button.
// 1. Re-push the flutter entry to keep it always at the top of history.
_setupFlutterEntry(_locationStrategy!); _setupFlutterEntry(_locationStrategy!);
// 2. Send a 'popRoute' platform message so the app can handle it accordingly. // 2. Send a 'popRoute' platform message so the app can handle it accordingly.
...@@ -146,7 +314,7 @@ class BrowserHistory { ...@@ -146,7 +314,7 @@ class BrowserHistory {
/// [_isOriginEntry] inside [_popStateListener]. /// [_isOriginEntry] inside [_popStateListener].
void _setupOriginEntry(LocationStrategy strategy) { void _setupOriginEntry(LocationStrategy strategy) {
assert(strategy != null); // ignore: unnecessary_null_comparison assert(strategy != null); // ignore: unnecessary_null_comparison
strategy.replaceState(_originState, 'origin', ''); strategy.replaceState(_wrapOriginState(currentState), 'origin', '');
} }
/// This method is used manipulate the Flutter Entry which is always the /// This method is used manipulate the Flutter Entry which is always the
...@@ -165,11 +333,8 @@ class BrowserHistory { ...@@ -165,11 +333,8 @@ class BrowserHistory {
} }
} }
void _setupStrategy(LocationStrategy? strategy) { @override
if (strategy == null) { Future<void> setup() {
return;
}
final String path = currentPath; final String path = currentPath;
if (_isFlutterEntry(html.window.history.state)) { if (_isFlutterEntry(html.window.history.state)) {
// This could happen if the user, for example, refreshes the page. They // This could happen if the user, for example, refreshes the page. They
...@@ -177,23 +342,19 @@ class BrowserHistory { ...@@ -177,23 +342,19 @@ class BrowserHistory {
// the "origin" and "flutter" entries, we can safely assume they are // the "origin" and "flutter" entries, we can safely assume they are
// already setup. // already setup.
} else { } else {
_setupOriginEntry(strategy); _setupOriginEntry(locationStrategy!);
_setupFlutterEntry(strategy, replace: false, path: path); _setupFlutterEntry(locationStrategy!, replace: false, path: path);
} }
_unsubscribe = strategy.onPopState(_popStateListener as dynamic Function(html.Event)); return Future<void>.value();
} }
void _tearoffStrategy(LocationStrategy? strategy) { @override
if (strategy == null) { Future<void> tearDown() async {
return; if (locationStrategy != null) {
// We need to remove the flutter entry that we pushed in setup.
await locationStrategy!.back();
// Restores original state.
locationStrategy!.replaceState(_unwrapOriginState(currentState), 'flutter', currentPath);
} }
assert(_unsubscribe != null);
_unsubscribe!();
_unsubscribe = null;
// Remove the "flutter" entry and go back to the "origin" entry so that the
// next location strategy can start from the right spot.
strategy.back();
} }
} }
...@@ -39,6 +39,11 @@ class TestLocationStrategy extends LocationStrategy { ...@@ -39,6 +39,11 @@ class TestLocationStrategy extends LocationStrategy {
@override @override
String get path => currentEntry.url; String get path => currentEntry.url;
@override
dynamic get state {
return currentEntry.state;
}
int _currentEntryIndex; int _currentEntryIndex;
int get currentEntryIndex => _currentEntryIndex; int get currentEntryIndex => _currentEntryIndex;
...@@ -100,12 +105,12 @@ class TestLocationStrategy extends LocationStrategy { ...@@ -100,12 +105,12 @@ class TestLocationStrategy extends LocationStrategy {
} }
@override @override
Future<void> back() { Future<void> back({int count = 1}) {
assert(withinAppHistory); assert(withinAppHistory);
// Browsers don't move back in history immediately. They do it at the next // Browsers don't move back in history immediately. They do it at the next
// event loop. So let's simulate that. // event loop. So let's simulate that.
return _nextEventLoop(() { return _nextEventLoop(() {
_currentEntryIndex--; _currentEntryIndex = _currentEntryIndex - count;
if (withinAppHistory) { if (withinAppHistory) {
_firePopStateEvent(); _firePopStateEvent();
} }
......
...@@ -153,7 +153,45 @@ class EngineWindow extends ui.Window { ...@@ -153,7 +153,45 @@ class EngineWindow extends ui.Window {
/// Handles the browser history integration to allow users to use the back /// Handles the browser history integration to allow users to use the back
/// button, etc. /// button, etc.
final BrowserHistory _browserHistory = BrowserHistory(); @visibleForTesting
BrowserHistory get browserHistory => _browserHistory;
BrowserHistory _browserHistory = MultiEntriesBrowserHistory();
@visibleForTesting
Future<void> debugSwitchBrowserHistory({required bool useSingle}) async {
if (useSingle)
await _useSingleEntryBrowserHistory();
else
await _useMultiEntryBrowserHistory();
}
/// This function should only be used for test setup. In real application, we
/// only allow one time switch from the MultiEntriesBrowserHistory to
/// the SingleEntryBrowserHistory to prevent the application to switch back
/// forth between router and non-router.
Future<void> _useMultiEntryBrowserHistory() async {
if (_browserHistory is MultiEntriesBrowserHistory) {
return;
}
final LocationStrategy? strategy = _browserHistory.locationStrategy;
if (strategy != null)
await _browserHistory.setLocationStrategy(null);
_browserHistory = MultiEntriesBrowserHistory();
if (strategy != null)
await _browserHistory.setLocationStrategy(strategy);
}
Future<void> _useSingleEntryBrowserHistory() async {
if (_browserHistory is SingleEntryBrowserHistory) {
return;
}
final LocationStrategy? strategy = _browserHistory.locationStrategy;
if (strategy != null)
await _browserHistory.setLocationStrategy(null);
_browserHistory = SingleEntryBrowserHistory();
if (strategy != null)
await _browserHistory.setLocationStrategy(strategy);
}
/// Simulates clicking the browser's back button. /// Simulates clicking the browser's back button.
Future<void> webOnlyBack() => _browserHistory.back(); Future<void> webOnlyBack() => _browserHistory.back();
...@@ -181,7 +219,7 @@ class EngineWindow extends ui.Window { ...@@ -181,7 +219,7 @@ class EngineWindow extends ui.Window {
/// ///
/// By setting this to null, the browser history will be disabled. /// By setting this to null, the browser history will be disabled.
set locationStrategy(LocationStrategy? strategy) { set locationStrategy(LocationStrategy? strategy) {
_browserHistory.locationStrategy = strategy; _browserHistory.setLocationStrategy(strategy);
} }
/// Returns the currently active location strategy. /// Returns the currently active location strategy.
...@@ -608,16 +646,20 @@ class EngineWindow extends ui.Window { ...@@ -608,16 +646,20 @@ class EngineWindow extends ui.Window {
final Map<String, dynamic>? message = decoded.arguments; final Map<String, dynamic>? message = decoded.arguments;
switch (decoded.method) { switch (decoded.method) {
case 'routeUpdated': case 'routeUpdated':
case 'routePushed': _useSingleEntryBrowserHistory().then((void data) {
case 'routeReplaced': _browserHistory.setRouteName(message!['routeName']);
_browserHistory.setRouteName(message!['routeName']); _replyToPlatformMessage(
_replyToPlatformMessage( callback, codec.encodeSuccessEnvelope(true));
callback, codec.encodeSuccessEnvelope(true)); });
break; break;
case 'routePopped': case 'routeInformationUpdated':
_browserHistory.setRouteName(message!['previousRouteName']); assert(_browserHistory is MultiEntriesBrowserHistory);
_browserHistory.setRouteName(
message!['location'],
state: message!['state'],
);
_replyToPlatformMessage( _replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(true)); callback, codec.encodeSuccessEnvelope(true));
break; break;
} }
// As soon as Flutter starts taking control of the app navigation, we // As soon as Flutter starts taking control of the app navigation, we
......
...@@ -16,10 +16,20 @@ import 'package:ui/src/engine.dart'; ...@@ -16,10 +16,20 @@ import 'package:ui/src/engine.dart';
import '../spy.dart'; import '../spy.dart';
TestLocationStrategy _strategy; TestLocationStrategy get strategy => window.browserHistory.locationStrategy;
TestLocationStrategy get strategy => _strategy; Future<void> setStrategy(TestLocationStrategy newStrategy) async {
set strategy(TestLocationStrategy newStrategy) { await window.browserHistory.setLocationStrategy(newStrategy);
window.locationStrategy = _strategy = newStrategy; }
Map<String, dynamic> _wrapOriginState(dynamic state) {
return <String, dynamic>{'origin': true, 'state': state};
}
Map<String, dynamic> _tagStateWithSerialCount(dynamic state, int serialCount) {
return <String, dynamic> {
'serialCount': serialCount,
'state': state,
};
} }
const Map<String, bool> originState = <String, bool>{'origin': true}; const Map<String, bool> originState = <String, bool>{'origin': true};
...@@ -34,28 +44,29 @@ void main() { ...@@ -34,28 +44,29 @@ void main() {
} }
void testMain() { void testMain() {
group('$BrowserHistory', () { group('$SingleEntryBrowserHistory', () {
final PlatformMessagesSpy spy = PlatformMessagesSpy(); final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() { setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: true);
spy.setUp(); spy.setUp();
}); });
tearDown(() { tearDown(() async {
spy.tearDown(); spy.tearDown();
strategy = null; await setStrategy(null);
}); });
test('basic setup works', () { test('basic setup works', () async {
strategy = TestLocationStrategy.fromEntry( await setStrategy(TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial')); TestHistoryEntry('initial state', null, '/initial')));
// There should be two entries: origin and flutter. // There should be two entries: origin and flutter.
expect(strategy.history, hasLength(2)); expect(strategy.history, hasLength(2));
// The origin entry is setup but its path should remain unchanged. // The origin entry is setup but its path should remain unchanged.
final TestHistoryEntry originEntry = strategy.history[0]; final TestHistoryEntry originEntry = strategy.history[0];
expect(originEntry.state, originState); expect(originEntry.state, _wrapOriginState('initial state'));
expect(originEntry.url, '/initial'); expect(originEntry.url, '/initial');
// The flutter entry is pushed and its path should be derived from the // The flutter entry is pushed and its path should be derived from the
...@@ -71,15 +82,12 @@ void testMain() { ...@@ -71,15 +82,12 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge); skip: browserEngine == BrowserEngine.edge);
test('browser back button pops routes correctly', () async { test('browser back button pops routes correctly', () async {
strategy = await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
// Initially, we should be on the flutter entry. // Initially, we should be on the flutter entry.
expect(strategy.history, hasLength(2)); expect(strategy.history, hasLength(2));
expect(strategy.currentEntry.state, flutterState); expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home'); expect(strategy.currentEntry.url, '/home');
await routeUpdated('/page1');
pushRoute('/page1');
// The number of entries shouldn't change. // The number of entries shouldn't change.
expect(strategy.history, hasLength(2)); expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1); expect(strategy.currentEntryIndex, 1);
...@@ -107,11 +115,10 @@ void testMain() { ...@@ -107,11 +115,10 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge); skip: browserEngine == BrowserEngine.edge);
test('multiple browser back clicks', () async { test('multiple browser back clicks', () async {
strategy = await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
pushRoute('/page1'); await routeUpdated('/page1');
pushRoute('/page2'); await routeUpdated('/page2');
// Make sure we are on page2. // Make sure we are on page2.
expect(strategy.history, hasLength(2)); expect(strategy.history, hasLength(2));
...@@ -128,7 +135,7 @@ void testMain() { ...@@ -128,7 +135,7 @@ void testMain() {
expect(spy.messages[0].methodArguments, isNull); expect(spy.messages[0].methodArguments, isNull);
spy.messages.clear(); spy.messages.clear();
// 2. The framework sends a `routePopped` platform message. // 2. The framework sends a `routePopped` platform message.
popRoute('/page1'); await routeUpdated('/page1');
// 3. The history state should reflect that /page1 is currently active. // 3. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2)); expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1); expect(strategy.currentEntryIndex, 1);
...@@ -144,15 +151,18 @@ void testMain() { ...@@ -144,15 +151,18 @@ void testMain() {
expect(spy.messages[0].methodArguments, isNull); expect(spy.messages[0].methodArguments, isNull);
spy.messages.clear(); spy.messages.clear();
// 2. The framework sends a `routePopped` platform message. // 2. The framework sends a `routePopped` platform message.
popRoute('/home'); await routeUpdated('/home');
// 3. The history state should reflect that /page1 is currently active. // 3. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2)); expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1); expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, flutterState); expect(strategy.currentEntry.state, flutterState);
expect(strategy.currentEntry.url, '/home'); expect(strategy.currentEntry.url, '/home');
// The next browser back will exit the app. // The next browser back will exit the app. We store the strategy locally
await strategy.back(); // because it will be remove from the browser history class once it exits
// the app.
TestLocationStrategy originalStrategy = strategy;
await originalStrategy.back();
// 1. The engine sends a `popRoute` platform message. // 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1)); expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation'); expect(spy.messages[0].channel, 'flutter/navigation');
...@@ -164,17 +174,16 @@ void testMain() { ...@@ -164,17 +174,16 @@ void testMain() {
await systemNavigatorPop(); await systemNavigatorPop();
// 3. The active entry doesn't belong to our history anymore because we // 3. The active entry doesn't belong to our history anymore because we
// navigated past it. // navigated past it.
expect(strategy.currentEntryIndex, -1); expect(originalStrategy.currentEntryIndex, -1);
}, },
// TODO(nurhan): https://github.com/flutter/flutter/issues/50836 // TODO(nurhan): https://github.com/flutter/flutter/issues/50836
skip: browserEngine == BrowserEngine.edge || skip: browserEngine == BrowserEngine.edge ||
browserEngine == BrowserEngine.webkit); browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async { test('handle user-provided url', () async {
strategy = await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
await _strategy.simulateUserTypingUrl('/page3'); await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it // This delay is necessary to wait for [BrowserHistory] because it
// performs a `back` operation which results in a new event loop. // performs a `back` operation which results in a new event loop.
await Future<void>.delayed(Duration.zero); await Future<void>.delayed(Duration.zero);
...@@ -185,7 +194,7 @@ void testMain() { ...@@ -185,7 +194,7 @@ void testMain() {
expect(spy.messages[0].methodArguments, '/page3'); expect(spy.messages[0].methodArguments, '/page3');
spy.messages.clear(); spy.messages.clear();
// 2. The framework sends a `routePushed` platform message. // 2. The framework sends a `routePushed` platform message.
pushRoute('/page3'); await routeUpdated('/page3');
// 3. The history state should reflect that /page3 is currently active. // 3. The history state should reflect that /page3 is currently active.
expect(strategy.history, hasLength(3)); expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 1); expect(strategy.currentEntryIndex, 1);
...@@ -201,7 +210,7 @@ void testMain() { ...@@ -201,7 +210,7 @@ void testMain() {
expect(spy.messages[0].methodArguments, isNull); expect(spy.messages[0].methodArguments, isNull);
spy.messages.clear(); spy.messages.clear();
// 2. The framework sends a `routePopped` platform message. // 2. The framework sends a `routePopped` platform message.
popRoute('/home'); await routeUpdated('/home');
// 3. The history state should reflect that /page1 is currently active. // 3. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2)); expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1); expect(strategy.currentEntryIndex, 1);
...@@ -212,10 +221,9 @@ void testMain() { ...@@ -212,10 +221,9 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge); skip: browserEngine == BrowserEngine.edge);
test('user types unknown url', () async { test('user types unknown url', () async {
strategy = await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
await _strategy.simulateUserTypingUrl('/unknown'); await strategy.simulateUserTypingUrl('/unknown');
// This delay is necessary to wait for [BrowserHistory] because it // This delay is necessary to wait for [BrowserHistory] because it
// performs a `back` operation which results in a new event loop. // performs a `back` operation which results in a new event loop.
await Future<void>.delayed(Duration.zero); await Future<void>.delayed(Duration.zero);
...@@ -236,6 +244,212 @@ void testMain() { ...@@ -236,6 +244,212 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge); skip: browserEngine == BrowserEngine.edge);
}); });
group('$MultiEntriesBrowserHistory', () {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: false);
spy.setUp();
});
tearDown(() async {
spy.tearDown();
await setStrategy(null);
});
test('basic setup works', () async {
await setStrategy(TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial')));
// There should be only one entry.
expect(strategy.history, hasLength(1));
// The origin entry is tagged and its path should remain unchanged.
final TestHistoryEntry taggedOriginEntry = strategy.history[0];
expect(taggedOriginEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(taggedOriginEntry.url, '/initial');
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50836
skip: browserEngine == BrowserEngine.edge);
test('browser back button push route infromation correctly', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
// Initially, we should be on the flutter entry.
expect(strategy.history, hasLength(1));
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
await routeInfomrationUpdated('/page1', 'page1 state');
// Should have two history entries now.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
// But the url of the current entry (flutter entry) should be updated.
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// No platform messages have been sent so far.
expect(spy.messages, isEmpty);
// Clicking back should take us to page1.
await strategy.back();
// First, the framework should've received a `pushRouteInformation`
// platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/home',
'state': 'initial state',
});
// There are still two browser history entries, but we are back to the
// original state.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 0);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50836
skip: browserEngine == BrowserEngine.edge);
test('multiple browser back clicks', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
await routeInfomrationUpdated('/page1', 'page1 state');
await routeInfomrationUpdated('/page2', 'page2 state');
// Make sure we are on page2.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 2);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page2 state', 2));
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page1',
'state': 'page1 state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// Back to home.
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/home',
'state': 'initial state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 0);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50836
skip: browserEngine == BrowserEngine.edge ||
browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it
// performs a `back` operation which results in a new event loop.
await Future<void>.delayed(Duration.zero);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page3',
'state': null,
});
spy.messages.clear();
// 2. The history state should reflect that /page3 is currently active.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, _tagStateWithSerialCount(null, 1));
expect(strategy.currentEntry.url, '/page3');
// Back to home.
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/home',
'state': 'initial state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(2));
expect(strategy.currentEntryIndex, 0);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
expect(strategy.currentEntry.url, '/home');
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50836
skip: browserEngine == BrowserEngine.edge);
test('forward button works', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
await routeInfomrationUpdated('/page1', 'page1 state');
await routeInfomrationUpdated('/page2', 'page2 state');
// Make sure we are on page2.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 2);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page2 state', 2));
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page1',
'state': 'page1 state',
});
spy.messages.clear();
// 2. The history state should reflect that /page1 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 1);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// Forward to page2
await strategy.back(count: -1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
expect(spy.messages[0].methodName, 'pushRouteInformation');
expect(spy.messages[0].methodArguments, <dynamic, dynamic>{
'location': '/page2',
'state': 'page2 state',
});
spy.messages.clear();
// 2. The history state should reflect that /page2 is currently active.
expect(strategy.history, hasLength(3));
expect(strategy.currentEntryIndex, 2);
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page2 state', 2));
expect(strategy.currentEntry.url, '/page2');
},
// TODO(nurhan): https://github.com/flutter/flutter/issues/50836
skip: browserEngine == BrowserEngine.edge);
});
group('$HashLocationStrategy', () { group('$HashLocationStrategy', () {
TestPlatformLocation location; TestPlatformLocation location;
...@@ -272,40 +486,30 @@ void testMain() { ...@@ -272,40 +486,30 @@ void testMain() {
}); });
} }
void pushRoute(String routeName) { Future<void> routeUpdated(String routeName) {
window.sendPlatformMessage( final Completer<void> completer = Completer<void>();
'flutter/navigation',
codec.encodeMethodCall(MethodCall(
'routePushed',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName},
)),
emptyCallback,
);
}
void replaceRoute(String routeName) {
window.sendPlatformMessage( window.sendPlatformMessage(
'flutter/navigation', 'flutter/navigation',
codec.encodeMethodCall(MethodCall( codec.encodeMethodCall(MethodCall(
'routeReplaced', 'routeUpdated',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName}, <String, dynamic>{'routeName': routeName},
)), )),
emptyCallback, (_) => completer.complete(),
); );
return completer.future;
} }
void popRoute(String previousRouteName) { Future<void> routeInfomrationUpdated(String location, dynamic state) {
final Completer<void> completer = Completer<void>();
window.sendPlatformMessage( window.sendPlatformMessage(
'flutter/navigation', 'flutter/navigation',
codec.encodeMethodCall(MethodCall( codec.encodeMethodCall(MethodCall(
'routePopped', 'routeInformationUpdated',
<String, dynamic>{ <String, dynamic>{'location': location, 'state': state},
'previousRouteName': previousRouteName,
'routeName': '/foo'
},
)), )),
emptyCallback, (_) => completer.complete(),
); );
return completer.future;
} }
Future<void> systemNavigatorPop() { Future<void> systemNavigatorPop() {
...@@ -323,6 +527,7 @@ class TestPlatformLocation extends PlatformLocation { ...@@ -323,6 +527,7 @@ class TestPlatformLocation extends PlatformLocation {
String pathname; String pathname;
String search; String search;
String hash; String hash;
dynamic state;
void onPopState(html.EventListener fn) { void onPopState(html.EventListener fn) {
throw UnimplementedError(); throw UnimplementedError();
...@@ -348,7 +553,7 @@ class TestPlatformLocation extends PlatformLocation { ...@@ -348,7 +553,7 @@ class TestPlatformLocation extends PlatformLocation {
throw UnimplementedError(); throw UnimplementedError();
} }
void back() { void back(int count) {
throw UnimplementedError(); throw UnimplementedError();
} }
} }
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
// @dart = 2.6 // @dart = 2.6
import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart'; import 'package:test/bootstrap/browser.dart';
...@@ -28,68 +29,17 @@ void testMain() { ...@@ -28,68 +29,17 @@ void testMain() {
engine.window.locationStrategy = _strategy = null; engine.window.locationStrategy = _strategy = null;
}); });
test('Tracks pushed, replaced and popped routes', () { test('Tracks pushed, replaced and popped routes', () async {
engine.window.sendPlatformMessage( final Completer<void> completer = Completer<void>();
'flutter/navigation',
codec.encodeMethodCall(const engine.MethodCall(
'routePushed',
<String, dynamic>{'previousRouteName': '/', 'routeName': '/foo'},
)),
emptyCallback,
);
expect(_strategy.path, '/foo');
engine.window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(const engine.MethodCall(
'routePushed',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': '/bar'},
)),
emptyCallback,
);
expect(_strategy.path, '/bar');
engine.window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(const engine.MethodCall(
'routePopped',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': '/bar'},
)),
emptyCallback,
);
expect(_strategy.path, '/foo');
engine.window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(const engine.MethodCall(
'routePushed',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': '/bar/baz'},
)),
emptyCallback,
);
expect(_strategy.path, '/bar/baz');
engine.window.sendPlatformMessage(
'flutter/navigation',
codec.encodeMethodCall(const engine.MethodCall(
'routeReplaced',
<String, dynamic>{
'previousRouteName': '/bar/baz',
'routeName': '/bar/baz2',
},
)),
emptyCallback,
);
expect(_strategy.path, '/bar/baz2');
engine.window.sendPlatformMessage( engine.window.sendPlatformMessage(
'flutter/navigation', 'flutter/navigation',
codec.encodeMethodCall(const engine.MethodCall( codec.encodeMethodCall(const engine.MethodCall(
'routeUpdated', 'routeUpdated',
<String, dynamic>{'previousRouteName': '/bar/baz2', 'routeName': '/foo/foo/2'}, <String, dynamic>{'routeName': '/foo'},
)), )),
emptyCallback, (_) => completer.complete(),
); );
expect(_strategy.path, '/foo/foo/2'); await completer.future;
expect(_strategy.path, '/foo');
}); });
} }
...@@ -16,13 +16,21 @@ const MethodCodec codec = JSONMethodCodec(); ...@@ -16,13 +16,21 @@ const MethodCodec codec = JSONMethodCodec();
void emptyCallback(ByteData date) {} void emptyCallback(ByteData date) {}
Future<void> setStrategy(TestLocationStrategy newStrategy) async {
await window.browserHistory.setLocationStrategy(newStrategy);
}
void main() { void main() {
internalBootstrapBrowserTest(() => testMain); internalBootstrapBrowserTest(() => testMain);
} }
void testMain() { void testMain() {
test('window.defaultRouteName should not change', () { setUp(() async {
window.locationStrategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')); await window.debugSwitchBrowserHistory(useSingle: true);
});
test('window.defaultRouteName should not change', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')));
expect(window.defaultRouteName, '/initial'); expect(window.defaultRouteName, '/initial');
// Changing the URL in the address bar later shouldn't affect [window.defaultRouteName]. // Changing the URL in the address bar later shouldn't affect [window.defaultRouteName].
...@@ -30,17 +38,16 @@ void testMain() { ...@@ -30,17 +38,16 @@ void testMain() {
expect(window.defaultRouteName, '/initial'); expect(window.defaultRouteName, '/initial');
}); });
test('window.defaultRouteName should reset after navigation platform message', () { test('window.defaultRouteName should reset after navigation platform message', () async {
window.locationStrategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')); await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')));
// Reading it multiple times should return the same value. // Reading it multiple times should return the same value.
expect(window.defaultRouteName, '/initial'); expect(window.defaultRouteName, '/initial');
expect(window.defaultRouteName, '/initial'); expect(window.defaultRouteName, '/initial');
window.sendPlatformMessage( window.sendPlatformMessage(
'flutter/navigation', 'flutter/navigation',
JSONMethodCodec().encodeMethodCall(MethodCall( JSONMethodCodec().encodeMethodCall(MethodCall(
'routePushed', 'routeUpdated',
<String, dynamic>{'previousRouteName': '/foo', 'routeName': '/bar'}, <String, dynamic>{'routeName': '/bar'},
)), )),
emptyCallback, emptyCallback,
); );
...@@ -50,22 +57,25 @@ void testMain() { ...@@ -50,22 +57,25 @@ void testMain() {
}); });
test('can disable location strategy', () async { test('can disable location strategy', () async {
await window.debugSwitchBrowserHistory(useSingle: true);
final testStrategy = TestLocationStrategy.fromEntry( final testStrategy = TestLocationStrategy.fromEntry(
TestHistoryEntry(null, null, '/'), TestHistoryEntry('initial state', null, '/'),
); );
window.locationStrategy = testStrategy; await setStrategy(testStrategy);
expect(window.locationStrategy, testStrategy); expect(window.locationStrategy, testStrategy);
// A single listener should've been setup. // A single listener should've been setup.
expect(testStrategy.listeners, hasLength(1)); expect(testStrategy.listeners, hasLength(1));
// The initial entry should be there, plus another "flutter" entry. // The initial entry should be there, plus another "flutter" entry.
expect(testStrategy.history, hasLength(2)); expect(testStrategy.history, hasLength(2));
expect(testStrategy.history[0].state, <String, bool>{'origin': true}); expect(testStrategy.history[0].state, <String, dynamic>{'origin': true, 'state': 'initial state'});
expect(testStrategy.history[1].state, <String, bool>{'flutter': true}); expect(testStrategy.history[1].state, <String, bool>{'flutter': true});
expect(testStrategy.currentEntry, testStrategy.history[1]); expect(testStrategy.currentEntry, testStrategy.history[1]);
// Now, let's disable location strategy and make sure things get cleaned up. // Now, let's disable location strategy and make sure things get cleaned up.
expect(() => jsSetLocationStrategy(null), returnsNormally); expect(() => jsSetLocationStrategy(null), returnsNormally);
// The locationStrategy is teared down asynchronously.
await Future<void>.delayed(Duration.zero);
expect(window.locationStrategy, isNull); expect(window.locationStrategy, isNull);
// The listener is removed asynchronously. // The listener is removed asynchronously.
...@@ -73,8 +83,8 @@ void testMain() { ...@@ -73,8 +83,8 @@ void testMain() {
// No more listeners. // No more listeners.
expect(testStrategy.listeners, isEmpty); expect(testStrategy.listeners, isEmpty);
// History should've moved back to the initial entry. // History should've moved back to the initial state.
expect(testStrategy.history[0].state, <String, bool>{'origin': true}); expect(testStrategy.history[0].state, "initial state");
expect(testStrategy.currentEntry, testStrategy.history[0]); expect(testStrategy.currentEntry, testStrategy.history[0]);
}); });
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册