diff --git a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart index f3bf957ba7d..901f730301c 100644 --- a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart +++ b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart @@ -102,17 +102,19 @@ extension ObjectToJSBoxedDartObject on Object { extension JSPromiseToFuture on JSPromise { @patch Future get toDart { - final completer = Completer(); - final success = (JSAny r) { + final completer = Completer(); + final success = (JSAny? r) { return completer.complete(r); }.toJS; - final error = (JSAny e) { + final error = (JSAny? e) { // TODO(joshualitt): Investigate reifying `JSNull` and `JSUndefined` on // all backends and if it is feasible, or feasible for some limited use // cases, then we should pass [e] directly to `completeError`. // TODO(joshualitt): Use helpers to avoid conflating `null` and `JSNull` / // `JSUndefined`. if (e == null) { + // Note that we pass false as a default. It's not currently possible to + // be able to differentiate between null and undefined. return completer.completeError(js_util.NullRejectionException(false)); } return completer.completeError(e); diff --git a/tests/lib/js/static_interop_test/js_types_test.dart b/tests/lib/js/static_interop_test/js_types_test.dart index 958dad2359c..6a0cf3db2d7 100644 --- a/tests/lib/js/static_interop_test/js_types_test.dart +++ b/tests/lib/js/static_interop_test/js_types_test.dart @@ -316,86 +316,98 @@ void syncTests() { expect(typeofEquals(definedNonNullAny, 'object'), true); } -@JS() -external JSPromise get resolvedPromise; - -@JS() -external JSPromise get rejectedPromise; - @JS() external JSPromise getResolvedPromise(); @JS() -external JSPromise getRejectablePromise(); +external JSPromise getRejectedPromise(); @JS() -external JSVoid rejectPromiseWithNull(); +external JSPromise resolvePromiseWithNullOrUndefined(bool resolveWithNull); @JS() -external JSVoid rejectPromiseWithUndefined(); +external JSPromise rejectPromiseWithNullOrUndefined(bool resolveWithNull); Future asyncTests() async { eval(r''' - globalThis.resolvedPromise = new Promise(resolve => resolve('resolved')); globalThis.getResolvedPromise = function() { - return resolvedPromise; + return Promise.resolve('resolved'); } - globalThis.getRejectablePromise = function() { - return new Promise(function(_, reject) { - globalThis.rejectPromise = reject; - }); + globalThis.getRejectedPromise = function() { + return Promise.reject(new Error('rejected')); } - globalThis.rejectPromiseWithNull = function() { - globalThis.rejectPromise(null); + globalThis.resolvePromiseWithNullOrUndefined = function(resolveWithNull) { + return Promise.resolve(resolveWithNull ? null : undefined); } - globalThis.rejectPromiseWithUndefined = function() { - globalThis.rejectPromise(undefined); + globalThis.rejectPromiseWithNullOrUndefined = function(rejectWithNull) { + return Promise.reject(rejectWithNull ? null : undefined); } '''); // [JSPromise] -> [Future]. - // Test resolved - { - Future f = resolvedPromise.toDart; - expect(((await f) as JSString).toDart, 'resolved'); - } - - // Test rejected - // TODO(joshualitt): Write a test for rejected promises that works on all - // backends. - - // Test return resolved + // Test resolution. { Future f = getResolvedPromise().toDart; expect(((await f) as JSString).toDart, 'resolved'); } - // Test promise chaining + // Test rejection. + { + try { + await getRejectedPromise().toDart; + fail('Expected rejected promise to throw.'); + } catch (e) { + final jsError = e as JSObject; + expect(jsError.toString(), 'Error: rejected'); + } + } + + // Test resolution Promise chaining. { bool didThen = false; - Future f = getResolvedPromise().toDart; - f.then((resolved) { + Future f = getResolvedPromise().toDart.then((resolved) { expect((resolved as JSString).toDart, 'resolved'); didThen = true; + return null; }); await f; expect(didThen, true); } - // Test rejecting promise with null should trigger an exception. - // TODO(joshualitt): `catchError` doesn't seem to clear the JS exception on - // Dart2Wasm. - //{ - // bool threw = false; - // Future f = getRejectablePromise().toDart; - // f.then((_) {}).catchError((e) { - // threw = true; - // expect(e is NullRejectionException, true); - // }); - // rejectPromiseWithNull(); - // await f; - // expect(threw, true); - //} + // Test rejection Promise chaining. + { + Future f = getRejectedPromise().toDart.then((_) { + fail('Expected rejected promise to throw.'); + return null; + }, onError: (e) { + final jsError = e as JSObject; + expect(jsError.toString(), 'Error: rejected'); + }); + await f; + } + + // Test resolving promise with null and undefined. + Future testResolveWithNullOrUndefined(bool resolveWithNull) async { + Future f = + resolvePromiseWithNullOrUndefined(resolveWithNull).toDart; + expect(((await f) as JSAny?), null); + } + + await testResolveWithNullOrUndefined(true); + await testResolveWithNullOrUndefined(false); + + // Test rejecting promise with null and undefined should trigger an exception. + Future testRejectionWithNullOrUndefined(bool rejectWithNull) async { + try { + await rejectPromiseWithNullOrUndefined(rejectWithNull).toDart; + fail('Expected rejected promise to throw.'); + } catch (e) { + expect(e is NullRejectionException, true); + } + } + + await testRejectionWithNullOrUndefined(true); + await testRejectionWithNullOrUndefined(false); } void main() async {