| // Copyright (c) 2023, 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 'dart:js_interop'; |
| import 'dart:js_interop_unsafe'; |
| import 'dart:typed_data'; |
| |
| import 'package:expect/expect.dart'; |
| |
| @JS() |
| external void eval(String code); |
| |
| void createObjectTest() { |
| JSObject o = JSObject(); |
| void testHasGetSet(String property, String? value) { |
| // has/[]/[]= |
| Expect.isFalse(o.has(property)); |
| o[property] = value?.toJS; |
| Expect.isTrue(o.has(property)); |
| Expect.equals(value, (o[property] as JSString?)?.toDart); |
| Expect.isTrue(o.delete(property.toJS).toDart); |
| |
| // Weirdly enough, delete almost always returns true. |
| Expect.isTrue(o.delete(property.toJS).toDart); |
| |
| // hasProperty/getProperty/setProperty |
| Expect.isFalse(o.hasProperty(property.toJS).toDart); |
| o.setProperty(property.toJS, value?.toJS); |
| Expect.isTrue(o.hasProperty(property.toJS).toDart); |
| Expect.equals(value, (o.getProperty(property.toJS) as JSString?)?.toDart); |
| Expect.isTrue(o.delete(property.toJS).toDart); |
| } |
| |
| testHasGetSet('foo', 'bar'); |
| testHasGetSet('baz', null); |
| } |
| |
| void equalTest() { |
| // Different objects aren't equal. |
| { |
| JSObject o1 = JSObject(); |
| JSObject o2 = JSObject(); |
| Expect.notEquals(o1, o2); |
| } |
| |
| { |
| eval(r''' |
| function JSClass() {} |
| |
| globalThis.boolData = true; |
| globalThis.boolData2 = true; |
| globalThis.numData = 4; |
| globalThis.numData2 = 4; |
| globalThis.arrData = [1, 2, 3]; |
| globalThis.strData = 'foo'; |
| globalThis.strData2 = 'foo'; |
| globalThis.funcData = function JSClass() {} |
| globalThis.JSClass = new globalThis.funcData(); |
| '''); |
| JSObject gc = globalContext; |
| void test(String propertyName, bool testCanonicalization) { |
| Expect.equals(gc[propertyName], gc[propertyName]); |
| if (testCanonicalization) { |
| Expect.equals(gc[propertyName], gc[propertyName + "2"]); |
| } |
| } |
| |
| test("boolData", true); |
| test("numData", true); |
| test("arrData", false); |
| test("strData", true); |
| test("funcData", false); |
| test("JSClass", false); |
| } |
| } |
| |
| // TODO(srujzs): This helper is no longer in dart:js_interop_unsafe. Move this |
| // test. |
| void typeofTest() { |
| eval(r''' |
| globalThis.b = true; |
| globalThis.n = 4; |
| globalThis.str = 'foo'; |
| globalThis.f = function foo() {} |
| globalThis.o = {}; |
| globalThis.nil = null; |
| globalThis.u = undefined; |
| globalThis.sym = Symbol('sym'); |
| '''); |
| |
| final types = { |
| 'boolean', |
| 'number', |
| 'string', |
| 'function', |
| 'object', |
| 'undefined', |
| 'symbol' |
| }; |
| void test(String property, String expectedType) { |
| Expect.isTrue(globalContext[property].typeofEquals(expectedType)); |
| for (final type in types) { |
| if (type != expectedType) { |
| Expect.isFalse(globalContext[property].typeofEquals(type)); |
| } |
| } |
| } |
| |
| test('b', 'boolean'); |
| test('n', 'number'); |
| test('str', 'string'); |
| test('f', 'function'); |
| test('o', 'object'); |
| test('nil', 'object'); |
| // TODO(joshualitt): Test for `undefined` when we it can flow into `JSAny?`. |
| // test('u', 'undefined'); |
| test('sym', 'symbol'); |
| } |
| |
| // TODO(srujzs): This helper is no longer in dart:js_interop_unsafe. Move this |
| // test. |
| void instanceOfTest() { |
| eval(r''' |
| globalThis.JSClass1 = function() {} |
| globalThis.JSClass2 = function() {} |
| |
| globalThis.obj = new JSClass1(); |
| '''); |
| JSObject gc = globalContext; |
| JSObject obj = gc['obj'] as JSObject; |
| JSFunction jsClass1Constructor = gc['JSClass1'] as JSFunction; |
| JSFunction jsClass2Constructor = gc['JSClass2'] as JSFunction; |
| Expect.isTrue(obj.instanceof(jsClass1Constructor)); |
| Expect.isFalse(obj.instanceof(jsClass2Constructor)); |
| Expect.isFalse(0.toJS.instanceof(jsClass2Constructor)); |
| Expect.isTrue(obj.instanceOfString('JSClass1')); |
| Expect.isFalse(obj.instanceOfString('JSClass2')); |
| Expect.isFalse(0.toJS.instanceOfString('JSClass1')); |
| } |
| |
| void _expectIterableEquals(Iterable<Object?> l, Iterable<Object?> r) { |
| final lIt = l.iterator; |
| final rIt = r.iterator; |
| while (lIt.moveNext()) { |
| Expect.isTrue(rIt.moveNext()); |
| _expectRecEquals(lIt.current, rIt.current); |
| } |
| Expect.isFalse(rIt.moveNext()); |
| } |
| |
| void _expectRecEquals(Object? l, Object? r) { |
| if (l is Iterable && r is Iterable) { |
| _expectIterableEquals(l, r); |
| } else if (l is Map && r is Map) { |
| _expectIterableEquals(l.keys, r.keys); |
| for (final key in l.keys) { |
| _expectRecEquals(l[key], r[key]); |
| } |
| } else { |
| Expect.equals(l, r); |
| } |
| } |
| |
| void methodsAndConstructorsTest() { |
| eval(r''' |
| function JSClass(c) { |
| this.c = c; |
| this.sum = (a, b) => { |
| console.log(a + ' ' + b); |
| return `${a}${b}${this.c}`; |
| } |
| } |
| globalThis.JSClass = JSClass; |
| '''); |
| JSObject gc = globalContext; |
| JSFunction constructor = gc['JSClass'] as JSFunction; |
| |
| // Var args one arg |
| JSObject jsObj1 = |
| constructor.callAsConstructorVarArgs<JSObject>(<JSAny?>['world!'.toJS]); |
| Expect.equals( |
| 'hello world!', |
| jsObj1.callMethodVarArgs<JSString>( |
| 'sum'.toJS, <JSAny?>['hello'.toJS, ' '.toJS]).toDart); |
| Expect.equals( |
| 'helloundefinedworld!', |
| jsObj1.callMethodVarArgs<JSString>( |
| 'sum'.toJS, <JSAny?>['hello'.toJS]).toDart); |
| Expect.equals('undefinedundefinedworld!', |
| jsObj1.callMethodVarArgs<JSString>('sum'.toJS, <JSAny?>[]).toDart); |
| Expect.equals('undefinedundefinedworld!', |
| jsObj1.callMethodVarArgs<JSString>('sum'.toJS).toDart); |
| Expect.equals( |
| 'nullnullworld!', |
| jsObj1.callMethodVarArgs<JSString>( |
| 'sum'.toJS, <JSAny?>[null, null]).toDart); |
| // Var args no args |
| jsObj1 = constructor.callAsConstructorVarArgs<JSObject>(); |
| Expect.equals(jsObj1['c'], null); |
| |
| // Fixed args one arg |
| jsObj1 = constructor.callAsConstructor<JSObject>('world!'.toJS); |
| Expect.equals('hello world!', |
| jsObj1.callMethod<JSString>('sum'.toJS, 'hello'.toJS, ' '.toJS).toDart); |
| Expect.equals('helloundefinedworld!', |
| jsObj1.callMethod<JSString>('sum'.toJS, 'hello'.toJS).toDart); |
| Expect.equals('undefinedundefinedworld!', |
| jsObj1.callMethod<JSString>('sum'.toJS, null).toDart); |
| Expect.equals('undefinedundefinedworld!', |
| jsObj1.callMethod<JSString>('sum'.toJS).toDart); |
| // Fixed args no args |
| jsObj1 = constructor.callAsConstructor<JSObject>(); |
| Expect.equals(jsObj1['c'], null); |
| } |
| |
| void deepConversionsTest() { |
| // Dart to JS. |
| Expect.isNull(null.jsify().dartify()); |
| Expect.equals(true, true.jsify().dartify()); |
| Expect.equals(2.0, 2.0.jsify().dartify()); |
| Expect.equals('foo', 'foo'.jsify().dartify()); |
| _expectRecEquals( |
| ['a', 'b', 'c'], ['a', 'b', 'c'].jsify().dartify() as List<Object?>); |
| _expectRecEquals( |
| { |
| 'null': 'foo', |
| 'foo': null, |
| 'a': 1, |
| 'b': true, |
| 'c': [1, 2, 3, null], |
| 'd': 'foo', |
| 'e': { |
| 'f': 2, |
| 'g': [2, 4, 6] |
| }, |
| }, |
| { |
| 'null': 'foo', |
| 'foo': null, |
| 'a': 1, |
| 'b': true, |
| 'c': [1, 2, 3, null], |
| 'd': 'foo', |
| 'e': { |
| 'f': 2, |
| 'g': [2, 4, 6] |
| }, |
| }.jsify().dartify()); |
| List<Object?> l = Int8List.fromList(<int>[-128, 0, 127]); |
| _expectIterableEquals(l, l.jsify().dartify() as Int8List); |
| l = Uint8List.fromList([-1, 0, 255, 256]); |
| _expectIterableEquals(l, l.jsify().dartify() as Uint8List); |
| l = Uint8ClampedList.fromList([-1, 0, 255, 256]); |
| _expectIterableEquals(l, l.jsify().dartify() as Uint8ClampedList); |
| l = Int16List.fromList([-32769, -32768, 0, 32767, 32768]); |
| _expectIterableEquals(l, l.jsify().dartify() as Int16List); |
| l = Uint16List.fromList([-1, 0, 65535, 65536]); |
| _expectIterableEquals(l, l.jsify().dartify() as Uint16List); |
| l = Int32List.fromList([-2147483648, 0, 2147483647]); |
| _expectIterableEquals(l, l.jsify().dartify() as Int32List); |
| l = Uint32List.fromList([-1, 0, 4294967295, 4294967296]); |
| _expectIterableEquals(l, l.jsify().dartify() as Uint32List); |
| l = Float32List.fromList([-1000.488, -0.00001, 0.0001, 10004.888]); |
| _expectIterableEquals(l, l.jsify().dartify() as Float32List); |
| l = Float64List.fromList([-1000.488, -0.00001, 0.0001, 10004.888]); |
| _expectIterableEquals(l, l.jsify().dartify() as Float64List); |
| ByteBuffer buffer = Uint8List.fromList([0, 1, 2, 3]).buffer; |
| _expectIterableEquals(buffer.asUint8List(), |
| (buffer.jsify().dartify() as ByteBuffer).asUint8List()); |
| ByteData byteData = ByteData.view(buffer); |
| _expectIterableEquals(byteData.buffer.asUint8List(), |
| (byteData.jsify().dartify() as ByteData).buffer.asUint8List()); |
| |
| // JS to Dart. |
| eval(r''' |
| globalThis.a = null; |
| globalThis.b = 'foo'; |
| globalThis.c = ['a', 'b', 'c']; |
| globalThis.d = 2.5; |
| globalThis.e = true; |
| globalThis.f = function () { return 'hello world'; }; |
| globalThis.g = { |
| null: 'foo', |
| 'foo': null, |
| 'a': 1, |
| 'b': true, |
| 'c': [1, 2, 3, null], |
| 'd': 'foo', |
| 'e': {'f': 2, 'g': [2, 4, 6]}, |
| }; |
| globalThis.invoke = function (f) { return f(); } |
| globalThis.rec = {}; |
| globalThis.rec = {'a': rec}; |
| globalThis.int8Array = new Int8Array([-128, 0, 127]); |
| globalThis.uint8Array = new Uint8Array([-1, 0, 255, 256]); |
| globalThis.uint8ClampedArray = new Uint8ClampedArray([-1, 0, 255, 256]); |
| globalThis.int16Array = new Int16Array([-32769, -32768, 0, 32767, 32768]); |
| globalThis.uint16Array = new Uint16Array([-1, 0, 65535, 65536]); |
| globalThis.int32Array = new Int32Array([-2147483648, 0, 2147483647]); |
| globalThis.uint32Array = new Uint32Array([-1, 0, 4294967295, 4294967296]); |
| globalThis.float32Array = new Float32Array([-1000.488, -0.00001, 0.0001, |
| 10004.888]); |
| globalThis.float64Array = new Float64Array([-1000.488, -0.00001, 0.0001, |
| 10004.888]); |
| globalThis.arrayBuffer = globalThis.uint8Array.buffer; |
| globalThis.dataView = new DataView(globalThis.arrayBuffer); |
| globalThis.implicitExplicit = [ |
| {'foo': 'bar'}, |
| [1, 2, 3, {'baz': 'boo'}], |
| ]; |
| let keyObject = function () {}; |
| globalThis.keyObject1 = keyObject; |
| globalThis.keyObject2 = keyObject; |
| '''); |
| JSObject gc = globalContext; |
| Expect.isNull(gc['a']); |
| Expect.equals('foo', gc.getProperty<JSString>('b'.toJS).toDart); |
| _expectRecEquals( |
| ['a', 'b', 'c'], |
| gc |
| .getProperty<JSArray>('c'.toJS) |
| .toDart |
| .map((JSAny? o) => (o as JSString).toDart)); |
| Expect.equals(2.5, gc.getProperty<JSNumber>('d'.toJS).toDartDouble); |
| Expect.equals(true, gc.getProperty<JSBoolean>('e'.toJS).toDart); |
| _expectRecEquals({ |
| 'null': 'foo', |
| 'foo': null, |
| 'a': 1, |
| 'b': true, |
| 'c': [1, 2, 3, null], |
| 'd': 'foo', |
| 'e': { |
| 'f': 2, |
| 'g': [2, 4, 6] |
| }, |
| }, gc.getProperty('g'.toJS).dartify()); |
| _expectRecEquals({ |
| 'a': {}, |
| }, gc.getProperty('rec'.toJS).dartify()); |
| |
| _expectIterableEquals(Int8List.fromList(<int>[-128, 0, 127]), |
| gc.getProperty<JSInt8Array>('int8Array'.toJS).toDart); |
| _expectIterableEquals(Uint8List.fromList([-1, 0, 255, 256]), |
| gc.getProperty<JSUint8Array>('uint8Array'.toJS).toDart); |
| _expectIterableEquals(Uint8ClampedList.fromList([-1, 0, 255, 256]), |
| gc.getProperty<JSUint8ClampedArray>('uint8ClampedArray'.toJS).toDart); |
| _expectIterableEquals(Int16List.fromList([-32769, -32768, 0, 32767, 32768]), |
| gc.getProperty<JSInt16Array>('int16Array'.toJS).toDart); |
| _expectIterableEquals(Uint16List.fromList([-1, 0, 65535, 65536]), |
| gc.getProperty<JSUint16Array>('uint16Array'.toJS).toDart); |
| _expectIterableEquals(Int32List.fromList([-2147483648, 0, 2147483647]), |
| gc.getProperty<JSInt32Array>('int32Array'.toJS).toDart); |
| _expectIterableEquals(Uint32List.fromList([-1, 0, 4294967295, 4294967296]), |
| gc.getProperty<JSUint32Array>('uint32Array'.toJS).toDart); |
| _expectIterableEquals( |
| Float32List.fromList([-1000.488, -0.00001, 0.0001, 10004.888]), |
| gc.getProperty<JSFloat32Array>('float32Array'.toJS).toDart); |
| _expectIterableEquals( |
| Float64List.fromList([-1000.488, -0.00001, 0.0001, 10004.888]), |
| gc.getProperty<JSFloat64Array>('float64Array'.toJS).toDart); |
| _expectIterableEquals(Uint8List.fromList([-1, 0, 255, 256]), |
| gc.getProperty<JSArrayBuffer>('arrayBuffer'.toJS).toDart.asUint8List()); |
| _expectIterableEquals(Uint8List.fromList([-1, 0, 255, 256]), |
| gc.getProperty<JSDataView>('dataView'.toJS).toDart.buffer.asUint8List()); |
| |
| // Confirm a function that takes a roundtrip remains a function. |
| JSFunction foo = gc['f'].dartify() as JSFunction; |
| Expect.equals( |
| 'hello world', gc.callMethod<JSString>('invoke'.toJS, foo).toDart); |
| |
| // Confirm arrays, which need to be converted implicitly, are still |
| // recursively converted by dartify() when desired. |
| _expectIterableEquals([ |
| {'foo': 'bar'}, |
| [ |
| 1, |
| 2, |
| 3, |
| {'baz': 'boo'} |
| ], |
| ], gc['implicitExplicit'].dartify() as Iterable); |
| |
| // Test that JS objects behave as expected in Map / Set. |
| Set<Object?> set = {}; |
| JSAny? key1 = gc['keyObject1']; |
| JSAny? key2 = gc['keyObject2']; |
| Expect.isTrue(set.add(key1)); |
| Expect.isTrue(set.contains(key1)); |
| Expect.isFalse(set.add(key2)); |
| Expect.isTrue(set.contains(key2)); |
| Expect.equals(1, set.length); |
| |
| Map<Object?, Object?> map = {}; |
| map[key1] = 'foo'; |
| map[key2] = 'bar'; |
| Expect.equals(1, map.length); |
| Expect.equals('bar', map[key1]); |
| } |
| |
| @JS('Symbol') |
| @staticInterop |
| class _JSSymbol { |
| @JS('for') |
| external static JSAny _for(JSString s); |
| external static JSString keyFor(JSAny s); |
| } |
| |
| @JS() |
| external JSAny get symbol; |
| |
| @JS() |
| external JSAny get symbol2; |
| |
| @JS() |
| external JSString methodWithSymbol(JSAny s); |
| |
| void symbolTest() { |
| eval(r''' |
| var s1 = Symbol.for('symbol'); |
| globalThis.symbol = s1; |
| globalThis[s1] = 'boo'; |
| globalThis.methodWithSymbol = function(s) { |
| return Symbol.keyFor(s); |
| } |
| var symbol2 = Symbol.for('symbolMethod'); |
| globalThis[symbol2] = function() { |
| return 'hello world'; |
| } |
| globalThis.symbol2 = symbol2; |
| '''); |
| JSObject gc = globalContext; |
| Expect.equals( |
| _JSSymbol.keyFor(_JSSymbol._for('symbol'.toJS)).toDart, 'symbol'); |
| Expect.equals( |
| gc.getProperty<JSString>(gc.getProperty<JSAny>('symbol'.toJS)).toDart, |
| 'boo'); |
| Expect.equals(methodWithSymbol(symbol).toDart, 'symbol'); |
| Expect.equals(_JSSymbol.keyFor(symbol).toDart, 'symbol'); |
| Expect.equals( |
| _JSSymbol.keyFor(gc.getProperty<JSAny>('symbol'.toJS)).toDart, 'symbol'); |
| Expect.equals(gc.callMethod<JSString>(symbol2).toDart, 'hello world'); |
| } |
| |
| void main() { |
| createObjectTest(); |
| equalTest(); |
| typeofTest(); |
| instanceOfTest(); |
| methodsAndConstructorsTest(); |
| deepConversionsTest(); |
| symbolTest(); |
| } |