diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/clipboard.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/clipboard.dart index 7cdbf9d225e..22fff343c94 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/clipboard.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/clipboard.dart @@ -7,7 +7,6 @@ import 'package:ui/ui.dart' as ui; import 'browser_detection.dart'; import 'dom.dart'; import 'services.dart'; -import 'util.dart'; /// Handles clipboard related platform messages. class ClipboardMessageHandler { @@ -89,7 +88,7 @@ class ClipboardMessageHandler { /// APIs and the browser. abstract class CopyToClipboardStrategy { factory CopyToClipboardStrategy() { - return !unsafeIsNull(domWindow.navigator.clipboard) + return domWindow.navigator.clipboard != null ? ClipboardAPICopyStrategy() : ExecCommandCopyStrategy(); } @@ -109,7 +108,7 @@ abstract class CopyToClipboardStrategy { abstract class PasteFromClipboardStrategy { factory PasteFromClipboardStrategy() { return (browserEngine == BrowserEngine.firefox || - unsafeIsNull(domWindow.navigator.clipboard)) + domWindow.navigator.clipboard == null) ? ExecCommandPasteStrategy() : ClipboardAPIPasteStrategy(); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart index b1a02a82d29..86ab835a798 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart @@ -1298,8 +1298,10 @@ extension DomScreenExtension on DomScreen { class DomScreenOrientation extends DomEventTarget {} extension DomScreenOrientationExtension on DomScreenOrientation { - Future lock(String orientation) => js_util - .promiseToFuture(js_util.callMethod(this, 'lock', [orientation])); + Future lock(String orientation) { + final Object jsResult = js_util.callMethod(this, 'lock', [orientation]); + return js_util.promiseToFuture(jsResult); + } external void unlock(); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart index fadec05e57a..7c2b0ffaf67 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/embedder.dart @@ -437,12 +437,12 @@ class FlutterViewEmbedder { /// /// See w3c screen api: https://www.w3.org/TR/screen-orientation/ Future setPreferredOrientation(List orientations) { - final DomScreen screen = domWindow.screen!; - if (!unsafeIsNull(screen)) { + final DomScreen? screen = domWindow.screen; + if (screen != null) { final DomScreenOrientation? screenOrientation = screen.orientation; - if (!unsafeIsNull(screenOrientation)) { + if (screenOrientation != null) { if (orientations.isEmpty) { - screenOrientation!.unlock(); + screenOrientation.unlock(); return Future.value(true); } else { final String? lockType = @@ -450,7 +450,7 @@ class FlutterViewEmbedder { if (lockType != null) { final Completer completer = Completer(); try { - screenOrientation!.lock(lockType).then((dynamic _) { + screenOrientation.lock(lockType).then((dynamic _) { completer.complete(true); }).catchError((dynamic error) { // On Chrome desktop an error with 'not supported on this device @@ -470,13 +470,15 @@ class FlutterViewEmbedder { } // Converts device orientation to w3c OrientationLockType enum. + // + // See also: https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/lock static String? _deviceOrientationToLockType(String? deviceOrientation) { switch (deviceOrientation) { case 'DeviceOrientation.portraitUp': return orientationLockTypePortraitPrimary; - case 'DeviceOrientation.landscapeLeft': - return orientationLockTypePortraitSecondary; case 'DeviceOrientation.portraitDown': + return orientationLockTypePortraitSecondary; + case 'DeviceOrientation.landscapeLeft': return orientationLockTypeLandscapePrimary; case 'DeviceOrientation.landscapeRight': return orientationLockTypeLandscapeSecondary; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart index eb0ca60c689..7bcc1cbbfdd 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/safe_browser_api.dart @@ -242,15 +242,19 @@ void debugResetBrowserSupportsImageDecoder() { _imageDecoderConstructor != null; } +/// The signature of the function passed to the constructor of JavaScript `Promise`. +typedef JsPromiseCallback = void Function(void Function(Object? value) resolve, void Function(Object? error) reject); + /// Corresponds to JavaScript's `Promise`. /// /// This type doesn't need any members. Instead, it should be first converted /// to Dart's [Future] using [promiseToFuture] then interacted with through the /// [Future] API. -@JS() -@anonymous +@JS('window.Promise') @staticInterop -class JsPromise {} +class JsPromise { + external factory JsPromise(JsPromiseCallback callback); +} /// Corresponds to the browser's `ImageDecoder` type. /// diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart index 39083997f3f..16c1446e08d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/util.dart @@ -552,11 +552,6 @@ String blurSigmasToCssString(double sigmaX, double sigmaY) { return 'blur(${(sigmaX + sigmaY) * 0.5}px)'; } -/// Checks if the dynamic [object] is equal to null. -bool unsafeIsNull(dynamic object) { - return object == null; -} - /// A typed variant of [domWindow.fetch]. Future httpFetch(String url) async { final Object? result = await domWindow.fetch(url); diff --git a/engine/src/flutter/lib/web_ui/test/engine/window_test.dart b/engine/src/flutter/lib/web_ui/test/engine/window_test.dart index 0a58624e818..e55ed6d9e86 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/window_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/window_test.dart @@ -299,25 +299,112 @@ Future testMain() async { expect(responded, isTrue); }); - /// Regression test for https://github.com/flutter/flutter/issues/66128. - test("setPreferredOrientation responds even if browser doesn't support api", () async { - final DomScreen screen = domWindow.screen!; - js_util.setProperty(screen, 'orientation', null); - + // Emulates the framework sending a request for screen orientation lock. + Future sendSetPreferredOrientations(List orientations) { final Completer completer = Completer(); - final ByteData inputData = const JSONMethodCodec().encodeMethodCall(const MethodCall( - 'SystemChrome.setPreferredOrientations', - []))!; + final ByteData? inputData = const JSONMethodCodec().encodeMethodCall(MethodCall( + 'SystemChrome.setPreferredOrientations', + orientations, + )); window.sendPlatformMessage( 'flutter/platform', - inputData, - (ByteData? outputData) { - completer.complete(true); + inputData, + (ByteData? outputData) { + const MethodCodec codec = JSONMethodCodec(); + completer.complete(codec.decodeEnvelope(outputData!) as bool); }, ); - expect(await completer.future, isTrue); + return completer.future; + } + + // Regression test for https://github.com/flutter/flutter/issues/88269 + test('sets preferred screen orientation', () async { + final DomScreen original = domWindow.screen!; + + final List lockCalls = []; + int unlockCount = 0; + bool simulateError = false; + + // The `orientation` property cannot be overridden, so this test overrides the entire `screen`. + js_util.setProperty(domWindow, 'screen', js_util.jsify({ + 'orientation': { + 'lock': allowInterop((String lockType) { + lockCalls.add(lockType); + return JsPromise(allowInterop((Function(Object? value) resolve, Function reject) { + if (!simulateError) { + resolve(null); + } else { + reject('Simulating error'); + } + })); + }), + 'unlock': allowInterop(() { + unlockCount += 1; + }), + }, + })); + + // Sanity-check the test setup. + expect(lockCalls, []); + expect(unlockCount, 0); + await domWindow.screen!.orientation!.lock('hi'); + domWindow.screen!.orientation!.unlock(); + expect(lockCalls, ['hi']); + expect(unlockCount, 1); + lockCalls.clear(); + unlockCount = 0; + + expect(await sendSetPreferredOrientations(['DeviceOrientation.portraitUp']), isTrue); + expect(lockCalls, [FlutterViewEmbedder.orientationLockTypePortraitPrimary]); + expect(unlockCount, 0); + lockCalls.clear(); + unlockCount = 0; + + expect(await sendSetPreferredOrientations(['DeviceOrientation.portraitDown']), isTrue); + expect(lockCalls, [FlutterViewEmbedder.orientationLockTypePortraitSecondary]); + expect(unlockCount, 0); + lockCalls.clear(); + unlockCount = 0; + + expect(await sendSetPreferredOrientations(['DeviceOrientation.landscapeLeft']), isTrue); + expect(lockCalls, [FlutterViewEmbedder.orientationLockTypeLandscapePrimary]); + expect(unlockCount, 0); + lockCalls.clear(); + unlockCount = 0; + + expect(await sendSetPreferredOrientations(['DeviceOrientation.landscapeRight']), isTrue); + expect(lockCalls, [FlutterViewEmbedder.orientationLockTypeLandscapeSecondary]); + expect(unlockCount, 0); + lockCalls.clear(); + unlockCount = 0; + + expect(await sendSetPreferredOrientations([]), isTrue); + expect(lockCalls, []); + expect(unlockCount, 1); + lockCalls.clear(); + unlockCount = 0; + + simulateError = true; + expect(await sendSetPreferredOrientations(['DeviceOrientation.portraitDown']), isFalse); + expect(lockCalls, [FlutterViewEmbedder.orientationLockTypePortraitSecondary]); + expect(unlockCount, 0); + + js_util.setProperty(domWindow, 'screen', original); + }); + + /// Regression test for https://github.com/flutter/flutter/issues/66128. + test("setPreferredOrientation responds even if browser doesn't support api", () async { + final DomScreen original = domWindow.screen!; + + // The `orientation` property cannot be overridden, so this test overrides the entire `screen`. + js_util.setProperty(domWindow, 'screen', js_util.jsify({ + 'orientation': null, + })); + expect(domWindow.screen!.orientation, isNull); + expect(await sendSetPreferredOrientations([]), isFalse); + js_util.setProperty(domWindow, 'screen', original); }); test('SingletonFlutterWindow implements locale, locales, and locale change notifications', () async {