未验证 提交 3905b9ec 编写于 作者: D Dan Field 提交者: GitHub

Revert "[web] Support custom url strategies (#19134)" (#21687)

This reverts commit 02324994.
上级 fbe68591
......@@ -426,6 +426,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/assets.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_location.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvas_pool.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
......@@ -462,9 +463,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_renderer.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/history.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/js_url_strategy.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/url_strategy.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/history.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/clip.dart
......
......@@ -26,6 +26,7 @@ part 'engine/alarm_clock.dart';
part 'engine/assets.dart';
part 'engine/bitmap_canvas.dart';
part 'engine/browser_detection.dart';
part 'engine/browser_location.dart';
part 'engine/canvaskit/canvas.dart';
part 'engine/canvaskit/canvaskit_canvas.dart';
part 'engine/canvaskit/canvaskit_api.dart';
......@@ -62,9 +63,7 @@ part 'engine/dom_canvas.dart';
part 'engine/dom_renderer.dart';
part 'engine/engine_canvas.dart';
part 'engine/frame_reference.dart';
part 'engine/navigation/history.dart';
part 'engine/navigation/js_url_strategy.dart';
part 'engine/navigation/url_strategy.dart';
part 'engine/history.dart';
part 'engine/html/backdrop_filter.dart';
part 'engine/html/canvas.dart';
part 'engine/html/clip.dart';
......
......@@ -5,88 +5,76 @@
// @dart = 2.10
part of engine;
/// Represents and reads route state from the browser's URL.
// TODO(mdebbar): add other strategies.
// Some parts of this file were inspired/copied from the AngularDart router.
/// [LocationStrategy] is responsible for representing and reading route state
/// from the browser's URL.
///
/// By default, the [HashUrlStrategy] subclass is used if the app doesn't
/// specify one.
abstract class UrlStrategy {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const UrlStrategy();
/// At the moment, only one strategy is implemented: [HashLocationStrategy].
///
/// This is used by [BrowserHistory] to interact with browser history APIs.
abstract class LocationStrategy {
const LocationStrategy();
/// Adds a listener to the `popstate` event and returns a function that, when
/// invoked, removes the listener.
ui.VoidCallback addPopStateListener(html.EventListener fn);
/// Subscribes to popstate events and returns a function that could be used to
/// unsubscribe from popstate events.
ui.VoidCallback onPopState(html.EventListener fn);
/// Returns the active path in the browser.
String getPath();
/// The active path in the browser history.
String get path;
/// The state of the current browser history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
Object? getState();
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);
/// Push a new history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
void pushState(Object? state, String title, String url);
void pushState(dynamic state, String title, String url);
/// Replace the currently active history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
void replaceState(Object? state, String title, String url);
void replaceState(dynamic state, String title, String url);
/// Moves forwards or backwards through the history stack.
///
/// A negative [count] value causes a backward move in the history stack. And
/// a positive [count] value causs a forward move.
///
/// Examples:
///
/// * `go(-2)` moves back 2 steps in history.
/// * `go(3)` moves forward 3 steps in hisotry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
Future<void> go(int count);
/// Go to the previous history entry.
Future<void> back({int count = 1});
}
/// This is an implementation of [UrlStrategy] that uses the browser URL's
/// This is an implementation of [LocationStrategy] that uses the browser URL's
/// [hash fragments](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax)
/// to represent its state.
///
/// In order to use this [UrlStrategy] for an app, it needs to be set like this:
/// In order to use this [LocationStrategy] for an app, it needs to be set in
/// [ui.window.locationStrategy]:
///
/// ```dart
/// import 'package:flutter_web_plugins/flutter_web_plugins.dart';
/// import 'package:flutter_web/material.dart';
/// import 'package:flutter_web/ui.dart' as ui;
///
/// // Somewhere before calling `runApp()` do:
/// setUrlStrategy(const HashUrlStrategy());
/// void main() {
/// ui.window.locationStrategy = const ui.HashLocationStrategy();
/// runApp(MyApp());
/// }
/// ```
class HashUrlStrategy extends UrlStrategy {
/// Creates an instance of [HashUrlStrategy].
///
/// The [PlatformLocation] parameter is useful for testing to mock out browser
/// interations.
const HashUrlStrategy(
[this._platformLocation = const BrowserPlatformLocation()]);
class HashLocationStrategy extends LocationStrategy {
final PlatformLocation _platformLocation;
const HashLocationStrategy(
[this._platformLocation = const BrowserPlatformLocation()]);
@override
ui.VoidCallback addPopStateListener(html.EventListener fn) {
_platformLocation.addPopStateListener(fn);
return () => _platformLocation.removePopStateListener(fn);
ui.VoidCallback onPopState(html.EventListener fn) {
_platformLocation.onPopState(fn);
return () => _platformLocation.offPopState(fn);
}
@override
String getPath() {
String get path {
// the hash value is always prefixed with a `#`
// and if it is empty then it will stay empty
final String path = _platformLocation.hash ?? '';
String path = _platformLocation.hash ?? '';
assert(path.isEmpty || path.startsWith('#'));
// We don't want to return an empty string as a path. Instead we default to "/".
......@@ -98,7 +86,7 @@ class HashUrlStrategy extends UrlStrategy {
}
@override
Object? getState() => _platformLocation.state;
dynamic get state => _platformLocation.state;
@override
String prepareExternalUrl(String internalUrl) {
......@@ -112,29 +100,29 @@ class HashUrlStrategy extends UrlStrategy {
}
@override
void pushState(Object? state, String title, String url) {
void pushState(dynamic state, String title, String url) {
_platformLocation.pushState(state, title, prepareExternalUrl(url));
}
@override
void replaceState(Object? state, String title, String url) {
void replaceState(dynamic state, String title, String url) {
_platformLocation.replaceState(state, title, prepareExternalUrl(url));
}
@override
Future<void> go(int count) {
_platformLocation.go(count);
Future<void> back({int count = 1}) {
_platformLocation.back(count);
return _waitForPopState();
}
/// Waits until the next popstate event is fired.
///
/// This is useful, for example, to wait until the browser has handled the
/// This is useful for example to wait until the browser has handled the
/// `history.back` transition.
Future<void> _waitForPopState() {
final Completer<void> completer = Completer<void>();
late ui.VoidCallback unsubscribe;
unsubscribe = addPopStateListener((_) {
unsubscribe = onPopState((_) {
unsubscribe();
completer.complete();
});
......@@ -142,128 +130,58 @@ class HashUrlStrategy extends UrlStrategy {
}
}
/// Wraps a custom implementation of [UrlStrategy] that was previously converted
/// to a [JsUrlStrategy].
class CustomUrlStrategy extends UrlStrategy {
/// Wraps the [delegate] in a [CustomUrlStrategy] instance.
CustomUrlStrategy.fromJs(this.delegate);
final JsUrlStrategy delegate;
@override
ui.VoidCallback addPopStateListener(html.EventListener fn) =>
delegate.addPopStateListener(fn);
@override
String getPath() => delegate.getPath();
@override
Object? getState() => delegate.getState();
@override
String prepareExternalUrl(String internalUrl) =>
delegate.prepareExternalUrl(internalUrl);
@override
void pushState(Object? state, String title, String url) =>
delegate.pushState(state, title, url);
@override
void replaceState(Object? state, String title, String url) =>
delegate.replaceState(state, title, url);
@override
Future<void> go(int count) => delegate.go(count);
}
/// Encapsulates all calls to DOM apis, which allows the [UrlStrategy] classes
/// to be platform agnostic and testable.
/// [PlatformLocation] encapsulates all calls to DOM apis, which allows the
/// [LocationStrategy] classes to be platform agnostic and testable.
///
/// For convenience, the [PlatformLocation] class can be used by implementations
/// of [UrlStrategy] to interact with DOM apis like pushState, popState, etc.
/// The [PlatformLocation] class is used directly by all implementations of
/// [LocationStrategy] when they need to interact with the DOM apis like
/// pushState, popState, etc...
abstract class PlatformLocation {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const PlatformLocation();
/// Registers an event listener for the `popstate` event.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate
void addPopStateListener(html.EventListener fn);
void onPopState(html.EventListener fn);
void offPopState(html.EventListener fn);
/// Unregisters the given listener (added by [addPopStateListener]) from the
/// `popstate` event.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate
void removePopStateListener(html.EventListener fn);
void onHashChange(html.EventListener fn);
void offHashChange(html.EventListener fn);
/// The `pathname` part of the URL in the browser address bar.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname
String get pathname;
/// The `query` part of the URL in the browser address bar.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/search
String get search;
/// The `hash]` part of the URL in the browser address bar.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Location/hash
String? get hash;
dynamic get state;
/// The `state` in the current history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
Object? get state;
/// Adds a new entry to the browser history stack.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
void pushState(Object? state, String title, String url);
/// Replaces the current entry in the browser history stack.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
void replaceState(Object? state, String title, String url);
/// Moves forwards or backwards through the history stack.
///
/// A negative [count] value causes a backward move in the history stack. And
/// a positive [count] value causs a forward move.
///
/// Examples:
///
/// * `go(-2)` moves back 2 steps in history.
/// * `go(3)` moves forward 3 steps in hisotry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
void go(int count);
/// The base href where the Flutter app is being served.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
String? getBaseHref();
void pushState(dynamic state, String title, String url);
void replaceState(dynamic state, String title, String url);
void back(int count);
}
/// Delegates to real browser APIs to provide platform location functionality.
/// An implementation of [PlatformLocation] for the browser.
class BrowserPlatformLocation extends PlatformLocation {
/// Default constructor for [BrowserPlatformLocation].
const BrowserPlatformLocation();
html.Location get _location => html.window.location;
html.History get _history => html.window.history;
const BrowserPlatformLocation();
@override
void addPopStateListener(html.EventListener fn) {
void onPopState(html.EventListener fn) {
html.window.addEventListener('popstate', fn);
}
@override
void removePopStateListener(html.EventListener fn) {
void offPopState(html.EventListener fn) {
html.window.removeEventListener('popstate', fn);
}
@override
void onHashChange(html.EventListener fn) {
html.window.addEventListener('hashchange', fn);
}
@override
void offHashChange(html.EventListener fn) {
html.window.removeEventListener('hashchange', fn);
}
@override
String get pathname => _location.pathname!;
......@@ -274,23 +192,20 @@ class BrowserPlatformLocation extends PlatformLocation {
String get hash => _location.hash;
@override
Object? get state => _history.state;
dynamic get state => _history.state;
@override
void pushState(Object? state, String title, String url) {
void pushState(dynamic state, String title, String url) {
_history.pushState(state, title, url);
}
@override
void replaceState(Object? state, String title, String url) {
void replaceState(dynamic state, String title, String url) {
_history.replaceState(state, title, url);
}
@override
void go(int count) {
_history.go(count);
void back(int count) {
_history.go(-count);
}
@override
String? getBaseHref() => html.document.baseUri;
}
......@@ -25,39 +25,64 @@ abstract class BrowserHistory {
late ui.VoidCallback _unsubscribe;
/// The strategy to interact with html browser history.
UrlStrategy? get urlStrategy;
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);
}
}
bool _isDisposed = false;
Future<void> _setupStrategy(LocationStrategy? strategy) async {
if (strategy == null) {
return;
}
_unsubscribe = strategy.onPopState(onPopState as dynamic Function(html.Event));
await setup();
}
void _setupStrategy(UrlStrategy strategy) {
_unsubscribe = strategy.addPopStateListener(
onPopState as html.EventListener,
);
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 (urlStrategy != null) {
await tearDown();
if (_locationStrategy != null) {
await _tearoffStrategy(_locationStrategy);
// Now the history should be in the original state, back one more time to
// exit the application.
await urlStrategy!.go(-1);
await _locationStrategy!.back();
_locationStrategy = null;
}
}
/// This method does the same thing as the browser back button.
Future<void> back() async {
return urlStrategy?.go(-1);
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 => urlStrategy?.getPath() ?? '/';
String get currentPath => locationStrategy?.path ?? '/';
/// The state of the current location of the user's browser.
Object? get currentState => urlStrategy?.getState();
dynamic get currentState => locationStrategy?.state;
/// Update the url with the given [routeName] and [state].
void setRouteName(String? routeName, {Object? state});
void setRouteName(String? routeName, {dynamic? state});
/// A callback method to handle browser backward or forward buttons.
///
......@@ -65,9 +90,12 @@ abstract class BrowserHistory {
/// applications accordingly.
void onPopState(covariant html.PopStateEvent event);
/// Sets up any prerequisites to use this browser history class.
Future<void> setup() => Future<void>.value();
/// Restore any modifications to the html browser history during the lifetime
/// of this class.
Future<void> tearDown();
Future<void> tearDown() => Future<void>.value();
}
/// A browser history class that creates a set of browser history entries to
......@@ -85,51 +113,31 @@ abstract class BrowserHistory {
/// * [SingleEntryBrowserHistory], which is used when the framework does not use
/// a Router for routing.
class MultiEntriesBrowserHistory extends BrowserHistory {
MultiEntriesBrowserHistory({required this.urlStrategy}) {
final UrlStrategy? strategy = urlStrategy;
if (strategy == null) {
return;
}
_setupStrategy(strategy);
if (!_hasSerialCount(currentState)) {
strategy.replaceState(
_tagWithSerialCount(currentState, 0), 'flutter', currentPath);
}
// If we restore from a page refresh, the _currentSerialCount may not be 0.
_lastSeenSerialCount = _currentSerialCount;
}
@override
final UrlStrategy? urlStrategy;
late int _lastSeenSerialCount;
int get _currentSerialCount {
if (_hasSerialCount(currentState)) {
final Map<dynamic, dynamic> stateMap =
currentState as Map<dynamic, dynamic>;
return stateMap['serialCount'] as int;
return currentState['serialCount'] as int;
}
return 0;
}
Object _tagWithSerialCount(Object? originialState, int count) {
return <dynamic, dynamic>{
dynamic _tagWithSerialCount(dynamic originialState, int count) {
return <dynamic, dynamic> {
'serialCount': count,
'state': originialState,
};
}
bool _hasSerialCount(Object? state) {
bool _hasSerialCount(dynamic state) {
return state is Map && state['serialCount'] != null;
}
@override
void setRouteName(String? routeName, {Object? state}) {
if (urlStrategy != null) {
void setRouteName(String? routeName, {dynamic? state}) {
if (locationStrategy != null) {
assert(routeName != null);
_lastSeenSerialCount += 1;
urlStrategy!.pushState(
locationStrategy!.pushState(
_tagWithSerialCount(state, _lastSeenSerialCount),
'flutter',
routeName!,
......@@ -139,51 +147,58 @@ class MultiEntriesBrowserHistory extends BrowserHistory {
@override
void onPopState(covariant html.PopStateEvent event) {
assert(urlStrategy != null);
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.
urlStrategy!.replaceState(
_tagWithSerialCount(event.state, _lastSeenSerialCount + 1),
'flutter',
currentPath);
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'],
})),
MethodCall('pushRouteInformation', <dynamic, dynamic>{
'location': currentPath,
'state': event.state?['state'],
})
),
(_) {},
);
}
}
@override
Future<void> tearDown() async {
if (_isDisposed || urlStrategy == null) {
return;
Future<void> setup() {
if (!_hasSerialCount(currentState)) {
locationStrategy!.replaceState(
_tagWithSerialCount(currentState, 0),
'flutter',
currentPath
);
}
_isDisposed = true;
_unsubscribe();
// 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 urlStrategy!.go(-backCount);
await locationStrategy!.back(count: backCount);
}
// Unwrap state.
assert(_hasSerialCount(currentState) && _currentSerialCount == 0);
final Map<dynamic, dynamic> stateMap =
currentState as Map<dynamic, dynamic>;
urlStrategy!.replaceState(
stateMap['state'],
locationStrategy!.replaceState(
currentState['state'],
'flutter',
currentPath,
);
......@@ -207,61 +222,37 @@ class MultiEntriesBrowserHistory extends BrowserHistory {
/// * [MultiEntriesBrowserHistory], which is used when the framework uses a
/// Router for routing.
class SingleEntryBrowserHistory extends BrowserHistory {
SingleEntryBrowserHistory({required this.urlStrategy}) {
final UrlStrategy? strategy = urlStrategy;
if (strategy == null) {
return;
}
_setupStrategy(strategy);
final String path = currentPath;
if (!_isFlutterEntry(html.window.history.state)) {
// An entry may not have come from Flutter, for example, when the user
// refreshes the page. They land directly on the "flutter" entry, so
// there's no need to setup the "origin" and "flutter" entries, we can
// safely assume they are already setup.
_setupOriginEntry(strategy);
_setupFlutterEntry(strategy, replace: false, path: path);
}
}
@override
final UrlStrategy? urlStrategy;
static const MethodCall _popRouteMethodCall = MethodCall('popRoute');
static const String _kFlutterTag = 'flutter';
static const String _kOriginTag = 'origin';
Map<String, dynamic> _wrapOriginState(Object? state) {
Map<String, dynamic> _wrapOriginState(dynamic state) {
return <String, dynamic>{_kOriginTag: true, 'state': state};
}
Object? _unwrapOriginState(Object? 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(Object? state) {
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(Object? state) {
bool _isFlutterEntry(dynamic state) {
return state is Map && state[_kFlutterTag] == true;
}
@override
void setRouteName(String? routeName, {Object? state}) {
if (urlStrategy != null) {
_setupFlutterEntry(urlStrategy!, replace: true, path: routeName);
void setRouteName(String? routeName, {dynamic? state}) {
if (locationStrategy != null) {
_setupFlutterEntry(locationStrategy!, replace: true, path: routeName);
}
}
......@@ -269,7 +260,7 @@ class SingleEntryBrowserHistory extends BrowserHistory {
@override
void onPopState(covariant html.PopStateEvent event) {
if (_isOriginEntry(event.state)) {
_setupFlutterEntry(urlStrategy!);
_setupFlutterEntry(_locationStrategy!);
// 2. Send a 'popRoute' platform message so the app can handle it accordingly.
if (window._onPlatformMessage != null) {
......@@ -311,14 +302,14 @@ class SingleEntryBrowserHistory extends BrowserHistory {
// 2. Then we remove the new entry.
// This will take us back to our "flutter" entry and it causes a new
// popstate event that will be handled in the "else if" section above.
urlStrategy!.go(-1);
_locationStrategy!.back();
}
}
/// This method should be called when the Origin Entry is active. It just
/// replaces the state of the entry so that we can recognize it later using
/// [_isOriginEntry] inside [_popStateListener].
void _setupOriginEntry(UrlStrategy strategy) {
void _setupOriginEntry(LocationStrategy strategy) {
assert(strategy != null); // ignore: unnecessary_null_comparison
strategy.replaceState(_wrapOriginState(currentState), 'origin', '');
}
......@@ -326,7 +317,7 @@ class SingleEntryBrowserHistory extends BrowserHistory {
/// This method is used manipulate the Flutter Entry which is always the
/// active entry while the Flutter app is running.
void _setupFlutterEntry(
UrlStrategy strategy, {
LocationStrategy strategy, {
bool replace = false,
String? path,
}) {
......@@ -340,17 +331,27 @@ class SingleEntryBrowserHistory extends BrowserHistory {
}
@override
Future<void> tearDown() async {
if (_isDisposed || urlStrategy == null) {
return;
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
// will land directly on the "flutter" entry, so there's no need to setup
// the "origin" and "flutter" entries, we can safely assume they are
// already setup.
} else {
_setupOriginEntry(locationStrategy!);
_setupFlutterEntry(locationStrategy!, replace: false, path: path);
}
_isDisposed = true;
_unsubscribe();
return Future<void>.value();
}
// We need to remove the flutter entry that we pushed in setup.
await urlStrategy!.go(-1);
// Restores original state.
urlStrategy!
.replaceState(_unwrapOriginState(currentState), 'flutter', currentPath);
@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);
}
}
}
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.10
part of engine;
typedef _PathGetter = String Function();
typedef _StateGetter = Object? Function();
typedef _AddPopStateListener = ui.VoidCallback Function(html.EventListener);
typedef _StringToString = String Function(String);
typedef _StateOperation = void Function(
Object? state, String title, String url);
typedef _HistoryMove = Future<void> Function(int count);
/// The JavaScript representation of a URL strategy.
///
/// This is used to pass URL strategy implementations across a JS-interop
/// bridge from the app to the engine.
@JS()
@anonymous
abstract class JsUrlStrategy {
/// Creates an instance of [JsUrlStrategy] from a bag of URL strategy
/// functions.
external factory JsUrlStrategy({
required _PathGetter getPath,
required _StateGetter getState,
required _AddPopStateListener addPopStateListener,
required _StringToString prepareExternalUrl,
required _StateOperation pushState,
required _StateOperation replaceState,
required _HistoryMove go,
});
/// Adds a listener to the `popstate` event and returns a function that, when
/// invoked, removes the listener.
external ui.VoidCallback addPopStateListener(html.EventListener fn);
/// Returns the active path in the browser.
external String getPath();
/// Returns the history state in the browser.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
external Object? getState();
/// Given a path that's internal to the app, create the external url that
/// will be used in the browser.
external String prepareExternalUrl(String internalUrl);
/// Push a new history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
external void pushState(Object? state, String title, String url);
/// Replace the currently active history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
external void replaceState(Object? state, String title, String url);
/// Moves forwards or backwards through the history stack.
///
/// A negative [count] value causes a backward move in the history stack. And
/// a positive [count] value causs a forward move.
///
/// Examples:
///
/// * `go(-2)` moves back 2 steps in history.
/// * `go(3)` moves forward 3 steps in hisotry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
external Future<void> go(int count);
}
......@@ -20,27 +20,29 @@ class TestHistoryEntry {
}
}
/// This URL strategy mimics the browser's history as closely as possible
/// This location strategy mimics the browser's history as closely as possible
/// while doing it all in memory with no interaction with the browser.
///
/// It keeps a list of history entries and event listeners in memory and
/// manipulates them in order to achieve the desired functionality.
class TestUrlStrategy extends UrlStrategy {
/// Creates a instance of [TestUrlStrategy] with an empty string as the
class TestLocationStrategy extends LocationStrategy {
/// Creates a instance of [TestLocationStrategy] with an empty string as the
/// path.
factory TestUrlStrategy() => TestUrlStrategy.fromEntry(TestHistoryEntry(null, null, ''));
factory TestLocationStrategy() => TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, ''));
/// Creates an instance of [TestUrlStrategy] and populates it with a list
/// Creates an instance of [TestLocationStrategy] and populates it with a list
/// that has [initialEntry] as the only item.
TestUrlStrategy.fromEntry(TestHistoryEntry initialEntry)
TestLocationStrategy.fromEntry(TestHistoryEntry initialEntry)
: _currentEntryIndex = 0,
history = <TestHistoryEntry>[initialEntry];
@override
String getPath() => currentEntry.url;
String get path => currentEntry.url;
@override
dynamic getState() => currentEntry.state;
dynamic get state {
return currentEntry.state;
}
int _currentEntryIndex;
int get currentEntryIndex => _currentEntryIndex;
......@@ -103,12 +105,12 @@ class TestUrlStrategy extends UrlStrategy {
}
@override
Future<void> go(int count) {
Future<void> back({int count = 1}) {
assert(withinAppHistory);
// Browsers don't move 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.
return _nextEventLoop(() {
_currentEntryIndex = _currentEntryIndex + count;
_currentEntryIndex = _currentEntryIndex - count;
if (withinAppHistory) {
_firePopStateEvent();
}
......@@ -122,7 +124,7 @@ class TestUrlStrategy extends UrlStrategy {
final List<html.EventListener> listeners = <html.EventListener>[];
@override
ui.VoidCallback addPopStateListener(html.EventListener fn) {
ui.VoidCallback onPopState(html.EventListener fn) {
listeners.add(fn);
return () {
// Schedule a micro task here to avoid removing the listener during
......
......@@ -13,27 +13,20 @@ const bool _debugPrintPlatformMessages = false;
/// This may be overridden in tests, for example, to pump fake frames.
ui.VoidCallback? scheduleFrameCallback;
typedef _JsSetUrlStrategy = void Function(JsUrlStrategy?);
/// A JavaScript hook to customize the URL strategy of a Flutter app.
//
// Keep this js name in sync with flutter_web_plugins. Find it at:
// https://github.com/flutter/flutter/blob/custom_location_strategy/packages/flutter_web_plugins/lib/src/navigation/js_url_strategy.dart
//
// TODO: Add integration test https://github.com/flutter/flutter/issues/66852
@JS('_flutter_web_set_location_strategy')
external set _jsSetUrlStrategy(_JsSetUrlStrategy? newJsSetUrlStrategy);
/// The Web implementation of [ui.Window].
class EngineWindow extends ui.Window {
EngineWindow() {
_addBrightnessMediaQueryListener();
_addUrlStrategyListener();
js.context['_flutter_web_set_location_strategy'] = (LocationStrategy strategy) {
locationStrategy = strategy;
};
registerHotRestartListener(() {
js.context['_flutter_web_set_location_strategy'] = null;
});
}
@override
double get devicePixelRatio =>
_debugDevicePixelRatio ?? browserDevicePixelRatio;
double get devicePixelRatio => _debugDevicePixelRatio ?? browserDevicePixelRatio;
/// Returns device pixel ratio returned by browser.
static double get browserDevicePixelRatio {
......@@ -124,8 +117,7 @@ class EngineWindow extends ui.Window {
double height = 0;
double width = 0;
if (html.window.visualViewport != null) {
height =
html.window.visualViewport!.height!.toDouble() * devicePixelRatio;
height = html.window.visualViewport!.height!.toDouble() * devicePixelRatio;
width = html.window.visualViewport!.width!.toDouble() * devicePixelRatio;
} else {
height = html.window.innerHeight! * devicePixelRatio;
......@@ -134,7 +126,7 @@ class EngineWindow extends ui.Window {
// This method compares the new dimensions with the previous ones.
// Return false if the previous dimensions are not set.
if (_physicalSize != null) {
if(_physicalSize != null) {
// First confirm both height and width are effected.
if (_physicalSize!.height != height && _physicalSize!.width != width) {
// If prior to rotation height is bigger than width it should be the
......@@ -162,41 +154,78 @@ class EngineWindow extends ui.Window {
/// Handles the browser history integration to allow users to use the back
/// button, etc.
@visibleForTesting
BrowserHistory get browserHistory {
return _browserHistory ??=
MultiEntriesBrowserHistory(urlStrategy: const HashUrlStrategy());
}
BrowserHistory get browserHistory => _browserHistory;
BrowserHistory _browserHistory = MultiEntriesBrowserHistory();
BrowserHistory? _browserHistory;
@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 UrlStrategy? strategy = _browserHistory?.urlStrategy;
await _browserHistory?.tearDown();
_browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy);
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();
/// Lazily initialized when the `defaultRouteName` getter is invoked.
///
/// The reason for the lazy initialization is to give enough time for the app to set [urlStrategy]
/// The reason for the lazy initialization is to give enough time for the app to set [locationStrategy]
/// in `lib/src/ui/initialization.dart`.
String? _defaultRouteName;
@override
String get defaultRouteName {
return _defaultRouteName ??= browserHistory.currentPath;
}
String get defaultRouteName => _defaultRouteName ??= _browserHistory.currentPath;
@override
void scheduleFrame() {
if (scheduleFrameCallback == null) {
throw new Exception('scheduleFrameCallback must be initialized first.');
throw new Exception(
'scheduleFrameCallback must be initialized first.');
}
scheduleFrameCallback!();
}
/// Change the strategy to use for handling browser history location.
/// Setting this member will automatically update [_browserHistory].
///
/// By setting this to null, the browser history will be disabled.
set locationStrategy(LocationStrategy? strategy) {
_browserHistory.setLocationStrategy(strategy);
}
/// Returns the currently active location strategy.
@visibleForTesting
LocationStrategy? get locationStrategy => _browserHistory.locationStrategy;
@override
ui.VoidCallback? get onTextScaleFactorChanged => _onTextScaleFactorChanged;
ui.VoidCallback? _onTextScaleFactorChanged;
......@@ -448,8 +477,8 @@ class EngineWindow extends ui.Window {
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnPlatformMessage(String name, ByteData? data,
ui.PlatformMessageResponseCallback callback) {
void invokeOnPlatformMessage(
String name, ByteData? data, ui.PlatformMessageResponseCallback callback) {
_invoke3<String, ByteData?, ui.PlatformMessageResponseCallback>(
_onPlatformMessage,
_onPlatformMessageZone,
......@@ -471,9 +500,7 @@ class EngineWindow extends ui.Window {
/// Wraps the given [callback] in another callback that ensures that the
/// original callback is called in the zone it was registered in.
static ui.PlatformMessageResponseCallback?
_zonedPlatformMessageResponseCallback(
ui.PlatformMessageResponseCallback? callback) {
static ui.PlatformMessageResponseCallback? _zonedPlatformMessageResponseCallback(ui.PlatformMessageResponseCallback? callback) {
if (callback == null) {
return null;
}
......@@ -537,7 +564,7 @@ class EngineWindow extends ui.Window {
final MethodCall decoded = codec.decodeMethodCall(data);
switch (decoded.method) {
case 'SystemNavigator.pop':
browserHistory.exit().then((_) {
_browserHistory.exit().then((_) {
_replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(true));
});
......@@ -558,8 +585,8 @@ class EngineWindow extends ui.Window {
case 'SystemChrome.setPreferredOrientations':
final List<dynamic>? arguments = decoded.arguments;
domRenderer.setPreferredOrientation(arguments).then((bool success) {
_replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(success));
_replyToPlatformMessage(callback,
codec.encodeSuccessEnvelope(success));
});
return;
case 'SystemSound.play':
......@@ -605,8 +632,7 @@ class EngineWindow extends ui.Window {
case 'flutter/platform_views':
if (experimentalUseSkia) {
rasterizer!.surface.viewEmbedder
.handlePlatformViewCall(data, callback);
rasterizer!.surface.viewEmbedder.handlePlatformViewCall(data, callback);
} else {
ui.handlePlatformViewCall(data!, callback!);
}
......@@ -620,11 +646,27 @@ class EngineWindow extends ui.Window {
return;
case 'flutter/navigation':
_handleNavigationMessage(data, callback).then((handled) {
if (!handled && callback != null) {
callback(null);
}
});
const MethodCodec codec = JSONMethodCodec();
final MethodCall decoded = codec.decodeMethodCall(data);
final Map<String, dynamic> message = decoded.arguments as Map<String, dynamic>;
switch (decoded.method) {
case 'routeUpdated':
_useSingleEntryBrowserHistory().then((void data) {
_browserHistory.setRouteName(message['routeName']);
_replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(true));
});
break;
case 'routeInformationUpdated':
assert(_browserHistory is MultiEntriesBrowserHistory);
_browserHistory.setRouteName(
message['location'],
state: message['state'],
);
_replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(true));
break;
}
// As soon as Flutter starts taking control of the app navigation, we
// should reset [_defaultRouteName] to "/" so it doesn't have any
// further effect after this point.
......@@ -643,51 +685,6 @@ class EngineWindow extends ui.Window {
_replyToPlatformMessage(callback, null);
}
@visibleForTesting
Future<void> debugInitializeHistory(
UrlStrategy? strategy, {
required bool useSingle,
}) async {
await _browserHistory?.tearDown();
if (useSingle) {
_browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy);
} else {
_browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy);
}
}
@visibleForTesting
Future<void> debugResetHistory() async {
await _browserHistory?.tearDown();
_browserHistory = null;
}
Future<bool> _handleNavigationMessage(
ByteData? data,
ui.PlatformMessageResponseCallback? callback,
) async {
const MethodCodec codec = JSONMethodCodec();
final MethodCall decoded = codec.decodeMethodCall(data);
final Map<String, dynamic> arguments = decoded.arguments;
switch (decoded.method) {
case 'routeUpdated':
await _useSingleEntryBrowserHistory();
browserHistory.setRouteName(arguments['routeName']);
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return true;
case 'routeInformationUpdated':
assert(browserHistory is MultiEntriesBrowserHistory);
browserHistory.setRouteName(
arguments['location'],
state: arguments['state'],
);
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return true;
}
return false;
}
int _getHapticFeedbackDuration(String? type) {
switch (type) {
case 'HapticFeedbackType.lightImpact':
......@@ -749,8 +746,7 @@ class EngineWindow extends ui.Window {
: ui.Brightness.light);
_brightnessMediaQueryListener = (html.Event event) {
final html.MediaQueryListEvent mqEvent =
event as html.MediaQueryListEvent;
final html.MediaQueryListEvent mqEvent = event as html.MediaQueryListEvent;
_updatePlatformBrightness(
mqEvent.matches! ? ui.Brightness.dark : ui.Brightness.light);
};
......@@ -760,21 +756,6 @@ class EngineWindow extends ui.Window {
});
}
void _addUrlStrategyListener() {
_jsSetUrlStrategy = allowInterop((JsUrlStrategy? jsStrategy) {
assert(
_browserHistory == null,
'Cannot set URL strategy more than once.',
);
final UrlStrategy? strategy =
jsStrategy == null ? null : CustomUrlStrategy.fromJs(jsStrategy);
_browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy);
});
registerHotRestartListener(() {
_jsSetUrlStrategy = null;
});
}
/// Remove the callback function for listening changes in [_brightnessMediaQuery] value.
void _removeBrightnessMediaQueryListener() {
_brightnessMediaQuery.removeListener(_brightnessMediaQueryListener);
......@@ -804,8 +785,7 @@ class EngineWindow extends ui.Window {
}
@visibleForTesting
late Rasterizer? rasterizer =
experimentalUseSkia ? Rasterizer(Surface(HtmlViewEmbedder())) : null;
late Rasterizer? rasterizer = experimentalUseSkia ? Rasterizer(Surface(HtmlViewEmbedder())) : null;
}
bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData? data) {
......@@ -851,8 +831,8 @@ void _invoke1<A>(void callback(A a)?, Zone? zone, A arg) {
}
/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3].
void _invoke3<A1, A2, A3>(void callback(A1 a1, A2 a2, A3 a3)?, Zone? zone,
A1 arg1, A2 arg2, A3 arg3) {
void _invoke3<A1, A2, A3>(
void callback(A1 a1, A2 a2, A3 a3)?, Zone? zone, A1 arg1, A2 arg2, A3 arg3) {
if (callback == null) {
return;
}
......
......@@ -21,6 +21,10 @@ Future<void> webOnlyInitializePlatform({
Future<void> _initializePlatform({
engine.AssetManager? assetManager,
}) async {
if (!debugEmulateFlutterTesterEnvironment) {
engine.window.locationStrategy = const engine.HashLocationStrategy();
}
engine.initializeEngine();
// This needs to be after `webOnlyInitializeEngine` because that is where the
......
......@@ -16,6 +16,11 @@ import 'package:ui/src/engine.dart';
import '../spy.dart';
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};
}
......@@ -43,19 +48,18 @@ void testMain() {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: true);
spy.setUp();
});
tearDown(() async {
spy.tearDown();
await window.debugResetHistory();
await setStrategy(null);
});
test('basic setup works', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await setStrategy(TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial')));
// There should be two entries: origin and flutter.
expect(strategy.history, hasLength(2));
......@@ -78,11 +82,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('browser back button pops routes correctly', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
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);
......@@ -98,7 +98,7 @@ void testMain() {
// No platform messages have been sent so far.
expect(spy.messages, isEmpty);
// Clicking back should take us to page1.
await strategy.go(-1);
await strategy.back();
// First, the framework should've received a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -115,10 +115,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('multiple browser back clicks', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
await routeUpdated('/page1');
await routeUpdated('/page2');
......@@ -130,7 +127,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.go(-1);
await strategy.back();
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -146,7 +143,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page1');
// Back to home.
await strategy.go(-1);
await strategy.back();
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -164,8 +161,8 @@ void testMain() {
// 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.
TestUrlStrategy originalStrategy = strategy;
await originalStrategy.go(-1);
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');
......@@ -184,10 +181,7 @@ void testMain() {
browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it
......@@ -208,7 +202,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page3');
// Back to home.
await strategy.go(-1);
await strategy.back();
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -227,10 +221,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('user types unknown url', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
await strategy.simulateUserTypingUrl('/unknown');
// This delay is necessary to wait for [BrowserHistory] because it
......@@ -257,19 +248,18 @@ void testMain() {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: false);
spy.setUp();
});
tearDown(() async {
spy.tearDown();
await window.debugResetHistory();
await setStrategy(null);
});
test('basic setup works', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await setStrategy(TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial')));
// There should be only one entry.
expect(strategy.history, hasLength(1));
......@@ -283,11 +273,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('browser back button push route infromation correctly', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
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));
......@@ -303,7 +289,7 @@ void testMain() {
// No platform messages have been sent so far.
expect(spy.messages, isEmpty);
// Clicking back should take us to page1.
await strategy.go(-1);
await strategy.back();
// First, the framework should've received a `pushRouteInformation`
// platform message.
expect(spy.messages, hasLength(1));
......@@ -324,10 +310,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('multiple browser back clicks', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
await routeInfomrationUpdated('/page1', 'page1 state');
await routeInfomrationUpdated('/page2', 'page2 state');
......@@ -339,7 +322,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.go(-1);
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -355,7 +338,7 @@ void testMain() {
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// Back to home.
await strategy.go(-1);
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -376,10 +359,7 @@ void testMain() {
browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it
......@@ -401,7 +381,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page3');
// Back to home.
await strategy.go(-1);
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -421,10 +401,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('forward button works', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
await routeInfomrationUpdated('/page1', 'page1 state');
await routeInfomrationUpdated('/page2', 'page2 state');
......@@ -436,7 +413,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.go(-1);
await strategy.back();
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -453,7 +430,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page1');
// Forward to page2
await strategy.go(1);
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');
......@@ -473,7 +450,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
});
group('$HashUrlStrategy', () {
group('$HashLocationStrategy', () {
TestPlatformLocation location;
setUp(() {
......@@ -485,26 +462,26 @@ void testMain() {
});
test('leading slash is optional', () {
final HashUrlStrategy strategy = HashUrlStrategy(location);
final HashLocationStrategy strategy = HashLocationStrategy(location);
location.hash = '#/';
expect(strategy.getPath(), '/');
expect(strategy.path, '/');
location.hash = '#/foo';
expect(strategy.getPath(), '/foo');
expect(strategy.path, '/foo');
location.hash = '#foo';
expect(strategy.getPath(), 'foo');
expect(strategy.path, 'foo');
});
test('path should not be empty', () {
final HashUrlStrategy strategy = HashUrlStrategy(location);
final HashLocationStrategy strategy = HashLocationStrategy(location);
location.hash = '';
expect(strategy.getPath(), '/');
expect(strategy.path, '/');
location.hash = '#';
expect(strategy.getPath(), '/');
expect(strategy.path, '/');
});
});
}
......@@ -552,31 +529,31 @@ class TestPlatformLocation extends PlatformLocation {
String hash;
dynamic state;
@override
void addPopStateListener(html.EventListener fn) {
void onPopState(html.EventListener fn) {
throw UnimplementedError();
}
@override
void removePopStateListener(html.EventListener fn) {
void offPopState(html.EventListener fn) {
throw UnimplementedError();
}
void onHashChange(html.EventListener fn) {
throw UnimplementedError();
}
void offHashChange(html.EventListener fn) {
throw UnimplementedError();
}
@override
void pushState(dynamic state, String title, String url) {
throw UnimplementedError();
}
@override
void replaceState(dynamic state, String title, String url) {
throw UnimplementedError();
}
@override
void go(int count) {
void back(int count) {
throw UnimplementedError();
}
@override
String getBaseHref() => '/';
}
......@@ -10,7 +10,7 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' as engine;
engine.TestUrlStrategy _strategy;
engine.TestLocationStrategy _strategy;
const engine.MethodCodec codec = engine.JSONMethodCodec();
......@@ -21,14 +21,12 @@ void main() {
}
void testMain() {
setUp(() async {
_strategy = engine.TestUrlStrategy();
await engine.window.debugInitializeHistory(_strategy, useSingle: true);
setUp(() {
engine.window.locationStrategy = _strategy = engine.TestLocationStrategy();
});
tearDown(() async {
_strategy = null;
await engine.window.debugResetHistory();
tearDown(() {
engine.window.locationStrategy = _strategy = null;
});
test('Tracks pushed, replaced and popped routes', () async {
......@@ -42,6 +40,6 @@ void testMain() {
(_) => completer.complete(),
);
await completer.future;
expect(_strategy.getPath(), '/foo');
expect(_strategy.path, '/foo');
});
}
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
// @dart = 2.6
import 'dart:async';
import 'dart:html' as html;
import 'dart:js_util' as js_util;
import 'dart:typed_data';
......@@ -11,39 +12,34 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'engine/history_test.dart';
import 'matchers.dart';
const MethodCodec codec = JSONMethodCodec();
void emptyCallback(ByteData data) {}
void emptyCallback(ByteData date) {}
Future<void> setStrategy(TestLocationStrategy newStrategy) async {
await window.browserHistory.setLocationStrategy(newStrategy);
}
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
tearDown(() async {
await window.debugResetHistory();
setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: true);
});
test('window.defaultRouteName should not change', () async {
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
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].
strategy.replaceState(null, null, '/newpath');
window.locationStrategy.replaceState(null, null, '/newpath');
expect(window.defaultRouteName, '/initial');
});
test('window.defaultRouteName should reset after navigation platform message',
() async {
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
), useSingle: true);
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');
......@@ -61,45 +57,45 @@ void testMain() {
});
test('can disable location strategy', () async {
// Disable URL strategy.
expect(() => jsSetUrlStrategy(null), returnsNormally);
// History should be initialized.
expect(window.browserHistory, isNotNull);
// But without a URL strategy.
expect(window.browserHistory.urlStrategy, isNull);
// Current path is always "/" in this case.
expect(window.browserHistory.currentPath, '/');
// Perform some navigation operations.
routeInfomrationUpdated('/foo/bar', null);
// Path should not be updated because URL strategy is disabled.
expect(window.browserHistory.currentPath, '/');
});
test('js interop throws on wrong type', () {
expect(() => jsSetUrlStrategy(123), throwsA(anything));
expect(() => jsSetUrlStrategy('foo'), throwsA(anything));
expect(() => jsSetUrlStrategy(false), throwsA(anything));
});
test('cannot set url strategy after it is initialized', () async {
final testStrategy = TestUrlStrategy.fromEntry(
await window.debugSwitchBrowserHistory(useSingle: true);
final testStrategy = TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/'),
);
await window.debugInitializeHistory(testStrategy, useSingle: true);
expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError));
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, 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.
await Future<void>.delayed(const Duration(milliseconds: 10));
// No more listeners.
expect(testStrategy.listeners, isEmpty);
// History should've moved back to the initial state.
expect(testStrategy.history[0].state, "initial state");
expect(testStrategy.currentEntry, testStrategy.history[0]);
});
test('cannot set url strategy more than once', () async {
// First time is okay.
expect(() => jsSetUrlStrategy(null), returnsNormally);
// Second time is not allowed.
expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError));
test('js interop throws on wrong type', () {
expect(() => jsSetLocationStrategy(123), throwsA(anything));
expect(() => jsSetLocationStrategy('foo'), throwsA(anything));
expect(() => jsSetLocationStrategy(false), throwsA(anything));
});
}
void jsSetUrlStrategy(dynamic strategy) {
void jsSetLocationStrategy(dynamic strategy) {
js_util.callMethod(
html.window,
'_flutter_web_set_location_strategy',
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册