未验证 提交 02324994 编写于 作者: M Mouad Debbar 提交者: GitHub

[web] Support custom url strategies (#19134)

上级 2e085660
......@@ -426,7 +426,6 @@ 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
......@@ -463,7 +462,9 @@ 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.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/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,7 +26,6 @@ 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';
......@@ -63,7 +62,9 @@ part 'engine/dom_canvas.dart';
part 'engine/dom_renderer.dart';
part 'engine/engine_canvas.dart';
part 'engine/frame_reference.dart';
part 'engine/history.dart';
part 'engine/navigation/history.dart';
part 'engine/navigation/js_url_strategy.dart';
part 'engine/navigation/url_strategy.dart';
part 'engine/html/backdrop_filter.dart';
part 'engine/html/canvas.dart';
part 'engine/html/clip.dart';
......
......@@ -25,64 +25,39 @@ abstract class BrowserHistory {
late ui.VoidCallback _unsubscribe;
/// 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);
}
}
UrlStrategy? get urlStrategy;
Future<void> _setupStrategy(LocationStrategy? strategy) async {
if (strategy == null) {
return;
}
_unsubscribe = strategy.onPopState(onPopState as dynamic Function(html.Event));
await setup();
}
bool _isDisposed = false;
Future<void> _tearoffStrategy(LocationStrategy? strategy) async {
if (strategy == null) {
return;
}
_unsubscribe();
await tearDown();
void _setupStrategy(UrlStrategy strategy) {
_unsubscribe = strategy.addPopStateListener(
onPopState as html.EventListener,
);
}
/// Exit this application and return to the previous page.
Future<void> exit() async {
if (_locationStrategy != null) {
await _tearoffStrategy(_locationStrategy);
if (urlStrategy != null) {
await tearDown();
// Now the history should be in the original state, back one more time to
// exit the application.
await _locationStrategy!.back();
_locationStrategy = null;
await urlStrategy!.go(-1);
}
}
/// This method does the same thing as the browser back button.
Future<void> back() {
if (locationStrategy != null) {
return locationStrategy!.back();
}
return Future<void>.value();
Future<void> back() async {
return urlStrategy?.go(-1);
}
/// The path of the current location of the user's browser.
String get currentPath => locationStrategy?.path ?? '/';
String get currentPath => urlStrategy?.getPath() ?? '/';
/// The state of the current location of the user's browser.
dynamic get currentState => locationStrategy?.state;
Object? get currentState => urlStrategy?.getState();
/// Update the url with the given [routeName] and [state].
void setRouteName(String? routeName, {dynamic? state});
void setRouteName(String? routeName, {Object? state});
/// A callback method to handle browser backward or forward buttons.
///
......@@ -90,12 +65,9 @@ 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>.value();
Future<void> tearDown();
}
/// A browser history class that creates a set of browser history entries to
......@@ -113,31 +85,51 @@ 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)) {
return currentState['serialCount'] as int;
final Map<dynamic, dynamic> stateMap =
currentState as Map<dynamic, dynamic>;
return stateMap['serialCount'] as int;
}
return 0;
}
dynamic _tagWithSerialCount(dynamic originialState, int count) {
return <dynamic, dynamic> {
Object _tagWithSerialCount(Object? originialState, int count) {
return <dynamic, dynamic>{
'serialCount': count,
'state': originialState,
};
}
bool _hasSerialCount(dynamic state) {
bool _hasSerialCount(Object? state) {
return state is Map && state['serialCount'] != null;
}
@override
void setRouteName(String? routeName, {dynamic? state}) {
if (locationStrategy != null) {
void setRouteName(String? routeName, {Object? state}) {
if (urlStrategy != null) {
assert(routeName != null);
_lastSeenSerialCount += 1;
locationStrategy!.pushState(
urlStrategy!.pushState(
_tagWithSerialCount(state, _lastSeenSerialCount),
'flutter',
routeName!,
......@@ -147,58 +139,51 @@ class MultiEntriesBrowserHistory extends BrowserHistory {
@override
void onPopState(covariant html.PopStateEvent event) {
assert(locationStrategy != null);
assert(urlStrategy != 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);
urlStrategy!.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> setup() {
if (!_hasSerialCount(currentState)) {
locationStrategy!.replaceState(
_tagWithSerialCount(currentState, 0),
'flutter',
currentPath
);
Future<void> tearDown() async {
if (_isDisposed || urlStrategy == null) {
return;
}
// If we retore from a page refresh, the _currentSerialCount may not be 0.
_lastSeenSerialCount = _currentSerialCount;
return Future<void>.value();
}
_isDisposed = true;
_unsubscribe();
@override
Future<void> tearDown() async {
// Restores the html browser history.
assert(_hasSerialCount(currentState));
int backCount = _currentSerialCount;
if (backCount > 0) {
await locationStrategy!.back(count: backCount);
await urlStrategy!.go(-backCount);
}
// Unwrap state.
assert(_hasSerialCount(currentState) && _currentSerialCount == 0);
locationStrategy!.replaceState(
currentState['state'],
final Map<dynamic, dynamic> stateMap =
currentState as Map<dynamic, dynamic>;
urlStrategy!.replaceState(
stateMap['state'],
'flutter',
currentPath,
);
......@@ -222,37 +207,61 @@ 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(dynamic state) {
Map<String, dynamic> _wrapOriginState(Object? state) {
return <String, dynamic>{_kOriginTag: true, 'state': state};
}
dynamic _unwrapOriginState(dynamic state) {
Object? _unwrapOriginState(Object? 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) {
bool _isOriginEntry(Object? state) {
return state is Map && state[_kOriginTag] == true;
}
/// The flutter entry is a history entry that we maintain on top of the origin
/// entry. It allows us to catch popstate events when the user hits the back
/// button.
bool _isFlutterEntry(dynamic state) {
bool _isFlutterEntry(Object? state) {
return state is Map && state[_kFlutterTag] == true;
}
@override
void setRouteName(String? routeName, {dynamic? state}) {
if (locationStrategy != null) {
_setupFlutterEntry(locationStrategy!, replace: true, path: routeName);
void setRouteName(String? routeName, {Object? state}) {
if (urlStrategy != null) {
_setupFlutterEntry(urlStrategy!, replace: true, path: routeName);
}
}
......@@ -260,7 +269,7 @@ class SingleEntryBrowserHistory extends BrowserHistory {
@override
void onPopState(covariant html.PopStateEvent event) {
if (_isOriginEntry(event.state)) {
_setupFlutterEntry(_locationStrategy!);
_setupFlutterEntry(urlStrategy!);
// 2. Send a 'popRoute' platform message so the app can handle it accordingly.
if (window._onPlatformMessage != null) {
......@@ -302,14 +311,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.
_locationStrategy!.back();
urlStrategy!.go(-1);
}
}
/// 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(LocationStrategy strategy) {
void _setupOriginEntry(UrlStrategy strategy) {
assert(strategy != null); // ignore: unnecessary_null_comparison
strategy.replaceState(_wrapOriginState(currentState), 'origin', '');
}
......@@ -317,7 +326,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(
LocationStrategy strategy, {
UrlStrategy strategy, {
bool replace = false,
String? path,
}) {
......@@ -330,28 +339,18 @@ class SingleEntryBrowserHistory extends BrowserHistory {
}
}
@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
// 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);
}
return Future<void>.value();
}
@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);
if (_isDisposed || urlStrategy == null) {
return;
}
_isDisposed = true;
_unsubscribe();
// 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);
}
}
// 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);
}
......@@ -5,76 +5,88 @@
// @dart = 2.10
part of engine;
// 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.
/// Represents and reads route state from the browser's URL.
///
/// 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();
/// 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();
/// Subscribes to popstate events and returns a function that could be used to
/// unsubscribe from popstate events.
ui.VoidCallback onPopState(html.EventListener fn);
/// Adds a listener to the `popstate` event and returns a function that, when
/// invoked, removes the listener.
ui.VoidCallback addPopStateListener(html.EventListener fn);
/// The active path in the browser history.
String get path;
/// Returns the active path in the browser.
String getPath();
/// The state of the current browser history entry.
dynamic get state;
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
Object? getState();
/// 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.
void pushState(dynamic state, String title, String url);
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
void pushState(Object? state, String title, String url);
/// Replace the currently active history entry.
void replaceState(dynamic state, String title, String url);
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
void replaceState(Object? state, String title, String url);
/// Go to the previous history entry.
Future<void> back({int count = 1});
/// 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);
}
/// This is an implementation of [LocationStrategy] that uses the browser URL's
/// This is an implementation of [UrlStrategy] 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 [LocationStrategy] for an app, it needs to be set in
/// [ui.window.locationStrategy]:
/// In order to use this [UrlStrategy] for an app, it needs to be set like this:
///
/// ```dart
/// import 'package:flutter_web/material.dart';
/// import 'package:flutter_web/ui.dart' as ui;
/// import 'package:flutter_web_plugins/flutter_web_plugins.dart';
///
/// void main() {
/// ui.window.locationStrategy = const ui.HashLocationStrategy();
/// runApp(MyApp());
/// }
/// // Somewhere before calling `runApp()` do:
/// setUrlStrategy(const HashUrlStrategy());
/// ```
class HashLocationStrategy extends LocationStrategy {
final PlatformLocation _platformLocation;
const HashLocationStrategy(
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()]);
final PlatformLocation _platformLocation;
@override
ui.VoidCallback onPopState(html.EventListener fn) {
_platformLocation.onPopState(fn);
return () => _platformLocation.offPopState(fn);
ui.VoidCallback addPopStateListener(html.EventListener fn) {
_platformLocation.addPopStateListener(fn);
return () => _platformLocation.removePopStateListener(fn);
}
@override
String get path {
String getPath() {
// the hash value is always prefixed with a `#`
// and if it is empty then it will stay empty
String path = _platformLocation.hash ?? '';
final 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 "/".
......@@ -86,7 +98,7 @@ class HashLocationStrategy extends LocationStrategy {
}
@override
dynamic get state => _platformLocation.state;
Object? getState() => _platformLocation.state;
@override
String prepareExternalUrl(String internalUrl) {
......@@ -100,29 +112,29 @@ class HashLocationStrategy extends LocationStrategy {
}
@override
void pushState(dynamic state, String title, String url) {
void pushState(Object? state, String title, String url) {
_platformLocation.pushState(state, title, prepareExternalUrl(url));
}
@override
void replaceState(dynamic state, String title, String url) {
void replaceState(Object? state, String title, String url) {
_platformLocation.replaceState(state, title, prepareExternalUrl(url));
}
@override
Future<void> back({int count = 1}) {
_platformLocation.back(count);
Future<void> go(int count) {
_platformLocation.go(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 = onPopState((_) {
unsubscribe = addPopStateListener((_) {
unsubscribe();
completer.complete();
});
......@@ -130,58 +142,128 @@ class HashLocationStrategy extends LocationStrategy {
}
}
/// [PlatformLocation] encapsulates all calls to DOM apis, which allows the
/// [LocationStrategy] classes to be platform agnostic and testable.
/// 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.
///
/// The [PlatformLocation] class is used directly by all implementations of
/// [LocationStrategy] when they need to interact with the DOM apis like
/// pushState, popState, etc...
/// For convenience, the [PlatformLocation] class can be used by implementations
/// of [UrlStrategy] to interact with 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();
void onPopState(html.EventListener fn);
void offPopState(html.EventListener fn);
/// 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 onHashChange(html.EventListener fn);
void offHashChange(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);
/// 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;
void pushState(dynamic state, String title, String url);
void replaceState(dynamic state, String title, String url);
void back(int count);
/// 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();
}
/// An implementation of [PlatformLocation] for the browser.
/// Delegates to real browser APIs to provide platform location functionality.
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 onPopState(html.EventListener fn) {
void addPopStateListener(html.EventListener fn) {
html.window.addEventListener('popstate', fn);
}
@override
void offPopState(html.EventListener fn) {
void removePopStateListener(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!;
......@@ -192,20 +274,23 @@ class BrowserPlatformLocation extends PlatformLocation {
String get hash => _location.hash;
@override
dynamic get state => _history.state;
Object? get state => _history.state;
@override
void pushState(dynamic state, String title, String url) {
void pushState(Object? state, String title, String url) {
_history.pushState(state, title, url);
}
@override
void replaceState(dynamic state, String title, String url) {
void replaceState(Object? state, String title, String url) {
_history.replaceState(state, title, url);
}
@override
void back(int count) {
_history.go(-count);
void go(int count) {
_history.go(count);
}
@override
String? getBaseHref() => html.document.baseUri;
}
......@@ -20,29 +20,27 @@ class TestHistoryEntry {
}
}
/// This location strategy mimics the browser's history as closely as possible
/// This URL 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 TestLocationStrategy extends LocationStrategy {
/// Creates a instance of [TestLocationStrategy] with an empty string as the
class TestUrlStrategy extends UrlStrategy {
/// Creates a instance of [TestUrlStrategy] with an empty string as the
/// path.
factory TestLocationStrategy() => TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, ''));
factory TestUrlStrategy() => TestUrlStrategy.fromEntry(TestHistoryEntry(null, null, ''));
/// Creates an instance of [TestLocationStrategy] and populates it with a list
/// Creates an instance of [TestUrlStrategy] and populates it with a list
/// that has [initialEntry] as the only item.
TestLocationStrategy.fromEntry(TestHistoryEntry initialEntry)
TestUrlStrategy.fromEntry(TestHistoryEntry initialEntry)
: _currentEntryIndex = 0,
history = <TestHistoryEntry>[initialEntry];
@override
String get path => currentEntry.url;
String getPath() => currentEntry.url;
@override
dynamic get state {
return currentEntry.state;
}
dynamic getState() => currentEntry.state;
int _currentEntryIndex;
int get currentEntryIndex => _currentEntryIndex;
......@@ -105,12 +103,12 @@ class TestLocationStrategy extends LocationStrategy {
}
@override
Future<void> back({int count = 1}) {
Future<void> go(int count) {
assert(withinAppHistory);
// Browsers don't move back in history immediately. They do it at the next
// Browsers don't move 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();
}
......@@ -124,7 +122,7 @@ class TestLocationStrategy extends LocationStrategy {
final List<html.EventListener> listeners = <html.EventListener>[];
@override
ui.VoidCallback onPopState(html.EventListener fn) {
ui.VoidCallback addPopStateListener(html.EventListener fn) {
listeners.add(fn);
return () {
// Schedule a micro task here to avoid removing the listener during
......
......@@ -13,20 +13,27 @@ 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();
js.context['_flutter_web_set_location_strategy'] = (LocationStrategy strategy) {
locationStrategy = strategy;
};
registerHotRestartListener(() {
js.context['_flutter_web_set_location_strategy'] = null;
});
_addUrlStrategyListener();
}
@override
double get devicePixelRatio => _debugDevicePixelRatio ?? browserDevicePixelRatio;
double get devicePixelRatio =>
_debugDevicePixelRatio ?? browserDevicePixelRatio;
/// Returns device pixel ratio returned by browser.
static double get browserDevicePixelRatio {
......@@ -117,7 +124,8 @@ 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;
......@@ -126,7 +134,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
......@@ -154,78 +162,41 @@ class EngineWindow extends ui.Window {
/// Handles the browser history integration to allow users to use the back
/// button, etc.
@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);
BrowserHistory get browserHistory {
return _browserHistory ??=
MultiEntriesBrowserHistory(urlStrategy: const HashUrlStrategy());
}
BrowserHistory? _browserHistory;
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);
final UrlStrategy? strategy = _browserHistory?.urlStrategy;
await _browserHistory?.tearDown();
_browserHistory = SingleEntryBrowserHistory(urlStrategy: 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 [locationStrategy]
/// The reason for the lazy initialization is to give enough time for the app to set [urlStrategy]
/// in `lib/src/ui/initialization.dart`.
String? _defaultRouteName;
@override
String get defaultRouteName => _defaultRouteName ??= _browserHistory.currentPath;
String get defaultRouteName {
return _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;
......@@ -477,8 +448,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,
......@@ -500,7 +471,9 @@ 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;
}
......@@ -564,7 +537,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));
});
......@@ -585,8 +558,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':
......@@ -632,7 +605,8 @@ 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!);
}
......@@ -646,27 +620,11 @@ class EngineWindow extends ui.Window {
return;
case 'flutter/navigation':
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;
}
_handleNavigationMessage(data, callback).then((handled) {
if (!handled && callback != null) {
callback(null);
}
});
// 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.
......@@ -685,6 +643,51 @@ 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':
......@@ -746,7 +749,8 @@ 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);
};
......@@ -756,6 +760,21 @@ 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);
......@@ -785,7 +804,8 @@ 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) {
......@@ -831,8 +851,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,10 +21,6 @@ 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,11 +16,6 @@ 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};
}
......@@ -48,18 +43,19 @@ void testMain() {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: true);
spy.setUp();
});
tearDown(() async {
spy.tearDown();
await setStrategy(null);
await window.debugResetHistory();
});
test('basic setup works', () async {
await setStrategy(TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
// There should be two entries: origin and flutter.
expect(strategy.history, hasLength(2));
......@@ -82,7 +78,11 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('browser back button pops routes correctly', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
// 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.back();
await strategy.go(-1);
// First, the framework should've received a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -115,7 +115,10 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('multiple browser back clicks', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await routeUpdated('/page1');
await routeUpdated('/page2');
......@@ -127,7 +130,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.back();
await strategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -143,7 +146,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page1');
// Back to home.
await strategy.back();
await strategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -161,8 +164,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.
TestLocationStrategy originalStrategy = strategy;
await originalStrategy.back();
TestUrlStrategy originalStrategy = strategy;
await originalStrategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -181,7 +184,10 @@ void testMain() {
browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it
......@@ -202,7 +208,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page3');
// Back to home.
await strategy.back();
await strategy.go(-1);
// 1. The engine sends a `popRoute` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -221,7 +227,10 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('user types unknown url', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry(null, null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
await strategy.simulateUserTypingUrl('/unknown');
// This delay is necessary to wait for [BrowserHistory] because it
......@@ -248,18 +257,19 @@ void testMain() {
final PlatformMessagesSpy spy = PlatformMessagesSpy();
setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: false);
spy.setUp();
});
tearDown(() async {
spy.tearDown();
await setStrategy(null);
await window.debugResetHistory();
});
test('basic setup works', () async {
await setStrategy(TestLocationStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
// There should be only one entry.
expect(strategy.history, hasLength(1));
......@@ -273,7 +283,11 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('browser back button push route infromation correctly', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
// Initially, we should be on the flutter entry.
expect(strategy.history, hasLength(1));
expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0));
......@@ -289,7 +303,7 @@ void testMain() {
// No platform messages have been sent so far.
expect(spy.messages, isEmpty);
// Clicking back should take us to page1.
await strategy.back();
await strategy.go(-1);
// First, the framework should've received a `pushRouteInformation`
// platform message.
expect(spy.messages, hasLength(1));
......@@ -310,7 +324,10 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('multiple browser back clicks', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await routeInfomrationUpdated('/page1', 'page1 state');
await routeInfomrationUpdated('/page2', 'page2 state');
......@@ -322,7 +339,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.back();
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -338,7 +355,7 @@ void testMain() {
expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1));
expect(strategy.currentEntry.url, '/page1');
// Back to home.
await strategy.back();
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -359,7 +376,10 @@ void testMain() {
browserEngine == BrowserEngine.webkit);
test('handle user-provided url', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await strategy.simulateUserTypingUrl('/page3');
// This delay is necessary to wait for [BrowserHistory] because it
......@@ -381,7 +401,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page3');
// Back to home.
await strategy.back();
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -401,7 +421,10 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
test('forward button works', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/home'),
);
await window.debugInitializeHistory(strategy, useSingle: false);
await routeInfomrationUpdated('/page1', 'page1 state');
await routeInfomrationUpdated('/page2', 'page2 state');
......@@ -413,7 +436,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page2');
// Back to page1.
await strategy.back();
await strategy.go(-1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -430,7 +453,7 @@ void testMain() {
expect(strategy.currentEntry.url, '/page1');
// Forward to page2
await strategy.back(count: -1);
await strategy.go(1);
// 1. The engine sends a `pushRouteInformation` platform message.
expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/navigation');
......@@ -450,7 +473,7 @@ void testMain() {
skip: browserEngine == BrowserEngine.edge);
});
group('$HashLocationStrategy', () {
group('$HashUrlStrategy', () {
TestPlatformLocation location;
setUp(() {
......@@ -462,26 +485,26 @@ void testMain() {
});
test('leading slash is optional', () {
final HashLocationStrategy strategy = HashLocationStrategy(location);
final HashUrlStrategy strategy = HashUrlStrategy(location);
location.hash = '#/';
expect(strategy.path, '/');
expect(strategy.getPath(), '/');
location.hash = '#/foo';
expect(strategy.path, '/foo');
expect(strategy.getPath(), '/foo');
location.hash = '#foo';
expect(strategy.path, 'foo');
expect(strategy.getPath(), 'foo');
});
test('path should not be empty', () {
final HashLocationStrategy strategy = HashLocationStrategy(location);
final HashUrlStrategy strategy = HashUrlStrategy(location);
location.hash = '';
expect(strategy.path, '/');
expect(strategy.getPath(), '/');
location.hash = '#';
expect(strategy.path, '/');
expect(strategy.getPath(), '/');
});
});
}
......@@ -529,31 +552,31 @@ class TestPlatformLocation extends PlatformLocation {
String hash;
dynamic state;
void onPopState(html.EventListener fn) {
@override
void addPopStateListener(html.EventListener fn) {
throw UnimplementedError();
}
void offPopState(html.EventListener fn) {
throw UnimplementedError();
}
void onHashChange(html.EventListener fn) {
throw UnimplementedError();
}
void offHashChange(html.EventListener fn) {
@override
void removePopStateListener(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();
}
void back(int count) {
@override
void go(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.TestLocationStrategy _strategy;
engine.TestUrlStrategy _strategy;
const engine.MethodCodec codec = engine.JSONMethodCodec();
......@@ -21,12 +21,14 @@ void main() {
}
void testMain() {
setUp(() {
engine.window.locationStrategy = _strategy = engine.TestLocationStrategy();
setUp(() async {
_strategy = engine.TestUrlStrategy();
await engine.window.debugInitializeHistory(_strategy, useSingle: true);
});
tearDown(() {
engine.window.locationStrategy = _strategy = null;
tearDown(() async {
_strategy = null;
await engine.window.debugResetHistory();
});
test('Tracks pushed, replaced and popped routes', () async {
......@@ -40,6 +42,6 @@ void testMain() {
(_) => completer.complete(),
);
await completer.future;
expect(_strategy.path, '/foo');
expect(_strategy.getPath(), '/foo');
});
}
......@@ -3,7 +3,6 @@
// 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';
......@@ -12,34 +11,39 @@ import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
const MethodCodec codec = JSONMethodCodec();
import 'engine/history_test.dart';
import 'matchers.dart';
void emptyCallback(ByteData date) {}
const MethodCodec codec = JSONMethodCodec();
Future<void> setStrategy(TestLocationStrategy newStrategy) async {
await window.browserHistory.setLocationStrategy(newStrategy);
}
void emptyCallback(ByteData data) {}
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
setUp(() async {
await window.debugSwitchBrowserHistory(useSingle: true);
tearDown(() async {
await window.debugResetHistory();
});
test('window.defaultRouteName should not change', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')));
final TestUrlStrategy strategy = TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
);
await window.debugInitializeHistory(strategy, useSingle: true);
expect(window.defaultRouteName, '/initial');
// Changing the URL in the address bar later shouldn't affect [window.defaultRouteName].
window.locationStrategy.replaceState(null, null, '/newpath');
strategy.replaceState(null, null, '/newpath');
expect(window.defaultRouteName, '/initial');
});
test('window.defaultRouteName should reset after navigation platform message', () async {
await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial')));
test('window.defaultRouteName should reset after navigation platform message',
() async {
await window.debugInitializeHistory(TestUrlStrategy.fromEntry(
TestHistoryEntry('initial state', null, '/initial'),
), useSingle: true);
// Reading it multiple times should return the same value.
expect(window.defaultRouteName, '/initial');
expect(window.defaultRouteName, '/initial');
......@@ -57,45 +61,45 @@ void testMain() {
});
test('can disable location strategy', () async {
await window.debugSwitchBrowserHistory(useSingle: true);
final testStrategy = TestLocationStrategy.fromEntry(
// 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(
TestHistoryEntry('initial state', null, '/'),
);
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]);
await window.debugInitializeHistory(testStrategy, useSingle: true);
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));
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));
});
}
void jsSetLocationStrategy(dynamic strategy) {
void jsSetUrlStrategy(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.
先完成此消息的编辑!
想要评论请 注册