diff --git a/dev/benchmarks/macrobenchmarks/pubspec.yaml b/dev/benchmarks/macrobenchmarks/pubspec.yaml index 8bfe5097cdf..c26ec3bff45 100644 --- a/dev/benchmarks/macrobenchmarks/pubspec.yaml +++ b/dev/benchmarks/macrobenchmarks/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: http: 0.12.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" intl: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -105,7 +106,6 @@ dev_dependencies: html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" mime: 0.9.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" node_interop: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/dev/integration_tests/flutter_gallery/pubspec.yaml b/dev/integration_tests/flutter_gallery/pubspec.yaml index 09c41330ff8..7079ec01119 100644 --- a/dev/integration_tests/flutter_gallery/pubspec.yaml +++ b/dev/integration_tests/flutter_gallery/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: connectivity_macos: 0.1.0+5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" connectivity_platform_interface: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" device_info_platform_interface: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" path: 1.8.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" plugin_platform_interface: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -73,7 +74,6 @@ dev_dependencies: http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - js: 0.6.3-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" json_rpc_2: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" matcher: 0.12.10-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" diff --git a/packages/flutter_web_plugins/lib/flutter_web_plugins.dart b/packages/flutter_web_plugins/lib/flutter_web_plugins.dart index cf5b0d16fb8..1bb8281b871 100644 --- a/packages/flutter_web_plugins/lib/flutter_web_plugins.dart +++ b/packages/flutter_web_plugins/lib/flutter_web_plugins.dart @@ -17,5 +17,6 @@ /// describing how the `url_launcher` package was created using [flutter_web_plugins]. library flutter_web_plugins; +export 'src/navigation/url_strategy.dart'; export 'src/plugin_event_channel.dart'; export 'src/plugin_registry.dart'; diff --git a/packages/flutter_web_plugins/lib/src/navigation/js_url_strategy.dart b/packages/flutter_web_plugins/lib/src/navigation/js_url_strategy.dart new file mode 100644 index 00000000000..9ebaac560e7 --- /dev/null +++ b/packages/flutter_web_plugins/lib/src/navigation/js_url_strategy.dart @@ -0,0 +1,118 @@ +// Copyright 2014 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.8 + +@JS() +library js_location_strategy; + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:ui' as ui; + +import 'package:js/js.dart'; +import 'package:meta/meta.dart'; + +import 'url_strategy.dart'; + +typedef _JsSetUrlStrategy = void Function(JsUrlStrategy); + +/// A JavaScript hook to customize the URL strategy of a Flutter app. +// +// Keep this in sync with the JS name in the web engine. Find it at: +// https://github.com/flutter/engine/blob/custom_location_strategy/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart +// +// TODO(mdebbar): Add integration test https://github.com/flutter/flutter/issues/66852 +@JS('_flutter_web_set_location_strategy') +external _JsSetUrlStrategy get jsSetUrlStrategy; + +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); + +/// Given a Dart implementation of URL strategy, converts it to a JavaScript +/// URL strategy to be passed through JS interop. +JsUrlStrategy convertToJsUrlStrategy(UrlStrategy strategy) { + if (strategy == null) { + return null; + } + + return JsUrlStrategy( + getPath: allowInterop(strategy.getPath), + getState: allowInterop(strategy.getState), + addPopStateListener: allowInterop(strategy.addPopStateListener), + prepareExternalUrl: allowInterop(strategy.prepareExternalUrl), + pushState: allowInterop(strategy.pushState), + replaceState: allowInterop(strategy.replaceState), + go: allowInterop(strategy.go), + ); +} + +/// 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 + /// 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/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart b/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart new file mode 100644 index 00000000000..2470f6bf0e4 --- /dev/null +++ b/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart @@ -0,0 +1,320 @@ +// Copyright 2014 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.8 + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:ui' as ui; + +import 'js_url_strategy.dart'; +import 'utils.dart'; + +/// Change the strategy to use for handling browser URL. +/// +/// Setting this to null disables all integration with the browser history. +void setUrlStrategy(UrlStrategy strategy) { + jsSetUrlStrategy(convertToJsUrlStrategy(strategy)); +} + +/// 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); +} + +/// Uses the browser URL's [hash fragments](https://en.wikipedia.org/wiki/Uniform_Resource_Locator#Syntax) +/// to represent its state. +/// +/// By default, this class is used as the URL strategy for the app. However, +/// this class is still useful for apps that want to extend it. +/// +/// In order to use [HashUrlStrategy] 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(); + ui.VoidCallback unsubscribe; + unsubscribe = addPopStateListener((_) { + unsubscribe(); + completer.complete(); + }); + return completer.future; + } +} + +/// Uses the browser URL's pathname to represent Flutter's route name. +/// +/// In order to use [PathUrlStrategy] 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(PathUrlStrategy()); +/// ``` +class PathUrlStrategy extends HashUrlStrategy { + /// Creates an instance of [PathUrlStrategy]. + /// + /// The [PlatformLocation] parameter is useful for testing to mock out browser + /// interations. + PathUrlStrategy([ + PlatformLocation _platformLocation = const BrowserPlatformLocation(), + ]) : _basePath = stripTrailingSlash(extractPathname(checkBaseHref( + _platformLocation.getBaseHref(), + ))), + super(_platformLocation); + + final String _basePath; + + @override + String getPath() { + final String path = _platformLocation.pathname + _platformLocation.search; + if (_basePath.isNotEmpty && path.startsWith(_basePath)) { + return ensureLeadingSlash(path.substring(_basePath.length)); + } + return ensureLeadingSlash(path); + } + + @override + String prepareExternalUrl(String internalUrl) { + if (internalUrl.isNotEmpty && !internalUrl.startsWith('/')) { + internalUrl = '/$internalUrl'; + } + return '$_basePath$internalUrl'; + } +} + +/// 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() => getBaseElementHrefFromDom(); + // String getBaseHref() => html.document.baseUri; +} diff --git a/packages/flutter_web_plugins/lib/src/navigation/utils.dart b/packages/flutter_web_plugins/lib/src/navigation/utils.dart new file mode 100644 index 00000000000..ea742aa8e7d --- /dev/null +++ b/packages/flutter_web_plugins/lib/src/navigation/utils.dart @@ -0,0 +1,67 @@ +// Copyright 2014 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.8 + +import 'dart:html'; + +AnchorElement _urlParsingNode; + +/// Extracts the pathname part of a full [url]. +/// +/// Example: for the url `http://example.com/foo`, the extracted pathname will +/// be `/foo`. +String extractPathname(String url) { + // TODO(mdebbar): Use the `URI` class instead? + _urlParsingNode ??= AnchorElement(); + _urlParsingNode.href = url; + final String pathname = _urlParsingNode.pathname; + return (pathname.isEmpty || pathname[0] == '/') ? pathname : '/$pathname'; +} + +Element _baseElement; + +/// Finds the element in the document and returns its `href` attribute. +/// +/// Returns null if the element isn't found. +String getBaseElementHrefFromDom() { + if (_baseElement == null) { + _baseElement = document.querySelector('base'); + if (_baseElement == null) { + return null; + } + } + return _baseElement.getAttribute('href'); +} + +/// Checks that [baseHref] is set. +/// +/// Throws an exception otherwise. +String checkBaseHref(String baseHref) { + if (baseHref == null) { + throw Exception('Please add a element to your index.html'); + } + if (!baseHref.endsWith('/')) { + throw Exception('The base href has to end with a "/" to work correctly'); + } + return baseHref; +} + +/// Prepends a forward slash to [path] if it doesn't start with one already. +/// +/// Returns [path] unchanged if it already starts with a forward slash. +String ensureLeadingSlash(String path) { + if (!path.startsWith('/')) { + return '/$path'; + } + return path; +} + +/// Removes the trailing forward slash from [path] if any. +String stripTrailingSlash(String path) { + if (path.endsWith('/')) { + return path.substring(0, path.length - 1); + } + return path; +} diff --git a/packages/flutter_web_plugins/pubspec.yaml b/packages/flutter_web_plugins/pubspec.yaml index 5577d28d9f2..f1fc02f006c 100644 --- a/packages/flutter_web_plugins/pubspec.yaml +++ b/packages/flutter_web_plugins/pubspec.yaml @@ -10,6 +10,8 @@ dependencies: flutter: sdk: flutter + js: 0.6.3-nullsafety.1 + characters: 1.1.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" collection: 1.15.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" meta: 1.3.0-nullsafety.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -34,4 +36,4 @@ dev_dependencies: term_glyph: 1.2.0-nullsafety.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" test_api: 0.2.19-nullsafety.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: 417a +# PUBSPEC CHECKSUM: 2180 diff --git a/packages/flutter_web_plugins/test/navigation/url_strategy_test.dart b/packages/flutter_web_plugins/test/navigation/url_strategy_test.dart new file mode 100644 index 00000000000..156a631aa40 --- /dev/null +++ b/packages/flutter_web_plugins/test/navigation/url_strategy_test.dart @@ -0,0 +1,188 @@ +// Copyright 2014 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.8 + +import 'dart:html'; + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +void main() { + group('$HashUrlStrategy', () { + TestPlatformLocation location; + + setUp(() { + location = TestPlatformLocation(); + }); + + tearDown(() { + location = null; + }); + + test('leading slash is optional', () { + final HashUrlStrategy strategy = HashUrlStrategy(location); + + location.hash = '#/'; + expect(strategy.getPath(), '/'); + + location.hash = '#/foo'; + expect(strategy.getPath(), '/foo'); + + location.hash = '#foo'; + expect(strategy.getPath(), 'foo'); + }); + + test('path should not be empty', () { + final HashUrlStrategy strategy = HashUrlStrategy(location); + + location.hash = ''; + expect(strategy.getPath(), '/'); + + location.hash = '#'; + expect(strategy.getPath(), '/'); + }); + }); + + group('$PathUrlStrategy', () { + TestPlatformLocation location; + + setUp(() { + location = TestPlatformLocation(); + }); + + tearDown(() { + location = null; + }); + + test('validates base href', () { + location.baseHref = '/'; + expect( + () => PathUrlStrategy(location), + returnsNormally, + ); + + location.baseHref = '/foo/'; + expect( + () => PathUrlStrategy(location), + returnsNormally, + ); + + location.baseHref = ''; + expect( + () => PathUrlStrategy(location), + throwsException, + ); + + location.baseHref = 'foo'; + expect( + () => PathUrlStrategy(location), + throwsException, + ); + + location.baseHref = '/foo'; + expect( + () => PathUrlStrategy(location), + throwsException, + ); + }); + + test('leading slash is always prepended', () { + location.baseHref = '/'; + final PathUrlStrategy strategy = PathUrlStrategy(location); + + location.pathname = ''; + expect(strategy.getPath(), '/'); + + location.pathname = 'foo'; + expect(strategy.getPath(), '/foo'); + }); + + test('gets path correctly in the presence of basePath', () { + location.baseHref = 'https://example.com/foo/'; + final PathUrlStrategy strategy = PathUrlStrategy(location); + + location.pathname = '/foo/'; + expect(strategy.getPath(), '/'); + + location.pathname = '/foo'; + expect(strategy.getPath(), '/'); + + location.pathname = '/foo/bar'; + expect(strategy.getPath(), '/bar'); + }); + + test('gets path correctly in the presence of query params', () { + location.baseHref = 'https://example.com/foo/'; + location.pathname = '/foo/bar'; + final PathUrlStrategy strategy = PathUrlStrategy(location); + + + location.search = '?q=1'; + expect(strategy.getPath(), '/bar?q=1'); + + location.search = '?q=1&t=r'; + expect(strategy.getPath(), '/bar?q=1&t=r'); + }); + + test('generates external path correctly in the presence of basePath', () { + location.baseHref = 'https://example.com/foo/'; + final PathUrlStrategy strategy = PathUrlStrategy(location); + + expect(strategy.prepareExternalUrl(''), '/foo'); + expect(strategy.prepareExternalUrl('/'), '/foo/'); + expect(strategy.prepareExternalUrl('bar'), '/foo/bar'); + expect(strategy.prepareExternalUrl('/bar'), '/foo/bar'); + expect(strategy.prepareExternalUrl('/bar/'), '/foo/bar/'); + }); + }); +} + +/// A mock implementation of [PlatformLocation] that doesn't access the browser. +class TestPlatformLocation extends PlatformLocation { + @override + String pathname = ''; + + @override + String search = ''; + + @override + String hash = ''; + + @override + dynamic state; + + /// Mocks the base href of the document. + String baseHref = ''; + + @override + void addPopStateListener(EventListener fn) { + throw UnimplementedError(); + } + + @override + void removePopStateListener(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) { + throw UnimplementedError(); + } + + @override + String getBaseHref() => baseHref; +} diff --git a/packages/flutter_web_plugins/test/navigation/utils_test.dart b/packages/flutter_web_plugins/test/navigation/utils_test.dart new file mode 100644 index 00000000000..419b49f7f64 --- /dev/null +++ b/packages/flutter_web_plugins/test/navigation/utils_test.dart @@ -0,0 +1,38 @@ +// Copyright 2014 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.8 + +@TestOn('browser') // Uses web-only Flutter SDK + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_web_plugins/src/navigation/utils.dart'; + +void main() { + test('checks base href', () { + expect(() => checkBaseHref(null), throwsException); + expect(() => checkBaseHref('foo'), throwsException); + expect(() => checkBaseHref('/foo'), throwsException); + expect(() => checkBaseHref('foo/bar'), throwsException); + expect(() => checkBaseHref('/foo/bar'), throwsException); + + expect(() => checkBaseHref('/'), returnsNormally); + expect(() => checkBaseHref('/foo/'), returnsNormally); + expect(() => checkBaseHref('/foo/bar/'), returnsNormally); + }); + + test('extracts pathname from URL', () { + expect(extractPathname('/'), '/'); + expect(extractPathname('/foo'), '/foo'); + expect(extractPathname('/foo/'), '/foo/'); + expect(extractPathname('/foo/bar'), '/foo/bar'); + expect(extractPathname('/foo/bar/'), '/foo/bar/'); + + expect(extractPathname('https://example.com'), '/'); + expect(extractPathname('https://example.com/'), '/'); + expect(extractPathname('https://example.com/foo'), '/foo'); + expect(extractPathname('https://example.com/foo#bar'), '/foo'); + expect(extractPathname('https://example.com/foo/#bar'), '/foo/'); + }); +}