| // Copyright (c) 2026, 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. | |
| // ignore_for_file: non_constant_identifier_names | |
| import 'dart:core'; | |
| import 'package:api_summary/api_summary.dart'; | |
| import 'package:api_summary/src/api_builder.dart'; | |
| import 'package:test/test.dart'; | |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; | |
| import 'test_utils.dart'; | |
| void main() { | |
| defineReflectiveSuite(() { | |
| defineReflectiveTests(JsInteropTest); | |
| }); | |
| } | |
| @reflectiveTest | |
| class JsInteropTest extends ApiSummaryTest { | |
| Future<ApiSummary> _buildModel(Map<String, String> files) async { | |
| files.forEach(newFile); | |
| for (final file in files.keys) { | |
| if (file.endsWith('.dart')) await assertNoDiagnosticsInFile(file); | |
| } | |
| final context = contextCollection.contextFor( | |
| convertPath(testPackageLibPath), | |
| ); | |
| return buildApiPackage('test', context, const ApiSummaryCustomizer()); | |
| } | |
| Future<void> test_js_interop_members_and_extension_types() async { | |
| final model = await _buildModel({ | |
| '$testPackageLibPath/interop.dart': ''' | |
| import 'dart:js_interop'; | |
| class JSExport { | |
| final String? name; | |
| const JSExport([this.name]); | |
| } | |
| @JS() | |
| extension type Window(JSObject _) implements JSObject { | |
| @JS('document') | |
| external Document get doc; | |
| @JS('alert') | |
| external void showAlert(String message); | |
| } | |
| @JS('CustomDoc') | |
| extension type Document(JSObject _) implements JSObject { | |
| @JS() | |
| external String get title; | |
| } | |
| @JS('globalFunc') | |
| external void performAction(); | |
| @JSExport() | |
| class ExportedService { | |
| @JSExport('customCompute') | |
| int computeValue() => 42; | |
| } | |
| ''', | |
| }); | |
| final text = model.toString(); | |
| expect(text, contains('Window (extension type implements JSObject, js):')); | |
| expect(text, contains('doc (getter: Document, js: "document")')); | |
| expect( | |
| text, | |
| contains('showAlert (method: void Function(String), js: "alert")'), | |
| ); | |
| expect( | |
| text, | |
| contains( | |
| 'Document (extension type implements JSObject, js: "CustomDoc"):', | |
| ), | |
| ); | |
| expect(text, contains('title (getter: String, js)')); | |
| expect( | |
| text, | |
| contains('performAction (function: void Function(), js: "globalFunc")'), | |
| ); | |
| expect(text, contains('ExportedService (class extends Object, jsExport):')); | |
| expect( | |
| text, | |
| contains( | |
| 'computeValue (method: int Function(), jsExport: "customCompute")', | |
| ), | |
| ); | |
| // Verify JSON round-trip | |
| final json = model.toJson(); | |
| final rehydrated = ApiSummary.fromJson(json); | |
| expect(rehydrated.toString(), equals(text)); | |
| final lib = rehydrated.libraries.single; | |
| final windowExt = lib.extensionTypes.singleWhere((e) => e.name == 'Window'); | |
| final windowJs = windowExt.facets.whereType<JsBindingFacet>().firstOrNull; | |
| expect(windowJs, isNotNull); | |
| expect(windowJs!.name, isNull); | |
| final docGetter = windowExt.methods.singleWhere((m) => m.name == 'doc'); | |
| final docJs = docGetter.facets.whereType<JsBindingFacet>().firstOrNull; | |
| expect(docJs, isNotNull); | |
| expect(docJs!.name, 'document'); | |
| final exportedClass = lib.classes.singleWhere( | |
| (c) => c.name == 'ExportedService', | |
| ); | |
| final exportedClassJs = exportedClass.facets | |
| .whereType<JsExportFacet>() | |
| .firstOrNull; | |
| expect(exportedClassJs, isNotNull); | |
| expect(exportedClassJs!.name, isNull); | |
| final computeMethod = exportedClass.methods.singleWhere( | |
| (m) => m.name == 'computeValue', | |
| ); | |
| final computeMethodJs = computeMethod.facets | |
| .whereType<JsExportFacet>() | |
| .firstOrNull; | |
| expect(computeMethodJs, isNotNull); | |
| expect(computeMethodJs!.name, 'customCompute'); | |
| } | |
| Future<void> test_library_js_annotation() async { | |
| final model = await _buildModel({ | |
| '$testPackageLibPath/browser.dart': ''' | |
| @JS('DOM') | |
| library; | |
| import 'dart:js_interop'; | |
| @JS('Element') | |
| extension type Element(JSObject _) implements JSObject {} | |
| ''', | |
| }); | |
| final text = model.toString(); | |
| expect(text, contains('package:test/browser.dart (js: "DOM"):')); | |
| expect( | |
| text, | |
| contains('Element (extension type implements JSObject, js: "Element"):'), | |
| ); | |
| final json = model.toJson(); | |
| final rehydrated = ApiSummary.fromJson(json); | |
| expect(rehydrated.toString(), equals(text)); | |
| final lib = rehydrated.libraries.single; | |
| final facet = lib.facets.whereType<JsBindingFacet>().single; | |
| expect(facet.name, 'DOM'); | |
| } | |
| void test_js_facets_sorting_equality_and_deduplication() { | |
| const js1 = JsBindingFacet('foo'); | |
| const js2 = JsBindingFacet('foo'); | |
| const js3 = JsBindingFacet('bar'); | |
| const jsExport1 = JsExportFacet('compute'); | |
| const jsExport2 = JsExportFacet('compute'); | |
| final meta = MetaContractFacet([MetaContract.protected]); | |
| // Equality & Hash code | |
| expect(js1, equals(js2)); | |
| expect(js1.hashCode, equals(js2.hashCode)); | |
| expect(js1, isNot(equals(js3))); | |
| expect(jsExport1, equals(jsExport2)); | |
| // Comparable sorting: 'js' < 'js_export' < 'meta', and within 'js': | |
| // 'bar' < 'foo'. | |
| final unsorted = [meta, js1, jsExport1, js3, js2]; | |
| final sorted = unsorted.toSet().toList()..sort(); | |
| expect( | |
| sorted, | |
| equals([ | |
| js3, // js: "bar" | |
| js1, // js: "foo" | |
| jsExport1, // js_export: "compute" | |
| meta, // meta: protected | |
| ]), | |
| ); | |
| // ApiDeclaration deduplication and sorting | |
| final exec = ApiExecutable( | |
| name: 'testFunc', | |
| kind: ApiExecutableKind.function, | |
| typeParameters: const {}, | |
| returnType: const ApiVoidType(), | |
| parameters: const [], | |
| isStatic: true, | |
| facets: [meta, js1, jsExport1, js2, js1], | |
| ); | |
| expect(exec.facets.toList(), equals([js1, jsExport1, meta])); | |
| expect(() => (exec.facets as dynamic).add(js3), throwsUnsupportedError); | |
| } | |
| } |