diff --git a/README.md b/README.md index b0a66d8597..a9155aa4ec 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,9 @@ suspend fun main() = coroutineScope { * [Dispatchers.IO] dispatcher for blocking coroutines; * [Executor.asCoroutineDispatcher][asCoroutineDispatcher] extension, custom thread pools, and more; * Integrations with `CompletableFuture` and JVM-specific extensions. +* [core/web](kotlinx-coroutines-core/web/) — additional core features available on Kotlin/JS and Kotlin/Wasm/JS: + * Integration with `Promise` via `Promise.await` and [promise] builder. * [core/js](kotlinx-coroutines-core/js/) — additional core features available on Kotlin/JS: - * Integration with `Promise` via [Promise.await] and [promise] builder; * Integration with `Window` via [Window.asCoroutineDispatcher], etc. * [test](kotlinx-coroutines-test/README.md) — test utilities for coroutines: * [Dispatchers.setMain] to override [Dispatchers.Main] in tests; @@ -213,8 +214,7 @@ See [Contributing Guidelines](CONTRIBUTING.md). [CoroutineExceptionHandler]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-exception-handler/index.html [Dispatchers.IO]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-i-o.html [asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html -[Promise.await]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/await.html -[promise]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/[js]promise.html +[promise]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/promise.html [Window.asCoroutineDispatcher]: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-coroutine-dispatcher.html diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api index 5fc0761121..809bacb864 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.klib.api @@ -1262,16 +1262,16 @@ final suspend fun (org.w3c.dom/Window).kotlinx.coroutines/awaitAnimationFrame(): final suspend fun <#A: kotlin/Any?> (kotlin.js/Promise<#A>).kotlinx.coroutines/await(): #A // kotlinx.coroutines/await|await@kotlin.js.Promise<0:0>(){0§}[0] // Targets: [wasmJs] -final fun <#A: kotlin/Any?> (kotlin.js/Promise).kotlinx.coroutines/asDeferred(): kotlinx.coroutines/Deferred<#A> // kotlinx.coroutines/asDeferred|asDeferred@kotlin.js.Promise(){0§}[0] +final fun <#A: kotlin.js/JsAny?> (kotlin.js/Promise<#A>).kotlinx.coroutines/asDeferred(): kotlinx.coroutines/Deferred<#A> // kotlinx.coroutines/asDeferred|asDeferred@kotlin.js.Promise<0:0>(){0§}[0] // Targets: [wasmJs] -final fun <#A: kotlin/Any?> (kotlinx.coroutines/CoroutineScope).kotlinx.coroutines/promise(kotlin.coroutines/CoroutineContext = ..., kotlinx.coroutines/CoroutineStart = ..., kotlin.coroutines/SuspendFunction1): kotlin.js/Promise // kotlinx.coroutines/promise|promise@kotlinx.coroutines.CoroutineScope(kotlin.coroutines.CoroutineContext;kotlinx.coroutines.CoroutineStart;kotlin.coroutines.SuspendFunction1){0§}[0] +final fun <#A: kotlin.js/JsAny?> (kotlinx.coroutines/CoroutineScope).kotlinx.coroutines/promise(kotlin.coroutines/CoroutineContext = ..., kotlinx.coroutines/CoroutineStart = ..., kotlin.coroutines/SuspendFunction1): kotlin.js/Promise<#A> // kotlinx.coroutines/promise|promise@kotlinx.coroutines.CoroutineScope(kotlin.coroutines.CoroutineContext;kotlinx.coroutines.CoroutineStart;kotlin.coroutines.SuspendFunction1){0§}[0] // Targets: [wasmJs] -final fun <#A: kotlin/Any?> (kotlinx.coroutines/Deferred<#A>).kotlinx.coroutines/asPromise(): kotlin.js/Promise // kotlinx.coroutines/asPromise|asPromise@kotlinx.coroutines.Deferred<0:0>(){0§}[0] +final fun <#A: kotlin.js/JsAny?> (kotlinx.coroutines/Deferred<#A>).kotlinx.coroutines/asPromise(): kotlin.js/Promise<#A> // kotlinx.coroutines/asPromise|asPromise@kotlinx.coroutines.Deferred<0:0>(){0§}[0] // Targets: [wasmJs] -final suspend fun <#A: kotlin/Any?> (kotlin.js/Promise).kotlinx.coroutines/await(): #A // kotlinx.coroutines/await|await@kotlin.js.Promise(){0§}[0] +final suspend fun <#A: kotlin.js/JsAny?> (kotlin.js/Promise<#A>).kotlinx.coroutines/await(): #A // kotlinx.coroutines/await|await@kotlin.js.Promise<0:0>(){0§}[0] // Targets: [wasmWasi] final fun kotlinx.coroutines.internal/runTestCoroutine(kotlin.coroutines/CoroutineContext, kotlin.coroutines/SuspendFunction1) // kotlinx.coroutines.internal/runTestCoroutine|runTestCoroutine(kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] diff --git a/kotlinx-coroutines-core/js/src/Promise.js.kt b/kotlinx-coroutines-core/js/src/Promise.js.kt new file mode 100644 index 0000000000..f686951c1e --- /dev/null +++ b/kotlinx-coroutines-core/js/src/Promise.js.kt @@ -0,0 +1,8 @@ +package kotlinx.coroutines + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual fun JsPromiseError.toThrowable(): Throwable = this as? Throwable ?: + Exception("Promise resolved with a non-Throwable exception '$this' (type ${this::class})") + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual fun Throwable.toJsPromiseError(): JsPromiseError = this diff --git a/kotlinx-coroutines-core/js/src/Promise.kt b/kotlinx-coroutines-core/js/src/Promise.kt deleted file mode 100644 index 5eb93d348e..0000000000 --- a/kotlinx-coroutines-core/js/src/Promise.kt +++ /dev/null @@ -1,67 +0,0 @@ -package kotlinx.coroutines - -import kotlin.coroutines.* -import kotlin.js.* - -/** - * Starts new coroutine and returns its result as an implementation of [Promise]. - * - * Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument. - * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used. - * The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden - * with corresponding [context] element. - * - * By default, the coroutine is immediately scheduled for execution. - * Other options can be specified via `start` parameter. See [CoroutineStart] for details. - * - * @param context additional to [CoroutineScope.coroutineContext] context of the coroutine. - * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. - * @param block the coroutine code. - */ -public fun CoroutineScope.promise( - context: CoroutineContext = EmptyCoroutineContext, - start: CoroutineStart = CoroutineStart.DEFAULT, - block: suspend CoroutineScope.() -> T -): Promise = - async(context, start, block).asPromise() - -/** - * Converts this deferred value to the instance of [Promise]. - */ -public fun Deferred.asPromise(): Promise { - val promise = Promise { resolve, reject -> - invokeOnCompletion { - val e = getCompletionExceptionOrNull() - if (e != null) { - reject(e) - } else { - resolve(getCompleted()) - } - } - } - promise.asDynamic().deferred = this - return promise -} - -/** - * Converts this promise value to the instance of [Deferred]. - */ -public fun Promise.asDeferred(): Deferred { - val deferred = asDynamic().deferred - @Suppress("UnsafeCastFromDynamic") - return deferred ?: GlobalScope.async(start = CoroutineStart.UNDISPATCHED) { await() } -} - -/** - * Awaits for completion of the promise without blocking. - * - * This suspending function is cancellable: if the [Job] of the current coroutine is cancelled while this - * suspending function is waiting on the promise, this function immediately resumes with [CancellationException]. - * There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled - * while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details. - */ -public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> - this@await.then( - onFulfilled = { cont.resume(it) }, - onRejected = { cont.resumeWithException(it as? Throwable ?: Exception("Non-Kotlin exception $it")) }) -} diff --git a/kotlinx-coroutines-core/wasmJs/src/Promise.wasm.kt b/kotlinx-coroutines-core/wasmJs/src/Promise.wasm.kt new file mode 100644 index 0000000000..c96a905ea2 --- /dev/null +++ b/kotlinx-coroutines-core/wasmJs/src/Promise.wasm.kt @@ -0,0 +1,11 @@ +package kotlinx.coroutines + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual fun JsPromiseError.toThrowable(): Throwable = try { + unsafeCast>().get() +} catch (_: Throwable) { + Exception("Non-Kotlin exception $this of type '${this::class}'") +} + +@OptIn(ExperimentalWasmJsInterop::class) +internal actual fun Throwable.toJsPromiseError(): JsPromiseError = toJsReference() diff --git a/kotlinx-coroutines-core/wasmJs/src/Promise.kt b/kotlinx-coroutines-core/web/src/Promise.kt similarity index 69% rename from kotlinx-coroutines-core/wasmJs/src/Promise.kt rename to kotlinx-coroutines-core/web/src/Promise.kt index 5f7dea2760..dd9b4dce38 100644 --- a/kotlinx-coroutines-core/wasmJs/src/Promise.kt +++ b/kotlinx-coroutines-core/web/src/Promise.kt @@ -12,10 +12,9 @@ internal fun promiseSetDeferred(promise: Promise, deferred: JsAny): Unit @Suppress("UNUSED_PARAMETER") internal fun promiseGetDeferred(promise: Promise): JsAny? = js("""{ console.assert(promise instanceof Promise, "promiseGetDeferred must receive a promise, but got ", promise); - return promise.deferred == null ? null : promise.deferred; + return promise.deferred == null ? null : promise.deferred; }""") - /** * Starts new coroutine and returns its result as an implementation of [Promise]. * @@ -31,26 +30,26 @@ internal fun promiseGetDeferred(promise: Promise): JsAny? = js("""{ * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. * @param block the coroutine code. */ -@ExperimentalWasmJsInterop -public fun CoroutineScope.promise( +@OptIn(ExperimentalWasmJsInterop::class) +public fun CoroutineScope.promise( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T -): Promise = +): Promise = async(context, start, block).asPromise() /** - * Converts this deferred value to the instance of [Promise]. + * Converts this deferred value to the instance of [Promise]. */ -@ExperimentalWasmJsInterop -public fun Deferred.asPromise(): Promise { - val promise = Promise { resolve, reject -> +@OptIn(ExperimentalWasmJsInterop::class) +public fun Deferred.asPromise(): Promise { + val promise = Promise { resolve, reject -> invokeOnCompletion { val e = getCompletionExceptionOrNull() if (e != null) { - reject(e.toJsReference()) + reject(e.toJsPromiseError()) } else { - resolve(getCompleted()?.toJsReference()) + resolve(getCompleted()) } } } @@ -61,9 +60,9 @@ public fun Deferred.asPromise(): Promise { /** * Converts this promise value to the instance of [Deferred]. */ -@ExperimentalWasmJsInterop -@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE", "UNCHECKED_CAST") -public fun Promise.asDeferred(): Deferred { +@OptIn(ExperimentalWasmJsInterop::class) +public fun Promise.asDeferred(): Deferred { + @Suppress("UNCHECKED_CAST", "UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") val deferred = promiseGetDeferred(this) as? JsReference> return deferred?.get() ?: GlobalScope.async(start = CoroutineStart.UNDISPATCHED) { await() } } @@ -76,11 +75,15 @@ public fun Promise.asDeferred(): Deferred { * There is a **prompt cancellation guarantee**: even if this function is ready to return the result, but was cancelled * while suspended, [CancellationException] will be thrown. See [suspendCancellableCoroutine] for low-level details. */ -@ExperimentalWasmJsInterop -@Suppress("UNCHECKED_CAST") -public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> +@OptIn(ExperimentalWasmJsInterop::class) +public suspend fun Promise.await(): T = suspendCancellableCoroutine { cont: CancellableContinuation -> this@await.then( - onFulfilled = { cont.resume(it as T); null }, - onRejected = { cont.resumeWithException(it.toThrowableOrNull() ?: error("Unexpected non-Kotlin exception $it")); null } - ) + onFulfilled = { cont.resume(it); null }, + onRejected = { cont.resumeWithException(it.toThrowable()); null }) } + +@OptIn(ExperimentalWasmJsInterop::class) +internal expect fun JsPromiseError.toThrowable(): Throwable + +@OptIn(ExperimentalWasmJsInterop::class) +internal expect fun Throwable.toJsPromiseError(): JsPromiseError diff --git a/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt b/kotlinx-coroutines-core/web/test/PromiseTest.kt similarity index 61% rename from kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt rename to kotlinx-coroutines-core/web/test/PromiseTest.kt index e72e661517..27b0a20552 100644 --- a/kotlinx-coroutines-core/wasmJs/test/PromiseTest.kt +++ b/kotlinx-coroutines-core/web/test/PromiseTest.kt @@ -4,25 +4,28 @@ import kotlinx.coroutines.testing.* import kotlin.js.* import kotlin.test.* -class PromiseTest : TestBase() { +@OptIn(ExperimentalWasmJsInterop::class) +class PromiseTestWeb : TestBase() { @Test fun testPromiseResolvedAsDeferred() = GlobalScope.promise { - val promise = Promise> { resolve, _ -> + val promise = Promise { resolve, _ -> resolve("OK".toJsReference()) } - val deferred = promise.asDeferred>() + val deferred = promise.asDeferred() assertEquals("OK", deferred.await().get()) + null } @Test fun testPromiseRejectedAsDeferred() = GlobalScope.promise { - lateinit var promiseReject: (JsAny) -> Unit + lateinit var promiseReject: (JsPromiseError) -> Unit val promise = Promise { _, reject -> promiseReject = reject } - val deferred = promise.asDeferred>() + val deferred = promise.asDeferred() // reject after converting to deferred to avoid "Unhandled promise rejection" warnings - promiseReject(TestException("Rejected").toJsReference()) + @Suppress("CAST_NEVER_SUCCEEDS") + promiseReject(TestException("Rejected").toJsReference() as JsPromiseError) try { deferred.await() expectUnreached() @@ -30,26 +33,29 @@ class PromiseTest : TestBase() { assertIs(e) assertEquals("Rejected", e.message) } + null } @Test fun testCompletedDeferredAsPromise() = GlobalScope.promise { val deferred = async(start = CoroutineStart.UNDISPATCHED) { // completed right away - "OK" + "OK".toJsReference() } val promise = deferred.asPromise() - assertEquals("OK", promise.await()) + assertEquals("OK", promise.await().get()) + null } @Test fun testWaitForDeferredAsPromise() = GlobalScope.promise { val deferred = async { // will complete later - "OK" + "OK".toJsReference() } val promise = deferred.asPromise() - assertEquals("OK", promise.await()) // await yields main thread to deferred coroutine + assertEquals("OK", promise.await().get()) // await yields main thread to deferred coroutine + null } @Test @@ -61,50 +67,44 @@ class PromiseTest : TestBase() { } job.cancel() // cancel the job r("fail".toJsString()) // too late, the waiting job was already cancelled + null } @Test fun testAsPromiseAsDeferred() = GlobalScope.promise { - val deferred = async { "OK" } + val deferred = async { "OK".toJsString() } val promise = deferred.asPromise() - val d2 = promise.asDeferred() + val d2 = promise.asDeferred() assertSame(d2, deferred) - assertEquals("OK", d2.await()) - } - - @Test - fun testLeverageTestResult(): TestResult { - // Cannot use expect(..) here - var seq = 0 - val result = runTest { - ++seq - } - return result.then { - if (seq != 1) error("Unexpected result: $seq") - null - } + assertEquals("OK", d2.await().toString()) + null } @Test fun testAwaitPromiseRejectedWithNonKotlinException() = GlobalScope.promise { - lateinit var r: (JsAny) -> Unit - val toAwait = Promise { _, reject -> r = reject } + val toAwait = jsPromiseRejectedWithString() val throwable = async(start = CoroutineStart.UNDISPATCHED) { - assertFails { toAwait.await() } + assertFails { toAwait.await() } } - r("Rejected".toJsString()) - assertIs(throwable.await()) + val throwableResolved = throwable.await() + assertEquals(true, throwableResolved.message?.contains("Rejected"), "${throwableResolved.message}") + null } @Test fun testAwaitPromiseRejectedWithKotlinException() = GlobalScope.promise { - lateinit var r: (JsAny) -> Unit + lateinit var r: (JsPromiseError) -> Unit val toAwait = Promise { _, reject -> r = reject } val throwable = async(start = CoroutineStart.UNDISPATCHED) { assertFails { toAwait.await() } } - r(RuntimeException("Rejected").toJsReference()) - assertIs(throwable.await()) + @Suppress("CAST_NEVER_SUCCEEDS") + r(RuntimeException("Rejected").toJsReference() as JsPromiseError) + assertIs(throwable.await()) assertEquals("Rejected", throwable.await().message) + null } } + +@OptIn(ExperimentalWasmJsInterop::class) +private fun jsPromiseRejectedWithString(): Promise = js("Promise.reject(\"Rejected\")") \ No newline at end of file diff --git a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.klib.api b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.klib.api index 8c4a13a274..b045f6ea4c 100644 --- a/kotlinx-coroutines-test/api/kotlinx-coroutines-test.klib.api +++ b/kotlinx-coroutines-test/api/kotlinx-coroutines-test.klib.api @@ -101,6 +101,6 @@ final class kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting { // k final class kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting : kotlin.js/JsAny { // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting|null[0] constructor () // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.|(){}[0] - final fun then(kotlin/Function1): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1){}[0] - final fun then(kotlin/Function1, kotlin/Function1): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1;kotlin.Function1){}[0] + final fun then(kotlin/Function1): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1){}[0] + final fun then(kotlin/Function1, kotlin/Function1): kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting // kotlinx.coroutines.test.internal/JsPromiseInterfaceForTesting.then|then(kotlin.Function1;kotlin.Function1){}[0] } diff --git a/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt b/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt index c39425360c..5f5c2c73f2 100644 --- a/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt +++ b/kotlinx-coroutines-test/wasmJs/src/TestBuilders.kt @@ -10,6 +10,7 @@ public actual typealias TestResult = JsPromiseInterfaceForTesting internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit): TestResult = GlobalScope.promise { testProcedure() + Unit.toJsReference() }.unsafeCast() internal actual fun dumpCoroutines() { } diff --git a/kotlinx-coroutines-test/wasmJs/src/internal/JsPromiseInterfaceForTesting.kt b/kotlinx-coroutines-test/wasmJs/src/internal/JsPromiseInterfaceForTesting.kt index a7da26c9c1..1458e0d623 100644 --- a/kotlinx-coroutines-test/wasmJs/src/internal/JsPromiseInterfaceForTesting.kt +++ b/kotlinx-coroutines-test/wasmJs/src/internal/JsPromiseInterfaceForTesting.kt @@ -1,7 +1,7 @@ package kotlinx.coroutines.test.internal -/* This is a declaration of JS's `Promise`. We need to keep it a separate class, because -`actual typealias TestResult = Promise` fails: you can't instantiate an `expect class` with a typealias to +/* This is a declaration of JS's `Promise`. We need to keep it a separate class, because +`actual typealias TestResult = Promise` fails: you can't instantiate an `expect class` with a typealias to a parametric class. So, we make a non-parametric class just for this. */ /** * @suppress @@ -12,9 +12,9 @@ public external class JsPromiseInterfaceForTesting : JsAny { /** * @suppress */ - public fun then(onFulfilled: ((JsAny) -> Unit), onRejected: ((JsAny) -> Unit)): JsPromiseInterfaceForTesting + public fun then(onFulfilled: ((JsAny) -> JsAny), onRejected: ((JsAny) -> JsAny)): JsPromiseInterfaceForTesting /** * @suppress */ - public fun then(onFulfilled: ((JsAny) -> Unit)): JsPromiseInterfaceForTesting -} \ No newline at end of file + public fun then(onFulfilled: ((JsAny) -> JsAny)): JsPromiseInterfaceForTesting +} diff --git a/kotlinx-coroutines-test/wasmJs/test/Helpers.kt b/kotlinx-coroutines-test/wasmJs/test/Helpers.kt index a394c1f19f..1cdbb8acc2 100644 --- a/kotlinx-coroutines-test/wasmJs/test/Helpers.kt +++ b/kotlinx-coroutines-test/wasmJs/test/Helpers.kt @@ -1,11 +1,10 @@ package kotlinx.coroutines.test +@OptIn(ExperimentalWasmJsInterop::class) actual fun testResultChain(block: () -> TestResult, after: (Result) -> TestResult): TestResult = block().then( { after(Result.success(Unit)) - null }, { after(Result.failure(it.toThrowableOrNull() ?: Throwable("Unexpected non-Kotlin exception $it"))) - null }) diff --git a/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt b/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt index f55517154a..5a8be51336 100644 --- a/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt +++ b/kotlinx-coroutines-test/wasmJs/test/PromiseTest.kt @@ -4,15 +4,17 @@ import kotlinx.coroutines.* import kotlin.test.* class PromiseTest { + @OptIn(ExperimentalWasmJsInterop::class) @Test fun testCompletionFromPromise() = runTest { var promiseEntered = false val p = promise { delay(1) promiseEntered = true + null } delay(2) - p.await() + p.await() assertTrue(promiseEntered) } } \ No newline at end of file diff --git a/test-utils/wasmJs/src/TestBase.kt b/test-utils/wasmJs/src/TestBase.kt index 021dc5ee89..70bbd6a0c0 100644 --- a/test-utils/wasmJs/src/TestBase.kt +++ b/test-utils/wasmJs/src/TestBase.kt @@ -65,7 +65,10 @@ actual open class TestBase( if (lastTestPromise != null) { error("Attempt to run multiple asynchronous test within one @Test method") } - val result = GlobalScope.promise(block = block, context = CoroutineExceptionHandler { _, e -> + val result = GlobalScope.promise(block = { + block() + null + }, context = CoroutineExceptionHandler { _, e -> if (e is CancellationException) return@CoroutineExceptionHandler // are ignored exCount++ when {