From 3905b9ec39ae63fee9ad16fed70350c1532f52fb Mon Sep 17 00:00:00 2001 From: Dan Field Date: Wed, 7 Oct 2020 21:42:20 -0700 Subject: [PATCH] Revert "[web] Support custom url strategies (#19134)" (#21687) This reverts commit 02324994a3f44a2777ade96b3d69aa61901fb9b4. --- ci/licenses_golden/licenses_flutter | 5 +- lib/web_ui/lib/src/engine.dart | 5 +- .../lib/src/engine/browser_location.dart | 211 +++++++++++++ .../src/engine/{navigation => }/history.dart | 213 ++++++------- .../engine/navigation/js_url_strategy.dart | 78 ----- .../src/engine/navigation/url_strategy.dart | 296 ------------------ lib/web_ui/lib/src/engine/test_embedding.dart | 26 +- lib/web_ui/lib/src/engine/window.dart | 202 ++++++------ lib/web_ui/lib/src/ui/initialization.dart | 4 + lib/web_ui/test/engine/history_test.dart | 127 +++----- lib/web_ui/test/engine/navigation_test.dart | 14 +- lib/web_ui/test/window_test.dart | 92 +++--- 12 files changed, 533 insertions(+), 740 deletions(-) create mode 100644 lib/web_ui/lib/src/engine/browser_location.dart rename lib/web_ui/lib/src/engine/{navigation => }/history.dart (69%) delete mode 100644 lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart delete mode 100644 lib/web_ui/lib/src/engine/navigation/url_strategy.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 72bfafac4..ed7436627 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -426,6 +426,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/assets.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_location.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvas_pool.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -462,9 +463,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/dom_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/engine_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/history.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/js_url_strategy.dart -FILE: ../../../flutter/lib/web_ui/lib/src/engine/history/url_strategy.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/history.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/clip.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index dcb19c8a3..d8c013738 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -26,6 +26,7 @@ part 'engine/alarm_clock.dart'; part 'engine/assets.dart'; part 'engine/bitmap_canvas.dart'; part 'engine/browser_detection.dart'; +part 'engine/browser_location.dart'; part 'engine/canvaskit/canvas.dart'; part 'engine/canvaskit/canvaskit_canvas.dart'; part 'engine/canvaskit/canvaskit_api.dart'; @@ -62,9 +63,7 @@ part 'engine/dom_canvas.dart'; part 'engine/dom_renderer.dart'; part 'engine/engine_canvas.dart'; part 'engine/frame_reference.dart'; -part 'engine/navigation/history.dart'; -part 'engine/navigation/js_url_strategy.dart'; -part 'engine/navigation/url_strategy.dart'; +part 'engine/history.dart'; part 'engine/html/backdrop_filter.dart'; part 'engine/html/canvas.dart'; part 'engine/html/clip.dart'; diff --git a/lib/web_ui/lib/src/engine/browser_location.dart b/lib/web_ui/lib/src/engine/browser_location.dart new file mode 100644 index 000000000..a9701cd99 --- /dev/null +++ b/lib/web_ui/lib/src/engine/browser_location.dart @@ -0,0 +1,211 @@ +// 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 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 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 _waitForPopState() { + final Completer completer = Completer(); + 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); + } +} diff --git a/lib/web_ui/lib/src/engine/navigation/history.dart b/lib/web_ui/lib/src/engine/history.dart similarity index 69% rename from lib/web_ui/lib/src/engine/navigation/history.dart rename to lib/web_ui/lib/src/engine/history.dart index 0a578162a..59e1ba5fd 100644 --- a/lib/web_ui/lib/src/engine/navigation/history.dart +++ b/lib/web_ui/lib/src/engine/history.dart @@ -25,39 +25,64 @@ abstract class BrowserHistory { late ui.VoidCallback _unsubscribe; /// The strategy to interact with html browser history. - UrlStrategy? get urlStrategy; + LocationStrategy? get locationStrategy => _locationStrategy; + LocationStrategy? _locationStrategy; + /// Updates the strategy. + /// + /// This method will also remove any previous modifications to the html + /// browser history and start anew. + Future setLocationStrategy(LocationStrategy? strategy) async { + if (strategy != _locationStrategy) { + await _tearoffStrategy(_locationStrategy); + _locationStrategy = strategy; + await _setupStrategy(_locationStrategy); + } + } - bool _isDisposed = false; + Future _setupStrategy(LocationStrategy? strategy) async { + if (strategy == null) { + return; + } + _unsubscribe = strategy.onPopState(onPopState as dynamic Function(html.Event)); + await setup(); + } - void _setupStrategy(UrlStrategy strategy) { - _unsubscribe = strategy.addPopStateListener( - onPopState as html.EventListener, - ); + Future _tearoffStrategy(LocationStrategy? strategy) async { + if (strategy == null) { + return; + } + _unsubscribe(); + + await tearDown(); } /// Exit this application and return to the previous page. Future exit() async { - if (urlStrategy != null) { - await tearDown(); + if (_locationStrategy != null) { + await _tearoffStrategy(_locationStrategy); // Now the history should be in the original state, back one more time to // exit the application. - await urlStrategy!.go(-1); + await _locationStrategy!.back(); + _locationStrategy = null; } } /// This method does the same thing as the browser back button. - Future back() async { - return urlStrategy?.go(-1); + Future back() { + if (locationStrategy != null) { + return locationStrategy!.back(); + } + return Future.value(); } /// The path of the current location of the user's browser. - String get currentPath => urlStrategy?.getPath() ?? '/'; + String get currentPath => locationStrategy?.path ?? '/'; /// The state of the current location of the user's browser. - Object? get currentState => urlStrategy?.getState(); + dynamic get currentState => locationStrategy?.state; /// Update the url with the given [routeName] and [state]. - void setRouteName(String? routeName, {Object? state}); + void setRouteName(String? routeName, {dynamic? state}); /// A callback method to handle browser backward or forward buttons. /// @@ -65,9 +90,12 @@ abstract class BrowserHistory { /// applications accordingly. void onPopState(covariant html.PopStateEvent event); + /// Sets up any prerequisites to use this browser history class. + Future setup() => Future.value(); + /// Restore any modifications to the html browser history during the lifetime /// of this class. - Future tearDown(); + Future tearDown() => Future.value(); } /// A browser history class that creates a set of browser history entries to @@ -85,51 +113,31 @@ abstract class BrowserHistory { /// * [SingleEntryBrowserHistory], which is used when the framework does not use /// a Router for routing. class MultiEntriesBrowserHistory extends BrowserHistory { - MultiEntriesBrowserHistory({required this.urlStrategy}) { - final UrlStrategy? strategy = urlStrategy; - if (strategy == null) { - return; - } - - _setupStrategy(strategy); - if (!_hasSerialCount(currentState)) { - strategy.replaceState( - _tagWithSerialCount(currentState, 0), 'flutter', currentPath); - } - // If we restore from a page refresh, the _currentSerialCount may not be 0. - _lastSeenSerialCount = _currentSerialCount; - } - - @override - final UrlStrategy? urlStrategy; - late int _lastSeenSerialCount; int get _currentSerialCount { if (_hasSerialCount(currentState)) { - final Map stateMap = - currentState as Map; - return stateMap['serialCount'] as int; + return currentState['serialCount'] as int; } return 0; } - Object _tagWithSerialCount(Object? originialState, int count) { - return { + dynamic _tagWithSerialCount(dynamic originialState, int count) { + return { 'serialCount': count, 'state': originialState, }; } - bool _hasSerialCount(Object? state) { + bool _hasSerialCount(dynamic state) { return state is Map && state['serialCount'] != null; } @override - void setRouteName(String? routeName, {Object? state}) { - if (urlStrategy != null) { + void setRouteName(String? routeName, {dynamic? state}) { + if (locationStrategy != null) { assert(routeName != null); _lastSeenSerialCount += 1; - urlStrategy!.pushState( + locationStrategy!.pushState( _tagWithSerialCount(state, _lastSeenSerialCount), 'flutter', routeName!, @@ -139,51 +147,58 @@ class MultiEntriesBrowserHistory extends BrowserHistory { @override void onPopState(covariant html.PopStateEvent event) { - assert(urlStrategy != null); + assert(locationStrategy != null); // May be a result of direct url access while the flutter application is // already running. if (!_hasSerialCount(event.state)) { // In this case we assume this will be the next history entry from the // last seen entry. - urlStrategy!.replaceState( - _tagWithSerialCount(event.state, _lastSeenSerialCount + 1), - 'flutter', - currentPath); + locationStrategy!.replaceState( + _tagWithSerialCount(event.state, _lastSeenSerialCount + 1), + 'flutter', + currentPath); } _lastSeenSerialCount = _currentSerialCount; if (window._onPlatformMessage != null) { window.invokeOnPlatformMessage( 'flutter/navigation', const JSONMethodCodec().encodeMethodCall( - MethodCall('pushRouteInformation', { - 'location': currentPath, - 'state': event.state?['state'], - })), + MethodCall('pushRouteInformation', { + 'location': currentPath, + 'state': event.state?['state'], + }) + ), (_) {}, ); } } @override - Future tearDown() async { - if (_isDisposed || urlStrategy == null) { - return; + Future setup() { + if (!_hasSerialCount(currentState)) { + locationStrategy!.replaceState( + _tagWithSerialCount(currentState, 0), + 'flutter', + currentPath + ); } - _isDisposed = true; - _unsubscribe(); + // If we retore from a page refresh, the _currentSerialCount may not be 0. + _lastSeenSerialCount = _currentSerialCount; + return Future.value(); + } + @override + Future tearDown() async { // Restores the html browser history. assert(_hasSerialCount(currentState)); int backCount = _currentSerialCount; if (backCount > 0) { - await urlStrategy!.go(-backCount); + await locationStrategy!.back(count: backCount); } // Unwrap state. assert(_hasSerialCount(currentState) && _currentSerialCount == 0); - final Map stateMap = - currentState as Map; - urlStrategy!.replaceState( - stateMap['state'], + locationStrategy!.replaceState( + currentState['state'], 'flutter', currentPath, ); @@ -207,61 +222,37 @@ class MultiEntriesBrowserHistory extends BrowserHistory { /// * [MultiEntriesBrowserHistory], which is used when the framework uses a /// Router for routing. class SingleEntryBrowserHistory extends BrowserHistory { - SingleEntryBrowserHistory({required this.urlStrategy}) { - final UrlStrategy? strategy = urlStrategy; - if (strategy == null) { - return; - } - - _setupStrategy(strategy); - - final String path = currentPath; - if (!_isFlutterEntry(html.window.history.state)) { - // An entry may not have come from Flutter, for example, when the user - // refreshes the page. They land directly on the "flutter" entry, so - // there's no need to setup the "origin" and "flutter" entries, we can - // safely assume they are already setup. - _setupOriginEntry(strategy); - _setupFlutterEntry(strategy, replace: false, path: path); - } - } - - @override - final UrlStrategy? urlStrategy; - static const MethodCall _popRouteMethodCall = MethodCall('popRoute'); static const String _kFlutterTag = 'flutter'; static const String _kOriginTag = 'origin'; - Map _wrapOriginState(Object? state) { + Map _wrapOriginState(dynamic state) { return {_kOriginTag: true, 'state': state}; } - - Object? _unwrapOriginState(Object? state) { + dynamic _unwrapOriginState(dynamic state) { assert(_isOriginEntry(state)); final Map originState = state as Map; return originState['state']; } - Map _flutterState = {_kFlutterTag: true}; /// The origin entry is the history entry that the Flutter app landed on. It's /// created by the browser when the user navigates to the url of the app. - bool _isOriginEntry(Object? state) { + bool _isOriginEntry(dynamic state) { return state is Map && state[_kOriginTag] == true; } /// The flutter entry is a history entry that we maintain on top of the origin /// entry. It allows us to catch popstate events when the user hits the back /// button. - bool _isFlutterEntry(Object? state) { + bool _isFlutterEntry(dynamic state) { return state is Map && state[_kFlutterTag] == true; } @override - void setRouteName(String? routeName, {Object? state}) { - if (urlStrategy != null) { - _setupFlutterEntry(urlStrategy!, replace: true, path: routeName); + void setRouteName(String? routeName, {dynamic? state}) { + if (locationStrategy != null) { + _setupFlutterEntry(locationStrategy!, replace: true, path: routeName); } } @@ -269,7 +260,7 @@ class SingleEntryBrowserHistory extends BrowserHistory { @override void onPopState(covariant html.PopStateEvent event) { if (_isOriginEntry(event.state)) { - _setupFlutterEntry(urlStrategy!); + _setupFlutterEntry(_locationStrategy!); // 2. Send a 'popRoute' platform message so the app can handle it accordingly. if (window._onPlatformMessage != null) { @@ -311,14 +302,14 @@ class SingleEntryBrowserHistory extends BrowserHistory { // 2. Then we remove the new entry. // This will take us back to our "flutter" entry and it causes a new // popstate event that will be handled in the "else if" section above. - urlStrategy!.go(-1); + _locationStrategy!.back(); } } /// This method should be called when the Origin Entry is active. It just /// replaces the state of the entry so that we can recognize it later using /// [_isOriginEntry] inside [_popStateListener]. - void _setupOriginEntry(UrlStrategy strategy) { + void _setupOriginEntry(LocationStrategy strategy) { assert(strategy != null); // ignore: unnecessary_null_comparison strategy.replaceState(_wrapOriginState(currentState), 'origin', ''); } @@ -326,7 +317,7 @@ class SingleEntryBrowserHistory extends BrowserHistory { /// This method is used manipulate the Flutter Entry which is always the /// active entry while the Flutter app is running. void _setupFlutterEntry( - UrlStrategy strategy, { + LocationStrategy strategy, { bool replace = false, String? path, }) { @@ -340,17 +331,27 @@ class SingleEntryBrowserHistory extends BrowserHistory { } @override - Future tearDown() async { - if (_isDisposed || urlStrategy == null) { - return; + Future setup() { + final String path = currentPath; + if (_isFlutterEntry(html.window.history.state)) { + // This could happen if the user, for example, refreshes the page. They + // will land directly on the "flutter" entry, so there's no need to setup + // the "origin" and "flutter" entries, we can safely assume they are + // already setup. + } else { + _setupOriginEntry(locationStrategy!); + _setupFlutterEntry(locationStrategy!, replace: false, path: path); } - _isDisposed = true; - _unsubscribe(); + return Future.value(); + } - // We need to remove the flutter entry that we pushed in setup. - await urlStrategy!.go(-1); - // Restores original state. - urlStrategy! - .replaceState(_unwrapOriginState(currentState), 'flutter', currentPath); + @override + Future 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); + } } } diff --git a/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart b/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart deleted file mode 100644 index decb7c249..000000000 --- a/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart +++ /dev/null @@ -1,78 +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; - -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 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 go(int count); -} diff --git a/lib/web_ui/lib/src/engine/navigation/url_strategy.dart b/lib/web_ui/lib/src/engine/navigation/url_strategy.dart deleted file mode 100644 index fcf2cecfd..000000000 --- a/lib/web_ui/lib/src/engine/navigation/url_strategy.dart +++ /dev/null @@ -1,296 +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; - -/// 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 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 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 _waitForPopState() { - final Completer completer = Completer(); - 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 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; -} diff --git a/lib/web_ui/lib/src/engine/test_embedding.dart b/lib/web_ui/lib/src/engine/test_embedding.dart index 0255e5fb1..f0d3a4291 100644 --- a/lib/web_ui/lib/src/engine/test_embedding.dart +++ b/lib/web_ui/lib/src/engine/test_embedding.dart @@ -20,27 +20,29 @@ class TestHistoryEntry { } } -/// This URL strategy mimics the browser's history as closely as possible +/// This location strategy mimics the browser's history as closely as possible /// while doing it all in memory with no interaction with the browser. /// /// It keeps a list of history entries and event listeners in memory and /// manipulates them in order to achieve the desired functionality. -class TestUrlStrategy extends UrlStrategy { - /// Creates a instance of [TestUrlStrategy] with an empty string as the +class TestLocationStrategy extends LocationStrategy { + /// Creates a instance of [TestLocationStrategy] with an empty string as the /// path. - factory TestUrlStrategy() => TestUrlStrategy.fromEntry(TestHistoryEntry(null, null, '')); + factory TestLocationStrategy() => TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '')); - /// Creates an instance of [TestUrlStrategy] and populates it with a list + /// Creates an instance of [TestLocationStrategy] and populates it with a list /// that has [initialEntry] as the only item. - TestUrlStrategy.fromEntry(TestHistoryEntry initialEntry) + TestLocationStrategy.fromEntry(TestHistoryEntry initialEntry) : _currentEntryIndex = 0, history = [initialEntry]; @override - String getPath() => currentEntry.url; + String get path => currentEntry.url; @override - dynamic getState() => currentEntry.state; + dynamic get state { + return currentEntry.state; + } int _currentEntryIndex; int get currentEntryIndex => _currentEntryIndex; @@ -103,12 +105,12 @@ class TestUrlStrategy extends UrlStrategy { } @override - Future go(int count) { + Future back({int count = 1}) { assert(withinAppHistory); - // Browsers don't move in history immediately. They do it at the next + // Browsers don't move back in history immediately. They do it at the next // event loop. So let's simulate that. return _nextEventLoop(() { - _currentEntryIndex = _currentEntryIndex + count; + _currentEntryIndex = _currentEntryIndex - count; if (withinAppHistory) { _firePopStateEvent(); } @@ -122,7 +124,7 @@ class TestUrlStrategy extends UrlStrategy { final List listeners = []; @override - ui.VoidCallback addPopStateListener(html.EventListener fn) { + ui.VoidCallback onPopState(html.EventListener fn) { listeners.add(fn); return () { // Schedule a micro task here to avoid removing the listener during diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 757d85ee4..94d73f6c7 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -13,27 +13,20 @@ const bool _debugPrintPlatformMessages = false; /// This may be overridden in tests, for example, to pump fake frames. ui.VoidCallback? scheduleFrameCallback; -typedef _JsSetUrlStrategy = void Function(JsUrlStrategy?); - -/// A JavaScript hook to customize the URL strategy of a Flutter app. -// -// Keep this js name in sync with flutter_web_plugins. Find it at: -// https://github.com/flutter/flutter/blob/custom_location_strategy/packages/flutter_web_plugins/lib/src/navigation/js_url_strategy.dart -// -// TODO: Add integration test https://github.com/flutter/flutter/issues/66852 -@JS('_flutter_web_set_location_strategy') -external set _jsSetUrlStrategy(_JsSetUrlStrategy? newJsSetUrlStrategy); - /// The Web implementation of [ui.Window]. class EngineWindow extends ui.Window { EngineWindow() { _addBrightnessMediaQueryListener(); - _addUrlStrategyListener(); + js.context['_flutter_web_set_location_strategy'] = (LocationStrategy strategy) { + locationStrategy = strategy; + }; + registerHotRestartListener(() { + js.context['_flutter_web_set_location_strategy'] = null; + }); } @override - double get devicePixelRatio => - _debugDevicePixelRatio ?? browserDevicePixelRatio; + double get devicePixelRatio => _debugDevicePixelRatio ?? browserDevicePixelRatio; /// Returns device pixel ratio returned by browser. static double get browserDevicePixelRatio { @@ -124,8 +117,7 @@ class EngineWindow extends ui.Window { double height = 0; double width = 0; if (html.window.visualViewport != null) { - height = - html.window.visualViewport!.height!.toDouble() * devicePixelRatio; + height = html.window.visualViewport!.height!.toDouble() * devicePixelRatio; width = html.window.visualViewport!.width!.toDouble() * devicePixelRatio; } else { height = html.window.innerHeight! * devicePixelRatio; @@ -134,7 +126,7 @@ class EngineWindow extends ui.Window { // This method compares the new dimensions with the previous ones. // Return false if the previous dimensions are not set. - if (_physicalSize != null) { + if(_physicalSize != null) { // First confirm both height and width are effected. if (_physicalSize!.height != height && _physicalSize!.width != width) { // If prior to rotation height is bigger than width it should be the @@ -162,41 +154,78 @@ class EngineWindow extends ui.Window { /// Handles the browser history integration to allow users to use the back /// button, etc. @visibleForTesting - BrowserHistory get browserHistory { - return _browserHistory ??= - MultiEntriesBrowserHistory(urlStrategy: const HashUrlStrategy()); - } + BrowserHistory get browserHistory => _browserHistory; + BrowserHistory _browserHistory = MultiEntriesBrowserHistory(); - BrowserHistory? _browserHistory; + @visibleForTesting + Future 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 _useMultiEntryBrowserHistory() async { + if (_browserHistory is MultiEntriesBrowserHistory) { + return; + } + final LocationStrategy? strategy = _browserHistory.locationStrategy; + if (strategy != null) + await _browserHistory.setLocationStrategy(null); + _browserHistory = MultiEntriesBrowserHistory(); + if (strategy != null) + await _browserHistory.setLocationStrategy(strategy); + } Future _useSingleEntryBrowserHistory() async { if (_browserHistory is SingleEntryBrowserHistory) { return; } - final UrlStrategy? strategy = _browserHistory?.urlStrategy; - await _browserHistory?.tearDown(); - _browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy); + final LocationStrategy? strategy = _browserHistory.locationStrategy; + if (strategy != null) + await _browserHistory.setLocationStrategy(null); + _browserHistory = SingleEntryBrowserHistory(); + if (strategy != null) + await _browserHistory.setLocationStrategy(strategy); } + /// Simulates clicking the browser's back button. + Future webOnlyBack() => _browserHistory.back(); + /// Lazily initialized when the `defaultRouteName` getter is invoked. /// - /// The reason for the lazy initialization is to give enough time for the app to set [urlStrategy] + /// The reason for the lazy initialization is to give enough time for the app to set [locationStrategy] /// in `lib/src/ui/initialization.dart`. String? _defaultRouteName; @override - String get defaultRouteName { - return _defaultRouteName ??= browserHistory.currentPath; - } + String get defaultRouteName => _defaultRouteName ??= _browserHistory.currentPath; @override void scheduleFrame() { if (scheduleFrameCallback == null) { - throw new Exception('scheduleFrameCallback must be initialized first.'); + throw new Exception( + 'scheduleFrameCallback must be initialized first.'); } scheduleFrameCallback!(); } + /// Change the strategy to use for handling browser history location. + /// Setting this member will automatically update [_browserHistory]. + /// + /// By setting this to null, the browser history will be disabled. + set locationStrategy(LocationStrategy? strategy) { + _browserHistory.setLocationStrategy(strategy); + } + + /// Returns the currently active location strategy. + @visibleForTesting + LocationStrategy? get locationStrategy => _browserHistory.locationStrategy; + @override ui.VoidCallback? get onTextScaleFactorChanged => _onTextScaleFactorChanged; ui.VoidCallback? _onTextScaleFactorChanged; @@ -448,8 +477,8 @@ class EngineWindow extends ui.Window { /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. - void invokeOnPlatformMessage(String name, ByteData? data, - ui.PlatformMessageResponseCallback callback) { + void invokeOnPlatformMessage( + String name, ByteData? data, ui.PlatformMessageResponseCallback callback) { _invoke3( _onPlatformMessage, _onPlatformMessageZone, @@ -471,9 +500,7 @@ class EngineWindow extends ui.Window { /// Wraps the given [callback] in another callback that ensures that the /// original callback is called in the zone it was registered in. - static ui.PlatformMessageResponseCallback? - _zonedPlatformMessageResponseCallback( - ui.PlatformMessageResponseCallback? callback) { + static ui.PlatformMessageResponseCallback? _zonedPlatformMessageResponseCallback(ui.PlatformMessageResponseCallback? callback) { if (callback == null) { return null; } @@ -537,7 +564,7 @@ class EngineWindow extends ui.Window { final MethodCall decoded = codec.decodeMethodCall(data); switch (decoded.method) { case 'SystemNavigator.pop': - browserHistory.exit().then((_) { + _browserHistory.exit().then((_) { _replyToPlatformMessage( callback, codec.encodeSuccessEnvelope(true)); }); @@ -558,8 +585,8 @@ class EngineWindow extends ui.Window { case 'SystemChrome.setPreferredOrientations': final List? arguments = decoded.arguments; domRenderer.setPreferredOrientation(arguments).then((bool success) { - _replyToPlatformMessage( - callback, codec.encodeSuccessEnvelope(success)); + _replyToPlatformMessage(callback, + codec.encodeSuccessEnvelope(success)); }); return; case 'SystemSound.play': @@ -605,8 +632,7 @@ class EngineWindow extends ui.Window { case 'flutter/platform_views': if (experimentalUseSkia) { - rasterizer!.surface.viewEmbedder - .handlePlatformViewCall(data, callback); + rasterizer!.surface.viewEmbedder.handlePlatformViewCall(data, callback); } else { ui.handlePlatformViewCall(data!, callback!); } @@ -620,11 +646,27 @@ class EngineWindow extends ui.Window { return; case 'flutter/navigation': - _handleNavigationMessage(data, callback).then((handled) { - if (!handled && callback != null) { - callback(null); - } - }); + const MethodCodec codec = JSONMethodCodec(); + final MethodCall decoded = codec.decodeMethodCall(data); + final Map message = decoded.arguments as Map; + switch (decoded.method) { + case 'routeUpdated': + _useSingleEntryBrowserHistory().then((void data) { + _browserHistory.setRouteName(message['routeName']); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope(true)); + }); + break; + case 'routeInformationUpdated': + assert(_browserHistory is MultiEntriesBrowserHistory); + _browserHistory.setRouteName( + message['location'], + state: message['state'], + ); + _replyToPlatformMessage( + callback, codec.encodeSuccessEnvelope(true)); + break; + } // As soon as Flutter starts taking control of the app navigation, we // should reset [_defaultRouteName] to "/" so it doesn't have any // further effect after this point. @@ -643,51 +685,6 @@ class EngineWindow extends ui.Window { _replyToPlatformMessage(callback, null); } - @visibleForTesting - Future debugInitializeHistory( - UrlStrategy? strategy, { - required bool useSingle, - }) async { - await _browserHistory?.tearDown(); - if (useSingle) { - _browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy); - } else { - _browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy); - } - } - - @visibleForTesting - Future debugResetHistory() async { - await _browserHistory?.tearDown(); - _browserHistory = null; - } - - Future _handleNavigationMessage( - ByteData? data, - ui.PlatformMessageResponseCallback? callback, - ) async { - const MethodCodec codec = JSONMethodCodec(); - final MethodCall decoded = codec.decodeMethodCall(data); - final Map arguments = decoded.arguments; - - switch (decoded.method) { - case 'routeUpdated': - await _useSingleEntryBrowserHistory(); - browserHistory.setRouteName(arguments['routeName']); - _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); - return true; - case 'routeInformationUpdated': - assert(browserHistory is MultiEntriesBrowserHistory); - browserHistory.setRouteName( - arguments['location'], - state: arguments['state'], - ); - _replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true)); - return true; - } - return false; - } - int _getHapticFeedbackDuration(String? type) { switch (type) { case 'HapticFeedbackType.lightImpact': @@ -749,8 +746,7 @@ class EngineWindow extends ui.Window { : ui.Brightness.light); _brightnessMediaQueryListener = (html.Event event) { - final html.MediaQueryListEvent mqEvent = - event as html.MediaQueryListEvent; + final html.MediaQueryListEvent mqEvent = event as html.MediaQueryListEvent; _updatePlatformBrightness( mqEvent.matches! ? ui.Brightness.dark : ui.Brightness.light); }; @@ -760,21 +756,6 @@ class EngineWindow extends ui.Window { }); } - void _addUrlStrategyListener() { - _jsSetUrlStrategy = allowInterop((JsUrlStrategy? jsStrategy) { - assert( - _browserHistory == null, - 'Cannot set URL strategy more than once.', - ); - final UrlStrategy? strategy = - jsStrategy == null ? null : CustomUrlStrategy.fromJs(jsStrategy); - _browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy); - }); - registerHotRestartListener(() { - _jsSetUrlStrategy = null; - }); - } - /// Remove the callback function for listening changes in [_brightnessMediaQuery] value. void _removeBrightnessMediaQueryListener() { _brightnessMediaQuery.removeListener(_brightnessMediaQueryListener); @@ -804,8 +785,7 @@ class EngineWindow extends ui.Window { } @visibleForTesting - late Rasterizer? rasterizer = - experimentalUseSkia ? Rasterizer(Surface(HtmlViewEmbedder())) : null; + late Rasterizer? rasterizer = experimentalUseSkia ? Rasterizer(Surface(HtmlViewEmbedder())) : null; } bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData? data) { @@ -851,8 +831,8 @@ void _invoke1(void callback(A a)?, Zone? zone, A arg) { } /// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3]. -void _invoke3(void callback(A1 a1, A2 a2, A3 a3)?, Zone? zone, - A1 arg1, A2 arg2, A3 arg3) { +void _invoke3( + void callback(A1 a1, A2 a2, A3 a3)?, Zone? zone, A1 arg1, A2 arg2, A3 arg3) { if (callback == null) { return; } diff --git a/lib/web_ui/lib/src/ui/initialization.dart b/lib/web_ui/lib/src/ui/initialization.dart index ca317304e..a7b06b358 100644 --- a/lib/web_ui/lib/src/ui/initialization.dart +++ b/lib/web_ui/lib/src/ui/initialization.dart @@ -21,6 +21,10 @@ Future webOnlyInitializePlatform({ Future _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 diff --git a/lib/web_ui/test/engine/history_test.dart b/lib/web_ui/test/engine/history_test.dart index f5be42ae6..4c11ed003 100644 --- a/lib/web_ui/test/engine/history_test.dart +++ b/lib/web_ui/test/engine/history_test.dart @@ -16,6 +16,11 @@ import 'package:ui/src/engine.dart'; import '../spy.dart'; +TestLocationStrategy get strategy => window.browserHistory.locationStrategy; +Future setStrategy(TestLocationStrategy newStrategy) async { + await window.browserHistory.setLocationStrategy(newStrategy); +} + Map _wrapOriginState(dynamic state) { return {'origin': true, 'state': state}; } @@ -43,19 +48,18 @@ void testMain() { final PlatformMessagesSpy spy = PlatformMessagesSpy(); setUp(() async { + await window.debugSwitchBrowserHistory(useSingle: true); spy.setUp(); }); tearDown(() async { spy.tearDown(); - await window.debugResetHistory(); + await setStrategy(null); }); test('basic setup works', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/initial'), - ); - await window.debugInitializeHistory(strategy, useSingle: true); + await setStrategy(TestLocationStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/initial'))); // There should be two entries: origin and flutter. expect(strategy.history, hasLength(2)); @@ -78,11 +82,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('browser back button pops routes correctly', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry(null, null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: true); - + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); // Initially, we should be on the flutter entry. expect(strategy.history, hasLength(2)); expect(strategy.currentEntry.state, flutterState); @@ -98,7 +98,7 @@ void testMain() { // No platform messages have been sent so far. expect(spy.messages, isEmpty); // Clicking back should take us to page1. - await strategy.go(-1); + await strategy.back(); // First, the framework should've received a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -115,10 +115,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('multiple browser back clicks', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry(null, null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: true); + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); await routeUpdated('/page1'); await routeUpdated('/page2'); @@ -130,7 +127,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page2'); // Back to page1. - await strategy.go(-1); + await strategy.back(); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -146,7 +143,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page1'); // Back to home. - await strategy.go(-1); + await strategy.back(); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -164,8 +161,8 @@ void testMain() { // The next browser back will exit the app. We store the strategy locally // because it will be remove from the browser history class once it exits // the app. - TestUrlStrategy originalStrategy = strategy; - await originalStrategy.go(-1); + TestLocationStrategy originalStrategy = strategy; + await originalStrategy.back(); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -184,10 +181,7 @@ void testMain() { browserEngine == BrowserEngine.webkit); test('handle user-provided url', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry(null, null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: true); + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); await strategy.simulateUserTypingUrl('/page3'); // This delay is necessary to wait for [BrowserHistory] because it @@ -208,7 +202,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page3'); // Back to home. - await strategy.go(-1); + await strategy.back(); // 1. The engine sends a `popRoute` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -227,10 +221,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('user types unknown url', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry(null, null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: true); + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry(null, null, '/home'))); await strategy.simulateUserTypingUrl('/unknown'); // This delay is necessary to wait for [BrowserHistory] because it @@ -257,19 +248,18 @@ void testMain() { final PlatformMessagesSpy spy = PlatformMessagesSpy(); setUp(() async { + await window.debugSwitchBrowserHistory(useSingle: false); spy.setUp(); }); tearDown(() async { spy.tearDown(); - await window.debugResetHistory(); + await setStrategy(null); }); test('basic setup works', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/initial'), - ); - await window.debugInitializeHistory(strategy, useSingle: false); + await setStrategy(TestLocationStrategy.fromEntry( + TestHistoryEntry('initial state', null, '/initial'))); // There should be only one entry. expect(strategy.history, hasLength(1)); @@ -283,11 +273,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('browser back button push route infromation correctly', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: false); - + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); // Initially, we should be on the flutter entry. expect(strategy.history, hasLength(1)); expect(strategy.currentEntry.state, _tagStateWithSerialCount('initial state', 0)); @@ -303,7 +289,7 @@ void testMain() { // No platform messages have been sent so far. expect(spy.messages, isEmpty); // Clicking back should take us to page1. - await strategy.go(-1); + await strategy.back(); // First, the framework should've received a `pushRouteInformation` // platform message. expect(spy.messages, hasLength(1)); @@ -324,10 +310,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('multiple browser back clicks', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: false); + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); await routeInfomrationUpdated('/page1', 'page1 state'); await routeInfomrationUpdated('/page2', 'page2 state'); @@ -339,7 +322,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page2'); // Back to page1. - await strategy.go(-1); + await strategy.back(); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -355,7 +338,7 @@ void testMain() { expect(strategy.currentEntry.state, _tagStateWithSerialCount('page1 state', 1)); expect(strategy.currentEntry.url, '/page1'); // Back to home. - await strategy.go(-1); + await strategy.back(); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -376,10 +359,7 @@ void testMain() { browserEngine == BrowserEngine.webkit); test('handle user-provided url', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: false); + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); await strategy.simulateUserTypingUrl('/page3'); // This delay is necessary to wait for [BrowserHistory] because it @@ -401,7 +381,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page3'); // Back to home. - await strategy.go(-1); + await strategy.back(); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -421,10 +401,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); test('forward button works', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/home'), - ); - await window.debugInitializeHistory(strategy, useSingle: false); + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/home'))); await routeInfomrationUpdated('/page1', 'page1 state'); await routeInfomrationUpdated('/page2', 'page2 state'); @@ -436,7 +413,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page2'); // Back to page1. - await strategy.go(-1); + await strategy.back(); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -453,7 +430,7 @@ void testMain() { expect(strategy.currentEntry.url, '/page1'); // Forward to page2 - await strategy.go(1); + await strategy.back(count: -1); // 1. The engine sends a `pushRouteInformation` platform message. expect(spy.messages, hasLength(1)); expect(spy.messages[0].channel, 'flutter/navigation'); @@ -473,7 +450,7 @@ void testMain() { skip: browserEngine == BrowserEngine.edge); }); - group('$HashUrlStrategy', () { + group('$HashLocationStrategy', () { TestPlatformLocation location; setUp(() { @@ -485,26 +462,26 @@ void testMain() { }); test('leading slash is optional', () { - final HashUrlStrategy strategy = HashUrlStrategy(location); + final HashLocationStrategy strategy = HashLocationStrategy(location); location.hash = '#/'; - expect(strategy.getPath(), '/'); + expect(strategy.path, '/'); location.hash = '#/foo'; - expect(strategy.getPath(), '/foo'); + expect(strategy.path, '/foo'); location.hash = '#foo'; - expect(strategy.getPath(), 'foo'); + expect(strategy.path, 'foo'); }); test('path should not be empty', () { - final HashUrlStrategy strategy = HashUrlStrategy(location); + final HashLocationStrategy strategy = HashLocationStrategy(location); location.hash = ''; - expect(strategy.getPath(), '/'); + expect(strategy.path, '/'); location.hash = '#'; - expect(strategy.getPath(), '/'); + expect(strategy.path, '/'); }); }); } @@ -552,31 +529,31 @@ class TestPlatformLocation extends PlatformLocation { String hash; dynamic state; - @override - void addPopStateListener(html.EventListener fn) { + void onPopState(html.EventListener fn) { throw UnimplementedError(); } - @override - void removePopStateListener(html.EventListener fn) { + void offPopState(html.EventListener fn) { + throw UnimplementedError(); + } + + void onHashChange(html.EventListener fn) { + throw UnimplementedError(); + } + + void offHashChange(html.EventListener fn) { throw UnimplementedError(); } - @override void pushState(dynamic state, String title, String url) { throw UnimplementedError(); } - @override void replaceState(dynamic state, String title, String url) { throw UnimplementedError(); } - @override - void go(int count) { + void back(int count) { throw UnimplementedError(); } - - @override - String getBaseHref() => '/'; } diff --git a/lib/web_ui/test/engine/navigation_test.dart b/lib/web_ui/test/engine/navigation_test.dart index 99bae818f..44d3bf293 100644 --- a/lib/web_ui/test/engine/navigation_test.dart +++ b/lib/web_ui/test/engine/navigation_test.dart @@ -10,7 +10,7 @@ import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart' as engine; -engine.TestUrlStrategy _strategy; +engine.TestLocationStrategy _strategy; const engine.MethodCodec codec = engine.JSONMethodCodec(); @@ -21,14 +21,12 @@ void main() { } void testMain() { - setUp(() async { - _strategy = engine.TestUrlStrategy(); - await engine.window.debugInitializeHistory(_strategy, useSingle: true); + setUp(() { + engine.window.locationStrategy = _strategy = engine.TestLocationStrategy(); }); - tearDown(() async { - _strategy = null; - await engine.window.debugResetHistory(); + tearDown(() { + engine.window.locationStrategy = _strategy = null; }); test('Tracks pushed, replaced and popped routes', () async { @@ -42,6 +40,6 @@ void testMain() { (_) => completer.complete(), ); await completer.future; - expect(_strategy.getPath(), '/foo'); + expect(_strategy.path, '/foo'); }); } diff --git a/lib/web_ui/test/window_test.dart b/lib/web_ui/test/window_test.dart index ef0a755f5..b83849bff 100644 --- a/lib/web_ui/test/window_test.dart +++ b/lib/web_ui/test/window_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // @dart = 2.6 +import 'dart:async'; import 'dart:html' as html; import 'dart:js_util' as js_util; import 'dart:typed_data'; @@ -11,39 +12,34 @@ import 'package:test/bootstrap/browser.dart'; import 'package:test/test.dart'; import 'package:ui/src/engine.dart'; -import 'engine/history_test.dart'; -import 'matchers.dart'; - const MethodCodec codec = JSONMethodCodec(); -void emptyCallback(ByteData data) {} +void emptyCallback(ByteData date) {} + +Future setStrategy(TestLocationStrategy newStrategy) async { + await window.browserHistory.setLocationStrategy(newStrategy); +} void main() { internalBootstrapBrowserTest(() => testMain); } void testMain() { - tearDown(() async { - await window.debugResetHistory(); + setUp(() async { + await window.debugSwitchBrowserHistory(useSingle: true); }); test('window.defaultRouteName should not change', () async { - final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/initial'), - ); - await window.debugInitializeHistory(strategy, useSingle: true); + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial'))); expect(window.defaultRouteName, '/initial'); // Changing the URL in the address bar later shouldn't affect [window.defaultRouteName]. - strategy.replaceState(null, null, '/newpath'); + window.locationStrategy.replaceState(null, null, '/newpath'); expect(window.defaultRouteName, '/initial'); }); - test('window.defaultRouteName should reset after navigation platform message', - () async { - await window.debugInitializeHistory(TestUrlStrategy.fromEntry( - TestHistoryEntry('initial state', null, '/initial'), - ), useSingle: true); + test('window.defaultRouteName should reset after navigation platform message', () async { + await setStrategy(TestLocationStrategy.fromEntry(TestHistoryEntry('initial state', null, '/initial'))); // Reading it multiple times should return the same value. expect(window.defaultRouteName, '/initial'); expect(window.defaultRouteName, '/initial'); @@ -61,45 +57,45 @@ void testMain() { }); test('can disable location strategy', () async { - // Disable URL strategy. - expect(() => jsSetUrlStrategy(null), returnsNormally); - // History should be initialized. - expect(window.browserHistory, isNotNull); - // But without a URL strategy. - expect(window.browserHistory.urlStrategy, isNull); - // Current path is always "/" in this case. - expect(window.browserHistory.currentPath, '/'); - - // Perform some navigation operations. - routeInfomrationUpdated('/foo/bar', null); - // Path should not be updated because URL strategy is disabled. - expect(window.browserHistory.currentPath, '/'); - }); - - test('js interop throws on wrong type', () { - expect(() => jsSetUrlStrategy(123), throwsA(anything)); - expect(() => jsSetUrlStrategy('foo'), throwsA(anything)); - expect(() => jsSetUrlStrategy(false), throwsA(anything)); - }); - - test('cannot set url strategy after it is initialized', () async { - final testStrategy = TestUrlStrategy.fromEntry( + await window.debugSwitchBrowserHistory(useSingle: true); + final testStrategy = TestLocationStrategy.fromEntry( TestHistoryEntry('initial state', null, '/'), ); - await window.debugInitializeHistory(testStrategy, useSingle: true); - - expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError)); + await setStrategy(testStrategy); + + expect(window.locationStrategy, testStrategy); + // A single listener should've been setup. + expect(testStrategy.listeners, hasLength(1)); + // The initial entry should be there, plus another "flutter" entry. + expect(testStrategy.history, hasLength(2)); + expect(testStrategy.history[0].state, {'origin': true, 'state': 'initial state'}); + expect(testStrategy.history[1].state, {'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.delayed(Duration.zero); + expect(window.locationStrategy, isNull); + + // The listener is removed asynchronously. + await Future.delayed(const Duration(milliseconds: 10)); + + // No more listeners. + expect(testStrategy.listeners, isEmpty); + // History should've moved back to the initial state. + expect(testStrategy.history[0].state, "initial state"); + expect(testStrategy.currentEntry, testStrategy.history[0]); }); - test('cannot set url strategy more than once', () async { - // First time is okay. - expect(() => jsSetUrlStrategy(null), returnsNormally); - // Second time is not allowed. - expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError)); + test('js interop throws on wrong type', () { + expect(() => jsSetLocationStrategy(123), throwsA(anything)); + expect(() => jsSetLocationStrategy('foo'), throwsA(anything)); + expect(() => jsSetLocationStrategy(false), throwsA(anything)); }); } -void jsSetUrlStrategy(dynamic strategy) { +void jsSetLocationStrategy(dynamic strategy) { js_util.callMethod( html.window, '_flutter_web_set_location_strategy', -- GitLab