[pkg:js] Add tests for export validation

Tests the various checks needed for @JSExport and createDartExport,
with and without inheritance. Also renames the mock directory to export.

Change-Id: Iae0233f1080a2f12f20b46ba1a6b30aeb36843a8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/260743
Reviewed-by: Riley Porter <rileyporter@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
diff --git a/tests/lib/js/export/export_inheritance_collision_test.dart b/tests/lib/js/export/export_inheritance_collision_test.dart
new file mode 100644
index 0000000..4d1fff6
--- /dev/null
+++ b/tests/lib/js/export/export_inheritance_collision_test.dart
@@ -0,0 +1,203 @@
+// Copyright (c) 2022, 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 collisions of `@JSExport` members using inheritance.
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart';
+
+// Overridden members do not count as an export name collision.
+@JSExport()
+class SuperclassNoCollision {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+@JSExport()
+mixin MixinNoCollision {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+@JSExport()
+mixin MixinNoCollision2 {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+@JSExport()
+class InheritanceOneOverrideNoCollision extends SuperclassNoCollision
+    with MixinNoCollision {}
+
+@JSExport()
+class InheritanceTwoOverridesNoCollision extends SuperclassNoCollision
+    with MixinNoCollision, MixinNoCollision2 {}
+
+@JSExport()
+class InheritanceThreeOverridesNoCollision extends SuperclassNoCollision
+    with MixinNoCollision, MixinNoCollision2 {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+// Export name collisions can exist across classes and mixin applications.
+@JSExport()
+class SuperclassCollision {
+  @JSExport('field')
+  int fieldSuper = throw '';
+  @JSExport('finalField')
+  final int finalFieldSuper = throw '';
+  @JSExport('getSet')
+  int get getSetSuper => throw '';
+  @JSExport('getSet')
+  set getSetSuper(int val) => throw '';
+  @JSExport('method')
+  void methodSuper() => throw '';
+}
+
+@JSExport()
+mixin MixinCollision {
+  @JSExport('field')
+  int fieldMixin = throw '';
+  @JSExport('finalField')
+  final int finalFieldMixin = throw '';
+  @JSExport('getSet')
+  int get getSetMixin => throw '';
+  @JSExport('getSet')
+  set getSetMixin(int val) => throw '';
+  @JSExport('method')
+  void methodMixin() => throw '';
+}
+
+@JSExport()
+mixin MixinCollision2 {
+  @JSExport('field')
+  int fieldMixin2 = throw '';
+  @JSExport('finalField')
+  final int finalFieldMixin2 = throw '';
+  @JSExport('getSet')
+  int get getSetMixin2 => throw '';
+  @JSExport('getSet')
+  set getSetMixin2(int val) => throw '';
+  @JSExport('method')
+  void methodMixin2() => throw '';
+}
+
+@JSExport()
+class InheritanceRenameOneCollision extends SuperclassCollision
+//    ^
+// [web] The following class members collide with the same export 'field': MixinCollision.fieldMixin, SuperclassCollision.fieldSuper.
+// [web] The following class members collide with the same export 'finalField': MixinCollision.finalFieldMixin, SuperclassCollision.finalFieldSuper.
+// [web] The following class members collide with the same export 'getSet': MixinCollision.getSetMixin, MixinCollision.getSetMixin, SuperclassCollision.getSetSuper, SuperclassCollision.getSetSuper.
+// [web] The following class members collide with the same export 'method': MixinCollision.methodMixin, SuperclassCollision.methodSuper.
+    with
+        MixinCollision {}
+
+@JSExport()
+class InheritanceRenameTwoCollisions extends SuperclassCollision
+//    ^
+// [web] The following class members collide with the same export 'field': MixinCollision.fieldMixin, MixinCollision2.fieldMixin2, SuperclassCollision.fieldSuper.
+// [web] The following class members collide with the same export 'finalField': MixinCollision.finalFieldMixin, MixinCollision2.finalFieldMixin2, SuperclassCollision.finalFieldSuper.
+// [web] The following class members collide with the same export 'getSet': MixinCollision.getSetMixin, MixinCollision.getSetMixin, MixinCollision2.getSetMixin2, MixinCollision2.getSetMixin2, SuperclassCollision.getSetSuper, SuperclassCollision.getSetSuper.
+// [web] The following class members collide with the same export 'method': MixinCollision.methodMixin, MixinCollision2.methodMixin2, SuperclassCollision.methodSuper.
+    with
+        MixinCollision,
+        MixinCollision2 {}
+
+@JSExport()
+class InheritanceRenameThreeCollisions extends SuperclassCollision
+//    ^
+// [web] The following class members collide with the same export 'field': InheritanceRenameThreeCollisions.fieldDerived, MixinCollision.fieldMixin, MixinCollision2.fieldMixin2, SuperclassCollision.fieldSuper.
+// [web] The following class members collide with the same export 'finalField': InheritanceRenameThreeCollisions.finalFieldDerived, MixinCollision.finalFieldMixin, MixinCollision2.finalFieldMixin2, SuperclassCollision.finalFieldSuper.
+// [web] The following class members collide with the same export 'getSet': InheritanceRenameThreeCollisions.getSetDerived, InheritanceRenameThreeCollisions.getSetDerived, MixinCollision.getSetMixin, MixinCollision.getSetMixin, MixinCollision2.getSetMixin2, MixinCollision2.getSetMixin2, SuperclassCollision.getSetSuper, SuperclassCollision.getSetSuper.
+// [web] The following class members collide with the same export 'method': InheritanceRenameThreeCollisions.methodDerived, MixinCollision.methodMixin, MixinCollision2.methodMixin2, SuperclassCollision.methodSuper.
+    with
+        MixinCollision,
+        MixinCollision2 {
+  @JSExport('field')
+  int fieldDerived = throw '';
+  @JSExport('finalField')
+  final int finalFieldDerived = throw '';
+  @JSExport('getSet')
+  int get getSetDerived => throw '';
+  @JSExport('getSet')
+  set getSetDerived(int val) => throw '';
+  @JSExport('method')
+  void methodDerived() => throw '';
+}
+
+// No collision if superclass doesn't contain any members marked for export.
+class SuperclassNoMembers {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+class InheritanceNoSuperclassMembers extends SuperclassNoMembers {
+  @JSExport('field')
+  int fieldDerived = throw '';
+  @JSExport('finalField')
+  final int finalFieldDerived = throw '';
+  @JSExport('getSet')
+  int get getSetDerived => throw '';
+  @JSExport('getSet')
+  set getSetDerived(int val) => throw '';
+  @JSExport('method')
+  void methodDerived() => throw '';
+}
+
+class Fields {
+  int getterField = throw '';
+  int setterField = throw '';
+}
+
+// These partial overrides are okay, as there's still one getter and one setter
+// per name.
+@JSExport()
+class PartialOverrideFieldNoCollision extends Fields {
+  set getterField(int val) => throw '';
+  int get setterField => throw '';
+}
+
+@JSExport()
+class GetSet {
+  int get getter => throw '';
+  set setter(int val) => throw '';
+}
+
+// Getters and setters through inheritance should be okay.
+@JSExport()
+class GetSetInheritanceNoCollision extends Fields {
+  int get setter => throw '';
+  set getter(int val) => throw '';
+}
+
+void main() {
+  createDartExport(InheritanceOneOverrideNoCollision());
+  createDartExport(InheritanceTwoOverridesNoCollision());
+  createDartExport(InheritanceThreeOverridesNoCollision());
+
+  createDartExport(InheritanceRenameOneCollision());
+  createDartExport(InheritanceRenameTwoCollisions());
+  createDartExport(InheritanceRenameThreeCollisions());
+
+  createDartExport(InheritanceNoSuperclassMembers());
+
+  createDartExport(PartialOverrideFieldNoCollision());
+  createDartExport(GetSetInheritanceNoCollision());
+}
diff --git a/tests/lib/js/export/export_validation_test.dart b/tests/lib/js/export/export_validation_test.dart
new file mode 100644
index 0000000..af2fab8
--- /dev/null
+++ b/tests/lib/js/export/export_validation_test.dart
@@ -0,0 +1,187 @@
+// Copyright (c) 2022, 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 uses of `@JSExport` and `createDartExport`.
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart';
+
+// You can either have a @JSExport annotation on the entire class or select
+// members only, and they may contain members that are ignored.
+@JSExport()
+class ExportAll {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  int method() => throw '';
+}
+
+class ExportSome {
+  ExportSome();
+  factory ExportSome.factory() => ExportSome();
+
+  @JSExport()
+  int field = throw '';
+  final int finalField = throw '';
+  @JSExport()
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  @JSExport()
+  int method() => throw '';
+
+  static int staticField = throw '';
+  static void staticMethod() => throw '';
+}
+
+extension on ExportSome {
+  int extensionMethod() => throw '';
+
+  static int extensionStaticField = throw '';
+  static void extensionStaticMethod() => throw '';
+}
+
+// We should leave Dart classes with no exports alone unless used in
+// `createDartExport`.
+class NoAnnotations {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  int method() => throw '';
+}
+
+// If there is a `@JSExport` annotation, but no exportable members, it's
+// considered an error.
+@JSExport()
+abstract class ExportNoneQualify {
+//             ^
+// [web] Class 'ExportNoneQualify' has no exportable members in the class or the inheritance chain.
+  factory ExportNoneQualify() => throw '';
+
+  abstract int abstractField;
+  void abstractMethod();
+
+  static int staticField = throw '';
+  static void staticMethod() => throw '';
+}
+
+extension on ExportNoneQualify {
+  int method() => throw '';
+
+  static int staticField = throw '';
+  static void staticMethod() => throw '';
+}
+
+@JSExport()
+class ExportEmpty {}
+//    ^
+// [web] Class 'ExportEmpty' has no exportable members in the class or the inheritance chain.
+
+// These are errors, as there are no exportable members that have the annotation
+// on them or on their class.
+@JSExport()
+class ExportWithNoExportSuperclass extends NoAnnotations {}
+//    ^
+// [web] Class 'ExportWithNoExportSuperclass' has no exportable members in the class or the inheritance chain.
+
+@JSExport()
+class ExportWithEmptyExportSuperclass extends ExportEmpty {}
+//    ^
+// [web] Class 'ExportWithEmptyExportSuperclass' has no exportable members in the class or the inheritance chain.
+
+// This isn't an error to write, but it will be when you use it as part of
+// `createDartExport`.
+class NoExportWithExportSuperclass extends ExportAll {}
+
+void testNumberOfExports() {
+  createDartExport(ExportAll());
+  createDartExport(ExportSome());
+  createDartExport(NoAnnotations());
+//^
+// [web] Class 'NoAnnotations' does not have a `@JSExport` on it or any of its members.
+  createDartExport(ExportNoneQualify());
+  createDartExport(ExportEmpty());
+  createDartExport(ExportWithNoExportSuperclass());
+  createDartExport(ExportWithEmptyExportSuperclass());
+  createDartExport(NoExportWithExportSuperclass());
+//^
+// [web] Class 'NoExportWithExportSuperclass' does not have a `@JSExport` on it or any of its members.
+}
+
+@JS()
+@staticInterop
+class StaticInterop {
+  external factory StaticInterop();
+}
+
+typedef InvalidType = void Function();
+
+void testUseDartInterface() {
+  // Needs to be an interface type.
+  createDartExport<InvalidType>(() {});
+//^
+// [web] Type argument 'void Function()' needs to be an interface type.
+
+  // Can't use an interop class.
+  createDartExport(StaticInterop());
+//^
+// [web] Type argument 'StaticInterop' needs to be a non-JS interop type.
+}
+
+// Incompatible members can't have the same export name using renaming.
+@JSExport()
+class RenameCollision {
+//    ^
+// [web] The following class members collide with the same export 'exportName': RenameCollision.exportName, RenameCollision.finalField, RenameCollision.getSet, RenameCollision.getSet, RenameCollision.method.
+  int exportName = throw '';
+  @JSExport('exportName')
+  final int finalField = throw '';
+  @JSExport('exportName')
+  int get getSet => throw '';
+  @JSExport('exportName')
+  set getSet(int val) => throw '';
+  @JSExport('exportName')
+  void method() => throw '';
+}
+
+// Allowed collisions are only between getters and setters.
+@JSExport()
+class GetSetNoCollision {
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+
+  @JSExport('renamedGetSet')
+  int get renamedGetter => throw '';
+  @JSExport('renamedGetSet')
+  set renamedSetter(int val) => throw '';
+}
+
+void testCollisions() {
+  createDartExport(RenameCollision());
+  createDartExport(GetSetNoCollision());
+}
+
+// Class annotation values are warnings, not values, so they don't show up in
+// static error tests.
+@JSExport('Invalid')
+class ClassWithValue {
+  int get getSet => throw '';
+}
+
+@JSExport('Invalid')
+mixin MixinWithValue {
+  int get getSet => throw '';
+}
+
+void testClassExportWithValue() {
+  createDartExport(ClassWithValue());
+}
+
+void main() {
+  testNumberOfExports();
+  testUseDartInterface();
+  testCollisions();
+  testClassExportWithValue();
+}
diff --git a/tests/lib/js/static_interop_test/mock/extension_conflict_test.dart b/tests/lib/js/export/extension_conflict_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/extension_conflict_test.dart
rename to tests/lib/js/export/extension_conflict_test.dart
diff --git a/tests/lib/js/static_interop_test/mock/functional_test.dart b/tests/lib/js/export/functional_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/functional_test.dart
rename to tests/lib/js/export/functional_test.dart
diff --git a/tests/lib/js/static_interop_test/mock/functional_test_lib.dart b/tests/lib/js/export/functional_test_lib.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/functional_test_lib.dart
rename to tests/lib/js/export/functional_test_lib.dart
diff --git a/tests/lib/js/static_interop_test/mock/incorrect_type_arguments_test.dart b/tests/lib/js/export/incorrect_type_arguments_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/incorrect_type_arguments_test.dart
rename to tests/lib/js/export/incorrect_type_arguments_test.dart
diff --git a/tests/lib/js/static_interop_test/mock/inheritance_test.dart b/tests/lib/js/export/inheritance_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/inheritance_test.dart
rename to tests/lib/js/export/inheritance_test.dart
diff --git a/tests/lib/js/static_interop_test/mock/missing_overrides_test.dart b/tests/lib/js/export/missing_overrides_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/missing_overrides_test.dart
rename to tests/lib/js/export/missing_overrides_test.dart
diff --git a/tests/lib/js/static_interop_test/mock/mockito_test.dart b/tests/lib/js/export/mockito_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/mockito_test.dart
rename to tests/lib/js/export/mockito_test.dart
diff --git a/tests/lib/js/static_interop_test/mock/proto_test.dart b/tests/lib/js/export/proto_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/proto_test.dart
rename to tests/lib/js/export/proto_test.dart
diff --git a/tests/lib/js/static_interop_test/mock/subtype_overrides_test.dart b/tests/lib/js/export/subtype_overrides_test.dart
similarity index 100%
rename from tests/lib/js/static_interop_test/mock/subtype_overrides_test.dart
rename to tests/lib/js/export/subtype_overrides_test.dart
diff --git a/tests/lib/lib.status b/tests/lib/lib.status
index 25070be..43869c7 100644
--- a/tests/lib/lib.status
+++ b/tests/lib/lib.status
@@ -12,10 +12,10 @@
 isolate/issue_24243_parent_isolate_test: Skip # Requires checked mode
 
 [ $runtime == d8 ]
+js/export/proto_test: SkipByDesign # Uses dart:html.
 js/js_util/javascriptobject_extensions_test: SkipByDesign # Uses dart:html.
 js/static_interop_test/constants_test: SkipByDesign # Uses dart:html.
 js/static_interop_test/futurevaluetype_test: SkipByDesign # Uses dart:html.
-js/static_interop_test/mock/proto_test: SkipByDesign # Uses dart:html.
 js/static_interop_test/supertype_transform_test: SkipByDesign # Uses dart:html.
 
 [ $runtime == dart_precompiled ]
diff --git a/tests/lib_2/js/export/export_inheritance_collision_test.dart b/tests/lib_2/js/export/export_inheritance_collision_test.dart
new file mode 100644
index 0000000..4d1fff6
--- /dev/null
+++ b/tests/lib_2/js/export/export_inheritance_collision_test.dart
@@ -0,0 +1,203 @@
+// Copyright (c) 2022, 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 collisions of `@JSExport` members using inheritance.
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart';
+
+// Overridden members do not count as an export name collision.
+@JSExport()
+class SuperclassNoCollision {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+@JSExport()
+mixin MixinNoCollision {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+@JSExport()
+mixin MixinNoCollision2 {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+@JSExport()
+class InheritanceOneOverrideNoCollision extends SuperclassNoCollision
+    with MixinNoCollision {}
+
+@JSExport()
+class InheritanceTwoOverridesNoCollision extends SuperclassNoCollision
+    with MixinNoCollision, MixinNoCollision2 {}
+
+@JSExport()
+class InheritanceThreeOverridesNoCollision extends SuperclassNoCollision
+    with MixinNoCollision, MixinNoCollision2 {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+// Export name collisions can exist across classes and mixin applications.
+@JSExport()
+class SuperclassCollision {
+  @JSExport('field')
+  int fieldSuper = throw '';
+  @JSExport('finalField')
+  final int finalFieldSuper = throw '';
+  @JSExport('getSet')
+  int get getSetSuper => throw '';
+  @JSExport('getSet')
+  set getSetSuper(int val) => throw '';
+  @JSExport('method')
+  void methodSuper() => throw '';
+}
+
+@JSExport()
+mixin MixinCollision {
+  @JSExport('field')
+  int fieldMixin = throw '';
+  @JSExport('finalField')
+  final int finalFieldMixin = throw '';
+  @JSExport('getSet')
+  int get getSetMixin => throw '';
+  @JSExport('getSet')
+  set getSetMixin(int val) => throw '';
+  @JSExport('method')
+  void methodMixin() => throw '';
+}
+
+@JSExport()
+mixin MixinCollision2 {
+  @JSExport('field')
+  int fieldMixin2 = throw '';
+  @JSExport('finalField')
+  final int finalFieldMixin2 = throw '';
+  @JSExport('getSet')
+  int get getSetMixin2 => throw '';
+  @JSExport('getSet')
+  set getSetMixin2(int val) => throw '';
+  @JSExport('method')
+  void methodMixin2() => throw '';
+}
+
+@JSExport()
+class InheritanceRenameOneCollision extends SuperclassCollision
+//    ^
+// [web] The following class members collide with the same export 'field': MixinCollision.fieldMixin, SuperclassCollision.fieldSuper.
+// [web] The following class members collide with the same export 'finalField': MixinCollision.finalFieldMixin, SuperclassCollision.finalFieldSuper.
+// [web] The following class members collide with the same export 'getSet': MixinCollision.getSetMixin, MixinCollision.getSetMixin, SuperclassCollision.getSetSuper, SuperclassCollision.getSetSuper.
+// [web] The following class members collide with the same export 'method': MixinCollision.methodMixin, SuperclassCollision.methodSuper.
+    with
+        MixinCollision {}
+
+@JSExport()
+class InheritanceRenameTwoCollisions extends SuperclassCollision
+//    ^
+// [web] The following class members collide with the same export 'field': MixinCollision.fieldMixin, MixinCollision2.fieldMixin2, SuperclassCollision.fieldSuper.
+// [web] The following class members collide with the same export 'finalField': MixinCollision.finalFieldMixin, MixinCollision2.finalFieldMixin2, SuperclassCollision.finalFieldSuper.
+// [web] The following class members collide with the same export 'getSet': MixinCollision.getSetMixin, MixinCollision.getSetMixin, MixinCollision2.getSetMixin2, MixinCollision2.getSetMixin2, SuperclassCollision.getSetSuper, SuperclassCollision.getSetSuper.
+// [web] The following class members collide with the same export 'method': MixinCollision.methodMixin, MixinCollision2.methodMixin2, SuperclassCollision.methodSuper.
+    with
+        MixinCollision,
+        MixinCollision2 {}
+
+@JSExport()
+class InheritanceRenameThreeCollisions extends SuperclassCollision
+//    ^
+// [web] The following class members collide with the same export 'field': InheritanceRenameThreeCollisions.fieldDerived, MixinCollision.fieldMixin, MixinCollision2.fieldMixin2, SuperclassCollision.fieldSuper.
+// [web] The following class members collide with the same export 'finalField': InheritanceRenameThreeCollisions.finalFieldDerived, MixinCollision.finalFieldMixin, MixinCollision2.finalFieldMixin2, SuperclassCollision.finalFieldSuper.
+// [web] The following class members collide with the same export 'getSet': InheritanceRenameThreeCollisions.getSetDerived, InheritanceRenameThreeCollisions.getSetDerived, MixinCollision.getSetMixin, MixinCollision.getSetMixin, MixinCollision2.getSetMixin2, MixinCollision2.getSetMixin2, SuperclassCollision.getSetSuper, SuperclassCollision.getSetSuper.
+// [web] The following class members collide with the same export 'method': InheritanceRenameThreeCollisions.methodDerived, MixinCollision.methodMixin, MixinCollision2.methodMixin2, SuperclassCollision.methodSuper.
+    with
+        MixinCollision,
+        MixinCollision2 {
+  @JSExport('field')
+  int fieldDerived = throw '';
+  @JSExport('finalField')
+  final int finalFieldDerived = throw '';
+  @JSExport('getSet')
+  int get getSetDerived => throw '';
+  @JSExport('getSet')
+  set getSetDerived(int val) => throw '';
+  @JSExport('method')
+  void methodDerived() => throw '';
+}
+
+// No collision if superclass doesn't contain any members marked for export.
+class SuperclassNoMembers {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  void method() => throw '';
+}
+
+class InheritanceNoSuperclassMembers extends SuperclassNoMembers {
+  @JSExport('field')
+  int fieldDerived = throw '';
+  @JSExport('finalField')
+  final int finalFieldDerived = throw '';
+  @JSExport('getSet')
+  int get getSetDerived => throw '';
+  @JSExport('getSet')
+  set getSetDerived(int val) => throw '';
+  @JSExport('method')
+  void methodDerived() => throw '';
+}
+
+class Fields {
+  int getterField = throw '';
+  int setterField = throw '';
+}
+
+// These partial overrides are okay, as there's still one getter and one setter
+// per name.
+@JSExport()
+class PartialOverrideFieldNoCollision extends Fields {
+  set getterField(int val) => throw '';
+  int get setterField => throw '';
+}
+
+@JSExport()
+class GetSet {
+  int get getter => throw '';
+  set setter(int val) => throw '';
+}
+
+// Getters and setters through inheritance should be okay.
+@JSExport()
+class GetSetInheritanceNoCollision extends Fields {
+  int get setter => throw '';
+  set getter(int val) => throw '';
+}
+
+void main() {
+  createDartExport(InheritanceOneOverrideNoCollision());
+  createDartExport(InheritanceTwoOverridesNoCollision());
+  createDartExport(InheritanceThreeOverridesNoCollision());
+
+  createDartExport(InheritanceRenameOneCollision());
+  createDartExport(InheritanceRenameTwoCollisions());
+  createDartExport(InheritanceRenameThreeCollisions());
+
+  createDartExport(InheritanceNoSuperclassMembers());
+
+  createDartExport(PartialOverrideFieldNoCollision());
+  createDartExport(GetSetInheritanceNoCollision());
+}
diff --git a/tests/lib_2/js/export/export_validation_test.dart b/tests/lib_2/js/export/export_validation_test.dart
new file mode 100644
index 0000000..af2fab8
--- /dev/null
+++ b/tests/lib_2/js/export/export_validation_test.dart
@@ -0,0 +1,187 @@
+// Copyright (c) 2022, 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 uses of `@JSExport` and `createDartExport`.
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart';
+
+// You can either have a @JSExport annotation on the entire class or select
+// members only, and they may contain members that are ignored.
+@JSExport()
+class ExportAll {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  int method() => throw '';
+}
+
+class ExportSome {
+  ExportSome();
+  factory ExportSome.factory() => ExportSome();
+
+  @JSExport()
+  int field = throw '';
+  final int finalField = throw '';
+  @JSExport()
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  @JSExport()
+  int method() => throw '';
+
+  static int staticField = throw '';
+  static void staticMethod() => throw '';
+}
+
+extension on ExportSome {
+  int extensionMethod() => throw '';
+
+  static int extensionStaticField = throw '';
+  static void extensionStaticMethod() => throw '';
+}
+
+// We should leave Dart classes with no exports alone unless used in
+// `createDartExport`.
+class NoAnnotations {
+  int field = throw '';
+  final int finalField = throw '';
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+  int method() => throw '';
+}
+
+// If there is a `@JSExport` annotation, but no exportable members, it's
+// considered an error.
+@JSExport()
+abstract class ExportNoneQualify {
+//             ^
+// [web] Class 'ExportNoneQualify' has no exportable members in the class or the inheritance chain.
+  factory ExportNoneQualify() => throw '';
+
+  abstract int abstractField;
+  void abstractMethod();
+
+  static int staticField = throw '';
+  static void staticMethod() => throw '';
+}
+
+extension on ExportNoneQualify {
+  int method() => throw '';
+
+  static int staticField = throw '';
+  static void staticMethod() => throw '';
+}
+
+@JSExport()
+class ExportEmpty {}
+//    ^
+// [web] Class 'ExportEmpty' has no exportable members in the class or the inheritance chain.
+
+// These are errors, as there are no exportable members that have the annotation
+// on them or on their class.
+@JSExport()
+class ExportWithNoExportSuperclass extends NoAnnotations {}
+//    ^
+// [web] Class 'ExportWithNoExportSuperclass' has no exportable members in the class or the inheritance chain.
+
+@JSExport()
+class ExportWithEmptyExportSuperclass extends ExportEmpty {}
+//    ^
+// [web] Class 'ExportWithEmptyExportSuperclass' has no exportable members in the class or the inheritance chain.
+
+// This isn't an error to write, but it will be when you use it as part of
+// `createDartExport`.
+class NoExportWithExportSuperclass extends ExportAll {}
+
+void testNumberOfExports() {
+  createDartExport(ExportAll());
+  createDartExport(ExportSome());
+  createDartExport(NoAnnotations());
+//^
+// [web] Class 'NoAnnotations' does not have a `@JSExport` on it or any of its members.
+  createDartExport(ExportNoneQualify());
+  createDartExport(ExportEmpty());
+  createDartExport(ExportWithNoExportSuperclass());
+  createDartExport(ExportWithEmptyExportSuperclass());
+  createDartExport(NoExportWithExportSuperclass());
+//^
+// [web] Class 'NoExportWithExportSuperclass' does not have a `@JSExport` on it or any of its members.
+}
+
+@JS()
+@staticInterop
+class StaticInterop {
+  external factory StaticInterop();
+}
+
+typedef InvalidType = void Function();
+
+void testUseDartInterface() {
+  // Needs to be an interface type.
+  createDartExport<InvalidType>(() {});
+//^
+// [web] Type argument 'void Function()' needs to be an interface type.
+
+  // Can't use an interop class.
+  createDartExport(StaticInterop());
+//^
+// [web] Type argument 'StaticInterop' needs to be a non-JS interop type.
+}
+
+// Incompatible members can't have the same export name using renaming.
+@JSExport()
+class RenameCollision {
+//    ^
+// [web] The following class members collide with the same export 'exportName': RenameCollision.exportName, RenameCollision.finalField, RenameCollision.getSet, RenameCollision.getSet, RenameCollision.method.
+  int exportName = throw '';
+  @JSExport('exportName')
+  final int finalField = throw '';
+  @JSExport('exportName')
+  int get getSet => throw '';
+  @JSExport('exportName')
+  set getSet(int val) => throw '';
+  @JSExport('exportName')
+  void method() => throw '';
+}
+
+// Allowed collisions are only between getters and setters.
+@JSExport()
+class GetSetNoCollision {
+  int get getSet => throw '';
+  set getSet(int val) => throw '';
+
+  @JSExport('renamedGetSet')
+  int get renamedGetter => throw '';
+  @JSExport('renamedGetSet')
+  set renamedSetter(int val) => throw '';
+}
+
+void testCollisions() {
+  createDartExport(RenameCollision());
+  createDartExport(GetSetNoCollision());
+}
+
+// Class annotation values are warnings, not values, so they don't show up in
+// static error tests.
+@JSExport('Invalid')
+class ClassWithValue {
+  int get getSet => throw '';
+}
+
+@JSExport('Invalid')
+mixin MixinWithValue {
+  int get getSet => throw '';
+}
+
+void testClassExportWithValue() {
+  createDartExport(ClassWithValue());
+}
+
+void main() {
+  testNumberOfExports();
+  testUseDartInterface();
+  testCollisions();
+  testClassExportWithValue();
+}
diff --git a/tests/lib_2/js/static_interop_test/mock/extension_conflict_test.dart b/tests/lib_2/js/export/extension_conflict_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/extension_conflict_test.dart
rename to tests/lib_2/js/export/extension_conflict_test.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/functional_test.dart b/tests/lib_2/js/export/functional_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/functional_test.dart
rename to tests/lib_2/js/export/functional_test.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/functional_test_lib.dart b/tests/lib_2/js/export/functional_test_lib.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/functional_test_lib.dart
rename to tests/lib_2/js/export/functional_test_lib.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/incorrect_type_arguments_test.dart b/tests/lib_2/js/export/incorrect_type_arguments_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/incorrect_type_arguments_test.dart
rename to tests/lib_2/js/export/incorrect_type_arguments_test.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/inheritance_test.dart b/tests/lib_2/js/export/inheritance_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/inheritance_test.dart
rename to tests/lib_2/js/export/inheritance_test.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/missing_overrides_test.dart b/tests/lib_2/js/export/missing_overrides_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/missing_overrides_test.dart
rename to tests/lib_2/js/export/missing_overrides_test.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/mockito_test.dart b/tests/lib_2/js/export/mockito_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/mockito_test.dart
rename to tests/lib_2/js/export/mockito_test.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/proto_test.dart b/tests/lib_2/js/export/proto_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/proto_test.dart
rename to tests/lib_2/js/export/proto_test.dart
diff --git a/tests/lib_2/js/static_interop_test/mock/subtype_overrides_test.dart b/tests/lib_2/js/export/subtype_overrides_test.dart
similarity index 100%
rename from tests/lib_2/js/static_interop_test/mock/subtype_overrides_test.dart
rename to tests/lib_2/js/export/subtype_overrides_test.dart
diff --git a/tests/lib_2/lib_2.status b/tests/lib_2/lib_2.status
index bf3222c..6357acb 100644
--- a/tests/lib_2/lib_2.status
+++ b/tests/lib_2/lib_2.status
@@ -12,10 +12,10 @@
 isolate/issue_24243_parent_isolate_test: Skip # Requires checked mode
 
 [ $runtime == d8 ]
+js/export/proto_test: SkipByDesign # Uses dart:html.
 js/js_util/javascriptobject_extensions_test: SkipByDesign # Uses dart:html.
 js/static_interop_test/constants_test: SkipByDesign # Uses dart:html.
 js/static_interop_test/futurevaluetype_test: SkipByDesign # Uses dart:html.
-js/static_interop_test/mock/proto_test: SkipByDesign # Uses dart:html.
 js/static_interop_test/supertype_transform_test: SkipByDesign # Uses dart:html.
 
 [ $runtime == dart_precompiled ]