From 6929718456c7459a9311f02a886cb3f2cfd40344 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam Date: Thu, 25 Mar 2021 01:30:23 +0000 Subject: [PATCH] [pkg:js] Handle null value in promise rejection Closes https://github.com/dart-lang/sdk/issues/44602 Creates an exception to signal a `null`/`undefined` value when a converted promise is rejected with `null`/`undefined`. Change-Id: Ic7f14e23c6c1d51d6dbcc5831ffa8491418a4267 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/192046 Commit-Queue: Srujan Gaddam Reviewed-by: Sigmund Cherem --- sdk/lib/js_util/js_util.dart | 29 ++++++++- .../js/js_util/promise_reject_null_test.dart | 64 +++++++++++++++++++ .../js/js_util/promise_reject_null_test.dart | 64 +++++++++++++++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 tests/lib/js/js_util/promise_reject_null_test.dart create mode 100644 tests/lib_2/js/js_util/promise_reject_null_test.dart diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart index b1d0e276f95..df75900bf7c 100644 --- a/sdk/lib/js_util/js_util.dart +++ b/sdk/lib/js_util/js_util.dart @@ -149,6 +149,24 @@ dynamic callConstructor(Object constr, List? arguments) { // return _wrapToDart(jsObj); } +/// Exception for when the promise is rejected with a `null` or `undefined` +/// value. +/// +/// This is public to allow users to catch when the promise is rejected with +/// `null` or `undefined` versus some other value. +class NullRejectionException implements Exception { + // Indicates whether the value is `undefined` or `null`. + final bool isUndefined; + + NullRejectionException._(this.isUndefined); + + @override + String toString() { + var value = this.isUndefined ? 'undefined' : 'null'; + return 'Promise was rejected with a value of `$value`.'; + } +} + /// Converts a JavaScript Promise to a Dart [Future]. /// /// ```dart @@ -163,7 +181,16 @@ Future promiseToFuture(Object jsPromise) { final completer = Completer(); final success = convertDartClosureToJS((r) => completer.complete(r), 1); - final error = convertDartClosureToJS((e) => completer.completeError(e), 1); + final error = convertDartClosureToJS((e) { + // Note that `completeError` expects a non-nullable error regardless of + // whether null-safety is enabled, so a `NullRejectionException` is always + // provided if the error is `null` or `undefined`. + if (e == null) { + return completer.completeError( + NullRejectionException._(JS('bool', '# === undefined', e))); + } + return completer.completeError(e); + }, 1); JS('', '#.then(#, #)', jsPromise, success, error); return completer.future; diff --git a/tests/lib/js/js_util/promise_reject_null_test.dart b/tests/lib/js/js_util/promise_reject_null_test.dart new file mode 100644 index 00000000000..70da7f9b55a --- /dev/null +++ b/tests/lib/js/js_util/promise_reject_null_test.dart @@ -0,0 +1,64 @@ +@JS() +library promise_reject_null_test; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' show promiseToFuture, NullRejectionException; + +import 'package:expect/minitest.dart'; + +@JS() +external void eval(String s); + +@JS('Promise.reject') +external dynamic getRejectedPromise(v); + +@JS() +external void reject(v); +@JS() +external dynamic getNewPromise(); + +void main() async { + eval(''' + self.getNewPromise = function () { + return new Promise(function (_, reject) { + self.reject = reject; + }); + }; + '''); + + // Rejected promise with a `null` value should trigger a + // `NullRejectionException`. + await promiseToFuture(getRejectedPromise(null)).then((_) { + fail("Expected promise to reject and not fulfill."); + }).catchError((e) { + expect(e is NullRejectionException, true); + expect(e.isUndefined, false); + }); + + // Similar to the above, except we reject using JS interop. + var future = promiseToFuture(getNewPromise()).then((_) { + fail("Expected promise to reject and not fulfill."); + }).catchError((e) { + expect(e is NullRejectionException, true); + expect(e.isUndefined, false); + }); + + reject(null); + + await future; + + // It's also possible to reject with `undefined`. Make sure that the exception + // correctly flags that case. + future = promiseToFuture(getNewPromise()).then((_) { + fail("Expected promise to reject and not fulfill."); + }).catchError((e) { + expect(e is NullRejectionException, true); + expect(e.isUndefined, true); + }); + + eval(''' + self.reject(undefined); + '''); + + await future; +} diff --git a/tests/lib_2/js/js_util/promise_reject_null_test.dart b/tests/lib_2/js/js_util/promise_reject_null_test.dart new file mode 100644 index 00000000000..70da7f9b55a --- /dev/null +++ b/tests/lib_2/js/js_util/promise_reject_null_test.dart @@ -0,0 +1,64 @@ +@JS() +library promise_reject_null_test; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' show promiseToFuture, NullRejectionException; + +import 'package:expect/minitest.dart'; + +@JS() +external void eval(String s); + +@JS('Promise.reject') +external dynamic getRejectedPromise(v); + +@JS() +external void reject(v); +@JS() +external dynamic getNewPromise(); + +void main() async { + eval(''' + self.getNewPromise = function () { + return new Promise(function (_, reject) { + self.reject = reject; + }); + }; + '''); + + // Rejected promise with a `null` value should trigger a + // `NullRejectionException`. + await promiseToFuture(getRejectedPromise(null)).then((_) { + fail("Expected promise to reject and not fulfill."); + }).catchError((e) { + expect(e is NullRejectionException, true); + expect(e.isUndefined, false); + }); + + // Similar to the above, except we reject using JS interop. + var future = promiseToFuture(getNewPromise()).then((_) { + fail("Expected promise to reject and not fulfill."); + }).catchError((e) { + expect(e is NullRejectionException, true); + expect(e.isUndefined, false); + }); + + reject(null); + + await future; + + // It's also possible to reject with `undefined`. Make sure that the exception + // correctly flags that case. + future = promiseToFuture(getNewPromise()).then((_) { + fail("Expected promise to reject and not fulfill."); + }).catchError((e) { + expect(e is NullRejectionException, true); + expect(e.isUndefined, true); + }); + + eval(''' + self.reject(undefined); + '''); + + await future; +}