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( _ 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, 150) // Test shorter sleep duration let shortSleepDiff = try await measureTime { try await Task.sleep(nanoseconds: 100_000_000) } XCTAssertGreaterThanOrEqual(shortSleepDiff, 50) } 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(String(result.number!)) } let thenDiff = try await measureTime { let result = try await promise2.value XCTAssertEqual(result, .string("3.0")) } XCTAssertGreaterThanOrEqual(thenDiff, 150) } 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 { _ -> JSValue in fatalError("Should not be called") } 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, 150) } // 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) 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, 150) } // 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, 50) // Test SuspendingClock.sleep let suspendingClockDiff = try await measureTime { let c = SuspendingClock() try await c.sleep(until: .now + .milliseconds(100)) } XCTAssertGreaterThanOrEqual(suspendingClockDiff, 50) } #endif }