import ExpoModulesJSI import Testing @Suite @JavaScriptActor struct JavaScriptValueTests { let runtime = JavaScriptRuntime() @Test func `create undefined`() { #expect(JavaScriptValue.undefined.isUndefined() == true) #expect(JavaScriptValue.undefined.isNull() == false) #expect(JavaScriptValue.undefined.isNumber() == false) } @Test func `create null`() { #expect(JavaScriptValue.null.isNull() == true) #expect(JavaScriptValue.null.isUndefined() == false) #expect(JavaScriptValue.null.isString() == false) } @Test func `create bool`() { let trueValue = JavaScriptValue(runtime, true) let falseValue = JavaScriptValue(runtime, false) #expect(trueValue.getBool() == true) #expect(falseValue.getBool() == false) } @Test func `create number`() { let value = JavaScriptValue(runtime, 12.34) #expect(value.getInt() == 12) #expect(value.getDouble() == 12.34) } @Test func `copy`() { let value = JavaScriptValue(runtime, "test") let copiedValue = value.copy() #expect(copiedValue.getString() == "test") #expect((copiedValue == value) == true) } @Test func `toString`() throws { #expect(JavaScriptValue(runtime, 20).toString() == "20") #expect(runtime.createObject().asValue().toString() == "[object Object]") #expect(try runtime.eval("({ toString() { return 'str' }})").toString() == "str") } @Test func `equality`() { let number = JavaScriptValue(runtime, 21) let string = JavaScriptValue(runtime, "test") let null = JavaScriptValue.null let undefined = JavaScriptValue.undefined #expect((number == number) == true) #expect((string == string) == true) #expect((number == string) == false) #expect((number != string) == true) #expect((number != null) == true) #expect((string == null) == false) #expect((null == undefined) == false) } @Test func `equality of runtime-free booleans`() { #expect((JavaScriptValue.true() == JavaScriptValue.false()) == false) #expect((JavaScriptValue.true() == JavaScriptValue(runtime, true)) == true) #expect((JavaScriptValue(runtime, false) != JavaScriptValue.true()) == true) } @Test func `equality of runtime-free numbers`() { #expect((JavaScriptValue.number(21) == JavaScriptValue(runtime, 21)) == true) #expect((JavaScriptValue.number(10) == JavaScriptValue.number(6)) == false) #expect((JavaScriptValue.number(48) == JavaScriptValue.number(48)) == true) } // MARK: - getString round-trip @Test func `getString returns ASCII unchanged`() throws { let value = try runtime.eval("'hello world'") #expect(value.getString() == "hello world") } @Test func `getString returns empty string`() throws { let value = try runtime.eval("''") #expect(value.getString() == "") } @Test func `getString returns BMP non-ASCII unchanged`() throws { let value = try runtime.eval("'café — déjà vu'") #expect(value.getString() == "café — déjà vu") } @Test func `getString preserves non-BMP characters via surrogate pairs`() throws { let value = try runtime.eval("'🎉🚀'") let result = value.getString() #expect(result == "🎉🚀") #expect(result.unicodeScalars.count == 2) } @Test func `getString handles mixed scripts`() throws { let value = try runtime.eval("'Hello, 世界! Привет 🌍'") #expect(value.getString() == "Hello, 世界! Привет 🌍") } @Test func `getString handles long ASCII string`() throws { let long = String(repeating: "x", count: 10_000) let value = JavaScriptValue(runtime, long) #expect(value.getString() == long) } @Test func `getString handles string built with String(runtime, ...)`() { let value = JavaScriptValue(runtime, "round-trip 🎉") #expect(value.getString() == "round-trip 🎉") } @Test func `toString returns BMP and non-BMP characters unchanged`() throws { let value = try runtime.eval("({ toString() { return 'café 世界 🎉' } })") let result = value.toString() #expect(result == "café 世界 🎉") } @Test func `toString preserves non-BMP characters via surrogate pairs`() throws { let value = try runtime.eval("({ toString() { return '🌍🚀' } })") let result = value.toString() #expect(result == "🌍🚀") #expect(result.unicodeScalars.count == 2) } @Test func `getString preserves embedded null characters`() throws { let value = try runtime.eval("'a\\u0000b'") let result = value.getString() #expect(result == "a\u{0000}b") #expect(result.utf8.count == 3) } @Test func `getString handles lone surrogate code units deterministically`() throws { let value = try runtime.eval("'\\uD800'") let result = value.getString() #expect(result == "\u{FFFD}") } // MARK: - Type Checking Tests @Test func `isSymbol for symbol value`() throws { let symbol = try runtime.eval("Symbol('test')") #expect(symbol.isSymbol() == true) #expect(symbol.isString() == false) #expect(symbol.isNumber() == false) } @Test func `isSymbol for non-symbol values`() { #expect(JavaScriptValue(runtime, "string").isSymbol() == false) #expect(JavaScriptValue(runtime, 42).isSymbol() == false) #expect(JavaScriptValue.undefined.isSymbol() == false) } @Test func `isTypedArray for typed arrays`() throws { let int8Array = try runtime.eval("new Int8Array([1, 2, 3])") let uint8Array = try runtime.eval("new Uint8Array([1, 2, 3])") let float32Array = try runtime.eval("new Float32Array([1.0, 2.0])") #expect(int8Array.isTypedArray() == true) #expect(uint8Array.isTypedArray() == true) #expect(float32Array.isTypedArray() == true) } @Test func `isTypedArray for non-typed arrays`() throws { let regularArray = try runtime.eval("[1, 2, 3]") let object = runtime.createObject().asValue() #expect(regularArray.isTypedArray() == false) #expect(object.isTypedArray() == false) #expect(JavaScriptValue(runtime, 42).isTypedArray() == false) } @Test func `is method for Promise`() throws { let promise = try runtime.eval("Promise.resolve(42)") #expect(promise.is("Promise") == true) #expect(promise.is("Array") == false) #expect(promise.is("Object") == true) } @Test func `is method for Date`() throws { let date = try runtime.eval("new Date()") #expect(date.is("Date") == true) #expect(date.is("Array") == false) #expect(date.is("Promise") == false) } @Test func `is method for Array`() throws { let array = try runtime.eval("[1, 2, 3]") #expect(array.is("Array") == true) #expect(array.is("Date") == false) } @Test func `is method for Error`() throws { let error = try runtime.eval("new Error('test')") #expect(error.is("Error") == true) #expect(error.is("Array") == false) } @Test func `is method for custom class`() throws { let instance = try runtime.eval("function MyClass() {}; new MyClass()") #expect(instance.is("MyClass") == true) #expect(instance.is("Array") == false) } @Test func `is method for non-object values`() { let number = JavaScriptValue(runtime, 42) let string = JavaScriptValue(runtime, "test") #expect(number.is("Number") == false) #expect(string.is("String") == false) } // MARK: - JSON Stringify @Test func `jsonStringify basic test`() throws { let jsonString = "{\"expoSdkVersion\":56}" let value = try runtime.eval("(\(jsonString))") #expect((try value.jsonStringify() == jsonString) == true) } @Test func `jsonStringify with array replacer`() throws { let obj = try runtime.eval("({ name: 'Alice', age: 30, city: 'Paris' })") let replacer = try runtime.eval("['name', 'age']") let json = try obj.jsonStringify(replacer: replacer) #expect(json == "{\"name\":\"Alice\",\"age\":30}") } @Test func `jsonStringify with function replacer`() throws { let obj = try runtime.eval("({ a: 1, b: 2, c: 3 })") let replacer = try runtime.eval("(key, value) => typeof value === 'number' ? value * 2 : value") let json = try obj.jsonStringify(replacer: replacer) #expect(json == "{\"a\":2,\"b\":4,\"c\":6}") } @Test func `jsonStringify with space as number`() throws { let obj = try runtime.eval("({ name: 'Bob', age: 25 })") let space = JavaScriptValue(runtime, 2) let json = try obj.jsonStringify(space: space) #expect(json?.contains(" \"name\"") == true) #expect(json?.contains(" \"age\"") == true) } @Test func `jsonStringify with space as string`() throws { let obj = try runtime.eval("({ x: 1, y: 2 })") let space = JavaScriptValue(runtime, " ") let json = try obj.jsonStringify(space: space) #expect(json?.contains(" \"x\"") == true) } @Test func `jsonStringify with custom space string`() throws { let obj = try runtime.eval("({ a: 1 })") let space = JavaScriptValue(runtime, "--") let json = try obj.jsonStringify(space: space) #expect(json?.contains("--\"a\"") == true) } @Test func `jsonStringify returns nil for undefined`() throws { let undefined = JavaScriptValue.undefined let json = try undefined.jsonStringify() #expect(json == nil) } @Test func `jsonStringify for array`() throws { let array = try runtime.eval("[1, 2, 3, 4]") let json = try array.jsonStringify() #expect(json == "[1,2,3,4]") } @Test func `jsonStringify for nested object`() throws { let obj = try runtime.eval("({ person: { name: 'Alice', details: { age: 30 } } })") let json = try obj.jsonStringify() #expect(json == "{\"person\":{\"name\":\"Alice\",\"details\":{\"age\":30}}}") } @Test func `jsonStringify for string value`() throws { let str = JavaScriptValue(runtime, "hello") let json = try str.jsonStringify() #expect(json == "\"hello\"") } @Test func `jsonStringify for boolean values`() throws { let trueVal = JavaScriptValue(runtime, true) let falseVal = JavaScriptValue(runtime, false) #expect(try trueVal.jsonStringify() == "true") #expect(try falseVal.jsonStringify() == "false") } @Test func `jsonStringify for null`() throws { let null = JavaScriptValue.null let json = try null.jsonStringify() #expect(json == "null") } @Test func `jsonStringify omits functions`() throws { let obj = try runtime.eval("({ name: 'Alice', fn: function() {} })") let json = try obj.jsonStringify() #expect(json == "{\"name\":\"Alice\"}") } @Test func `jsonStringify omits undefined properties`() throws { let obj = try runtime.eval("({ a: 1, b: undefined, c: 2 })") let json = try obj.jsonStringify() #expect(json == "{\"a\":1,\"c\":2}") } @Test func `jsonStringify with both replacer and space`() throws { let obj = try runtime.eval("({ name: 'Alice', age: 30, city: 'Paris' })") let replacer = try runtime.eval("['name', 'age']") let space = JavaScriptValue(runtime, 2) let json = try obj.jsonStringify(replacer: replacer, space: space) #expect(json?.contains("\"name\"") == true) #expect(json?.contains("\"age\"") == true) #expect(json?.contains("city") == false) #expect(json?.contains(" ") == true) } // MARK: - as* conversion tests @Test func `asBool returns Bool for boolean values`() throws { let trueValue = JavaScriptValue(runtime, true) let falseValue = JavaScriptValue(runtime, false) #expect(try trueValue.asBool() == true) #expect(try falseValue.asBool() == false) } @Test func `asBool throws TypeError for non-boolean values`() throws { let numberValue = JavaScriptValue.number(42) let stringValue = try runtime.eval("'hello'") #expect(throws: JavaScriptValue.TypeError.self) { try numberValue.asBool() } #expect(throws: JavaScriptValue.TypeError.self) { try stringValue.asBool() } } @Test func `asInt returns Int for number values`() throws { let intValue = try runtime.eval("42") let floatValue = try runtime.eval("3.7") #expect(try intValue.asInt() == 42) #expect(try floatValue.asInt() == 3) } @Test func `asInt throws TypeError for non-number values`() throws { let stringValue = try runtime.eval("'123'") let boolValue = JavaScriptValue(runtime, true) #expect(throws: JavaScriptValue.TypeError.self) { try stringValue.asInt() } #expect(throws: JavaScriptValue.TypeError.self) { try boolValue.asInt() } } @Test func `asDouble returns Double for number values`() throws { let intValue = try runtime.eval("42") let floatValue = try runtime.eval("3.14159") #expect(try intValue.asDouble() == 42.0) #expect(try floatValue.asDouble() == 3.14159) } @Test func `asDouble throws TypeError for non-number values`() throws { let stringValue = try runtime.eval("'3.14'") let nullValue = JavaScriptValue.null #expect(throws: JavaScriptValue.TypeError.self) { try stringValue.asDouble() } #expect(throws: JavaScriptValue.TypeError.self) { try nullValue.asDouble() } } @Test func `asString returns String for string values`() throws { let stringValue = try runtime.eval("'hello world'") let emptyString = try runtime.eval("''") #expect(try stringValue.asString() == "hello world") #expect(try emptyString.asString() == "") } @Test func `asString throws TypeError for non-string values`() throws { let numberValue = JavaScriptValue.number(123) let objectValue = try runtime.eval("({ key: 'value' })") #expect(throws: JavaScriptValue.TypeError.self) { try numberValue.asString() } #expect(throws: JavaScriptValue.TypeError.self) { try objectValue.asString() } } @Test func `asObject returns JavaScriptObject for object values`() throws { let objectValue = try runtime.eval("({ key: 'value' })") let object = try objectValue.asObject() #expect(object.getProperty("key").getString() == "value") } @Test func `asObject throws TypeError for non-object values`() throws { let numberValue = JavaScriptValue.number(42) let stringValue = try runtime.eval("'hello'") #expect(throws: JavaScriptValue.TypeError.self) { _ = try numberValue.asObject() } #expect(throws: JavaScriptValue.TypeError.self) { _ = try stringValue.asObject() } } @Test func `asArray returns JavaScriptArray for array values`() throws { let arrayValue = try runtime.eval("[1, 2, 3]") let array = try arrayValue.asArray() #expect(array.length == 3) } @Test func `asArray throws TypeError for non-array values`() throws { let objectValue = try runtime.eval("({ key: 'value' })") let stringValue = try runtime.eval("'[1,2,3]'") #expect(throws: JavaScriptValue.TypeError.self) { _ = try objectValue.asArray() } #expect(throws: JavaScriptValue.TypeError.self) { _ = try stringValue.asArray() } } @Test func `asFunction returns JavaScriptFunction for function values`() throws { let functionValue = try runtime.eval("(x) => x * 2") let function = try functionValue.asFunction() let result = try function.call(arguments: JavaScriptValue.number(5)) #expect(result.getDouble() == 10.0) } @Test func `asFunction throws TypeError for non-function values`() throws { let objectValue = try runtime.eval("({ key: 'value' })") let arrayValue = try runtime.eval("[1, 2, 3]") #expect(throws: JavaScriptValue.TypeError.self) { _ = try objectValue.asFunction() } #expect(throws: JavaScriptValue.TypeError.self) { _ = try arrayValue.asFunction() } } @Test func `asTypedArray returns JavaScriptTypedArray for typed array values`() throws { let typedArrayValue = try runtime.eval("new Uint8Array([1, 2, 3])") let typedArray = try typedArrayValue.asTypedArray() #expect(typedArray.byteLength == 3) } @Test func `asTypedArray throws TypeError for non-typed-array values`() throws { let arrayValue = try runtime.eval("[1, 2, 3]") let objectValue = try runtime.eval("({ key: 'value' })") #expect(throws: JavaScriptValue.TypeError.self) { _ = try arrayValue.asTypedArray() } #expect(throws: JavaScriptValue.TypeError.self) { _ = try objectValue.asTypedArray() } } @Test func `asPromise returns JavaScriptPromise for promise values`() async throws { let promiseValue = try runtime.eval("Promise.resolve(42)") let promise = try promiseValue.asPromise() let result = try await promise.await() #expect(result.getDouble() == 42.0) } @Test func `asPromise throws TypeError for non-promise values`() throws { let objectValue = try runtime.eval("({ key: 'value' })") let numberValue = JavaScriptValue(runtime, 42) #expect(throws: JavaScriptValue.TypeError.self) { _ = try objectValue.asPromise() } #expect(throws: JavaScriptValue.TypeError.self) { _ = try numberValue.asPromise() } } // MARK: - ArrayBuffer @Test func `isArrayBuffer returns true for ArrayBuffer`() throws { let value = try runtime.eval("new ArrayBuffer(16)") #expect(value.isArrayBuffer() == true) } @Test func `isArrayBuffer returns false for non-ArrayBuffer`() throws { let array = try runtime.eval("[1, 2, 3]") let object = try runtime.eval("({ key: 'value' })") let typedArray = try runtime.eval("new Uint8Array(4)") #expect(array.isArrayBuffer() == false) #expect(object.isArrayBuffer() == false) #expect(typedArray.isArrayBuffer() == false) } @Test func `getArrayBuffer round-trip preserves size`() throws { let ab = runtime.createArrayBuffer(size: 32) let value = ab.asValue() let recovered = value.getArrayBuffer() #expect(recovered.size == 32) #expect(recovered.byteLength == 32) } @Test func `asArrayBuffer returns ArrayBuffer for valid value`() throws { let value = try runtime.eval("new ArrayBuffer(8)") let ab = try value.asArrayBuffer() #expect(ab.size == 8) } @Test func `asArrayBuffer throws TypeError for non-ArrayBuffer`() throws { let object = try runtime.eval("({ key: 'value' })") let number = JavaScriptValue(runtime, 42) #expect(throws: JavaScriptValue.TypeError.self) { _ = try object.asArrayBuffer() } #expect(throws: JavaScriptValue.TypeError.self) { _ = try number.asArrayBuffer() } } // MARK: - Unsafe pointee access @Test func `withUnsafePointee returns non-null pointee`() { let value = JavaScriptValue(runtime, 42) value.withUnsafePointee { valuePointee in #expect(valuePointee != UnsafeRawPointer(bitPattern: 0)) } } @Test func `withUnsafePointee returns value from closure`() { let value = JavaScriptValue(runtime, 42) let result = value.withUnsafePointee { _ in return "hello" } #expect(result == "hello") } @Test func `withUnsafePointee returns same pointee across calls`() { let value = JavaScriptValue(runtime, 42) let pointee1 = value.withUnsafePointee { return $0 } let pointee2 = value.withUnsafePointee { return $0 } #expect(pointee1 == pointee2) } }