[js_types] Add JSPromise and JSVoid.

CoreLibraryReviewExempt: Changes to Web specific libraries.
Change-Id: I71cc720dcf1cea3ca8a219259ccd35912ed00d9d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/282940
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
diff --git a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
index a451837..87f8dd7 100644
--- a/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_interop_patch.dart
@@ -5,6 +5,7 @@
 import 'dart:_internal' show patch;
 import 'dart:_js_types';
 import 'dart:js';
+import 'dart:js_util';
 import 'dart:typed_data';
 
 /// [JSExportedDartFunction] <-> [Function]
@@ -29,6 +30,12 @@
   JSExportedDartObject get toJS => this;
 }
 
+/// [JSPromise] -> [Future<JSAny?>].
+extension JSPromiseToFuture on JSPromise {
+  @patch
+  Future<JSAny?> get toDart => promiseToFuture<JSAny?>(this);
+}
+
 /// [JSArrayBuffer] <-> [ByteBuffer]
 extension JSArrayBufferToByteBuffer on JSArrayBuffer {
   @patch
diff --git a/sdk/lib/_internal/js_shared/lib/js_types.dart b/sdk/lib/_internal/js_shared/lib/js_types.dart
index 48e9d30..e1f66e3 100644
--- a/sdk/lib/_internal/js_shared/lib/js_types.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_types.dart
@@ -8,6 +8,7 @@
 /// library.
 library _js_types;
 
+import 'dart:_js_annotations';
 import 'dart:_interceptors' as interceptors;
 import 'dart:_internal' show patch;
 import 'dart:typed_data';
@@ -25,7 +26,7 @@
 typedef JSObject = interceptors.JSObject;
 typedef JSFunction = Function;
 typedef JSExportedDartFunction = Function;
-typedef JSArray = List;
+typedef JSArray = List<JSAny?>;
 typedef JSExportedDartObject = Object;
 typedef JSArrayBuffer = ByteBuffer;
 typedef JSDataView = ByteData;
@@ -42,3 +43,8 @@
 typedef JSNumber = double;
 typedef JSBoolean = bool;
 typedef JSString = String;
+typedef JSVoid = void;
+
+@JS()
+@staticInterop
+class JSPromise {}
diff --git a/sdk/lib/_internal/js_shared/lib/js_util_patch.dart b/sdk/lib/_internal/js_shared/lib/js_util_patch.dart
index fe45f9b..eb72b93 100644
--- a/sdk/lib/_internal/js_shared/lib/js_util_patch.dart
+++ b/sdk/lib/_internal/js_shared/lib/js_util_patch.dart
@@ -465,7 +465,7 @@
     // provided if the error is `null` or `undefined`.
     if (e == null) {
       return completer.completeError(
-          NullRejectionException._(JS('bool', '# === undefined', e)));
+          NullRejectionException(JS('bool', '# === undefined', e)));
     }
     return completer.completeError(e);
   }, 1);
diff --git a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
index a11fe57..56731fc 100644
--- a/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/js_interop_patch.dart
@@ -4,6 +4,8 @@
 
 import 'dart:_internal' show patch;
 import 'dart:_js_helper';
+import 'dart:async' show Completer;
+import 'dart:js_util' show NullRejectionException;
 import 'dart:typed_data';
 import 'dart:wasm';
 
@@ -37,6 +39,30 @@
       _box<JSExportedDartObject>(jsObjectFromDartObject(this));
 }
 
+/// [JSPromise] -> [Future<JSAny?>].
+extension JSPromiseToFuture on JSPromise {
+  @patch
+  Future<JSAny?> get toDart {
+    final completer = Completer<JSAny>();
+    final success = (JSAny r) {
+      return completer.complete(r);
+    }.toJS;
+    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) {
+        return completer.completeError(NullRejectionException(false));
+      }
+      return completer.completeError(e);
+    }.toJS;
+    promiseThen(_ref(this), _ref(success), _ref(error));
+    return completer.future;
+  }
+}
+
 /// [JSArrayBuffer] <-> [ByteBuffer]
 extension JSArrayBufferToByteBuffer on JSArrayBuffer {
   @patch
diff --git a/sdk/lib/_internal/wasm/lib/js_types.dart b/sdk/lib/_internal/wasm/lib/js_types.dart
index 65ae68d..a06cbb1 100644
--- a/sdk/lib/_internal/wasm/lib/js_types.dart
+++ b/sdk/lib/_internal/wasm/lib/js_types.dart
@@ -61,6 +61,10 @@
 @staticInterop
 class JSExportedDartFunction implements JSFunction {}
 
+@JS()
+@staticInterop
+class JSPromise implements JSObject {}
+
 @JS('Array')
 @staticInterop
 class JSArray implements JSObject {
@@ -131,3 +135,8 @@
 @JS()
 @staticInterop
 class JSString implements JSAny {}
+
+/// [JSVoid] is just a typedef for [void]. While we could just use
+/// `JSUndefined`, in the future we may be able to use this to elide `return`s
+/// in JS trampolines.
+typedef JSVoid = void;
diff --git a/sdk/lib/_internal/wasm/lib/js_util_patch.dart b/sdk/lib/_internal/wasm/lib/js_util_patch.dart
index a14a25e..df4010a 100644
--- a/sdk/lib/_internal/wasm/lib/js_util_patch.dart
+++ b/sdk/lib/_internal/wasm/lib/js_util_patch.dart
@@ -161,7 +161,7 @@
     // so we cannot tell them apart. In the future we should reify `undefined`
     // in Dart.
     if (e == null) {
-      return completer.completeError(NullRejectionException._(false));
+      return completer.completeError(NullRejectionException(false));
     }
     return completer.completeError(e);
   });
diff --git a/sdk/lib/js_interop/js_interop.dart b/sdk/lib/js_interop/js_interop.dart
index d3c7ea9..a85b15f 100644
--- a/sdk/lib/js_interop/js_interop.dart
+++ b/sdk/lib/js_interop/js_interop.dart
@@ -49,6 +49,9 @@
 /// TODO(joshualitt): Detail exactly what are the requirements.
 typedef JSExportedDartFunction = js_types.JSExportedDartFunction;
 
+/// The type of JS promises and promise-like objects, [JSPromise] <: [JSObject].
+typedef JSPromise = js_types.JSPromise;
+
 /// The type of all JS arrays, [JSArray] <: [JSObject].
 typedef JSArray = js_types.JSArray;
 
@@ -92,6 +95,10 @@
 
 /// TODO(joshualitt): Figure out how we want to handle JSUndefined and JSNull.
 
+/// The type of `JSUndefined` when returned from functions. Unlike pure JS,
+/// no actual object will be returned.
+typedef JSVoid = js_types.JSVoid;
+
 /// Extension members to support conversions between Dart types and JS types.
 /// Not all Dart types can be converted to JS types and vice versa.
 /// TODO(joshualitt): We might want to investigate using inline classes instead
@@ -115,6 +122,11 @@
   external JSExportedDartObject get toJS;
 }
 
+/// [JSPromise] -> [Future<JSAny?>].
+extension JSPromiseToFuture on JSPromise {
+  external Future<JSAny?> get toDart;
+}
+
 /// TODO(joshualitt): On Wasm backends List / Array conversion methods will
 /// copy, and on JS backends they will not. We should find a path towards
 /// consistent semantics.
diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart
index 513daaf..1196871 100644
--- a/sdk/lib/js_util/js_util.dart
+++ b/sdk/lib/js_util/js_util.dart
@@ -131,7 +131,7 @@
   // Indicates whether the value is `undefined` or `null`.
   final bool isUndefined;
 
-  NullRejectionException._(this.isUndefined);
+  NullRejectionException(this.isUndefined);
 
   @override
   String toString() {
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 373c5b3..cb77cc9 100644
--- a/tests/lib/js/static_interop_test/js_types_test.dart
+++ b/tests/lib/js/static_interop_test/js_types_test.dart
@@ -5,6 +5,7 @@
 // Check that JS types work.
 
 import 'dart:js_interop';
+import 'dart:js_util';
 import 'dart:typed_data';
 
 import 'package:expect/minitest.dart';
@@ -91,7 +92,7 @@
   String get foo => 'bar';
 }
 
-void main() {
+void syncTests() {
   eval('''
     globalThis.obj = {
       'foo': 'bar',
@@ -218,3 +219,90 @@
   String dartStr = str.toDart;
   expect(dartStr, 'foo');
 }
+
+@JS()
+external JSPromise get resolvedPromise;
+
+@JS()
+external JSPromise get rejectedPromise;
+
+@JS()
+external JSPromise getResolvedPromise();
+
+@JS()
+external JSPromise getRejectablePromise();
+
+@JS()
+external JSVoid rejectPromiseWithNull();
+
+@JS()
+external JSVoid rejectPromiseWithUndefined();
+
+Future<void> asyncTests() async {
+  eval(r'''
+    globalThis.resolvedPromise = new Promise(resolve => resolve('resolved'));
+    globalThis.getResolvedPromise = function() {
+      return resolvedPromise;
+    }
+    globalThis.getRejectablePromise = function() {
+      return new Promise(function(_, reject) {
+        globalThis.rejectPromise = reject;
+      });
+    }
+    globalThis.rejectPromiseWithNull = function() {
+      globalThis.rejectPromise(null);
+    }
+    globalThis.rejectPromiseWithUndefined = function() {
+      globalThis.rejectPromise(undefined);
+    }
+  ''');
+
+  // [JSPromise] -> [Future].
+  // Test resolved
+  {
+    Future<JSAny?> 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
+  {
+    Future<JSAny?> f = getResolvedPromise().toDart;
+    expect(((await f) as JSString).toDart, 'resolved');
+  }
+
+  // Test promise chaining
+  {
+    bool didThen = false;
+    Future<JSAny?> f = getResolvedPromise().toDart;
+    f.then((resolved) {
+      expect((resolved as JSString).toDart, 'resolved');
+      didThen = true;
+    });
+    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<JSAny?> f = getRejectablePromise().toDart;
+  //  f.then((_) {}).catchError((e) {
+  //    threw = true;
+  //    expect(e is NullRejectionException, true);
+  //  });
+  //  rejectPromiseWithNull();
+  //  await f;
+  //  expect(threw, true);
+  //}
+}
+
+void main() async {
+  syncTests();
+  await asyncTests();
+}