import JavaScriptKit import XCTest class JavaScriptKitTests: XCTestCase { func testLiteralConversion() { let global = JSObject.global let inputs: [JSValue] = [ .boolean(true), .boolean(false), .string("foobar"), .string("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง Family Emoji"), .number(0), .number(Double(Int32.max)), .number(Double(Int32.min)), .number(Double.infinity), .number(Double.nan), .null, .undefined, ] for (index, input) in inputs.enumerated() { let prop = JSString("prop_\(index)") setJSValue(this: global, name: prop, value: input) let got = getJSValue(this: global, name: prop) switch (got.number, input.number) { case (let lhs?, let rhs?): // Compare bitPattern because nan == nan is always false XCTAssertEqual(lhs.bitPattern, rhs.bitPattern) default: XCTAssertEqual(got, input) } } } func testObjectConversion() { // Notes: globalObject1 is defined in JavaScript environment // // ```js // global.globalObject1 = { // "prop_1": { // "nested_prop": 1, // }, // "prop_2": 2, // "prop_3": true, // "prop_4": [ // 3, 4, "str_elm_1", 5, // ], // ... // } // ``` let globalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1Ref = try! XCTUnwrap(globalObject1.object) let prop_1 = getJSValue(this: globalObject1Ref, name: "prop_1") let prop_1Ref = try! XCTUnwrap(prop_1.object) let nested_prop = getJSValue(this: prop_1Ref, name: "nested_prop") XCTAssertEqual(nested_prop, .number(1)) let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") XCTAssertEqual(prop_2, .number(2)) let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") XCTAssertEqual(prop_3, .boolean(true)) let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") let prop_4Array = try! XCTUnwrap(prop_4.object) let expectedProp_4: [JSValue] = [ .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), ] for (index, expectedElement) in expectedProp_4.enumerated() { let actualElement = getJSValue(this: prop_4Array, index: Int32(index)) XCTAssertEqual(actualElement, expectedElement) } XCTAssertEqual(getJSValue(this: globalObject1Ref, name: "undefined_prop"), .undefined) } func testValueConstruction() { let globalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1Ref = try! XCTUnwrap(globalObject1.object) let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") XCTAssertEqual(Int.construct(from: prop_2), 2) let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") XCTAssertEqual(Bool.construct(from: prop_3), true) let prop_7 = getJSValue(this: globalObject1Ref, name: "prop_7") XCTAssertEqual(Double.construct(from: prop_7), 3.14) XCTAssertEqual(Float.construct(from: prop_7), 3.14) for source: JSValue in [ .number(.infinity), .number(.nan), .number(Double(UInt64.max).nextUp), .number(Double(Int64.min).nextDown), ] { XCTAssertNil(Int.construct(from: source)) XCTAssertNil(Int8.construct(from: source)) XCTAssertNil(Int16.construct(from: source)) XCTAssertNil(Int32.construct(from: source)) XCTAssertNil(Int64.construct(from: source)) XCTAssertNil(UInt.construct(from: source)) XCTAssertNil(UInt8.construct(from: source)) XCTAssertNil(UInt16.construct(from: source)) XCTAssertNil(UInt32.construct(from: source)) XCTAssertNil(UInt64.construct(from: source)) } } func testArrayIterator() { let globalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1Ref = try! XCTUnwrap(globalObject1.object) let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") let array1 = try! XCTUnwrap(prop_4.array) let expectedProp_4: [JSValue] = [ .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), ] XCTAssertEqual(Array(array1), expectedProp_4) // Ensure that iterator skips empty hole as JavaScript does. let prop_8 = getJSValue(this: globalObject1Ref, name: "prop_8") let array2 = try! XCTUnwrap(prop_8.array) let expectedProp_8: [JSValue] = [0, 2, 3, 6] XCTAssertEqual(Array(array2), expectedProp_8) } func testArrayRandomAccessCollection() { let globalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1Ref = try! XCTUnwrap(globalObject1.object) let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") let array1 = try! XCTUnwrap(prop_4.array) let expectedProp_4: [JSValue] = [ .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), ] XCTAssertEqual([array1[0], array1[1], array1[2], array1[3], array1[4], array1[5]], expectedProp_4) // Ensure that subscript can access empty hole let prop_8 = getJSValue(this: globalObject1Ref, name: "prop_8") let array2 = try! XCTUnwrap(prop_8.array) let expectedProp_8: [JSValue] = [ 0, .undefined, 2, 3, .undefined, .undefined, 6, ] XCTAssertEqual([array2[0], array2[1], array2[2], array2[3], array2[4], array2[5], array2[6]], expectedProp_8) } func testValueDecoder() { struct GlobalObject1: Codable { struct Prop1: Codable { let nested_prop: Int } let prop_1: Prop1 let prop_2: Int let prop_3: Bool let prop_7: Float } let decoder = JSValueDecoder() let rawGlobalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1 = try! decoder.decode(GlobalObject1.self, from: rawGlobalObject1) XCTAssertEqual(globalObject1.prop_1.nested_prop, 1) XCTAssertEqual(globalObject1.prop_2, 2) XCTAssertEqual(globalObject1.prop_3, true) XCTAssertEqual(globalObject1.prop_7, 3.14) } func testFunctionCall() { // Notes: globalObject1 is defined in JavaScript environment // // ```js // global.globalObject1 = { // ... // "prop_5": { // "func1": function () { return }, // "func2": function () { return 1 }, // "func3": function (n) { return n * 2 }, // "func4": function (a, b, c) { return a + b + c }, // "func5": function (x) { return "Hello, " + x }, // "func6": function (c, a, b) { // if (c) { return a } else { return b } // }, // } // ... // } // ``` let globalObject1 = getJSValue(this: .global, name: "globalObject1") let globalObject1Ref = try! XCTUnwrap(globalObject1.object) let prop_5 = getJSValue(this: globalObject1Ref, name: "prop_5") let prop_5Ref = try! XCTUnwrap(prop_5.object) let func1 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func1").function) XCTAssertEqual(func1(), .undefined) let func2 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func2").function) XCTAssertEqual(func2(), .number(1)) let func3 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func3").function) XCTAssertEqual(func3(2), .number(4)) let func4 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func4").function) XCTAssertEqual(func4(2, 3, 4), .number(9)) XCTAssertEqual(func4(2, 3, 4, 5), .number(9)) let func5 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func5").function) XCTAssertEqual(func5("World!"), .string("Hello, World!")) let func6 = try! XCTUnwrap(getJSValue(this: prop_5Ref, name: "func6").function) XCTAssertEqual(func6(true, 1, 2), .number(1)) XCTAssertEqual(func6(false, 1, 2), .number(2)) XCTAssertEqual(func6(true, "OK", 2), .string("OK")) } func testNewObjectConstruction() { // ```js // global.Animal = function(name, age, isCat) { // this.name = name // this.age = age // this.bark = () => { // return isCat ? "nyan" : "wan" // } // } // ``` let objectConstructor = try! XCTUnwrap(getJSValue(this: .global, name: "Animal").function) let cat1 = objectConstructor.new("Tama", 3, true) XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Tama")) XCTAssertEqual(getJSValue(this: cat1, name: "age"), .number(3)) XCTAssertEqual(cat1.isInstanceOf(objectConstructor), true) XCTAssertEqual(cat1.isInstanceOf(try! XCTUnwrap(getJSValue(this: .global, name: "Array").function)), false) let cat1Bark = try! XCTUnwrap(getJSValue(this: cat1, name: "bark").function) XCTAssertEqual(cat1Bark(), .string("nyan")) let dog1 = objectConstructor.new("Pochi", 3, false) let dog1Bark = try! XCTUnwrap(getJSValue(this: dog1, name: "bark").function) XCTAssertEqual(dog1Bark(), .string("wan")) } func testObjectDecoding() { /* ```js global.objectDecodingTest = { obj: {}, fn: () => {}, sym: Symbol("s"), bi: BigInt(3) }; ``` */ let js: JSValue = JSObject.global.objectDecodingTest // I can't use regular name like `js.object` here // cz its conflicting with case name and DML. // so I use abbreviated names let object: JSValue = js.obj let function: JSValue = js.fn let symbol: JSValue = js.sym let bigInt: JSValue = js.bi XCTAssertNotNil(JSObject.construct(from: object)) XCTAssertEqual(JSObject.construct(from: symbol).map { $0 is JSSymbol }, .some(true)) XCTAssertEqual(JSObject.construct(from: bigInt).map { $0 is JSBigInt }, .some(true)) XCTAssertNil(JSSymbol.construct(from: object)) XCTAssertNil(JSSymbol.construct(from: function)) XCTAssertNotNil(JSSymbol.construct(from: symbol)) XCTAssertNil(JSSymbol.construct(from: bigInt)) XCTAssertNil(JSBigInt.construct(from: object)) XCTAssertNil(JSBigInt.construct(from: function)) XCTAssertNil(JSBigInt.construct(from: symbol)) XCTAssertNotNil(JSBigInt.construct(from: bigInt)) } func testCallFunctionWithThis() { // ```js // global.Animal = function(name, age, isCat) { // this.name = name // this.age = age // this.bark = () => { // return isCat ? "nyan" : "wan" // } // this.isCat = isCat // this.getIsCat = function() { // return this.isCat // } // } // ``` let objectConstructor = try! XCTUnwrap(getJSValue(this: .global, name: "Animal").function) let cat1 = objectConstructor.new("Tama", 3, true) let cat1Value = JSValue.object(cat1) let getIsCat = try! XCTUnwrap(getJSValue(this: cat1, name: "getIsCat").function) let setName = try! XCTUnwrap(getJSValue(this: cat1, name: "setName").function) // Direct call without this XCTAssertThrowsError(try getIsCat.throws()) // Call with this let gotIsCat = getIsCat(this: cat1) XCTAssertEqual(gotIsCat, .boolean(true)) XCTAssertEqual(cat1.getIsCat!(), .boolean(true)) XCTAssertEqual(cat1Value.getIsCat(), .boolean(true)) // Call with this and argument setName(this: cat1, JSValue.string("Shiro")) XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Shiro")) _ = cat1.setName!("Tora") XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Tora")) _ = cat1Value.setName("Chibi") XCTAssertEqual(getJSValue(this: cat1, name: "name"), .string("Chibi")) } func testJSObjectConversion() { let array1 = [1, 2, 3] let jsArray1 = array1.jsValue.object! XCTAssertEqual(jsArray1.length, .number(3)) XCTAssertEqual(jsArray1[0], .number(1)) XCTAssertEqual(jsArray1[1], .number(2)) XCTAssertEqual(jsArray1[2], .number(3)) let array2: [ConvertibleToJSValue] = [1, "str", false] let jsArray2 = array2.jsValue.object! XCTAssertEqual(jsArray2.length, .number(3)) XCTAssertEqual(jsArray2[0], .number(1)) XCTAssertEqual(jsArray2[1], .string("str")) XCTAssertEqual(jsArray2[2], .boolean(false)) _ = jsArray2.push!(5) XCTAssertEqual(jsArray2.length, .number(4)) _ = jsArray2.push!(jsArray1) XCTAssertEqual(jsArray2[4], .object(jsArray1)) let dict1: [String: JSValue] = [ "prop1": 1.jsValue, "prop2": "foo".jsValue, ] let jsDict1 = dict1.jsValue.object! XCTAssertEqual(jsDict1.prop1, .number(1)) XCTAssertEqual(jsDict1.prop2, .string("foo")) } func testObjectRefLifetime() { // ```js // global.globalObject1 = { // "prop_1": { // "nested_prop": 1, // }, // "prop_2": 2, // "prop_3": true, // "prop_4": [ // 3, 4, "str_elm_1", 5, // ], // ... // } // ``` let evalClosure = JSObject.global.globalObject1.eval_closure.function! let identity = JSClosure { $0[0] } let ref1 = getJSValue(this: .global, name: "globalObject1").object! let ref2 = evalClosure(identity, ref1).object! XCTAssertEqual(ref1.prop_2, .number(2)) XCTAssertEqual(ref2.prop_2, .number(2)) #if JAVASCRIPTKIT_WITHOUT_WEAKREFS identity.release() #endif } func testDate() { let date1Milliseconds = JSDate.now() let date1 = JSDate(millisecondsSinceEpoch: date1Milliseconds) let date2 = JSDate(millisecondsSinceEpoch: date1.valueOf()) XCTAssertEqual(date1.valueOf(), date2.valueOf()) XCTAssertEqual(date1.fullYear, date2.fullYear) XCTAssertEqual(date1.month, date2.month) XCTAssertEqual(date1.date, date2.date) XCTAssertEqual(date1.day, date2.day) XCTAssertEqual(date1.hours, date2.hours) XCTAssertEqual(date1.minutes, date2.minutes) XCTAssertEqual(date1.seconds, date2.seconds) XCTAssertEqual(date1.milliseconds, date2.milliseconds) XCTAssertEqual(date1.utcFullYear, date2.utcFullYear) XCTAssertEqual(date1.utcMonth, date2.utcMonth) XCTAssertEqual(date1.utcDate, date2.utcDate) XCTAssertEqual(date1.utcDay, date2.utcDay) XCTAssertEqual(date1.utcHours, date2.utcHours) XCTAssertEqual(date1.utcMinutes, date2.utcMinutes) XCTAssertEqual(date1.utcSeconds, date2.utcSeconds) XCTAssertEqual(date1.utcMilliseconds, date2.utcMilliseconds) XCTAssertEqual(date1, date2) let date3 = JSDate(millisecondsSinceEpoch: 0) XCTAssertEqual(date3.valueOf(), 0) XCTAssertEqual(date3.utcFullYear, 1970) XCTAssertEqual(date3.utcMonth, 0) XCTAssertEqual(date3.utcDate, 1) // the epoch date was on Friday XCTAssertEqual(date3.utcDay, 4) XCTAssertEqual(date3.utcHours, 0) XCTAssertEqual(date3.utcMinutes, 0) XCTAssertEqual(date3.utcSeconds, 0) XCTAssertEqual(date3.utcMilliseconds, 0) XCTAssertEqual(date3.toISOString(), "1970-01-01T00:00:00.000Z") XCTAssertTrue(date3 < date1) } func testError() { let message = "test error" let expectedDescription = "Error: test error" let error = JSError(message: message) XCTAssertEqual(error.name, "Error") XCTAssertEqual(error.message, message) XCTAssertEqual(error.description, expectedDescription) XCTAssertFalse(error.stack?.isEmpty ?? true) XCTAssertNil(JSError(from: .string("error"))?.description) XCTAssertEqual(JSError(from: .object(error.jsObject))?.description, expectedDescription) } func testJSValueAccessor() { let globalObject1 = JSObject.global.globalObject1 XCTAssertEqual(globalObject1.prop_1.nested_prop, .number(1)) XCTAssertEqual(globalObject1.object!.prop_1.object!.nested_prop, .number(1)) XCTAssertEqual(globalObject1.prop_4[0], .number(3)) XCTAssertEqual(globalObject1.prop_4[1], .number(4)) let originalProp1 = globalObject1.prop_1.object!.nested_prop globalObject1.prop_1.nested_prop = "bar" XCTAssertEqual(globalObject1.prop_1.nested_prop, .string("bar")) globalObject1.prop_1.nested_prop = originalProp1 } func testSymbols() { let symbol1 = JSSymbol("abc") let symbol2 = JSSymbol("abc") XCTAssertNotEqual(symbol1, symbol2) XCTAssertEqual(symbol1.name, symbol2.name) XCTAssertEqual(symbol1.name, "abc") XCTAssertEqual(JSSymbol.iterator, JSSymbol.iterator) // let hasInstanceClass = { // prop: function () {} // }.prop // Object.defineProperty(hasInstanceClass, Symbol.hasInstance, { value: () => true }) let hasInstanceObject = JSObject.global.Object.function!.new() hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue let hasInstanceClass = hasInstanceObject.prop.function! let propertyDescriptor = JSObject.global.Object.function!.new() propertyDescriptor.value = JSClosure { _ in .boolean(true) }.jsValue _ = JSObject.global.Object.function!.defineProperty!( hasInstanceClass, JSSymbol.hasInstance, propertyDescriptor ) XCTAssertEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) XCTAssertEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) } func testJSValueDecoder() { struct AnimalStruct: Decodable { let name: String let age: Int let isCat: Bool } let Animal = JSObject.global.Animal.function! let tama = try! Animal.throws.new("Tama", 3, true) let decoder = JSValueDecoder() let decodedTama = try! decoder.decode(AnimalStruct.self, from: tama.jsValue) XCTAssertEqual(decodedTama.name, tama.name.string) XCTAssertEqual(decodedTama.name, "Tama") XCTAssertEqual(decodedTama.age, tama.age.number.map(Int.init)) XCTAssertEqual(decodedTama.age, 3) XCTAssertEqual(decodedTama.isCat, tama.isCat.boolean) XCTAssertEqual(decodedTama.isCat, true) } func testConvertibleToJSValue() { let array1 = [1, 2, 3] let jsArray1 = array1.jsValue.object! XCTAssertEqual(jsArray1.length, .number(3)) XCTAssertEqual(jsArray1[0], .number(1)) XCTAssertEqual(jsArray1[1], .number(2)) XCTAssertEqual(jsArray1[2], .number(3)) let array2: [ConvertibleToJSValue] = [1, "str", false] let jsArray2 = array2.jsValue.object! XCTAssertEqual(jsArray2.length, .number(3)) XCTAssertEqual(jsArray2[0], .number(1)) XCTAssertEqual(jsArray2[1], .string("str")) XCTAssertEqual(jsArray2[2], .boolean(false)) _ = jsArray2.push!(5) XCTAssertEqual(jsArray2.length, .number(4)) _ = jsArray2.push!(jsArray1) XCTAssertEqual(jsArray2[4], .object(jsArray1)) let dict1: [String: JSValue] = [ "prop1": 1.jsValue, "prop2": "foo".jsValue, ] let jsDict1 = dict1.jsValue.object! XCTAssertEqual(jsDict1.prop1, .number(1)) XCTAssertEqual(jsDict1.prop2, .string("foo")) } func testGrowMemory() { // If WebAssembly.Memory is not accessed correctly (i.e. creating a new view each time), // this test will fail with `TypeError: Cannot perform Construct on a detached ArrayBuffer`, // since asking to grow memory will detach the backing ArrayBuffer. // See https://github.com/swiftwasm/JavaScriptKit/pull/153 let string = "Hello" let jsString = JSValue.string(string) _ = growMemory(0, 1) XCTAssertEqual(string, jsString.description) } func testHashableConformance() { let globalObject1 = JSObject.global.console.object! let globalObject2 = JSObject.global.console.object! XCTAssertEqual(globalObject1.hashValue, globalObject2.hashValue) // These are 2 different objects in Swift referencing the same object in JavaScript XCTAssertNotEqual(ObjectIdentifier(globalObject1), ObjectIdentifier(globalObject2)) let objectConstructor = JSObject.global.Object.function! let obj = objectConstructor.new() obj.a = 1.jsValue let firstHash = obj.hashValue obj.b = 2.jsValue let secondHash = obj.hashValue XCTAssertEqual(firstHash, secondHash) } } #if arch(wasm32) @_extern(c, "llvm.wasm.memory.grow.i32") func growMemory(_ memory: Int32, _ pages: Int32) -> Int32 #else func growMemory(_ memory: Int32, _ pages: Int32) -> Int32 { return 0 } #endif