mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Implement browser history class for router widget (flutter/engine#20794)
This commit is contained in:
parent
84995bd516
commit
da3c08e3ba
@ -25,6 +25,9 @@ abstract class LocationStrategy {
|
||||
/// The active path in the browser history.
|
||||
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
|
||||
/// will be used in the browser.
|
||||
String prepareExternalUrl(String internalUrl);
|
||||
@ -36,7 +39,7 @@ abstract class LocationStrategy {
|
||||
void replaceState(dynamic state, String title, String url);
|
||||
|
||||
/// 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
|
||||
@ -82,6 +85,9 @@ class HashLocationStrategy extends LocationStrategy {
|
||||
return path.substring(1);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic get state => _platformLocation.state;
|
||||
|
||||
@override
|
||||
String prepareExternalUrl(String internalUrl) {
|
||||
// It's convention that if the hash path is empty, we omit the `#`; however,
|
||||
@ -104,8 +110,8 @@ class HashLocationStrategy extends LocationStrategy {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> back() {
|
||||
_platformLocation.back();
|
||||
Future<void> back({int count = 1}) {
|
||||
_platformLocation.back(count);
|
||||
return _waitForPopState();
|
||||
}
|
||||
|
||||
@ -142,10 +148,11 @@ abstract class PlatformLocation {
|
||||
String get pathname;
|
||||
String get search;
|
||||
String? get hash;
|
||||
dynamic get state;
|
||||
|
||||
void pushState(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.
|
||||
@ -184,6 +191,9 @@ class BrowserPlatformLocation extends PlatformLocation {
|
||||
@override
|
||||
String get hash => _location.hash;
|
||||
|
||||
@override
|
||||
dynamic get state => _history.state;
|
||||
|
||||
@override
|
||||
void pushState(dynamic state, String title, String url) {
|
||||
_history.pushState(state, title, url);
|
||||
@ -195,7 +205,7 @@ class BrowserPlatformLocation extends PlatformLocation {
|
||||
}
|
||||
|
||||
@override
|
||||
void back() {
|
||||
_history.back();
|
||||
void back(int count) {
|
||||
_history.go(-count);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,25 +5,210 @@
|
||||
// @dart = 2.10
|
||||
part of engine;
|
||||
|
||||
const MethodCall _popRouteMethodCall = MethodCall('popRoute');
|
||||
/// An abstract class that provides the API for [EngineWindow] to delegate its
|
||||
/// navigating events.
|
||||
///
|
||||
/// Subclasses will have access to [BrowserHistory.locationStrategy] to
|
||||
/// interact with the html browser history and should come up with their own
|
||||
/// ways to manage the states in the browser history.
|
||||
///
|
||||
/// There should only be one global instance among all all subclasses.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [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;
|
||||
|
||||
Map<String, bool> _originState = <String, bool>{'origin': true};
|
||||
Map<String, bool> _flutterState = <String, bool>{'flutter': true};
|
||||
/// The strategy to interact with html browser history.
|
||||
LocationStrategy? get locationStrategy => _locationStrategy;
|
||||
LocationStrategy? _locationStrategy;
|
||||
/// Updates the strategy.
|
||||
///
|
||||
/// This method will also remove any previous modifications to the html
|
||||
/// browser history and start anew.
|
||||
Future<void> setLocationStrategy(LocationStrategy? strategy) async {
|
||||
if (strategy != _locationStrategy) {
|
||||
await _tearoffStrategy(_locationStrategy);
|
||||
_locationStrategy = strategy;
|
||||
await _setupStrategy(_locationStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
Future<void> _setupStrategy(LocationStrategy? strategy) async {
|
||||
if (strategy == null) {
|
||||
return;
|
||||
}
|
||||
_unsubscribe = strategy.onPopState(onPopState as dynamic Function(html.Event));
|
||||
await setup();
|
||||
}
|
||||
|
||||
Future<void> _tearoffStrategy(LocationStrategy? strategy) async {
|
||||
if (strategy == null) {
|
||||
return;
|
||||
}
|
||||
_unsubscribe();
|
||||
|
||||
await tearDown();
|
||||
}
|
||||
|
||||
/// Exit this application and return to the previous page.
|
||||
Future<void> exit() async {
|
||||
if (_locationStrategy != null) {
|
||||
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.
|
||||
Future<void> back() {
|
||||
if (locationStrategy != null) {
|
||||
return locationStrategy!.back();
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
/// The path of the current location of the user's browser.
|
||||
String get currentPath => locationStrategy?.path ?? '/';
|
||||
|
||||
/// The state of the current location of the user's browser.
|
||||
dynamic get currentState => locationStrategy?.state;
|
||||
|
||||
/// Update the url with the given [routeName] and [state].
|
||||
void setRouteName(String? routeName, {dynamic? state});
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// 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;
|
||||
/// 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();
|
||||
}
|
||||
|
||||
@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 [BrowserHistory] class is responsible for integrating Flutter Web apps
|
||||
/// 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
|
||||
@ -32,69 +217,52 @@ bool _isFlutterEntry(dynamic state) {
|
||||
/// 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.
|
||||
class BrowserHistory {
|
||||
LocationStrategy? _locationStrategy;
|
||||
ui.VoidCallback? _unsubscribe;
|
||||
/// 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';
|
||||
|
||||
/// Changing the location strategy will unsubscribe from the old strategy's
|
||||
/// event listeners, and subscribe to the new one.
|
||||
///
|
||||
/// If the given [strategy] is the same as the existing one, nothing will
|
||||
/// happen.
|
||||
///
|
||||
/// If the given strategy is null, it will render this [BrowserHistory]
|
||||
/// instance inactive.
|
||||
set locationStrategy(LocationStrategy? strategy) {
|
||||
if (strategy != _locationStrategy) {
|
||||
_tearoffStrategy(_locationStrategy);
|
||||
_locationStrategy = strategy;
|
||||
_setupStrategy(_locationStrategy);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/// Returns the currently active location strategy.
|
||||
@visibleForTesting
|
||||
LocationStrategy? get locationStrategy => _locationStrategy;
|
||||
|
||||
/// The path of the current location of the user's browser.
|
||||
String get currentPath => _locationStrategy?.path ?? '/';
|
||||
|
||||
/// Update the url with the given [routeName].
|
||||
void setRouteName(String? routeName) {
|
||||
if (_locationStrategy != null) {
|
||||
_setupFlutterEntry(_locationStrategy!, replace: true, path: routeName);
|
||||
}
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// This method does the same thing as the browser back button.
|
||||
Future<void> back() {
|
||||
if (_locationStrategy != null) {
|
||||
return _locationStrategy!.back();
|
||||
@override
|
||||
void setRouteName(String? routeName, {dynamic? state}) {
|
||||
if (locationStrategy != null) {
|
||||
_setupFlutterEntry(locationStrategy!, replace: true, path: routeName);
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
/// This method exits the app and goes to whatever website was active before.
|
||||
Future<void> exit() {
|
||||
if (_locationStrategy != null) {
|
||||
_tearoffStrategy(_locationStrategy);
|
||||
// After tearing off the location strategy, we should be on the "origin"
|
||||
// entry. So we need to go back one more time to exit the app.
|
||||
final Future<void> backFuture = _locationStrategy!.back();
|
||||
_locationStrategy = null;
|
||||
return backFuture;
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
String? _userProvidedRouteName;
|
||||
void _popStateListener(covariant html.PopStateEvent event) {
|
||||
@override
|
||||
void onPopState(covariant html.PopStateEvent event) {
|
||||
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!);
|
||||
|
||||
// 2. Send a 'popRoute' platform message so the app can handle it accordingly.
|
||||
@ -146,7 +314,7 @@ class BrowserHistory {
|
||||
/// [_isOriginEntry] inside [_popStateListener].
|
||||
void _setupOriginEntry(LocationStrategy strategy) {
|
||||
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
|
||||
@ -165,11 +333,8 @@ class BrowserHistory {
|
||||
}
|
||||
}
|
||||
|
||||
void _setupStrategy(LocationStrategy? strategy) {
|
||||
if (strategy == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setup() {
|
||||
final String path = currentPath;
|
||||
if (_isFlutterEntry(html.window.history.state)) {
|
||||
// This could happen if the user, for example, refreshes the page. They
|
||||
@ -177,23 +342,19 @@ class BrowserHistory {
|
||||
// the "origin" and "flutter" entries, we can safely assume they are
|
||||
// already setup.
|
||||
} else {
|
||||
_setupOriginEntry(strategy);
|
||||
_setupFlutterEntry(strategy, replace: false, path: path);
|
||||
_setupOriginEntry(locationStrategy!);
|
||||
_setupFlutterEntry(locationStrategy!, replace: false, path: path);
|
||||
}
|
||||
_unsubscribe = strategy.onPopState(_popStateListener as dynamic Function(html.Event));
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
void _tearoffStrategy(LocationStrategy? strategy) {
|
||||
if (strategy == null) {
|
||||
return;
|
||||
@override
|
||||
Future<void> tearDown() async {
|
||||
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 {
|
||||
@override
|
||||
String get path => currentEntry.url;
|
||||
|
||||
@override
|
||||
dynamic get state {
|
||||
return currentEntry.state;
|
||||
}
|
||||
|
||||
int _currentEntryIndex;
|
||||
int get currentEntryIndex => _currentEntryIndex;
|
||||
|
||||
@ -100,12 +105,12 @@ class TestLocationStrategy extends LocationStrategy {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> back() {
|
||||
Future<void> back({int count = 1}) {
|
||||
assert(withinAppHistory);
|
||||
// Browsers don't move back in history immediately. They do it at the next
|
||||
// event loop. So let's simulate that.
|
||||
return _nextEventLoop(() {
|
||||
_currentEntryIndex--;
|
||||
_currentEntryIndex = _currentEntryIndex - count;
|
||||
if (withinAppHistory) {
|
||||
_firePopStateEvent();
|
||||
}
|
||||
|
||||
@ -153,7 +153,45 @@ class EngineWindow extends ui.Window {
|
||||
|
||||
/// Handles the browser history integration to allow users to use the back
|
||||
/// 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.
|
||||
Future<void> webOnlyBack() => _browserHistory.back();
|
||||
@ -181,7 +219,7 @@ class EngineWindow extends ui.Window {
|
||||
///
|
||||
/// By setting this to null, the browser history will be disabled.
|
||||
set locationStrategy(LocationStrategy? strategy) {
|
||||
_browserHistory.locationStrategy = strategy;
|
||||
_browserHistory.setLocationStrategy(strategy);
|
||||
}
|
||||
|
||||
/// Returns the currently active location strategy.
|
||||
@ -608,16 +646,20 @@ class EngineWindow extends ui.Window {
|
||||
final Map<String, dynamic>? message = decoded.arguments;
|
||||
switch (decoded.method) {
|
||||
case 'routeUpdated':
|
||||
case 'routePushed':
|
||||
case 'routeReplaced':
|
||||
_browserHistory.setRouteName(message!['routeName']);
|
||||
_replyToPlatformMessage(
|
||||
callback, codec.encodeSuccessEnvelope(true));
|
||||
_useSingleEntryBrowserHistory().then((void data) {
|
||||
_browserHistory.setRouteName(message!['routeName']);
|
||||
_replyToPlatformMessage(
|
||||
callback, codec.encodeSuccessEnvelope(true));
|
||||
});
|
||||
break;
|
||||
case 'routePopped':
|
||||
_browserHistory.setRouteName(message!['previousRouteName']);
|
||||
case 'routeInformationUpdated':
|
||||
assert(_browserHistory is MultiEntriesBrowserHistory);
|
||||
_browserHistory.setRouteName(
|
||||
message!['location'],
|
||||
state: message!['state'],
|
||||
);
|
||||
_replyToPlatformMessage(
|
||||
callback, codec.encodeSuccessEnvelope(true));
|
||||
callback, codec.encodeSuccessEnvelope(true));
|
||||
break;
|
||||
}
|
||||
// As soon as Flutter starts taking control of the app navigation, we
|
||||
|
||||
@ -16,10 +16,20 @@ import 'package:ui/src/engine.dart';
|
||||
|
||||
import '../spy.dart';
|
||||
|
||||
TestLocationStrategy _strategy;
|
||||
TestLocationStrategy get strategy => _strategy;
|
||||
set strategy(TestLocationStrategy newStrategy) {
|
||||
window.locationStrategy = _strategy = newStrategy;
|
||||
TestLocationStrategy get strategy => window.browserHistory.locationStrategy;
|
||||
Future<void> setStrategy(TestLocationStrategy newStrategy) async {
|
||||
await window.browserHistory.setLocationStrategy(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};
|
||||
@ -34,28 +44,29 @@ void main() {
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('$BrowserHistory', () {
|
||||
group('$SingleEntryBrowserHistory', () {
|
||||
final PlatformMessagesSpy spy = PlatformMessagesSpy();
|
||||
|
||||
setUp(() {
|
||||
setUp(() async {
|
||||
await window.debugSwitchBrowserHistory(useSingle: true);
|
||||
spy.setUp();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
tearDown(() async {
|
||||
spy.tearDown();
|
||||
strategy = null;
|
||||
await setStrategy(null);
|
||||
});
|
||||
|
||||
test('basic setup works', () {
|
||||
strategy = TestLocationStrategy.fromEntry(
|
||||
TestHistoryEntry('initial state', null, '/initial'));
|
||||
test('basic setup works', () async {
|
||||
await setStrategy(TestLocationStrategy.fromEntry(
|
||||
TestHistoryEntry('initial state', null, '/initial')));
|
||||
|
||||
// There should be two entries: origin and flutter.
|
||||
expect(strategy.history, hasLength(2));
|
||||
|
||||
// The origin entry is setup but its path should remain unchanged.
|
||||
final TestHistoryEntry originEntry = strategy.history[0];
|
||||
expect(originEntry.state, originState);
|
||||
expect(originEntry.state, _wrapOriginState('initial state'));
|
||||
expect(originEntry.url, '/initial');
|
||||
|
||||
// The flutter entry is pushed and its path should be derived from the
|
||||
@ -71,15 +82,12 @@ void testMain() {
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('browser back button pops routes correctly', () async {
|
||||
strategy =
|
||||
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
|
||||
|
||||
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
|
||||
// Initially, we should be on the flutter entry.
|
||||
expect(strategy.history, hasLength(2));
|
||||
expect(strategy.currentEntry.state, flutterState);
|
||||
expect(strategy.currentEntry.url, '/home');
|
||||
|
||||
pushRoute('/page1');
|
||||
await routeUpdated('/page1');
|
||||
// The number of entries shouldn't change.
|
||||
expect(strategy.history, hasLength(2));
|
||||
expect(strategy.currentEntryIndex, 1);
|
||||
@ -107,11 +115,10 @@ void testMain() {
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('multiple browser back clicks', () async {
|
||||
strategy =
|
||||
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
|
||||
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
|
||||
|
||||
pushRoute('/page1');
|
||||
pushRoute('/page2');
|
||||
await routeUpdated('/page1');
|
||||
await routeUpdated('/page2');
|
||||
|
||||
// Make sure we are on page2.
|
||||
expect(strategy.history, hasLength(2));
|
||||
@ -128,7 +135,7 @@ void testMain() {
|
||||
expect(spy.messages[0].methodArguments, isNull);
|
||||
spy.messages.clear();
|
||||
// 2. The framework sends a `routePopped` platform message.
|
||||
popRoute('/page1');
|
||||
await routeUpdated('/page1');
|
||||
// 3. The history state should reflect that /page1 is currently active.
|
||||
expect(strategy.history, hasLength(2));
|
||||
expect(strategy.currentEntryIndex, 1);
|
||||
@ -144,15 +151,18 @@ void testMain() {
|
||||
expect(spy.messages[0].methodArguments, isNull);
|
||||
spy.messages.clear();
|
||||
// 2. The framework sends a `routePopped` platform message.
|
||||
popRoute('/home');
|
||||
await routeUpdated('/home');
|
||||
// 3. The history state should reflect that /page1 is currently active.
|
||||
expect(strategy.history, hasLength(2));
|
||||
expect(strategy.currentEntryIndex, 1);
|
||||
expect(strategy.currentEntry.state, flutterState);
|
||||
expect(strategy.currentEntry.url, '/home');
|
||||
|
||||
// The next browser back will exit the app.
|
||||
await strategy.back();
|
||||
// The next browser back will exit the app. We store the strategy locally
|
||||
// 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.
|
||||
expect(spy.messages, hasLength(1));
|
||||
expect(spy.messages[0].channel, 'flutter/navigation');
|
||||
@ -164,17 +174,16 @@ void testMain() {
|
||||
await systemNavigatorPop();
|
||||
// 3. The active entry doesn't belong to our history anymore because we
|
||||
// navigated past it.
|
||||
expect(strategy.currentEntryIndex, -1);
|
||||
expect(originalStrategy.currentEntryIndex, -1);
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50836
|
||||
skip: browserEngine == BrowserEngine.edge ||
|
||||
browserEngine == BrowserEngine.webkit);
|
||||
|
||||
test('handle user-provided url', () async {
|
||||
strategy =
|
||||
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
|
||||
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
|
||||
|
||||
await _strategy.simulateUserTypingUrl('/page3');
|
||||
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);
|
||||
@ -185,7 +194,7 @@ void testMain() {
|
||||
expect(spy.messages[0].methodArguments, '/page3');
|
||||
spy.messages.clear();
|
||||
// 2. The framework sends a `routePushed` platform message.
|
||||
pushRoute('/page3');
|
||||
await routeUpdated('/page3');
|
||||
// 3. The history state should reflect that /page3 is currently active.
|
||||
expect(strategy.history, hasLength(3));
|
||||
expect(strategy.currentEntryIndex, 1);
|
||||
@ -201,7 +210,7 @@ void testMain() {
|
||||
expect(spy.messages[0].methodArguments, isNull);
|
||||
spy.messages.clear();
|
||||
// 2. The framework sends a `routePopped` platform message.
|
||||
popRoute('/home');
|
||||
await routeUpdated('/home');
|
||||
// 3. The history state should reflect that /page1 is currently active.
|
||||
expect(strategy.history, hasLength(2));
|
||||
expect(strategy.currentEntryIndex, 1);
|
||||
@ -212,10 +221,9 @@ void testMain() {
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('user types unknown url', () async {
|
||||
strategy =
|
||||
TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'));
|
||||
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
|
||||
|
||||
await _strategy.simulateUserTypingUrl('/unknown');
|
||||
await strategy.simulateUserTypingUrl('/unknown');
|
||||
// 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);
|
||||
@ -236,6 +244,212 @@ void testMain() {
|
||||
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', () {
|
||||
TestPlatformLocation location;
|
||||
|
||||
@ -272,40 +486,30 @@ void testMain() {
|
||||
});
|
||||
}
|
||||
|
||||
void pushRoute(String routeName) {
|
||||
Future<void> routeUpdated(String routeName) {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
window.sendPlatformMessage(
|
||||
'flutter/navigation',
|
||||
codec.encodeMethodCall(MethodCall(
|
||||
'routePushed',
|
||||
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName},
|
||||
'routeUpdated',
|
||||
<String, dynamic>{'routeName': routeName},
|
||||
)),
|
||||
emptyCallback,
|
||||
(_) => completer.complete(),
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void replaceRoute(String routeName) {
|
||||
Future<void> routeInfomrationUpdated(String location, dynamic state) {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
window.sendPlatformMessage(
|
||||
'flutter/navigation',
|
||||
codec.encodeMethodCall(MethodCall(
|
||||
'routeReplaced',
|
||||
<String, dynamic>{'previousRouteName': '/foo', 'routeName': routeName},
|
||||
'routeInformationUpdated',
|
||||
<String, dynamic>{'location': location, 'state': state},
|
||||
)),
|
||||
emptyCallback,
|
||||
);
|
||||
}
|
||||
|
||||
void popRoute(String previousRouteName) {
|
||||
window.sendPlatformMessage(
|
||||
'flutter/navigation',
|
||||
codec.encodeMethodCall(MethodCall(
|
||||
'routePopped',
|
||||
<String, dynamic>{
|
||||
'previousRouteName': previousRouteName,
|
||||
'routeName': '/foo'
|
||||
},
|
||||
)),
|
||||
emptyCallback,
|
||||
(_) => completer.complete(),
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> systemNavigatorPop() {
|
||||
@ -323,6 +527,7 @@ class TestPlatformLocation extends PlatformLocation {
|
||||
String pathname;
|
||||
String search;
|
||||
String hash;
|
||||
dynamic state;
|
||||
|
||||
void onPopState(html.EventListener fn) {
|
||||
throw UnimplementedError();
|
||||
@ -348,7 +553,7 @@ class TestPlatformLocation extends PlatformLocation {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void back() {
|
||||
void back(int count) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
// @dart = 2.6
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
@ -28,68 +29,17 @@ void testMain() {
|
||||
engine.window.locationStrategy = _strategy = null;
|
||||
});
|
||||
|
||||
test('Tracks pushed, replaced and popped routes', () {
|
||||
engine.window.sendPlatformMessage(
|
||||
'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');
|
||||
|
||||
test('Tracks pushed, replaced and popped routes', () async {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
engine.window.sendPlatformMessage(
|
||||
'flutter/navigation',
|
||||
codec.encodeMethodCall(const engine.MethodCall(
|
||||
'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();
|
||||
|
||||
void emptyCallback(ByteData date) {}
|
||||
|
||||
Future<void> setStrategy(TestLocationStrategy newStrategy) async {
|
||||
await window.browserHistory.setLocationStrategy(newStrategy);
|
||||
}
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
test('window.defaultRouteName should not change', () {
|
||||
window.locationStrategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial'));
|
||||
setUp(() async {
|
||||
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');
|
||||
|
||||
// Changing the URL in the address bar later shouldn't affect [window.defaultRouteName].
|
||||
@ -30,17 +38,16 @@ void testMain() {
|
||||
expect(window.defaultRouteName, '/initial');
|
||||
});
|
||||
|
||||
test('window.defaultRouteName should reset after navigation platform message', () {
|
||||
window.locationStrategy = TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial'));
|
||||
test('window.defaultRouteName should reset after navigation platform message', () async {
|
||||
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')));
|
||||
// Reading it multiple times should return the same value.
|
||||
expect(window.defaultRouteName, '/initial');
|
||||
expect(window.defaultRouteName, '/initial');
|
||||
|
||||
window.sendPlatformMessage(
|
||||
'flutter/navigation',
|
||||
JSONMethodCodec().encodeMethodCall(MethodCall(
|
||||
'routePushed',
|
||||
<String, dynamic>{'previousRouteName': '/foo', 'routeName': '/bar'},
|
||||
'routeUpdated',
|
||||
<String, dynamic>{'routeName': '/bar'},
|
||||
)),
|
||||
emptyCallback,
|
||||
);
|
||||
@ -50,22 +57,25 @@ void testMain() {
|
||||
});
|
||||
|
||||
test('can disable location strategy', () async {
|
||||
await window.debugSwitchBrowserHistory(useSingle: true);
|
||||
final testStrategy = TestLocationStrategy.fromEntry(
|
||||
TestHistoryEntry(null, null, '/'),
|
||||
TestHistoryEntry('initial state', null, '/'),
|
||||
);
|
||||
window.locationStrategy = testStrategy;
|
||||
await setStrategy(testStrategy);
|
||||
|
||||
expect(window.locationStrategy, testStrategy);
|
||||
// A single listener should've been setup.
|
||||
expect(testStrategy.listeners, hasLength(1));
|
||||
// The initial entry should be there, plus another "flutter" entry.
|
||||
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.currentEntry, testStrategy.history[1]);
|
||||
|
||||
// Now, let's disable location strategy and make sure things get cleaned up.
|
||||
expect(() => jsSetLocationStrategy(null), returnsNormally);
|
||||
// The locationStrategy is teared down asynchronously.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(window.locationStrategy, isNull);
|
||||
|
||||
// The listener is removed asynchronously.
|
||||
@ -73,8 +83,8 @@ void testMain() {
|
||||
|
||||
// No more listeners.
|
||||
expect(testStrategy.listeners, isEmpty);
|
||||
// History should've moved back to the initial entry.
|
||||
expect(testStrategy.history[0].state, <String, bool>{'origin': true});
|
||||
// History should've moved back to the initial state.
|
||||
expect(testStrategy.history[0].state, "initial state");
|
||||
expect(testStrategy.currentEntry, testStrategy.history[0]);
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user