Skip to content

Commit c971909

Browse files
authored
Add a blocking resources loading for web tests (#5248)
For compose resource tests it's acceptable to use blocking http requests. This way we can run the tests right now, without waiting for https://android-review.googlesource.com/c/platform/frameworks/support/+/3509670 to land on our fork. <!-- Optional --> Fixes https://youtrack.jetbrains.com/issue/CMP-7711/Add-blocking-compose-resources-loader-for-web-tests ## Release Notes N/A
1 parent a2e7825 commit c971909

File tree

9 files changed

+134
-6
lines changed

9 files changed

+134
-6
lines changed

components/resources/library/build.gradle.kts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import kotlinx.validation.ExperimentalBCVApi
22
import org.jetbrains.compose.ExperimentalComposeLibrary
3-
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
43

54
plugins {
65
kotlin("multiplatform")
@@ -31,8 +30,13 @@ kotlin {
3130
})
3231
}
3332
}
34-
@OptIn(ExperimentalWasmDsl::class)
33+
34+
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
3535
wasmJs {
36+
compilations.getByName("test").compileTaskProvider.configure {
37+
// https://youtrack.jetbrains.com/issue/KT-69014
38+
compilerOptions.freeCompilerArgs.add("-Xwasm-enable-array-range-checks")
39+
}
3640
browser {
3741
testTask(Action {
3842
useKarma {

components/resources/library/karma.config.d/wasm/config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ config.proxies["/"] = path.resolve(basePath, "kotlin");
1919
config.files = [
2020
{pattern: path.resolve(generatedAssetsPath, "**/*"), included: false, served: true, watched: false},
2121
{pattern: path.resolve(basePath, "kotlin", "**/*.png"), included: false, served: true, watched: false},
22+
{pattern: path.resolve(basePath, "kotlin", "**/*.cvr"), included: false, served: true, watched: false},
23+
{pattern: path.resolve(basePath, "kotlin", "**/*.otf"), included: false, served: true, watched: false},
2224
{pattern: path.resolve(basePath, "kotlin", "**/*.gif"), included: false, served: true, watched: false},
2325
{pattern: path.resolve(basePath, "kotlin", "**/*.ttf"), included: false, served: true, watched: false},
2426
{pattern: path.resolve(basePath, "kotlin", "**/*.txt"), included: false, served: true, watched: false},
2527
{pattern: path.resolve(basePath, "kotlin", "**/*.json"), included: false, served: true, watched: false},
2628
{pattern: path.resolve(basePath, "kotlin", "**/*.xml"), included: false, served: true, watched: false},
29+
path.resolve(basePath, "kotlin", "test_setup.js"),
2730
].concat(config.files);
2831

2932
function KarmaWebpackOutputFramework(config) {

components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/StringFormatTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.jetbrains.compose.resources
22

3+
import org.jetbrains.compose.resources.internal.IgnoreWasmTest
34
import kotlin.test.Test
45
import kotlin.test.assertEquals
56
import kotlin.test.assertFailsWith
@@ -147,6 +148,7 @@ class StringFormatTest {
147148
}
148149

149150
@Test
151+
@IgnoreWasmTest // https://youtrack.jetbrains.com/issue/KT-69014, wasm throws RuntimeError instead of IndexOutOfBounds
150152
fun `replaceWithArgs throws exception for unmatched placeholders`() {
151153
val template = "Hello %1\$s, your rank is %3\$s"
152154
val args = listOf("Alice", "1")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.jetbrains.compose.resources.internal
2+
3+
@OptIn(ExperimentalMultiplatform::class)
4+
@OptionalExpectation
5+
expect annotation class IgnoreWasmTest()

components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import kotlinx.browser.window
44
import kotlinx.coroutines.await
55
import org.khronos.webgl.ArrayBuffer
66
import org.khronos.webgl.Int8Array
7+
import org.khronos.webgl.Uint8Array
78
import org.w3c.files.Blob
9+
import org.w3c.xhr.XMLHttpRequest
810
import kotlin.js.Promise
911

10-
internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader {
12+
internal actual fun getPlatformResourceReader(): ResourceReader {
13+
if (isInTestEnvironment()) return TestJsResourceReader
14+
return DefaultJsResourceReader
15+
}
16+
17+
private val DefaultJsResourceReader = object : ResourceReader {
1118
override suspend fun read(path: String): ByteArray {
1219
return readAsBlob(path).asByteArray()
1320
}
@@ -36,4 +43,41 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
3643
val buffer = asDynamic().arrayBuffer() as Promise<ArrayBuffer>
3744
return Int8Array(buffer.await()).unsafeCast<ByteArray>()
3845
}
39-
}
46+
}
47+
48+
// It uses a synchronous XmlHttpRequest (blocking!!!)
49+
private val TestJsResourceReader by lazy {
50+
object : ResourceReader {
51+
override suspend fun read(path: String): ByteArray {
52+
return readByteArray(path)
53+
}
54+
55+
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
56+
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
57+
}
58+
59+
override fun getUri(path: String): String {
60+
val location = window.location
61+
return getResourceUrl(location.origin, location.pathname, path)
62+
}
63+
64+
private fun readByteArray(path: String): ByteArray {
65+
val resPath = WebResourcesConfiguration.getResourcePath(path)
66+
val request = XMLHttpRequest()
67+
request.open("GET", resPath, false)
68+
request.overrideMimeType("text/plain; charset=x-user-defined")
69+
request.send()
70+
if (request.status == 200.toShort()) {
71+
// For blocking XmlHttpRequest the response can be only in text form, so we convert it to bytes manually
72+
val text = request.responseText
73+
val bytes = Uint8Array(text.length)
74+
js("for (var i = 0; i < text.length; i++) { bytes[i] = text.charCodeAt(i) & 0xFF; }")
75+
return bytes.unsafeCast<ByteArray>()
76+
}
77+
throw MissingResourceException("$resPath")
78+
}
79+
}
80+
}
81+
82+
private fun isInTestEnvironment(): Boolean =
83+
js("window.composeResourcesTesting == true")

components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import org.khronos.webgl.ArrayBuffer
66
import org.khronos.webgl.Int8Array
77
import org.w3c.fetch.Response
88
import org.w3c.files.Blob
9+
import org.w3c.xhr.XMLHttpRequest
910
import kotlin.js.Promise
1011
import kotlin.wasm.unsafe.UnsafeWasmMemoryApi
1112
import kotlin.wasm.unsafe.withScopedMemoryAllocator
@@ -22,7 +23,12 @@ private external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr:
2223
@JsFun("(blob) => blob.arrayBuffer()")
2324
private external fun jsExportBlobAsArrayBuffer(blob: Blob): Promise<ArrayBuffer>
2425

25-
internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader {
26+
internal actual fun getPlatformResourceReader(): ResourceReader {
27+
if (isInTestEnvironment()) return TestWasmResourceReader
28+
return DefaultWasmResourceReader
29+
}
30+
31+
private val DefaultWasmResourceReader = object : ResourceReader {
2632
override suspend fun read(path: String): ByteArray {
2733
return readAsBlob(path).asByteArray()
2834
}
@@ -63,4 +69,62 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
6369
ByteArray(size) { i -> (memBuffer + i).loadByte() }
6470
}
6571
}
66-
}
72+
}
73+
74+
// It uses a synchronous XmlHttpRequest (blocking!!!)
75+
private val TestWasmResourceReader by lazy {
76+
object : ResourceReader {
77+
override suspend fun read(path: String): ByteArray {
78+
return readByteArray(path)
79+
}
80+
81+
override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray {
82+
return readByteArray(path).sliceArray(offset.toInt() until (offset + size).toInt())
83+
}
84+
85+
override fun getUri(path: String): String {
86+
val location = window.location
87+
return getResourceUrl(location.origin, location.pathname, path)
88+
}
89+
90+
private fun readByteArray(path: String): ByteArray {
91+
val resPath = WebResourcesConfiguration.getResourcePath(path)
92+
val request = XMLHttpRequest()
93+
request.open("GET", resPath, false)
94+
request.overrideMimeType("text/plain; charset=x-user-defined")
95+
request.send()
96+
if (request.status == 200.toShort()) {
97+
return requestResponseAsByteArray(request).asByteArray()
98+
}
99+
println("Request status is not 200 - $resPath, status: ${request.status}")
100+
throw MissingResourceException("$resPath")
101+
}
102+
103+
private fun Int8Array.asByteArray(): ByteArray {
104+
val array = this
105+
val size = array.length
106+
107+
@OptIn(UnsafeWasmMemoryApi::class)
108+
return withScopedMemoryAllocator { allocator ->
109+
val memBuffer = allocator.allocate(size)
110+
val dstAddress = memBuffer.address.toInt()
111+
jsExportInt8ArrayToWasm(array, size, dstAddress)
112+
ByteArray(size) { i -> (memBuffer + i).loadByte() }
113+
}
114+
}
115+
}
116+
}
117+
118+
// For blocking XmlHttpRequest the response can be only in text form, so we convert it to bytes manually
119+
private fun requestResponseAsByteArray(req: XMLHttpRequest): Int8Array =
120+
js(""" {
121+
var text = req.responseText;
122+
var int8Arr = new Int8Array(text.length);
123+
for (var i = 0; i < text.length; i++) {
124+
int8Arr[i] = text.charCodeAt(i) & 0xFF;
125+
}
126+
return int8Arr;
127+
}""")
128+
129+
private fun isInTestEnvironment(): Boolean =
130+
js("window.composeResourcesTesting == true")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.jetbrains.compose.resources.internal
2+
3+
actual typealias IgnoreWasmTest = kotlin.test.Ignore
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Setting this will make the tests use a blocking XMLHttpRequest instead of the asynchronous 'fetch'
2+
window.composeResourcesTesting = true;

components/test.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ set -euo pipefail # Fail fast
55
./gradlew :resources:library:desktopTest
66
./gradlew :resources:library:pixel5DebugAndroidTest
77
./gradlew :resources:library:iosSimulatorArm64Test
8+
./gradlew :resources:library:wasmJsBrowserTest

0 commit comments

Comments
 (0)