mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Support custom url strategies (flutter/engine#19134)
This commit is contained in:
parent
6d05412a7e
commit
17d107f94a
@ -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';
|
||||
|
||||
@ -1,211 +0,0 @@
|
||||
// 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;
|
||||
|
||||
// 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.
|
||||
///
|
||||
/// 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();
|
||||
|
||||
/// Subscribes to popstate events and returns a function that could be used to
|
||||
/// unsubscribe from popstate events.
|
||||
ui.VoidCallback onPopState(html.EventListener fn);
|
||||
|
||||
/// The active path in the browser history.
|
||||
String get path;
|
||||
|
||||
/// The state of the current browser history entry.
|
||||
dynamic get state;
|
||||
|
||||
/// Given a path that's internal to the app, create the external url that
|
||||
/// will be used in the browser.
|
||||
String prepareExternalUrl(String internalUrl);
|
||||
|
||||
/// Push a new history entry.
|
||||
void pushState(dynamic state, String title, String url);
|
||||
|
||||
/// Replace the currently active history entry.
|
||||
void replaceState(dynamic state, String title, String url);
|
||||
|
||||
/// Go to the previous history entry.
|
||||
Future<void> back({int count = 1});
|
||||
}
|
||||
|
||||
/// 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 [LocationStrategy] for an app, it needs to be set in
|
||||
/// [ui.window.locationStrategy]:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'package:flutter_web/material.dart';
|
||||
/// import 'package:flutter_web/ui.dart' as ui;
|
||||
///
|
||||
/// void main() {
|
||||
/// ui.window.locationStrategy = const ui.HashLocationStrategy();
|
||||
/// runApp(MyApp());
|
||||
/// }
|
||||
/// ```
|
||||
class HashLocationStrategy extends LocationStrategy {
|
||||
final PlatformLocation _platformLocation;
|
||||
|
||||
const HashLocationStrategy(
|
||||
[this._platformLocation = const BrowserPlatformLocation()]);
|
||||
|
||||
@override
|
||||
ui.VoidCallback onPopState(html.EventListener fn) {
|
||||
_platformLocation.onPopState(fn);
|
||||
return () => _platformLocation.offPopState(fn);
|
||||
}
|
||||
|
||||
@override
|
||||
String get path {
|
||||
// the hash value is always prefixed with a `#`
|
||||
// and if it is empty then it will stay empty
|
||||
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 "/".
|
||||
if (path.isEmpty || path == '#') {
|
||||
return '/';
|
||||
}
|
||||
// At this point, we know [path] starts with "#" and isn't empty.
|
||||
return path.substring(1);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic get state => _platformLocation.state;
|
||||
|
||||
@override
|
||||
String prepareExternalUrl(String internalUrl) {
|
||||
// It's convention that if the hash path is empty, we omit the `#`; however,
|
||||
// if the empty URL is pushed it won't replace any existing fragment. So
|
||||
// when the hash path is empty, we instead return the location's path and
|
||||
// query.
|
||||
return internalUrl.isEmpty
|
||||
? '${_platformLocation.pathname}${_platformLocation.search}'
|
||||
: '#$internalUrl';
|
||||
}
|
||||
|
||||
@override
|
||||
void pushState(dynamic state, String title, String url) {
|
||||
_platformLocation.pushState(state, title, prepareExternalUrl(url));
|
||||
}
|
||||
|
||||
@override
|
||||
void replaceState(dynamic state, String title, String url) {
|
||||
_platformLocation.replaceState(state, title, prepareExternalUrl(url));
|
||||
}
|
||||
|
||||
@override
|
||||
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
|
||||
/// `history.back` transition.
|
||||
Future<void> _waitForPopState() {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
late ui.VoidCallback unsubscribe;
|
||||
unsubscribe = onPopState((_) {
|
||||
unsubscribe();
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
/// [PlatformLocation] encapsulates all calls to DOM apis, which allows the
|
||||
/// [LocationStrategy] 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...
|
||||
abstract class PlatformLocation {
|
||||
const PlatformLocation();
|
||||
|
||||
void onPopState(html.EventListener fn);
|
||||
void offPopState(html.EventListener fn);
|
||||
|
||||
void onHashChange(html.EventListener fn);
|
||||
void offHashChange(html.EventListener fn);
|
||||
|
||||
String get pathname;
|
||||
String get search;
|
||||
String? get hash;
|
||||
dynamic get state;
|
||||
|
||||
void pushState(dynamic state, String title, String url);
|
||||
void replaceState(dynamic state, String title, String url);
|
||||
void back(int count);
|
||||
}
|
||||
|
||||
/// An implementation of [PlatformLocation] for the browser.
|
||||
class BrowserPlatformLocation extends PlatformLocation {
|
||||
html.Location get _location => html.window.location;
|
||||
html.History get _history => html.window.history;
|
||||
|
||||
const BrowserPlatformLocation();
|
||||
|
||||
@override
|
||||
void onPopState(html.EventListener fn) {
|
||||
html.window.addEventListener('popstate', fn);
|
||||
}
|
||||
|
||||
@override
|
||||
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!;
|
||||
|
||||
@override
|
||||
String get search => _location.search!;
|
||||
|
||||
@override
|
||||
String get hash => _location.hash;
|
||||
|
||||
@override
|
||||
dynamic get state => _history.state;
|
||||
|
||||
@override
|
||||
void pushState(dynamic state, String title, String url) {
|
||||
_history.pushState(state, title, url);
|
||||
}
|
||||
|
||||
@override
|
||||
void replaceState(dynamic state, String title, String url) {
|
||||
_history.replaceState(state, title, url);
|
||||
}
|
||||
|
||||
@override
|
||||
void back(int count) {
|
||||
_history.go(-count);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
// If we retore from a page refresh, the _currentSerialCount may not be 0.
|
||||
_lastSeenSerialCount = _currentSerialCount;
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> tearDown() async {
|
||||
if (_isDisposed || urlStrategy == null) {
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
_unsubscribe();
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
// 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);
|
||||
}
|
||||
@ -0,0 +1,296 @@
|
||||
// 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;
|
||||
|
||||
/// Represents and reads 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();
|
||||
|
||||
/// Adds a listener to the `popstate` event and returns a function that, when
|
||||
/// invoked, removes the listener.
|
||||
ui.VoidCallback addPopStateListener(html.EventListener fn);
|
||||
|
||||
/// Returns the active path in the browser.
|
||||
String getPath();
|
||||
|
||||
/// The state of the current browser history entry.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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
|
||||
Future<void> go(int count);
|
||||
}
|
||||
|
||||
/// 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 [UrlStrategy] for an app, it needs to be set like this:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||
///
|
||||
/// // Somewhere before calling `runApp()` do:
|
||||
/// setUrlStrategy(const HashUrlStrategy());
|
||||
/// ```
|
||||
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 addPopStateListener(html.EventListener fn) {
|
||||
_platformLocation.addPopStateListener(fn);
|
||||
return () => _platformLocation.removePopStateListener(fn);
|
||||
}
|
||||
|
||||
@override
|
||||
String getPath() {
|
||||
// the hash value is always prefixed with a `#`
|
||||
// and if it is empty then it will stay empty
|
||||
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 "/".
|
||||
if (path.isEmpty || path == '#') {
|
||||
return '/';
|
||||
}
|
||||
// At this point, we know [path] starts with "#" and isn't empty.
|
||||
return path.substring(1);
|
||||
}
|
||||
|
||||
@override
|
||||
Object? getState() => _platformLocation.state;
|
||||
|
||||
@override
|
||||
String prepareExternalUrl(String internalUrl) {
|
||||
// It's convention that if the hash path is empty, we omit the `#`; however,
|
||||
// if the empty URL is pushed it won't replace any existing fragment. So
|
||||
// when the hash path is empty, we instead return the location's path and
|
||||
// query.
|
||||
return internalUrl.isEmpty
|
||||
? '${_platformLocation.pathname}${_platformLocation.search}'
|
||||
: '#$internalUrl';
|
||||
}
|
||||
|
||||
@override
|
||||
void pushState(Object? state, String title, String url) {
|
||||
_platformLocation.pushState(state, title, prepareExternalUrl(url));
|
||||
}
|
||||
|
||||
@override
|
||||
void replaceState(Object? state, String title, String url) {
|
||||
_platformLocation.replaceState(state, title, prepareExternalUrl(url));
|
||||
}
|
||||
|
||||
@override
|
||||
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
|
||||
/// `history.back` transition.
|
||||
Future<void> _waitForPopState() {
|
||||
final Completer<void> completer = Completer<void>();
|
||||
late ui.VoidCallback unsubscribe;
|
||||
unsubscribe = addPopStateListener((_) {
|
||||
unsubscribe();
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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();
|
||||
|
||||
/// 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);
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
@override
|
||||
void addPopStateListener(html.EventListener fn) {
|
||||
html.window.addEventListener('popstate', fn);
|
||||
}
|
||||
|
||||
@override
|
||||
void removePopStateListener(html.EventListener fn) {
|
||||
html.window.removeEventListener('popstate', fn);
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathname => _location.pathname!;
|
||||
|
||||
@override
|
||||
String get search => _location.search!;
|
||||
|
||||
@override
|
||||
String get hash => _location.hash;
|
||||
|
||||
@override
|
||||
Object? get state => _history.state;
|
||||
|
||||
@override
|
||||
void pushState(Object? state, String title, String url) {
|
||||
_history.pushState(state, title, url);
|
||||
}
|
||||
|
||||
@override
|
||||
void replaceState(Object? state, String title, String url) {
|
||||
_history.replaceState(state, title, url);
|
||||
}
|
||||
|
||||
@override
|
||||
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();
|
||||
BrowserHistory get browserHistory {
|
||||
return _browserHistory ??=
|
||||
MultiEntriesBrowserHistory(urlStrategy: const HashUrlStrategy());
|
||||
}
|
||||
|
||||
/// 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? _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';
|
||||
|
||||
import 'engine/history_test.dart';
|
||||
import 'matchers.dart';
|
||||
|
||||
const MethodCodec codec = JSONMethodCodec();
|
||||
|
||||
void emptyCallback(ByteData date) {}
|
||||
|
||||
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(
|
||||
TestHistoryEntry('initial state', null, '/'),
|
||||
);
|
||||
await setStrategy(testStrategy);
|
||||
// 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, '/');
|
||||
|
||||
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]);
|
||||
// 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(() => jsSetLocationStrategy(123), throwsA(anything));
|
||||
expect(() => jsSetLocationStrategy('foo'), throwsA(anything));
|
||||
expect(() => jsSetLocationStrategy(false), throwsA(anything));
|
||||
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 window.debugInitializeHistory(testStrategy, useSingle: true);
|
||||
|
||||
expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError));
|
||||
});
|
||||
|
||||
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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user