[package:js] Add/modify tests for JS interop subtyping

Adds some basic tests to check for runtime subtyping with JS
interop types. Uses function types only to check for subtyping
relationships. Extends some tests on is and as checks as well.

Change-Id: I9cbc91d0c475fba3213dd9a3443921232c286804
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/170660
Commit-Queue: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/tests/lib/js/extends_test/extends_subtyping_test.dart b/tests/lib/js/extends_test/extends_subtyping_test.dart
new file mode 100644
index 0000000..76dc51e
--- /dev/null
+++ b/tests/lib/js/extends_test/extends_subtyping_test.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'extends_test_util.dart';
+
+void main() {
+  setUpWithoutES6Syntax();
+  testSubtyping();
+}
diff --git a/tests/lib/js/extends_test/extends_test.dart b/tests/lib/js/extends_test/extends_test.dart
index 12f8e3f..a9c8781 100644
--- a/tests/lib/js/extends_test/extends_test.dart
+++ b/tests/lib/js/extends_test/extends_test.dart
@@ -5,53 +5,6 @@
 import 'extends_test_util.dart';
 
 void main() {
-  // Use the old way to define inheritance between JS objects.
-  eval(r"""
-    function inherits(child, parent) {
-      if (child.prototype.__proto__) {
-        child.prototype.__proto__ = parent.prototype;
-      } else {
-        function tmp() {};
-        tmp.prototype = parent.prototype;
-        child.prototype = new tmp();
-        child.prototype.constructor = child;
-      }
-    }
-    function JSClass(a) {
-      this.a = a;
-      this.getA = function() {
-        return this.a;
-      }
-      this.getAOrB = function() {
-        return this.getA();
-      }
-    }
-    function JSExtendJSClass(a, b) {
-      JSClass.call(this, a);
-      this.b = b;
-      this.getB = function() {
-        return this.b;
-      }
-      this.getAOrB = function() {
-        return this.getB();
-      }
-    }
-    inherits(JSExtendJSClass, JSClass);
-    function JSExtendAnonymousClass(a, b) {
-      this.a = a;
-      this.b = b;
-      this.getA = function() {
-        return this.a;
-      }
-      this.getB = function() {
-        return this.b;
-      }
-      this.getAOrB = function() {
-        return this.getB();
-      }
-    }
-    self.anonExtendAnon = new JSExtendAnonymousClass(1, 2);
-    self.anonExtendJS = new JSExtendJSClass(1, 2);
-  """);
+  setUpWithoutES6Syntax();
   testInheritance();
 }
diff --git a/tests/lib/js/extends_test/extends_test_util.dart b/tests/lib/js/extends_test/extends_test_util.dart
index 5185176..e39e1b9 100644
--- a/tests/lib/js/extends_test/extends_test_util.dart
+++ b/tests/lib/js/extends_test/extends_test_util.dart
@@ -60,6 +60,108 @@
 external AnonymousExtendAnonymousClass get anonExtendAnon;
 external AnonymousExtendJSClass get anonExtendJS;
 
+void useJSClass(JSClass js) {}
+void useAnonymousClass(AnonymousClass a) {}
+
+void setUpWithoutES6Syntax() {
+  // Use the old way to define inheritance between JS objects.
+  eval(r"""
+    function inherits(child, parent) {
+      if (child.prototype.__proto__) {
+        child.prototype.__proto__ = parent.prototype;
+      } else {
+        function tmp() {};
+        tmp.prototype = parent.prototype;
+        child.prototype = new tmp();
+        child.prototype.constructor = child;
+      }
+    }
+    function JSClass(a) {
+      this.a = a;
+      this.getA = function() {
+        return this.a;
+      }
+      this.getAOrB = function() {
+        return this.getA();
+      }
+    }
+    function JSExtendJSClass(a, b) {
+      JSClass.call(this, a);
+      this.b = b;
+      this.getB = function() {
+        return this.b;
+      }
+      this.getAOrB = function() {
+        return this.getB();
+      }
+    }
+    inherits(JSExtendJSClass, JSClass);
+    function JSExtendAnonymousClass(a, b) {
+      this.a = a;
+      this.b = b;
+      this.getA = function() {
+        return this.a;
+      }
+      this.getB = function() {
+        return this.b;
+      }
+      this.getAOrB = function() {
+        return this.getB();
+      }
+    }
+    self.anonExtendAnon = new JSExtendAnonymousClass(1, 2);
+    self.anonExtendJS = new JSExtendJSClass(1, 2);
+  """);
+}
+
+void setUpWithES6Syntax() {
+  // Use the ES6 syntax for classes to make inheritance easier.
+  eval(r"""
+    class JSClass {
+      constructor(a) {
+        this.a = a;
+      }
+      getA() {
+        return this.a;
+      }
+      getAOrB() {
+        return this.getA();
+      }
+    }
+    class JSExtendJSClass extends JSClass {
+      constructor(a, b) {
+        super(a);
+        this.b = b;
+      }
+      getB() {
+        return this.b;
+      }
+      getAOrB() {
+        return this.getB();
+      }
+    }
+    self.JSExtendJSClass = JSExtendJSClass;
+    class JSExtendAnonymousClass {
+      constructor(a, b) {
+        this.a = a;
+        this.b = b;
+      }
+      getA() {
+        return this.a;
+      }
+      getB() {
+        return this.b;
+      }
+      getAOrB() {
+        return this.getB();
+      }
+    }
+    self.JSExtendAnonymousClass = JSExtendAnonymousClass;
+    self.anonExtendAnon = new JSExtendAnonymousClass(1, 2);
+    self.anonExtendJS = new JSExtendJSClass(1, 2);
+  """);
+}
+
 void testInheritance() {
   // Note that for the following, there are no meaningful tests for is checks or
   // as casts, since the web compilers should return true and succeed for all JS
@@ -92,3 +194,17 @@
   expect(anonExtendJS.getAOrB(), 2);
   expect((anonExtendJS as JSClass).getAOrB(), 2);
 }
+
+void testSubtyping() {
+  // Test subtyping for inheritance between JS and anonymous classes.
+  expect(useJSClass is void Function(JSExtendJSClass js), true);
+  expect(useAnonymousClass is void Function(AnonymousExtendAnonymousClass a),
+      true);
+  expect(useJSClass is void Function(AnonymousExtendJSClass a), true);
+  expect(useAnonymousClass is void Function(JSExtendAnonymousClass js), true);
+
+  expect(useJSClass is void Function(AnonymousExtendAnonymousClass a), false);
+  expect(useAnonymousClass is void Function(JSExtendJSClass js), false);
+  expect(useJSClass is void Function(JSExtendAnonymousClass js), false);
+  expect(useAnonymousClass is void Function(AnonymousExtendJSClass a), false);
+}
diff --git a/tests/lib/js/extends_test/extends_with_es6_subtyping_test.dart b/tests/lib/js/extends_test/extends_with_es6_subtyping_test.dart
new file mode 100644
index 0000000..e6185da
--- /dev/null
+++ b/tests/lib/js/extends_test/extends_with_es6_subtyping_test.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'extends_test_util.dart';
+
+void main() {
+  setUpWithES6Syntax();
+  testSubtyping();
+}
diff --git a/tests/lib/js/extends_test/extends_with_es6_test.dart b/tests/lib/js/extends_test/extends_with_es6_test.dart
index 00eb41f..8daae16 100644
--- a/tests/lib/js/extends_test/extends_with_es6_test.dart
+++ b/tests/lib/js/extends_test/extends_with_es6_test.dart
@@ -5,50 +5,6 @@
 import 'extends_test_util.dart';
 
 void main() {
-  // Use the ES6 syntax for classes to make inheritance easier.
-  eval(r"""
-    class JSClass {
-      constructor(a) {
-        this.a = a;
-      }
-      getA() {
-        return this.a;
-      }
-      getAOrB() {
-        return this.getA();
-      }
-    }
-    class JSExtendJSClass extends JSClass {
-      constructor(a, b) {
-        super(a);
-        this.b = b;
-      }
-      getB() {
-        return this.b;
-      }
-      getAOrB() {
-        return this.getB();
-      }
-    }
-    self.JSExtendJSClass = JSExtendJSClass;
-    class JSExtendAnonymousClass {
-      constructor(a, b) {
-        this.a = a;
-        this.b = b;
-      }
-      getA() {
-        return this.a;
-      }
-      getB() {
-        return this.b;
-      }
-      getAOrB() {
-        return this.getB();
-      }
-    }
-    self.JSExtendAnonymousClass = JSExtendAnonymousClass;
-    self.anonExtendAnon = new JSExtendAnonymousClass(1, 2);
-    self.anonExtendJS = new JSExtendJSClass(1, 2);
-  """);
+  setUpWithES6Syntax();
   testInheritance();
 }
diff --git a/tests/lib/js/is_check_and_as_cast_test.dart b/tests/lib/js/is_check_and_as_cast_test.dart
index ef48137..d88255c 100644
--- a/tests/lib/js/is_check_and_as_cast_test.dart
+++ b/tests/lib/js/is_check_and_as_cast_test.dart
@@ -9,6 +9,7 @@
 library is_check_and_as_cast_test;
 
 import 'package:js/js.dart';
+import 'package:expect/expect.dart' show hasUnsoundNullSafety;
 import 'package:expect/minitest.dart';
 
 @JS()
@@ -57,6 +58,8 @@
 external LiteralA get a;
 external LiteralB get b;
 
+class DartClass {}
+
 void main() {
   eval(r"""
     function Foo(a) {
@@ -119,4 +122,26 @@
   expect(() => (foo as LiteralB), returnsNormally);
   expect(a is Foo, isTrue);
   expect(() => (a as Foo), returnsNormally);
+
+  // You cannot cast between JS interop objects and Dart objects, however.
+  var dartClass = DartClass();
+  expect(dartClass is Foo, isFalse);
+  expect(() => (dartClass as Foo), throws);
+  expect(dartClass is LiteralA, isFalse);
+  expect(() => (dartClass as LiteralA), throws);
+
+  expect(foo is DartClass, isFalse);
+  expect(() => (foo as DartClass), throws);
+  expect(a is DartClass, isFalse);
+  expect(() => (a as DartClass), throws);
+
+  // Test that nullability is still respected with JS types.
+  Foo? nullableFoo = null;
+  expect(nullableFoo is Foo, false);
+  expect(() => (nullableFoo as Foo),
+      hasUnsoundNullSafety ? returnsNormally : throws);
+  LiteralA? nullableA = null;
+  expect(nullableA is LiteralA, false);
+  expect(() => (nullableA as LiteralA),
+      hasUnsoundNullSafety ? returnsNormally : throws);
 }
diff --git a/tests/lib/js/subtyping_test.dart b/tests/lib/js/subtyping_test.dart
new file mode 100644
index 0000000..2551fcd
--- /dev/null
+++ b/tests/lib/js/subtyping_test.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Tests subtyping relationships between JS and anonymous classes.
+
+@JS()
+library subtyping_test;
+
+import 'package:js/js.dart';
+import 'package:expect/expect.dart' show hasUnsoundNullSafety;
+import 'package:expect/minitest.dart';
+
+@JS()
+class JSClassA {}
+
+@JS()
+class JSClassB {}
+
+@JS()
+@anonymous
+class AnonymousClassA {}
+
+@JS()
+@anonymous
+class AnonymousClassB {}
+
+class DartClass {}
+
+void useJSClassA(JSClassA _) {}
+void useAnonymousClassA(AnonymousClassA _) {}
+void useDartClass(DartClass _) {}
+
+void useNullableJSClassA(JSClassA? _) {}
+void useNullableAnonymousClassA(AnonymousClassA? _) {}
+
+// Avoid static type optimization by running all tests using this.
+@pragma('dart2js:noInline')
+@pragma('dart2js:assumeDynamic')
+confuse(x) => x;
+
+void main() {
+  // Checks subtyping with the same type and nullability subtyping.
+  expect(useJSClassA is void Function(JSClassA), true);
+  expect(useAnonymousClassA is void Function(AnonymousClassA), true);
+  expect(useJSClassA is void Function(JSClassA?), hasUnsoundNullSafety);
+  expect(useAnonymousClassA is void Function(AnonymousClassA?),
+      hasUnsoundNullSafety);
+  expect(useNullableJSClassA is void Function(JSClassA?), true);
+  expect(useNullableAnonymousClassA is void Function(AnonymousClassA?), true);
+  expect(useNullableJSClassA is void Function(JSClassA), true);
+  expect(useNullableAnonymousClassA is void Function(AnonymousClassA), true);
+
+  expect(confuse(useJSClassA) is void Function(JSClassA), true);
+  expect(confuse(useAnonymousClassA) is void Function(AnonymousClassA), true);
+  expect(
+      confuse(useJSClassA) is void Function(JSClassA?), hasUnsoundNullSafety);
+  expect(confuse(useAnonymousClassA) is void Function(AnonymousClassA?),
+      hasUnsoundNullSafety);
+  expect(confuse(useNullableJSClassA) is void Function(JSClassA?), true);
+  expect(confuse(useNullableAnonymousClassA) is void Function(AnonymousClassA?),
+      true);
+  expect(confuse(useNullableJSClassA) is void Function(JSClassA), true);
+  expect(confuse(useNullableAnonymousClassA) is void Function(AnonymousClassA),
+      true);
+
+  // No subtyping between JS and anonymous classes.
+  expect(useJSClassA is void Function(AnonymousClassA), false);
+  expect(useAnonymousClassA is void Function(JSClassA), false);
+
+  expect(confuse(useJSClassA) is void Function(AnonymousClassA), false);
+  expect(confuse(useAnonymousClassA) is void Function(JSClassA), false);
+
+  // No subtyping between separate classes even if they're both JS classes or
+  // anonymous classes.
+  expect(useJSClassA is void Function(JSClassB), false);
+  expect(useAnonymousClassA is void Function(AnonymousClassB), false);
+
+  expect(confuse(useJSClassA) is void Function(JSClassB), false);
+  expect(confuse(useAnonymousClassA) is void Function(AnonymousClassB), false);
+
+  // No subtyping between JS/anonymous classes and Dart classes.
+  expect(useJSClassA is void Function(DartClass), false);
+  expect(useAnonymousClassA is void Function(DartClass), false);
+
+  expect(confuse(useJSClassA) is void Function(DartClass), false);
+  expect(confuse(useAnonymousClassA) is void Function(DartClass), false);
+}