diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62e2a8ac9..c50de248a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,6 @@ jobs: - name: Configure Swift SDK run: echo "SWIFT_SDK_ID=${{ steps.setup-swiftwasm.outputs.swift-sdk-id }}" >> $GITHUB_ENV - run: make bootstrap - - run: make test - run: make unittest # Skip unit tests with uwasi because its proc_exit throws # unhandled promise rejection. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2526556c6..38454374a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,14 +58,10 @@ Thank you for considering contributing to JavaScriptKit! We welcome contribution ``` ### Running Tests -- Run unit tests: - ```bash - make unittest SWIFT_SDK_ID=wasm32-unknown-wasi - ``` -- Run integration tests: - ```bash - make test SWIFT_SDK_ID=wasm32-unknown-wasi - ``` + +```bash +make unittest SWIFT_SDK_ID=wasm32-unknown-wasi +``` ### Editing `./Runtime` directory diff --git a/IntegrationTests/TestSuites/Package.swift b/IntegrationTests/TestSuites/Package.swift index 95b47f94c..3d583d082 100644 --- a/IntegrationTests/TestSuites/Package.swift +++ b/IntegrationTests/TestSuites/Package.swift @@ -11,12 +11,6 @@ let package = Package( .macOS("12.0"), ], products: [ - .executable( - name: "PrimaryTests", targets: ["PrimaryTests"] - ), - .executable( - name: "ConcurrencyTests", targets: ["ConcurrencyTests"] - ), .executable( name: "BenchmarkTests", targets: ["BenchmarkTests"] ), @@ -24,17 +18,6 @@ let package = Package( dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ .target(name: "CHelpers"), - .executableTarget(name: "PrimaryTests", dependencies: [ - .product(name: "JavaScriptBigIntSupport", package: "JavaScriptKit"), - "JavaScriptKit", - "CHelpers", - ]), - .executableTarget( - name: "ConcurrencyTests", - dependencies: [ - .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"), - ] - ), .executableTarget(name: "BenchmarkTests", dependencies: ["JavaScriptKit", "CHelpers"]), ] ) diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift deleted file mode 100644 index acd81e6d9..000000000 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/UnitTestUtils.swift +++ /dev/null @@ -1,141 +0,0 @@ -import JavaScriptKit - -#if compiler(>=5.5) -var printTestNames = false -// Uncomment the next line to print the name of each test suite before running it. -// This will make it easier to debug any errors that occur on the JS side. -//printTestNames = true - -func test(_ name: String, testBlock: () throws -> Void) throws { - if printTestNames { print(name) } - do { - try testBlock() - } catch { - print("Error in \(name)") - print(error) - throw error - } - print("โ \(name)") -} - -func asyncTest(_ name: String, testBlock: () async throws -> Void) async throws -> Void { - if printTestNames { print(name) } - do { - try await testBlock() - } catch { - print("Error in \(name)") - print(error) - throw error - } - print("โ \(name)") -} - -struct MessageError: Error { - let message: String - let file: StaticString - let line: UInt - let column: UInt - init(_ message: String, file: StaticString, line: UInt, column: UInt) { - self.message = message - self.file = file - self.line = line - self.column = column - } -} - -func expectGTE<T: Comparable>( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs < rhs { - throw MessageError( - "Expected \(lhs) to be greater than or equal to \(rhs)", - file: file, line: line, column: column - ) - } -} - -func expectEqual<T: Equatable>( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs != rhs { - throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column) - } -} - -func expectCast<T, U>( - _ value: T, to type: U.Type = U.self, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws -> U { - guard let value = value as? U else { - throw MessageError("Expect \"\(value)\" to be \(U.self)", file: file, line: line, column: column) - } - return value -} - -func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject { - switch value { - case let .object(ref): return ref - default: - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } -} - -func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray { - guard let array = value.array else { - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } - return array -} - -func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction { - switch value { - case let .function(ref): return ref - default: - throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column) - } -} - -func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool { - switch value { - case let .boolean(bool): return bool - default: - throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column) - } -} - -func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double { - switch value { - case let .number(number): return number - default: - throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column) - } -} - -func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String { - switch value { - case let .string(string): return String(string) - default: - throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column) - } -} - -func expectAsyncThrow<T>(_ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) async throws -> Error { - do { - _ = try await body() - } catch { - return error - } - throw MessageError("Expect to throw an exception", file: file, line: line, column: column) -} - -func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - switch value { - case .some: return - case .none: - throw MessageError("Expect a non-nil value", file: file, line: line, column: column) - } -} - -#endif diff --git a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift b/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift deleted file mode 100644 index 1f0764e14..000000000 --- a/IntegrationTests/TestSuites/Sources/ConcurrencyTests/main.swift +++ /dev/null @@ -1,221 +0,0 @@ -import JavaScriptEventLoop -import JavaScriptKit -#if canImport(WASILibc) -import WASILibc -#elseif canImport(Darwin) -import Darwin -#endif - -func performanceNow() -> Double { - return JSObject.global.performance.now().number! -} - -func measure(_ block: () async throws -> Void) async rethrows -> Double { - let start = performanceNow() - try await block() - return performanceNow() - start -} - -func entrypoint() async throws { - struct E: Error, Equatable { - let value: Int - } - - try await asyncTest("Task.init value") { - let handle = Task { 1 } - try expectEqual(await handle.value, 1) - } - - try await asyncTest("Task.init throws") { - let handle = Task { - throw E(value: 2) - } - let error = try await expectAsyncThrow(await handle.value) - let e = try expectCast(error, to: E.self) - try expectEqual(e, E(value: 2)) - } - - try await asyncTest("await resolved Promise") { - let p = JSPromise(resolver: { resolve in - resolve(.success(1)) - }) - try await expectEqual(p.value, 1) - try await expectEqual(p.result, .success(.number(1))) - } - - try await asyncTest("await rejected Promise") { - let p = JSPromise(resolver: { resolve in - resolve(.failure(.number(3))) - }) - let error = try await expectAsyncThrow(await p.value) - let jsValue = try expectCast(error, to: JSException.self).thrownValue - try expectEqual(jsValue, 3) - try await expectEqual(p.result, .failure(.number(3))) - } - - try await asyncTest("Continuation") { - let value = await withUnsafeContinuation { cont in - cont.resume(returning: 1) - } - try expectEqual(value, 1) - - let error = try await expectAsyncThrow( - try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation<Never, Error>) in - cont.resume(throwing: E(value: 2)) - } - ) - let e = try expectCast(error, to: E.self) - try expectEqual(e.value, 2) - } - - try await asyncTest("Task.sleep(_:)") { - let diff = try await measure { - try await Task.sleep(nanoseconds: 200_000_000) - } - try expectGTE(diff, 200) - } - - try await asyncTest("Job reordering based on priority") { - class Context: @unchecked Sendable { - var completed: [String] = [] - } - let context = Context() - - // When no priority, they should be ordered by the enqueued order - let t1 = Task(priority: nil) { - context.completed.append("t1") - } - let t2 = Task(priority: nil) { - context.completed.append("t2") - } - - _ = await (t1.value, t2.value) - try expectEqual(context.completed, ["t1", "t2"]) - - context.completed = [] - // When high priority is enqueued after a low one, they should be re-ordered - let t3 = Task(priority: .low) { - context.completed.append("t3") - } - let t4 = Task(priority: .high) { - context.completed.append("t4") - } - let t5 = Task(priority: .low) { - context.completed.append("t5") - } - - _ = await (t3.value, t4.value, t5.value) - try expectEqual(context.completed, ["t4", "t3", "t5"]) - } - - try await asyncTest("Async JSClosure") { - let delayClosure = JSClosure.async { _ -> JSValue in - try await Task.sleep(nanoseconds: 200_000_000) - return JSValue.number(3) - } - let delayObject = JSObject.global.Object.function!.new() - delayObject.closure = delayClosure.jsValue - - let diff = try await measure { - let promise = JSPromise(from: delayObject.closure!()) - try expectNotNil(promise) - let result = try await promise!.value - try expectEqual(result, .number(3)) - } - try expectGTE(diff, 200) - } - - try await asyncTest("Async JSPromise: then") { - let promise = JSPromise { resolve in - _ = JSObject.global.setTimeout!( - JSClosure { _ in - resolve(.success(JSValue.number(3))) - return .undefined - }.jsValue, - 100 - ) - } - let promise2 = promise.then { result in - try await Task.sleep(nanoseconds: 100_000_000) - return String(result.number!) - } - let diff = try await measure { - let result = try await promise2.value - try expectEqual(result, .string("3.0")) - } - try expectGTE(diff, 200) - } - - try await asyncTest("Async JSPromise: then(success:failure:)") { - let promise = JSPromise { resolve in - _ = JSObject.global.setTimeout!( - JSClosure { _ in - resolve(.failure(JSError(message: "test").jsValue)) - return .undefined - }.jsValue, - 100 - ) - } - let promise2 = promise.then { _ in - throw MessageError("Should not be called", file: #file, line: #line, column: #column) - } failure: { err in - return err - } - let result = try await promise2.value - try expectEqual(result.object?.message, .string("test")) - } - - try await asyncTest("Async JSPromise: catch") { - let promise = JSPromise { resolve in - _ = JSObject.global.setTimeout!( - JSClosure { _ in - resolve(.failure(JSError(message: "test").jsValue)) - return .undefined - }.jsValue, - 100 - ) - } - let promise2 = promise.catch { err in - try await Task.sleep(nanoseconds: 100_000_000) - return err - } - let diff = try await measure { - let result = try await promise2.value - try expectEqual(result.object?.message, .string("test")) - } - try expectGTE(diff, 200) - } - - try await asyncTest("Task.sleep(nanoseconds:)") { - let diff = try await measure { - try await Task.sleep(nanoseconds: 100_000_000) - } - try expectGTE(diff, 100) - } - - #if compiler(>=5.7) - try await asyncTest("ContinuousClock.sleep") { - let diff = try await measure { - let c = ContinuousClock() - try await c.sleep(until: .now + .milliseconds(100)) - } - try expectGTE(diff, 99) - } - try await asyncTest("SuspendingClock.sleep") { - let diff = try await measure { - let c = SuspendingClock() - try await c.sleep(until: .now + .milliseconds(100)) - } - try expectGTE(diff, 99) - } - #endif -} - -JavaScriptEventLoop.installGlobalExecutor() -Task { - do { - try await entrypoint() - } catch { - print(error) - } -} diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift deleted file mode 100644 index 8d8dda331..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/I64.swift +++ /dev/null @@ -1,39 +0,0 @@ -import JavaScriptBigIntSupport -import JavaScriptKit - -func testI64() throws { - try test("BigInt") { - func expectPassesThrough(signed value: Int64) throws { - let bigInt = JSBigInt(value) - try expectEqual(bigInt.description, value.description) - let bigInt2 = JSBigInt(_slowBridge: value) - try expectEqual(bigInt2.description, value.description) - } - - func expectPassesThrough(unsigned value: UInt64) throws { - let bigInt = JSBigInt(unsigned: value) - try expectEqual(bigInt.description, value.description) - let bigInt2 = JSBigInt(_slowBridge: value) - try expectEqual(bigInt2.description, value.description) - } - - try expectPassesThrough(signed: 0) - try expectPassesThrough(signed: 1 << 62) - try expectPassesThrough(signed: -2305) - for _ in 0 ..< 100 { - try expectPassesThrough(signed: .random(in: .min ... .max)) - } - try expectPassesThrough(signed: .min) - try expectPassesThrough(signed: .max) - - try expectPassesThrough(unsigned: 0) - try expectPassesThrough(unsigned: 1 << 62) - try expectPassesThrough(unsigned: 1 << 63) - try expectPassesThrough(unsigned: .min) - try expectPassesThrough(unsigned: .max) - try expectPassesThrough(unsigned: ~0) - for _ in 0 ..< 100 { - try expectPassesThrough(unsigned: .random(in: .min ... .max)) - } - } -} diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift deleted file mode 100644 index 0d51c6ff5..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ /dev/null @@ -1,161 +0,0 @@ -import JavaScriptKit - -var printTestNames = false -// Uncomment the next line to print the name of each test suite before running it. -// This will make it easier to debug any errors that occur on the JS side. -//printTestNames = true - -func test(_ name: String, testBlock: () throws -> Void) throws { - if printTestNames { print(name) } - do { - try testBlock() - } catch { - print("Error in \(name)") - print(error) - throw error - } - print("โ \(name)") -} - -struct MessageError: Error { - let message: String - let file: StaticString - let line: UInt - let column: UInt - init(_ message: String, file: StaticString, line: UInt, column: UInt) { - self.message = message - self.file = file - self.line = line - self.column = column - } -} - -func expectEqual<T: Equatable>( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs != rhs { - throw MessageError("Expect to be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column) - } -} - -func expectNotEqual<T: Equatable>( - _ lhs: T, _ rhs: T, - file: StaticString = #file, line: UInt = #line, column: UInt = #column -) throws { - if lhs == rhs { - throw MessageError("Expect to not be equal \"\(lhs)\" and \"\(rhs)\"", file: file, line: line, column: column) - } -} - -func expectObject(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSObject { - switch value { - case let .object(ref): return ref - default: - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } -} - -func expectArray(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSArray { - guard let array = value.array else { - throw MessageError("Type of \(value) should be \"object\"", file: file, line: line, column: column) - } - return array -} - -func expectFunction(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSFunction { - switch value { - case let .function(ref): return ref - default: - throw MessageError("Type of \(value) should be \"function\"", file: file, line: line, column: column) - } -} - -func expectBoolean(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Bool { - switch value { - case let .boolean(bool): return bool - default: - throw MessageError("Type of \(value) should be \"boolean\"", file: file, line: line, column: column) - } -} - -func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Double { - switch value { - case let .number(number): return number - default: - throw MessageError("Type of \(value) should be \"number\"", file: file, line: line, column: column) - } -} - -func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String { - switch value { - case let .string(string): return String(string) - default: - throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column) - } -} - -func expect(_ description: String, _ result: Bool, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - if !result { - throw MessageError(description, file: file, line: line, column: column) - } -} - -func expectThrow<T>(_ body: @autoclosure () throws -> T, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> Error { - do { - _ = try body() - } catch { - return error - } - throw MessageError("Expect to throw an exception", file: file, line: line, column: column) -} - -func wrapUnsafeThrowableFunction(_ body: @escaping () -> Void, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> JSValue { - JSObject.global.callThrowingClosure.function!(JSClosure { _ in - body() - return .undefined - }) -} -func expectNotNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - switch value { - case .some: return - case .none: - throw MessageError("Expect a non-nil value", file: file, line: line, column: column) - } -} -func expectNil<T>(_ value: T?, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws { - switch value { - case .some: - throw MessageError("Expect an nil", file: file, line: line, column: column) - case .none: return - } -} - -class Expectation { - private(set) var isFulfilled: Bool = false - private let label: String - private let expectedFulfillmentCount: Int - private var fulfillmentCount: Int = 0 - - init(label: String, expectedFulfillmentCount: Int = 1) { - self.label = label - self.expectedFulfillmentCount = expectedFulfillmentCount - } - - func fulfill() { - assert(!isFulfilled, "Too many fulfillment (label: \(label)): expectedFulfillmentCount is \(expectedFulfillmentCount)") - fulfillmentCount += 1 - if fulfillmentCount == expectedFulfillmentCount { - isFulfilled = true - } - } - - static func wait(_ expectations: [Expectation]) { - var timer: JSTimer! - timer = JSTimer(millisecondsDelay: 5.0, isRepeating: true) { - guard expectations.allSatisfy(\.isFulfilled) else { return } - assert(timer != nil) - timer = nil - } - } -} diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift deleted file mode 100644 index 12cc91cc9..000000000 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ /dev/null @@ -1,918 +0,0 @@ -import JavaScriptKit -import CHelpers - -try test("Literal Conversion") { - 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, input) { - case let (.number(lhs), .number(rhs)): - // Compare bitPattern because nan == nan is always false - try expectEqual(lhs.bitPattern, rhs.bitPattern) - default: - try expectEqual(got, input) - } - } -} - -try test("Object Conversion") { - // 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 expectObject(globalObject1) - let prop_1 = getJSValue(this: globalObject1Ref, name: "prop_1") - let prop_1Ref = try expectObject(prop_1) - let nested_prop = getJSValue(this: prop_1Ref, name: "nested_prop") - try expectEqual(nested_prop, .number(1)) - let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") - try expectEqual(prop_2, .number(2)) - let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") - try expectEqual(prop_3, .boolean(true)) - let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let prop_4Array = try expectObject(prop_4) - 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)) - try expectEqual(actualElement, expectedElement) - } - - try expectEqual(getJSValue(this: globalObject1Ref, name: "undefined_prop"), .undefined) -} - -try test("Value Construction") { - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_2 = getJSValue(this: globalObject1Ref, name: "prop_2") - try expectEqual(Int.construct(from: prop_2), 2) - let prop_3 = getJSValue(this: globalObject1Ref, name: "prop_3") - try expectEqual(Bool.construct(from: prop_3), true) - let prop_7 = getJSValue(this: globalObject1Ref, name: "prop_7") - try expectEqual(Double.construct(from: prop_7), 3.14) - try expectEqual(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) - ] { - try expectNil(Int.construct(from: source)) - try expectNil(Int8.construct(from: source)) - try expectNil(Int16.construct(from: source)) - try expectNil(Int32.construct(from: source)) - try expectNil(Int64.construct(from: source)) - try expectNil(UInt.construct(from: source)) - try expectNil(UInt8.construct(from: source)) - try expectNil(UInt16.construct(from: source)) - try expectNil(UInt32.construct(from: source)) - try expectNil(UInt64.construct(from: source)) - } -} - -try test("Array Iterator") { - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let array1 = try expectArray(prop_4) - let expectedProp_4: [JSValue] = [ - .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), - ] - try expectEqual(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 expectArray(prop_8) - let expectedProp_8: [JSValue] = [0, 2, 3, 6] - try expectEqual(Array(array2), expectedProp_8) -} - -try test("Array RandomAccessCollection") { - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_4 = getJSValue(this: globalObject1Ref, name: "prop_4") - let array1 = try expectArray(prop_4) - let expectedProp_4: [JSValue] = [ - .number(3), .number(4), .string("str_elm_1"), .null, .undefined, .number(5), - ] - try expectEqual([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 expectArray(prop_8) - let expectedProp_8: [JSValue] = [ - 0, .undefined, 2, 3, .undefined, .undefined, 6 - ] - try expectEqual([array2[0], array2[1], array2[2], array2[3], array2[4], array2[5], array2[6]], expectedProp_8) -} - -try test("Value Decoder") { - 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) - try expectEqual(globalObject1.prop_1.nested_prop, 1) - try expectEqual(globalObject1.prop_2, 2) - try expectEqual(globalObject1.prop_3, true) - try expectEqual(globalObject1.prop_7, 3.14) -} - -try test("Function Call") { - // 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 } - // }, - // } - // ... - // } - // ``` - // - - // Notes: If the size of `RawJSValue` is updated, these test suites will fail. - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_5 = getJSValue(this: globalObject1Ref, name: "prop_5") - let prop_5Ref = try expectObject(prop_5) - - let func1 = try expectFunction(getJSValue(this: prop_5Ref, name: "func1")) - try expectEqual(func1(), .undefined) - let func2 = try expectFunction(getJSValue(this: prop_5Ref, name: "func2")) - try expectEqual(func2(), .number(1)) - let func3 = try expectFunction(getJSValue(this: prop_5Ref, name: "func3")) - try expectEqual(func3(2), .number(4)) - let func4 = try expectFunction(getJSValue(this: prop_5Ref, name: "func4")) - try expectEqual(func4(2, 3, 4), .number(9)) - try expectEqual(func4(2, 3, 4, 5), .number(9)) - let func5 = try expectFunction(getJSValue(this: prop_5Ref, name: "func5")) - try expectEqual(func5("World!"), .string("Hello, World!")) - let func6 = try expectFunction(getJSValue(this: prop_5Ref, name: "func6")) - try expectEqual(func6(true, 1, 2), .number(1)) - try expectEqual(func6(false, 1, 2), .number(2)) - try expectEqual(func6(true, "OK", 2), .string("OK")) -} - -let evalClosure = JSObject.global.globalObject1.eval_closure.function! - -try test("Closure Lifetime") { - func expectCrashByCall(ofClosure c: JSClosureProtocol) throws { - print("======= BEGIN OF EXPECTED FATAL ERROR =====") - _ = try expectThrow(try evalClosure.throws(c)) - print("======= END OF EXPECTED FATAL ERROR =======") - } - - do { - let c1 = JSClosure { arguments in - return arguments[0] - } - try expectEqual(evalClosure(c1, JSValue.number(1.0)), .number(1.0)) -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif - } - - do { - let c1 = JSClosure { _ in .undefined } -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif - } - - do { - let array = JSObject.global.Array.function!.new() - let c1 = JSClosure { _ in .number(3) } - _ = array.push!(c1) - try expectEqual(array[0].function!().number, 3.0) -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - c1.release() -#endif - } - -// do { -// let weakRef = { () -> JSObject in -// let c1 = JSClosure { _ in .undefined } -// return JSObject.global.WeakRef.function!.new(c1) -// }() -// -// // unsure if this will actually work since GC may not run immediately -// try expectEqual(weakRef.deref!(), .undefined) -// } - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - do { - let c1 = JSOneshotClosure { _ in - return .boolean(true) - } - try expectEqual(evalClosure(c1), .boolean(true)) - // second call will cause `fatalError` that can be caught as a JavaScript exception - try expectCrashByCall(ofClosure: c1) - // OneshotClosure won't call fatalError even if it's deallocated before `release` - } -#endif - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - // Check diagnostics of use-after-free - do { - let c1Line = #line + 1 - let c1 = JSClosure { $0[0] } - c1.release() - let error = try expectThrow(try evalClosure.throws(c1, JSValue.number(42.0))) as! JSException - try expect("Error message should contains definition location", error.thrownValue.description.hasSuffix("PrimaryTests/main.swift:\(c1Line)")) - } -#endif - - do { - let c1 = JSClosure { _ in .number(4) } - try expectEqual(c1(), .number(4)) - } - - do { - let c1 = JSClosure { _ in fatalError("Crash while closure evaluation") } - let error = try expectThrow(try evalClosure.throws(c1)) as! JSException - try expectEqual(error.thrownValue.description, "RuntimeError: unreachable") - } -} - -try test("Host Function Registration") { - // ```js - // global.globalObject1 = { - // ... - // "prop_6": { - // "call_host_1": function() { - // return global.globalObject1.prop_6.host_func_1() - // } - // } - // } - // ``` - let globalObject1 = getJSValue(this: .global, name: "globalObject1") - let globalObject1Ref = try expectObject(globalObject1) - let prop_6 = getJSValue(this: globalObject1Ref, name: "prop_6") - let prop_6Ref = try expectObject(prop_6) - - var isHostFunc1Called = false - let hostFunc1 = JSClosure { (_) -> JSValue in - isHostFunc1Called = true - return .number(1) - } - - setJSValue(this: prop_6Ref, name: "host_func_1", value: .object(hostFunc1)) - - let call_host_1 = getJSValue(this: prop_6Ref, name: "call_host_1") - let call_host_1Func = try expectFunction(call_host_1) - try expectEqual(call_host_1Func(), .number(1)) - try expectEqual(isHostFunc1Called, true) - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - hostFunc1.release() -#endif - - let hostFunc2 = JSClosure { (arguments) -> JSValue in - do { - let input = try expectNumber(arguments[0]) - return .number(input * 2) - } catch { - return .string(String(describing: error)) - } - } - - try expectEqual(evalClosure(hostFunc2, 3), .number(6)) - _ = try expectString(evalClosure(hostFunc2, true)) - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - hostFunc2.release() -#endif -} - -try test("New Object Construction") { - // ```js - // global.Animal = function(name, age, isCat) { - // this.name = name - // this.age = age - // this.bark = () => { - // return isCat ? "nyan" : "wan" - // } - // } - // ``` - let objectConstructor = try expectFunction(getJSValue(this: .global, name: "Animal")) - let cat1 = objectConstructor.new("Tama", 3, true) - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Tama")) - try expectEqual(getJSValue(this: cat1, name: "age"), .number(3)) - try expectEqual(cat1.isInstanceOf(objectConstructor), true) - try expectEqual(cat1.isInstanceOf(try expectFunction(getJSValue(this: .global, name: "Array"))), false) - let cat1Bark = try expectFunction(getJSValue(this: cat1, name: "bark")) - try expectEqual(cat1Bark(), .string("nyan")) - - let dog1 = objectConstructor.new("Pochi", 3, false) - let dog1Bark = try expectFunction(getJSValue(this: dog1, name: "bark")) - try expectEqual(dog1Bark(), .string("wan")) -} - -try test("Object Decoding") { - /* - ```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 - - try expectNotNil(JSObject.construct(from: object)) - try expectEqual(JSObject.construct(from: function).map { $0 is JSFunction }, .some(true)) - try expectEqual(JSObject.construct(from: symbol).map { $0 is JSSymbol }, .some(true)) - try expectEqual(JSObject.construct(from: bigInt).map { $0 is JSBigInt }, .some(true)) - - try expectNil(JSFunction.construct(from: object)) - try expectNotNil(JSFunction.construct(from: function)) - try expectNil(JSFunction.construct(from: symbol)) - try expectNil(JSFunction.construct(from: bigInt)) - - try expectNil(JSSymbol.construct(from: object)) - try expectNil(JSSymbol.construct(from: function)) - try expectNotNil(JSSymbol.construct(from: symbol)) - try expectNil(JSSymbol.construct(from: bigInt)) - - try expectNil(JSBigInt.construct(from: object)) - try expectNil(JSBigInt.construct(from: function)) - try expectNil(JSBigInt.construct(from: symbol)) - try expectNotNil(JSBigInt.construct(from: bigInt)) -} - -try test("Call Function With This") { - // ```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 expectFunction(getJSValue(this: .global, name: "Animal")) - let cat1 = objectConstructor.new("Tama", 3, true) - let cat1Value = JSValue.object(cat1) - let getIsCat = try expectFunction(getJSValue(this: cat1, name: "getIsCat")) - let setName = try expectFunction(getJSValue(this: cat1, name: "setName")) - - // Direct call without this - _ = try expectThrow(try getIsCat.throws()) - - // Call with this - let gotIsCat = getIsCat(this: cat1) - try expectEqual(gotIsCat, .boolean(true)) - try expectEqual(cat1.getIsCat!(), .boolean(true)) - try expectEqual(cat1Value.getIsCat(), .boolean(true)) - - // Call with this and argument - setName(this: cat1, JSValue.string("Shiro")) - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Shiro")) - _ = cat1.setName!("Tora") - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Tora")) - _ = cat1Value.setName("Chibi") - try expectEqual(getJSValue(this: cat1, name: "name"), .string("Chibi")) -} - -try test("Object Conversion") { - let array1 = [1, 2, 3] - let jsArray1 = array1.jsValue.object! - try expectEqual(jsArray1.length, .number(3)) - try expectEqual(jsArray1[0], .number(1)) - try expectEqual(jsArray1[1], .number(2)) - try expectEqual(jsArray1[2], .number(3)) - - let array2: [ConvertibleToJSValue] = [1, "str", false] - let jsArray2 = array2.jsValue.object! - try expectEqual(jsArray2.length, .number(3)) - try expectEqual(jsArray2[0], .number(1)) - try expectEqual(jsArray2[1], .string("str")) - try expectEqual(jsArray2[2], .boolean(false)) - _ = jsArray2.push!(5) - try expectEqual(jsArray2.length, .number(4)) - _ = jsArray2.push!(jsArray1) - - try expectEqual(jsArray2[4], .object(jsArray1)) - - let dict1: [String: JSValue] = [ - "prop1": 1.jsValue, - "prop2": "foo".jsValue, - ] - let jsDict1 = dict1.jsValue.object! - try expectEqual(jsDict1.prop1, .number(1)) - try expectEqual(jsDict1.prop2, .string("foo")) -} - -try test("ObjectRef Lifetime") { - // ```js - // global.globalObject1 = { - // "prop_1": { - // "nested_prop": 1, - // }, - // "prop_2": 2, - // "prop_3": true, - // "prop_4": [ - // 3, 4, "str_elm_1", 5, - // ], - // ... - // } - // ``` - - let identity = JSClosure { $0[0] } - let ref1 = getJSValue(this: .global, name: "globalObject1").object! - let ref2 = evalClosure(identity, ref1).object! - try expectEqual(ref1.prop_2, .number(2)) - try expectEqual(ref2.prop_2, .number(2)) - -#if JAVASCRIPTKIT_WITHOUT_WEAKREFS - identity.release() -#endif -} - -func checkArray<T>(_ array: [T]) throws where T: TypedArrayElement & Equatable { - try expectEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array)) - try checkArrayUnsafeBytes(array) -} - -func toString<T: JSObject>(_ object: T) -> String { - return object.toString!().string! -} - -func jsStringify(_ array: [Any]) -> String { - array.map({ String(describing: $0) }).joined(separator: ",") -} - -func checkArrayUnsafeBytes<T>(_ array: [T]) throws where T: TypedArrayElement & Equatable { - let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in - Array(buffer) - } - try expectEqual(copyOfArray, array) -} - -try test("TypedArray") { - let numbers = [UInt8](0 ... 255) - let typedArray = JSTypedArray(numbers) - try expectEqual(typedArray[12], 12) - try expectEqual(numbers.count, typedArray.lengthInBytes) - - let numbersSet = Set(0 ... 255) - let typedArrayFromSet = JSTypedArray(numbersSet) - try expectEqual(typedArrayFromSet.jsObject.length, 256) - try expectEqual(typedArrayFromSet.lengthInBytes, 256 * MemoryLayout<Int>.size) - - try checkArray([0, .max, 127, 1] as [UInt8]) - try checkArray([0, 1, .max, .min, -1] as [Int8]) - - try checkArray([0, .max, 255, 1] as [UInt16]) - try checkArray([0, 1, .max, .min, -1] as [Int16]) - - try checkArray([0, .max, 255, 1] as [UInt32]) - try checkArray([0, 1, .max, .min, -1] as [Int32]) - - try checkArray([0, .max, 255, 1] as [UInt]) - try checkArray([0, 1, .max, .min, -1] as [Int]) - - let float32Array: [Float32] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42] - let jsFloat32Array = JSTypedArray(float32Array) - for (i, num) in float32Array.enumerated() { - try expectEqual(num, jsFloat32Array[i]) - } - - let float64Array: [Float64] = [0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42] - let jsFloat64Array = JSTypedArray(float64Array) - for (i, num) in float64Array.enumerated() { - try expectEqual(num, jsFloat64Array[i]) - } -} - -try test("TypedArray_Mutation") { - let array = JSTypedArray<Int>(length: 100) - for i in 0..<100 { - array[i] = i - } - for i in 0..<100 { - try expectEqual(i, array[i]) - } - try expectEqual(toString(array.jsValue.object!), jsStringify(Array(0..<100))) -} - -try test("Date") { - let date1Milliseconds = JSDate.now() - let date1 = JSDate(millisecondsSinceEpoch: date1Milliseconds) - let date2 = JSDate(millisecondsSinceEpoch: date1.valueOf()) - - try expectEqual(date1.valueOf(), date2.valueOf()) - try expectEqual(date1.fullYear, date2.fullYear) - try expectEqual(date1.month, date2.month) - try expectEqual(date1.date, date2.date) - try expectEqual(date1.day, date2.day) - try expectEqual(date1.hours, date2.hours) - try expectEqual(date1.minutes, date2.minutes) - try expectEqual(date1.seconds, date2.seconds) - try expectEqual(date1.milliseconds, date2.milliseconds) - try expectEqual(date1.utcFullYear, date2.utcFullYear) - try expectEqual(date1.utcMonth, date2.utcMonth) - try expectEqual(date1.utcDate, date2.utcDate) - try expectEqual(date1.utcDay, date2.utcDay) - try expectEqual(date1.utcHours, date2.utcHours) - try expectEqual(date1.utcMinutes, date2.utcMinutes) - try expectEqual(date1.utcSeconds, date2.utcSeconds) - try expectEqual(date1.utcMilliseconds, date2.utcMilliseconds) - try expectEqual(date1, date2) - - let date3 = JSDate(millisecondsSinceEpoch: 0) - try expectEqual(date3.valueOf(), 0) - try expectEqual(date3.utcFullYear, 1970) - try expectEqual(date3.utcMonth, 0) - try expectEqual(date3.utcDate, 1) - // the epoch date was on Friday - try expectEqual(date3.utcDay, 4) - try expectEqual(date3.utcHours, 0) - try expectEqual(date3.utcMinutes, 0) - try expectEqual(date3.utcSeconds, 0) - try expectEqual(date3.utcMilliseconds, 0) - try expectEqual(date3.toISOString(), "1970-01-01T00:00:00.000Z") - - try expectEqual(date3 < date1, true) -} - -// make the timers global to prevent early deallocation -var timeouts: [JSTimer] = [] -var interval: JSTimer? - -try test("Timer") { - let start = JSDate().valueOf() - let timeoutMilliseconds = 5.0 - var timeout: JSTimer! - timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { - // verify that at least `timeoutMilliseconds` passed since the `timeout` timer started - try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) - } - timeouts += [timeout] - - timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { - fatalError("timer should be cancelled") - } - timeout = nil - - var count = 0.0 - let maxCount = 5.0 - interval = JSTimer(millisecondsDelay: 5, isRepeating: true) { - // ensure that JSTimer is living - try! expectNotNil(interval) - // verify that at least `timeoutMilliseconds * count` passed since the `timeout` - // timer started - try! expectEqual(start + timeoutMilliseconds * count <= JSDate().valueOf(), true) - - guard count < maxCount else { - // stop the timer after `maxCount` reached - interval = nil - return - } - - count += 1 - } -} - -var timer: JSTimer? -var expectations: [Expectation] = [] - -try test("Promise") { - - let p1 = JSPromise.resolve(JSValue.null) - let exp1 = Expectation(label: "Promise.then testcase", expectedFulfillmentCount: 4) - p1.then { value in - try! expectEqual(value, .null) - exp1.fulfill() - return JSValue.number(1.0) - } - .then { value in - try! expectEqual(value, .number(1.0)) - exp1.fulfill() - return JSPromise.resolve(JSValue.boolean(true)) - } - .then { value in - try! expectEqual(value, .boolean(true)) - exp1.fulfill() - return JSValue.undefined - } - .catch { err -> JSValue in - print(err.object!.stack.string!) - fatalError("Not fired due to no throw") - } - .finally { exp1.fulfill() } - - let exp2 = Expectation(label: "Promise.catch testcase", expectedFulfillmentCount: 4) - let p2 = JSPromise.reject(JSValue.boolean(false)) - p2.then { _ -> JSValue in - fatalError("Not fired due to no success") - } - .catch { reason in - try! expectEqual(reason, .boolean(false)) - exp2.fulfill() - return JSValue.boolean(true) - } - .then { value in - try! expectEqual(value, .boolean(true)) - exp2.fulfill() - return JSPromise.reject(JSValue.number(2.0)) - } - .catch { reason in - try! expectEqual(reason, .number(2.0)) - exp2.fulfill() - return JSValue.undefined - } - .finally { exp2.fulfill() } - - - let start = JSDate().valueOf() - let timeoutMilliseconds = 5.0 - let exp3 = Expectation(label: "Promise and Timer testcae", expectedFulfillmentCount: 2) - - let p3 = JSPromise { resolve in - timer = JSTimer(millisecondsDelay: timeoutMilliseconds) { - exp3.fulfill() - resolve(.success(.undefined)) - } - } - - p3.then { _ in - // verify that at least `timeoutMilliseconds` passed since the timer started - try! expectEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) - exp3.fulfill() - return JSValue.undefined - } - - let exp4 = Expectation(label: "Promise lifetime") - // Ensure that users don't need to manage JSPromise lifetime - JSPromise.resolve(JSValue.boolean(true)).then { _ in - exp4.fulfill() - return JSValue.undefined - } - expectations += [exp1, exp2, exp3, exp4] -} - -try test("Error") { - let message = "test error" - let expectedDescription = "Error: test error" - let error = JSError(message: message) - try expectEqual(error.name, "Error") - try expectEqual(error.message, message) - try expectEqual(error.description, expectedDescription) - try expectEqual(error.stack?.isEmpty, false) - try expectEqual(JSError(from: .string("error"))?.description, nil) - try expectEqual(JSError(from: .object(error.jsObject))?.description, expectedDescription) -} - -try test("JSValue accessor") { - var globalObject1 = JSObject.global.globalObject1 - try expectEqual(globalObject1.prop_1.nested_prop, .number(1)) - try expectEqual(globalObject1.object!.prop_1.object!.nested_prop, .number(1)) - - try expectEqual(globalObject1.prop_4[0], .number(3)) - try expectEqual(globalObject1.prop_4[1], .number(4)) - - globalObject1.prop_1.nested_prop = "bar" - try expectEqual(globalObject1.prop_1.nested_prop, .string("bar")) - - /* TODO: Fix https://github.com/swiftwasm/JavaScriptKit/issues/132 and un-comment this test - `nested` should not be set again to `target.nested` by `target.nested.prop = .number(1)` - - let observableObj = globalObject1.observable_obj.object! - observableObj.set_called = .boolean(false) - observableObj.target.nested.prop = .number(1) - try expectEqual(observableObj.set_called, .boolean(false)) - - */ -} - -try test("Exception") { - // ```js - // global.globalObject1 = { - // ... - // prop_9: { - // func1: function () { - // throw new Error(); - // }, - // func2: function () { - // throw "String Error"; - // }, - // func3: function () { - // throw 3.0 - // }, - // }, - // ... - // } - // ``` - // - let globalObject1 = JSObject.global.globalObject1 - let prop_9: JSValue = globalObject1.prop_9 - - // MARK: Throwing method calls - let error1 = try expectThrow(try prop_9.object!.throwing.func1!()) - try expectEqual(error1 is JSException, true) - let errorObject = JSError(from: (error1 as! JSException).thrownValue) - try expectNotNil(errorObject) - - let error2 = try expectThrow(try prop_9.object!.throwing.func2!()) - try expectEqual(error2 is JSException, true) - let errorString = try expectString((error2 as! JSException).thrownValue) - try expectEqual(errorString, "String Error") - - let error3 = try expectThrow(try prop_9.object!.throwing.func3!()) - try expectEqual(error3 is JSException, true) - let errorNumber = try expectNumber((error3 as! JSException).thrownValue) - try expectEqual(errorNumber, 3.0) - - // MARK: Simple function calls - let error4 = try expectThrow(try prop_9.func1.function!.throws()) - try expectEqual(error4 is JSException, true) - let errorObject2 = JSError(from: (error4 as! JSException).thrownValue) - try expectNotNil(errorObject2) - - // MARK: Throwing constructor call - let Animal = JSObject.global.Animal.function! - _ = try Animal.throws.new("Tama", 3, true) - let ageError = try expectThrow(try Animal.throws.new("Tama", -3, true)) - try expectEqual(ageError is JSException, true) - let errorObject3 = JSError(from: (ageError as! JSException).thrownValue) - try expectNotNil(errorObject3) -} - -try test("Unhandled Exception") { - // ```js - // global.globalObject1 = { - // ... - // prop_9: { - // func1: function () { - // throw new Error(); - // }, - // func2: function () { - // throw "String Error"; - // }, - // func3: function () { - // throw 3.0 - // }, - // }, - // ... - // } - // ``` - // - - let globalObject1 = JSObject.global.globalObject1 - let prop_9: JSValue = globalObject1.prop_9 - - // MARK: Throwing method calls - let error1 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func1!() } - let errorObject = JSError(from: error1) - try expectNotNil(errorObject) - - let error2 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func2!() } - let errorString = try expectString(error2) - try expectEqual(errorString, "String Error") - - let error3 = try wrapUnsafeThrowableFunction { _ = prop_9.object!.func3!() } - let errorNumber = try expectNumber(error3) - try expectEqual(errorNumber, 3.0) -} - -/// 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 -try test("Grow Memory") { - let string = "Hello" - let jsString = JSValue.string(string) - growMemory(1) - try expectEqual(string, jsString.description) -} - -try test("Hashable Conformance") { - let globalObject1 = JSObject.global.console.object! - let globalObject2 = JSObject.global.console.object! - try expectEqual(globalObject1.hashValue, globalObject2.hashValue) - // These are 2 different objects in Swift referencing the same object in JavaScript - try expectNotEqual(ObjectIdentifier(globalObject1), ObjectIdentifier(globalObject2)) - - let sameObjectSet: Set<JSObject> = [globalObject1, globalObject2] - try expectEqual(sameObjectSet.count, 1) - - 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 - try expectEqual(firstHash, secondHash) -} - -try test("Symbols") { - let symbol1 = JSSymbol("abc") - let symbol2 = JSSymbol("abc") - try expectNotEqual(symbol1, symbol2) - try expectEqual(symbol1.name, symbol2.name) - try expectEqual(symbol1.name, "abc") - - try expectEqual(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 - ) - try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) - try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) -} - -struct AnimalStruct: Decodable { - let name: String - let age: Int - let isCat: Bool -} - -try test("JSValueDecoder") { - 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) - - try expectEqual(decodedTama.name, tama.name.string) - try expectEqual(decodedTama.name, "Tama") - - try expectEqual(decodedTama.age, tama.age.number.map(Int.init)) - try expectEqual(decodedTama.age, 3) - - try expectEqual(decodedTama.isCat, tama.isCat.boolean) - try expectEqual(decodedTama.isCat, true) -} - -try testI64() -Expectation.wait(expectations) diff --git a/IntegrationTests/bin/concurrency-tests.js b/IntegrationTests/bin/concurrency-tests.js deleted file mode 100644 index 02489c959..000000000 --- a/IntegrationTests/bin/concurrency-tests.js +++ /dev/null @@ -1,8 +0,0 @@ -import { startWasiTask } from "../lib.js"; - -Error.stackTraceLimit = Infinity; - -startWasiTask("./dist/ConcurrencyTests.wasm").catch((err) => { - console.log(err); - process.exit(1); -}); diff --git a/IntegrationTests/bin/primary-tests.js b/IntegrationTests/bin/primary-tests.js deleted file mode 100644 index 36ac65812..000000000 --- a/IntegrationTests/bin/primary-tests.js +++ /dev/null @@ -1,110 +0,0 @@ -Error.stackTraceLimit = Infinity; - -global.globalObject1 = { - prop_1: { - nested_prop: 1, - }, - prop_2: 2, - prop_3: true, - prop_4: [3, 4, "str_elm_1", null, undefined, 5], - 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; - } - }, - }, - prop_6: { - call_host_1: () => { - return global.globalObject1.prop_6.host_func_1(); - }, - }, - prop_7: 3.14, - prop_8: [0, , 2, 3, , , 6], - prop_9: { - func1: function () { - throw new Error(); - }, - func2: function () { - throw "String Error"; - }, - func3: function () { - throw 3.0; - }, - }, - eval_closure: function (fn) { - return fn(arguments[1]); - }, - observable_obj: { - set_called: false, - target: new Proxy( - { - nested: {}, - }, - { - set(target, key, value) { - global.globalObject1.observable_obj.set_called = true; - target[key] = value; - return true; - }, - } - ), - }, -}; - -global.Animal = function (name, age, isCat) { - if (age < 0) { - throw new Error("Invalid age " + age); - } - this.name = name; - this.age = age; - this.bark = () => { - return isCat ? "nyan" : "wan"; - }; - this.isCat = isCat; - this.getIsCat = function () { - return this.isCat; - }; - this.setName = function (name) { - this.name = name; - }; -}; - -global.callThrowingClosure = (c) => { - try { - c(); - } catch (error) { - return error; - } -}; - -global.objectDecodingTest = { - obj: {}, - fn: () => {}, - sym: Symbol("s"), - bi: BigInt(3) -}; - -import { startWasiTask } from "../lib.js"; - -startWasiTask("./dist/PrimaryTests.wasm").catch((err) => { - console.log(err); - process.exit(1); -}); diff --git a/IntegrationTests/lib.js b/IntegrationTests/lib.js index a2f10e565..d9c424f0e 100644 --- a/IntegrationTests/lib.js +++ b/IntegrationTests/lib.js @@ -3,7 +3,6 @@ import { WASI as NodeWASI } from "wasi" import { WASI as MicroWASI, useAll } from "uwasi" import * as fs from "fs/promises" import path from "path"; -import { Worker, parentPort } from "node:worker_threads"; const WASI = { MicroWASI: ({ args }) => { @@ -53,16 +52,6 @@ const selectWASIBackend = () => { return "Node" }; -function isUsingSharedMemory(module) { - const imports = WebAssembly.Module.imports(module); - for (const entry of imports) { - if (entry.module === "env" && entry.name === "memory" && entry.kind == "memory") { - return true; - } - } - return false; -} - function constructBaseImportObject(wasi, swift) { return { wasi_snapshot_preview1: wasi.wasiImport, @@ -74,79 +63,6 @@ function constructBaseImportObject(wasi, swift) { } } -export async function startWasiChildThread(event) { - const { module, programName, memory, tid, startArg } = event; - const swift = new SwiftRuntime({ - sharedMemory: true, - threadChannel: { - postMessageToMainThread: (message, transfer) => { - parentPort.postMessage(message, transfer); - }, - listenMessageFromMainThread: (listener) => { - parentPort.on("message", listener) - } - } - }); - // Use uwasi for child threads because Node.js WASI cannot be used without calling - // `WASI.start` or `WASI.initialize`, which is already called in the main thread and - // will cause an error if called again. - const wasi = WASI.MicroWASI({ programName }); - - const importObject = constructBaseImportObject(wasi, swift); - - importObject["wasi"] = { - "thread-spawn": () => { - throw new Error("Cannot spawn a new thread from a worker thread") - } - }; - importObject["env"] = { memory }; - importObject["JavaScriptEventLoopTestSupportTests"] = { - "isMainThread": () => false, - } - - const instance = await WebAssembly.instantiate(module, importObject); - swift.setInstance(instance); - wasi.setInstance(instance); - swift.startThread(tid, startArg); -} - -class ThreadRegistry { - workers = new Map(); - nextTid = 1; - - spawnThread(module, programName, memory, startArg) { - const tid = this.nextTid++; - const selfFilePath = new URL(import.meta.url).pathname; - const worker = new Worker(` - const { parentPort } = require('node:worker_threads'); - - Error.stackTraceLimit = 100; - parentPort.once("message", async (event) => { - const { selfFilePath } = event; - const { startWasiChildThread } = await import(selfFilePath); - await startWasiChildThread(event); - }) - `, { type: "module", eval: true }) - - worker.on("error", (error) => { - console.error(`Worker thread ${tid} error:`, error); - throw error; - }); - this.workers.set(tid, worker); - worker.postMessage({ selfFilePath, module, programName, memory, tid, startArg }); - return tid; - } - - worker(tid) { - return this.workers.get(tid); - } - - wakeUpWorkerThread(tid, message, transfer) { - const worker = this.workers.get(tid); - worker.postMessage(message, transfer); - } -} - export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBackend()) => { // Fetch our Wasm File const wasmBinary = await fs.readFile(wasmPath); @@ -157,38 +73,10 @@ export const startWasiTask = async (wasmPath, wasiConstructorKey = selectWASIBac const module = await WebAssembly.compile(wasmBinary); - const sharedMemory = isUsingSharedMemory(module); - const threadRegistry = new ThreadRegistry(); - const swift = new SwiftRuntime({ - sharedMemory, - threadChannel: { - postMessageToWorkerThread: threadRegistry.wakeUpWorkerThread.bind(threadRegistry), - listenMessageFromWorkerThread: (tid, listener) => { - const worker = threadRegistry.worker(tid); - worker.on("message", listener); - } - } - }); + const swift = new SwiftRuntime(); const importObject = constructBaseImportObject(wasi, swift); - importObject["JavaScriptEventLoopTestSupportTests"] = { - "isMainThread": () => true, - } - - if (sharedMemory) { - // We don't have JS API to get memory descriptor of imported memory - // at this moment, so we assume 256 pages (16MB) memory is enough - // large for initial memory size. - const memory = new WebAssembly.Memory({ initial: 1024, maximum: 16384, shared: true }) - importObject["env"] = { memory }; - importObject["wasi"] = { - "thread-spawn": (startArg) => { - return threadRegistry.spawnThread(module, programName, memory, startArg); - } - } - } - // Instantiate the WebAssembly file const instance = await WebAssembly.instantiate(module, importObject); diff --git a/Makefile b/Makefile index ed0727ce8..c8b79b4ab 100644 --- a/Makefile +++ b/Makefile @@ -12,19 +12,16 @@ build: swift build --triple wasm32-unknown-wasi npm run build -.PHONY: test -test: - @echo Running integration tests - cd IntegrationTests && \ - CONFIGURATION=debug SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ - CONFIGURATION=debug SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test && \ - CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS)" $(MAKE) test && \ - CONFIGURATION=release SWIFT_BUILD_FLAGS="$(SWIFT_BUILD_FLAGS) -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS" $(MAKE) test - .PHONY: unittest unittest: @echo Running unit tests - swift package --swift-sdk "$(SWIFT_SDK_ID)" js test --prelude ./Tests/prelude.mjs + swift package --swift-sdk "$(SWIFT_SDK_ID)" \ + --disable-sandbox \ + -Xlinker --stack-first \ + -Xlinker --global-base=524288 \ + -Xlinker -z \ + -Xlinker stack-size=524288 \ + js test --prelude ./Tests/prelude.mjs .PHONY: benchmark_setup benchmark_setup: diff --git a/Package.swift b/Package.swift index 173add2dd..9b8e1ca38 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,10 @@ let package = Package( .target(name: "_CJavaScriptKit"), .testTarget( name: "JavaScriptKitTests", - dependencies: ["JavaScriptKit"] + dependencies: ["JavaScriptKit"], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] ), .target( @@ -42,6 +45,10 @@ let package = Package( dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"] ), .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), + .testTarget( + name: "JavaScriptBigIntSupportTests", + dependencies: ["JavaScriptBigIntSupport", "JavaScriptKit"] + ), .target( name: "JavaScriptEventLoop", diff --git a/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift b/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift new file mode 100644 index 000000000..e1fb8a96f --- /dev/null +++ b/Tests/JavaScriptBigIntSupportTests/JavaScriptBigIntSupportTests.swift @@ -0,0 +1,50 @@ +import XCTest +import JavaScriptBigIntSupport +import JavaScriptKit + +class JavaScriptBigIntSupportTests: XCTestCase { + func testBigIntSupport() { + // Test signed values + func testSignedValue(_ value: Int64, file: StaticString = #filePath, line: UInt = #line) { + let bigInt = JSBigInt(value) + XCTAssertEqual(bigInt.description, value.description, file: file, line: line) + let bigInt2 = JSBigInt(_slowBridge: value) + XCTAssertEqual(bigInt2.description, value.description, file: file, line: line) + } + + // Test unsigned values + func testUnsignedValue(_ value: UInt64, file: StaticString = #filePath, line: UInt = #line) { + let bigInt = JSBigInt(unsigned: value) + XCTAssertEqual(bigInt.description, value.description, file: file, line: line) + let bigInt2 = JSBigInt(_slowBridge: value) + XCTAssertEqual(bigInt2.description, value.description, file: file, line: line) + } + + // Test specific signed values + testSignedValue(0) + testSignedValue(1 << 62) + testSignedValue(-2305) + + // Test random signed values + for _ in 0..<100 { + testSignedValue(.random(in: .min ... .max)) + } + + // Test edge signed values + testSignedValue(.min) + testSignedValue(.max) + + // Test specific unsigned values + testUnsignedValue(0) + testUnsignedValue(1 << 62) + testUnsignedValue(1 << 63) + testUnsignedValue(.min) + testUnsignedValue(.max) + testUnsignedValue(~0) + + // Test random unsigned values + for _ in 0..<100 { + testUnsignedValue(.random(in: .min ... .max)) + } + } +} diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift new file mode 100644 index 000000000..e19d356e5 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift @@ -0,0 +1,97 @@ +import XCTest +@testable import JavaScriptKit + +final class JSPromiseTests: XCTestCase { + func testPromiseThen() async throws { + var p1 = JSPromise.resolve(JSValue.null) + await withCheckedContinuation { continuation in + p1 = p1.then { value in + XCTAssertEqual(value, .null) + continuation.resume() + return JSValue.number(1.0) + } + } + await withCheckedContinuation { continuation in + p1 = p1.then { value in + XCTAssertEqual(value, .number(1.0)) + continuation.resume() + return JSPromise.resolve(JSValue.boolean(true)) + } + } + await withCheckedContinuation { continuation in + p1 = p1.then { value in + XCTAssertEqual(value, .boolean(true)) + continuation.resume() + return JSValue.undefined + } + } + await withCheckedContinuation { continuation in + p1 = p1.catch { error in + XCTFail("Not fired due to no throw") + return JSValue.undefined + } + .finally { continuation.resume() } + } + } + + func testPromiseCatch() async throws { + var p2 = JSPromise.reject(JSValue.boolean(false)) + await withCheckedContinuation { continuation in + p2 = p2.catch { error in + XCTAssertEqual(error, .boolean(false)) + continuation.resume() + return JSValue.boolean(true) + } + } + await withCheckedContinuation { continuation in + p2 = p2.then { value in + XCTAssertEqual(value, .boolean(true)) + continuation.resume() + return JSPromise.reject(JSValue.number(2.0)) + } + } + await withCheckedContinuation { continuation in + p2 = p2.catch { error in + XCTAssertEqual(error, .number(2.0)) + continuation.resume() + return JSValue.undefined + } + } + await withCheckedContinuation { continuation in + p2 = p2.finally { continuation.resume() } + } + } + + func testPromiseAndTimer() async throws { + let start = JSDate().valueOf() + let timeoutMilliseconds = 5.0 + var timer: JSTimer? + + var p3: JSPromise? + await withCheckedContinuation { continuation in + p3 = JSPromise { resolve in + timer = JSTimer(millisecondsDelay: timeoutMilliseconds) { + continuation.resume() + resolve(.success(.undefined)) + } + } + } + + await withCheckedContinuation { continuation in + p3?.then { _ in + XCTAssertEqual(start + timeoutMilliseconds <= JSDate().valueOf(), true) + continuation.resume() + return JSValue.undefined + } + } + + // Ensure that users don't need to manage JSPromise lifetime + await withCheckedContinuation { continuation in + JSPromise.resolve(JSValue.boolean(true)).then { _ in + continuation.resume() + return JSValue.undefined + } + } + withExtendedLifetime(timer) {} + } +} diff --git a/Tests/JavaScriptEventLoopTests/JSTimerTests.swift b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift new file mode 100644 index 000000000..2ee92cebd --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JSTimerTests.swift @@ -0,0 +1,56 @@ +import XCTest + +@testable import JavaScriptKit + +final class JSTimerTests: XCTestCase { + + func testOneshotTimerCancelled() { + let timeoutMilliseconds = 5.0 + var timeout: JSTimer! + timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { + XCTFail("timer should be cancelled") + } + _ = timeout + timeout = nil + } + + func testRepeatingTimerCancelled() async throws { + var count = 0.0 + let maxCount = 5.0 + var interval: JSTimer? + let start = JSDate().valueOf() + let timeoutMilliseconds = 5.0 + + await withCheckedContinuation { continuation in + interval = JSTimer(millisecondsDelay: 5, isRepeating: true) { + // ensure that JSTimer is living + XCTAssertNotNil(interval) + // verify that at least `timeoutMilliseconds * count` passed since the `timeout` + // timer started + XCTAssertTrue(start + timeoutMilliseconds * count <= JSDate().valueOf()) + + guard count < maxCount else { + // stop the timer after `maxCount` reached + interval = nil + continuation.resume() + return + } + + count += 1 + } + } + withExtendedLifetime(interval) {} + } + + func testTimer() async throws { + let start = JSDate().valueOf() + let timeoutMilliseconds = 5.0 + var timeout: JSTimer! + await withCheckedContinuation { continuation in + timeout = JSTimer(millisecondsDelay: timeoutMilliseconds, isRepeating: false) { + continuation.resume() + } + } + withExtendedLifetime(timeout) {} + } +} diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift new file mode 100644 index 000000000..40eb96af0 --- /dev/null +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -0,0 +1,260 @@ +import JavaScriptEventLoop +import JavaScriptKit +import XCTest + +final class JavaScriptEventLoopTests: XCTestCase { + // Helper utilities for testing + struct MessageError: Error { + let message: String + let file: StaticString + let line: UInt + let column: UInt + init(_ message: String, file: StaticString, line: UInt, column: UInt) { + self.message = message + self.file = file + self.line = line + self.column = column + } + } + + func expectAsyncThrow<T>( + _ body: @autoclosure () async throws -> T, file: StaticString = #file, line: UInt = #line, + column: UInt = #column + ) async throws -> Error { + do { + _ = try await body() + } catch { + return error + } + throw MessageError("Expect to throw an exception", file: file, line: line, column: column) + } + + func performanceNow() -> Double { + return JSObject.global.performance.now().number! + } + + func measureTime(_ block: () async throws -> Void) async rethrows -> Double { + let start = performanceNow() + try await block() + return performanceNow() - start + } + + // Error type used in tests + struct E: Error, Equatable { + let value: Int + } + + // MARK: - Task Tests + + func testTaskInit() async throws { + // Test Task.init value + let handle = Task { 1 } + let value = await handle.value + XCTAssertEqual(value, 1) + } + + func testTaskInitThrows() async throws { + // Test Task.init throws + let throwingHandle = Task { + throw E(value: 2) + } + let error = try await expectAsyncThrow(await throwingHandle.value) + let e = try XCTUnwrap(error as? E) + XCTAssertEqual(e, E(value: 2)) + } + + func testTaskSleep() async throws { + // Test Task.sleep(_:) + let sleepDiff = try await measureTime { + try await Task.sleep(nanoseconds: 200_000_000) + } + XCTAssertGreaterThanOrEqual(sleepDiff, 200) + + // Test shorter sleep duration + let shortSleepDiff = try await measureTime { + try await Task.sleep(nanoseconds: 100_000_000) + } + XCTAssertGreaterThanOrEqual(shortSleepDiff, 100) + } + + func testTaskPriority() async throws { + // Test Job reordering based on priority + class Context: @unchecked Sendable { + var completed: [String] = [] + } + let context = Context() + + // When no priority, they should be ordered by the enqueued order + let t1 = Task(priority: nil) { + context.completed.append("t1") + } + let t2 = Task(priority: nil) { + context.completed.append("t2") + } + + _ = await (t1.value, t2.value) + XCTAssertEqual(context.completed, ["t1", "t2"]) + + context.completed = [] + // When high priority is enqueued after a low one, they should be re-ordered + let t3 = Task(priority: .low) { + context.completed.append("t3") + } + let t4 = Task(priority: .high) { + context.completed.append("t4") + } + let t5 = Task(priority: .low) { + context.completed.append("t5") + } + + _ = await (t3.value, t4.value, t5.value) + XCTAssertEqual(context.completed, ["t4", "t3", "t5"]) + } + + // MARK: - Promise Tests + + func testPromiseResolution() async throws { + // Test await resolved Promise + let p = JSPromise(resolver: { resolve in + resolve(.success(1)) + }) + let resolutionValue = try await p.value + XCTAssertEqual(resolutionValue, .number(1)) + let resolutionResult = await p.result + XCTAssertEqual(resolutionResult, .success(.number(1))) + } + + func testPromiseRejection() async throws { + // Test await rejected Promise + let rejectedPromise = JSPromise(resolver: { resolve in + resolve(.failure(.number(3))) + }) + let promiseError = try await expectAsyncThrow(try await rejectedPromise.value) + let jsValue = try XCTUnwrap(promiseError as? JSException).thrownValue + XCTAssertEqual(jsValue, .number(3)) + let rejectionResult = await rejectedPromise.result + XCTAssertEqual(rejectionResult, .failure(.number(3))) + } + + func testPromiseThen() async throws { + // Test Async JSPromise: then + let promise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.success(JSValue.number(3))) + return .undefined + }.jsValue, + 100 + ) + } + let promise2 = promise.then { result in + try await Task.sleep(nanoseconds: 100_000_000) + return String(result.number!) + } + let thenDiff = try await measureTime { + let result = try await promise2.value + XCTAssertEqual(result, .string("3.0")) + } + XCTAssertGreaterThanOrEqual(thenDiff, 200) + } + + func testPromiseThenWithFailure() async throws { + // Test Async JSPromise: then(success:failure:) + let failingPromise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 100 + ) + } + let failingPromise2 = failingPromise.then { _ in + throw MessageError("Should not be called", file: #file, line: #line, column: #column) + } failure: { err in + return err + } + let failingResult = try await failingPromise2.value + XCTAssertEqual(failingResult.object?.message, .string("test")) + } + + func testPromiseCatch() async throws { + // Test Async JSPromise: catch + let catchPromise = JSPromise { resolve in + _ = JSObject.global.setTimeout!( + JSClosure { _ in + resolve(.failure(JSError(message: "test").jsValue)) + return .undefined + }.jsValue, + 100 + ) + } + let catchPromise2 = catchPromise.catch { err in + try await Task.sleep(nanoseconds: 100_000_000) + return err + } + let catchDiff = try await measureTime { + let result = try await catchPromise2.value + XCTAssertEqual(result.object?.message, .string("test")) + } + XCTAssertGreaterThanOrEqual(catchDiff, 200) + } + + // MARK: - Continuation Tests + + func testContinuation() async throws { + // Test Continuation + let continuationValue = await withUnsafeContinuation { cont in + cont.resume(returning: 1) + } + XCTAssertEqual(continuationValue, 1) + + let continuationError = try await expectAsyncThrow( + try await withUnsafeThrowingContinuation { (cont: UnsafeContinuation<Never, Error>) in + cont.resume(throwing: E(value: 2)) + } + ) + let errorValue = try XCTUnwrap(continuationError as? E) + XCTAssertEqual(errorValue.value, 2) + } + + // MARK: - JSClosure Tests + + func testAsyncJSClosure() async throws { + // Test Async JSClosure + let delayClosure = JSClosure.async { _ -> JSValue in + try await Task.sleep(nanoseconds: 200_000_000) + return JSValue.number(3) + } + let delayObject = JSObject.global.Object.function!.new() + delayObject.closure = delayClosure.jsValue + + let closureDiff = try await measureTime { + let promise = JSPromise(from: delayObject.closure!()) + XCTAssertNotNil(promise) + let result = try await promise!.value + XCTAssertEqual(result, .number(3)) + } + XCTAssertGreaterThanOrEqual(closureDiff, 200) + } + + // MARK: - Clock Tests + + #if compiler(>=5.7) + func testClockSleep() async throws { + // Test ContinuousClock.sleep + let continuousClockDiff = try await measureTime { + let c = ContinuousClock() + try await c.sleep(until: .now + .milliseconds(100)) + } + XCTAssertGreaterThanOrEqual(continuousClockDiff, 99) + + // Test SuspendingClock.sleep + let suspendingClockDiff = try await measureTime { + let c = SuspendingClock() + try await c.sleep(until: .now + .milliseconds(100)) + } + XCTAssertGreaterThanOrEqual(suspendingClockDiff, 99) + } + #endif +} diff --git a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift index 87b81ae16..8e2556f8d 100644 --- a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift +++ b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift @@ -1,5 +1,5 @@ -import XCTest import JavaScriptKit +import XCTest final class JSTypedArrayTests: XCTestCase { func testEmptyArray() { @@ -15,4 +15,86 @@ final class JSTypedArrayTests: XCTestCase { _ = JSTypedArray<Float32>([Float32]()) _ = JSTypedArray<Float64>([Float64]()) } + + func testTypedArray() { + func checkArray<T>(_ array: [T]) where T: TypedArrayElement & Equatable { + XCTAssertEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array)) + checkArrayUnsafeBytes(array) + } + + func toString<T: JSObject>(_ object: T) -> String { + return object.toString!().string! + } + + func jsStringify(_ array: [Any]) -> String { + array.map({ String(describing: $0) }).joined(separator: ",") + } + + func checkArrayUnsafeBytes<T>(_ array: [T]) where T: TypedArrayElement & Equatable { + let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in + Array(buffer) + } + XCTAssertEqual(copyOfArray, array) + } + + let numbers = [UInt8](0...255) + let typedArray = JSTypedArray(numbers) + XCTAssertEqual(typedArray[12], 12) + XCTAssertEqual(numbers.count, typedArray.lengthInBytes) + + let numbersSet = Set(0...255) + let typedArrayFromSet = JSTypedArray(numbersSet) + XCTAssertEqual(typedArrayFromSet.jsObject.length, 256) + XCTAssertEqual(typedArrayFromSet.lengthInBytes, 256 * MemoryLayout<Int>.size) + + checkArray([0, .max, 127, 1] as [UInt8]) + checkArray([0, 1, .max, .min, -1] as [Int8]) + + checkArray([0, .max, 255, 1] as [UInt16]) + checkArray([0, 1, .max, .min, -1] as [Int16]) + + checkArray([0, .max, 255, 1] as [UInt32]) + checkArray([0, 1, .max, .min, -1] as [Int32]) + + checkArray([0, .max, 255, 1] as [UInt]) + checkArray([0, 1, .max, .min, -1] as [Int]) + + let float32Array: [Float32] = [ + 0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, + .leastNormalMagnitude, 42, + ] + let jsFloat32Array = JSTypedArray(float32Array) + for (i, num) in float32Array.enumerated() { + XCTAssertEqual(num, jsFloat32Array[i]) + } + + let float64Array: [Float64] = [ + 0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, + .leastNormalMagnitude, 42, + ] + let jsFloat64Array = JSTypedArray(float64Array) + for (i, num) in float64Array.enumerated() { + XCTAssertEqual(num, jsFloat64Array[i]) + } + } + + func testTypedArrayMutation() { + let array = JSTypedArray<Int>(length: 100) + for i in 0..<100 { + array[i] = i + } + for i in 0..<100 { + XCTAssertEqual(i, array[i]) + } + + func toString<T: JSObject>(_ object: T) -> String { + return object.toString!().string! + } + + func jsStringify(_ array: [Any]) -> String { + array.map({ String(describing: $0) }).joined(separator: ",") + } + + XCTAssertEqual(toString(array.jsValue.object!), jsStringify(Array(0..<100))) + } } diff --git a/Tests/JavaScriptKitTests/JavaScriptKitTests.swift b/Tests/JavaScriptKitTests/JavaScriptKitTests.swift new file mode 100644 index 000000000..6c90afead --- /dev/null +++ b/Tests/JavaScriptKitTests/JavaScriptKitTests.swift @@ -0,0 +1,674 @@ +import XCTest +import JavaScriptKit + +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, input) { + case let (.number(lhs), .number(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 testClosureLifetime() { + let evalClosure = JSObject.global.globalObject1.eval_closure.function! + + do { + let c1 = JSClosure { arguments in + return arguments[0] + } + XCTAssertEqual(evalClosure(c1, JSValue.number(1.0)), .number(1.0)) +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + c1.release() +#endif + } + + do { + let array = JSObject.global.Array.function!.new() + let c1 = JSClosure { _ in .number(3) } + _ = array.push!(c1) + XCTAssertEqual(array[0].function!().number, 3.0) +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + c1.release() +#endif + } + + do { + let c1 = JSClosure { _ in .undefined } + XCTAssertEqual(c1(), .undefined) + } + + do { + let c1 = JSClosure { _ in .number(4) } + XCTAssertEqual(c1(), .number(4)) + } + } + + func testHostFunctionRegistration() { + // ```js + // global.globalObject1 = { + // ... + // "prop_6": { + // "call_host_1": function() { + // return global.globalObject1.prop_6.host_func_1() + // } + // } + // } + // ``` + let globalObject1 = getJSValue(this: .global, name: "globalObject1") + let globalObject1Ref = try! XCTUnwrap(globalObject1.object) + let prop_6 = getJSValue(this: globalObject1Ref, name: "prop_6") + let prop_6Ref = try! XCTUnwrap(prop_6.object) + + var isHostFunc1Called = false + let hostFunc1 = JSClosure { (_) -> JSValue in + isHostFunc1Called = true + return .number(1) + } + + setJSValue(this: prop_6Ref, name: "host_func_1", value: .object(hostFunc1)) + + let call_host_1 = getJSValue(this: prop_6Ref, name: "call_host_1") + let call_host_1Func = try! XCTUnwrap(call_host_1.function) + XCTAssertEqual(call_host_1Func(), .number(1)) + XCTAssertEqual(isHostFunc1Called, true) + +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + hostFunc1.release() +#endif + + let evalClosure = JSObject.global.globalObject1.eval_closure.function! + let hostFunc2 = JSClosure { (arguments) -> JSValue in + if let input = arguments[0].number { + return .number(input * 2) + } else { + return .string(String(describing: arguments[0])) + } + } + + XCTAssertEqual(evalClosure(hostFunc2, 3), .number(6)) + XCTAssertTrue(evalClosure(hostFunc2, true).string != nil) + +#if JAVASCRIPTKIT_WITHOUT_WEAKREFS + hostFunc2.release() +#endif + } + + 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: function).map { $0 is JSFunction }, .some(true)) + XCTAssertEqual(JSObject.construct(from: symbol).map { $0 is JSSymbol }, .some(true)) + XCTAssertEqual(JSObject.construct(from: bigInt).map { $0 is JSBigInt }, .some(true)) + + XCTAssertNil(JSFunction.construct(from: object)) + XCTAssertNotNil(JSFunction.construct(from: function)) + XCTAssertNil(JSFunction.construct(from: symbol)) + XCTAssertNil(JSFunction.construct(from: bigInt)) + + 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() { + var 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 testException() { + // ```js + // global.globalObject1 = { + // ... + // prop_9: { + // func1: function () { + // throw new Error(); + // }, + // func2: function () { + // throw "String Error"; + // }, + // func3: function () { + // throw 3.0 + // }, + // }, + // ... + // } + // ``` + // + let globalObject1 = JSObject.global.globalObject1 + let prop_9: JSValue = globalObject1.prop_9 + + // MARK: Throwing method calls + XCTAssertThrowsError(try prop_9.object!.throwing.func1!()) { error in + XCTAssertTrue(error is JSException) + let errorObject = JSError(from: (error as! JSException).thrownValue) + XCTAssertNotNil(errorObject) + } + + XCTAssertThrowsError(try prop_9.object!.throwing.func2!()) { error in + XCTAssertTrue(error is JSException) + let thrownValue = (error as! JSException).thrownValue + XCTAssertEqual(thrownValue.string, "String Error") + } + + XCTAssertThrowsError(try prop_9.object!.throwing.func3!()) { error in + XCTAssertTrue(error is JSException) + let thrownValue = (error as! JSException).thrownValue + XCTAssertEqual(thrownValue.number, 3.0) + } + + // MARK: Simple function calls + XCTAssertThrowsError(try prop_9.func1.function!.throws()) { error in + XCTAssertTrue(error is JSException) + let errorObject = JSError(from: (error as! JSException).thrownValue) + XCTAssertNotNil(errorObject) + } + + // MARK: Throwing constructor call + let Animal = JSObject.global.Animal.function! + XCTAssertNoThrow(try Animal.throws.new("Tama", 3, true)) + XCTAssertThrowsError(try Animal.throws.new("Tama", -3, true)) { error in + XCTAssertTrue(error is JSException) + let errorObject = JSError(from: (error as! JSException).thrownValue) + XCTAssertNotNil(errorObject) + } + } + + 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) + } +} + +@_extern(c, "llvm.wasm.memory.grow.i32") +func growMemory(_ memory: Int32, _ pages: Int32) -> Int32 diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 53073a850..ab5723587 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,5 +1,7 @@ /** @type {import('./../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').Prelude["setupOptions"]} */ export function setupOptions(options, context) { + Error.stackTraceLimit = 100; + setupTestGlobals(globalThis); return { ...options, addToCoreImports(importObject) { @@ -10,3 +12,107 @@ export function setupOptions(options, context) { } } } + +function setupTestGlobals(global) { + global.globalObject1 = { + prop_1: { + nested_prop: 1, + }, + prop_2: 2, + prop_3: true, + prop_4: [3, 4, "str_elm_1", null, undefined, 5], + 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; + } + }, + }, + prop_6: { + call_host_1: () => { + return global.globalObject1.prop_6.host_func_1(); + }, + }, + prop_7: 3.14, + prop_8: [0, , 2, 3, , , 6], + prop_9: { + func1: function () { + throw new Error(); + }, + func2: function () { + throw "String Error"; + }, + func3: function () { + throw 3.0; + }, + }, + eval_closure: function (fn) { + return fn(arguments[1]); + }, + observable_obj: { + set_called: false, + target: new Proxy( + { + nested: {}, + }, + { + set(target, key, value) { + global.globalObject1.observable_obj.set_called = true; + target[key] = value; + return true; + }, + } + ), + }, + }; + + global.Animal = function (name, age, isCat) { + if (age < 0) { + throw new Error("Invalid age " + age); + } + this.name = name; + this.age = age; + this.bark = () => { + return isCat ? "nyan" : "wan"; + }; + this.isCat = isCat; + this.getIsCat = function () { + return this.isCat; + }; + this.setName = function (name) { + this.name = name; + }; + }; + + global.callThrowingClosure = (c) => { + try { + c(); + } catch (error) { + return error; + } + }; + + global.objectDecodingTest = { + obj: {}, + fn: () => { }, + sym: Symbol("s"), + bi: BigInt(3) + }; +}