blob: b3702d6aab05060bb51be96c91e2962b9564723c [file] [edit]
// 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);
}
}