Revert "[web] Remove the JS API for url strategy" (flutter/engine#42468)

Reverts flutter/engine#42134

This is blocking the engine into framework roller:

See: https://cirrus-ci.com/task/5610586755563520

```
Analyzing 3 items...                                            
  error • The class 'UrlStrategy' can't be extended outside of its library because it's an interface class • dev/integration_tests/web_e2e_tests/test_driver/url_strategy_integration.dart:48:31 • invalid_use_of_type_outside_library
1 issue found. (ran in 321.8s)
  🙙  🙛  
  ```
This commit is contained in:
Jonah Williams 2023-05-31 18:53:05 -07:00 committed by GitHub
parent c30b19a6d9
commit fd5cbcfe04
11 changed files with 235 additions and 76 deletions

View File

@ -1975,7 +1975,10 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart + ../../../flutter/LICENSE
@ -4636,7 +4639,10 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse_cursor.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/history.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/js_url_strategy.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/navigation/url_strategy.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/noto_font.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart

View File

@ -113,6 +113,8 @@ export 'engine/key_map.g.dart';
export 'engine/keyboard_binding.dart';
export 'engine/mouse_cursor.dart';
export 'engine/navigation/history.dart';
export 'engine/navigation/js_url_strategy.dart';
export 'engine/navigation/url_strategy.dart';
export 'engine/noto_font.dart';
export 'engine/onscreen_logging.dart';
export 'engine/picture.dart';

View File

@ -8,6 +8,7 @@ import 'dart:js_interop';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import 'package:web_test_fonts/web_test_fonts.dart';
/// The mode the app is running in.
@ -132,6 +133,10 @@ Future<void> initializeEngineServices({
// Store `jsConfiguration` so user settings are available to the engine.
configuration.setUserConfiguration(jsConfiguration);
// Setup the hook that allows users to customize URL strategy before running
// the app.
_addUrlStrategyListener();
// Called by the Web runtime just before hot restarting the app.
//
// This extension cleans up resources that are registered with browser's
@ -258,6 +263,26 @@ Future<void> _downloadAssetFonts() async {
}
}
void _addUrlStrategyListener() {
jsSetUrlStrategy = allowInterop((JsUrlStrategy? jsStrategy) {
if (jsStrategy == null) {
ui_web.urlStrategy = null;
} else {
// Because `JSStrategy` could be anything, we check for the
// `addPopStateListener` property and throw if it is missing.
if (!hasJsProperty(jsStrategy, 'addPopStateListener')) {
throw StateError(
'Unexpected JsUrlStrategy: $jsStrategy is missing '
'`addPopStateListener` property');
}
ui_web.urlStrategy = CustomUrlStrategy.fromJs(jsStrategy);
}
});
registerHotRestartListener(() {
jsSetUrlStrategy = null;
});
}
/// Whether to disable the font fallback system.
///
/// We need to disable font fallbacks for some framework tests because

View File

@ -0,0 +1,7 @@
// 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.
export 'navigation/history.dart';
export 'navigation/js_url_strategy.dart';
export 'navigation/url_strategy.dart';

View File

@ -0,0 +1,87 @@
// 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.
@JS()
library js_url_strategy;
import 'dart:js_interop';
import 'package:ui/ui.dart' as ui;
import '../dom.dart';
typedef _PathGetter = String Function();
typedef _StateGetter = Object? Function();
typedef _AddPopStateListener = ui.VoidCallback Function(DartDomEventListener);
typedef _StringToString = String Function(String);
typedef _StateOperation = void Function(
Object? state, String title, String url);
typedef _HistoryMove = Future<void> Function(double 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
@staticInterop
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,
});
}
extension JsUrlStrategyExtension on JsUrlStrategy {
/// Adds a listener to the `popstate` event and returns a function that, when
/// invoked, removes the listener.
external ui.VoidCallback addPopStateListener(DartDomEventListener fn);
/// Returns the active path in the browser.
external String getPath();
/// Returns the history state in the browser.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/state
external Object? getState();
/// Given a path that's internal to the app, create the external url that
/// will be used in the browser.
external String prepareExternalUrl(String internalUrl);
/// Push a new history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
external void pushState(Object? state, String title, String url);
/// Replace the currently active history entry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
external void replaceState(Object? state, String title, String url);
/// Moves forwards or backwards through the history stack.
///
/// A negative [count] value causes a backward move in the history stack. And
/// a positive [count] value causs a forward move.
///
/// Examples:
///
/// * `go(-2)` moves back 2 steps in history.
/// * `go(3)` moves forward 3 steps in hisotry.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/History/go
external Future<void> go(double count);
}

View File

@ -0,0 +1,48 @@
// 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.
import 'dart:async';
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../dom.dart';
import '../safe_browser_api.dart';
import 'js_url_strategy.dart';
/// Wraps a custom implementation of [ui_web.UrlStrategy] that was previously converted
/// to a [JsUrlStrategy].
class CustomUrlStrategy extends ui_web.UrlStrategy {
/// Wraps the [delegate] in a [CustomUrlStrategy] instance.
CustomUrlStrategy.fromJs(this.delegate);
final JsUrlStrategy delegate;
@override
ui.VoidCallback addPopStateListener(ui_web.PopStateListener fn) =>
delegate.addPopStateListener(allowInterop((DomEvent event) =>
fn((event as DomPopStateEvent).state)
));
@override
String getPath() => delegate.getPath();
@override
Object? getState() => delegate.getState();
@override
String prepareExternalUrl(String internalUrl) =>
delegate.prepareExternalUrl(internalUrl);
@override
void pushState(Object? state, String title, String url) =>
delegate.pushState(state, title, url);
@override
void replaceState(Object? state, String title, String url) =>
delegate.replaceState(state, title, url);
@override
Future<void> go(int count) => delegate.go(count.toDouble());
}

View File

@ -30,7 +30,7 @@ class TestHistoryEntry {
///
/// It keeps a list of history entries and event listeners in memory and
/// manipulates them in order to achieve the desired functionality.
class TestUrlStrategy implements ui_web.UrlStrategy {
class TestUrlStrategy extends ui_web.UrlStrategy {
/// Creates a instance of [TestUrlStrategy] with an empty string as the
/// path.
factory TestUrlStrategy() => TestUrlStrategy.fromEntry(const TestHistoryEntry(null, null, ''));

View File

@ -16,6 +16,7 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../engine.dart' show DimensionsProvider, registerHotRestartListener, renderer;
import 'dom.dart';
import 'navigation/history.dart';
import 'navigation/js_url_strategy.dart';
import 'platform_dispatcher.dart';
import 'services.dart';
import 'util.dart';
@ -323,6 +324,16 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
ui.Size? webOnlyDebugPhysicalSizeOverride;
}
typedef _JsSetUrlStrategy = void Function(JsUrlStrategy?);
/// A JavaScript hook to customize the URL strategy of a Flutter app.
//
// DO NOT CHANGE THE JS NAME, IT IS PUBLIC API AT THIS POINT.
//
// TODO(mdebbar): 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.SingletonFlutterWindow].
class EngineSingletonFlutterWindow extends EngineFlutterWindow {
EngineSingletonFlutterWindow(

View File

@ -82,7 +82,11 @@ typedef PopStateListener = void Function(Object? state);
///
/// By default, the [HashUrlStrategy] subclass is used if the app doesn't
/// specify one.
abstract interface class UrlStrategy {
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(PopStateListener fn);
@ -135,7 +139,7 @@ abstract interface class UrlStrategy {
/// // Somewhere before calling `runApp()` do:
/// setUrlStrategy(const HashUrlStrategy());
/// ```
class HashUrlStrategy implements UrlStrategy {
class HashUrlStrategy extends UrlStrategy {
/// Creates an instance of [HashUrlStrategy].
///
/// The [PlatformLocation] parameter is useful for testing to mock out browser

View File

@ -8,8 +8,9 @@ import 'package:quiver/testing/async.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' show window;
import 'package:ui/src/engine/dom.dart' show DomEvent, createDomPopStateEvent;
import 'package:ui/src/engine/navigation/history.dart';
import 'package:ui/src/engine/dom.dart'
show DomEvent, createDomPopStateEvent;
import 'package:ui/src/engine/navigation.dart';
import 'package:ui/src/engine/services.dart';
import 'package:ui/src/engine/test_embedding.dart';
import 'package:ui/ui_web/src/ui_web.dart';

View File

@ -3,12 +3,12 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:js_util' as js_util;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart' hide window;
import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
import '../common/matchers.dart';
import 'history_test.dart';
@ -48,15 +48,27 @@ void testMain() {
expect(window.viewId, 0);
});
test('window.defaultRouteName should work with a custom url strategy', () async {
const String path = '/initial';
const Object state = <dynamic, dynamic>{'origin': true};
final _SampleUrlStrategy customStrategy = _SampleUrlStrategy(path, state);
await window.debugInitializeHistory(customStrategy, useSingle: true);
test('window.defaultRouteName should work with JsUrlStrategy', () async {
dynamic state = <dynamic, dynamic>{};
final JsUrlStrategy jsUrlStrategy = JsUrlStrategy(
getPath: allowInterop(() => '/initial'),
getState: allowInterop(() => state),
addPopStateListener: allowInterop((DartDomEventListener listener) => allowInterop(() {})),
prepareExternalUrl: allowInterop((String value) => ''),
pushState: allowInterop((Object? newState, String title, String url) {
expect(newState is Map, true);
}),
replaceState: allowInterop((Object? newState, String title, String url) {
expect(newState is Map, true);
state = newState;
}),
go: allowInterop(([double? delta]) async {
expect(delta, -1);
}));
final CustomUrlStrategy strategy =
CustomUrlStrategy.fromJs(jsUrlStrategy);
await window.debugInitializeHistory(strategy, useSingle: true);
expect(window.defaultRouteName, '/initial');
// Also make sure that the custom url strategy was actually used.
expect(customStrategy.wasUsed, isTrue);
});
test('window.defaultRouteName should not change', () async {
@ -429,12 +441,7 @@ void testMain() {
test('can disable location strategy', () async {
// Disable URL strategy.
expect(
() {
ui_web.urlStrategy = null;
},
returnsNormally,
);
expect(() => jsSetUrlStrategy(null), returnsNormally);
// History should be initialized.
expect(window.browserHistory, isNotNull);
// But without a URL strategy.
@ -448,35 +455,27 @@ void testMain() {
expect(window.browserHistory.currentPath, '/');
});
test('cannot set url strategy after it was initialized', () async {
test('js interop throws on wrong type', () {
expect(() => jsSetUrlStrategy(123), throwsA(anything));
expect(() => jsSetUrlStrategy('foo'), throwsA(anything));
expect(() => jsSetUrlStrategy(false), throwsA(anything));
expect(() => jsSetUrlStrategy(<Object?>[]), throwsA(anything));
});
test('cannot set url strategy after it is initialized', () async {
final TestUrlStrategy testStrategy = TestUrlStrategy.fromEntry(
const TestHistoryEntry('initial state', null, '/'),
);
await window.debugInitializeHistory(testStrategy, useSingle: true);
expect(
() {
ui_web.urlStrategy = null;
},
throwsA(isAssertionError),
);
expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError));
});
test('cannot set url strategy more than once', () async {
// First time is okay.
expect(
() {
ui_web.urlStrategy = null;
},
returnsNormally,
);
expect(() => jsSetUrlStrategy(null), returnsNormally);
// Second time is not allowed.
expect(
() {
ui_web.urlStrategy = null;
},
throwsA(isAssertionError),
);
expect(() => jsSetUrlStrategy(null), throwsA(isAssertionError));
});
// Regression test for https://github.com/flutter/flutter/issues/77817
@ -487,41 +486,10 @@ void testMain() {
});
}
class _SampleUrlStrategy implements ui_web.UrlStrategy {
_SampleUrlStrategy(this._path, this._state);
final String _path;
final Object? _state;
bool wasUsed = false;
@override
String getPath() => _path;
@override
Object? getState() => _state;
@override
ui.VoidCallback addPopStateListener(DartDomEventListener listener) {
wasUsed = true;
return () {};
}
@override
String prepareExternalUrl(String value) => '';
@override
void pushState(Object? newState, String title, String url) {
wasUsed = true;
}
@override
void replaceState(Object? newState, String title, String url) {
wasUsed = true;
}
@override
Future<void> go(int delta) async {
wasUsed = true;
}
void jsSetUrlStrategy(dynamic strategy) {
js_util.callMethod<void>(
domWindow,
'_flutter_web_set_location_strategy',
<dynamic>[strategy],
);
}