Skip to content

Commit 361381e

Browse files
committed
Verify snapshot
1 parent b5a6995 commit 361381e

File tree

4 files changed

+113
-8
lines changed

4 files changed

+113
-8
lines changed

redwood-dom-testing/src/commonMain/kotlin/app/cash/redwood/dom/testing/DomSnapshotter.kt

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@
1515
*/
1616
package app.cash.redwood.dom.testing
1717

18+
import kotlin.coroutines.resume
19+
import kotlin.coroutines.resumeWithException
20+
import kotlinx.browser.document
1821
import kotlinx.coroutines.await
22+
import kotlinx.coroutines.suspendCancellableCoroutine
23+
import org.khronos.webgl.get
24+
import org.w3c.dom.CanvasRenderingContext2D
1925
import org.w3c.dom.Element
26+
import org.w3c.dom.HTMLCanvasElement
27+
import org.w3c.dom.HTMLImageElement
28+
import org.w3c.dom.url.URL
29+
import org.w3c.files.Blob
2030

2131
public class DomSnapshotter @PublishedApi internal constructor(
2232
private val path: String,
@@ -40,9 +50,76 @@ public class DomSnapshotter @PublishedApi internal constructor(
4050
},
4151
).await()
4252

43-
snapshotStore.put("$path/${name ?: "snapshot"}.png", image)
53+
54+
val fileName = "$path/${name ?: "snapshot"}.png"
55+
56+
snapshotStore.getBlob(fileName)?.let { existing ->
57+
check(existing.contentEquals(image)) {
58+
"Current snapshot does not match the existing file $fileName"
59+
}
60+
} ?: snapshotStore.put(fileName, image)
61+
}
62+
63+
private suspend fun Blob.contentEquals(other: Blob): Boolean {
64+
if (this.size != other.size) return false
65+
66+
val url1 = URL.createObjectURL(this)
67+
val url2 = URL.createObjectURL(other)
68+
69+
try {
70+
val img1 = loadImage(url1)
71+
val img2 = loadImage(url2)
72+
73+
if (img1.width != img2.width || img1.height != img2.height) {
74+
return false
75+
}
76+
77+
val canvas = document.createElement("canvas") as HTMLCanvasElement
78+
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D
79+
80+
canvas.width = img1.width
81+
canvas.height = img1.height
82+
83+
// Get data for first image
84+
ctx.drawImage(img1, 0.0, 0.0)
85+
val data1 = ctx.getImageData(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
86+
87+
// Get data for second image
88+
ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
89+
ctx.drawImage(img2, 0.0, 0.0)
90+
val data2 = ctx.getImageData(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
91+
92+
// Compare pixel by pixel
93+
val pixels1 = data1.data
94+
val pixels2 = data2.data
95+
for (i in 0 until pixels1.length) {
96+
if (pixels1[i] != pixels2[i]) {
97+
return false
98+
}
99+
}
100+
101+
return true
102+
} finally {
103+
URL.revokeObjectURL(url1)
104+
URL.revokeObjectURL(url2)
105+
}
44106
}
45107

108+
private suspend fun loadImage(url: String): HTMLImageElement =
109+
suspendCancellableCoroutine { continuation ->
110+
val img = document.createElement("img") as HTMLImageElement
111+
112+
img.onload = { _ -> continuation.resume(img) }
113+
img.onerror = { _: dynamic, _: String, _: Int, _: Int, _: Any? ->
114+
continuation.resumeWithException(Exception("Failed to load image"))
115+
}
116+
img.src = url
117+
118+
continuation.invokeOnCancellation {
119+
img.src = ""
120+
}
121+
}
122+
46123
public companion object Companion {
47124
public inline operator fun invoke(): DomSnapshotter {
48125
return DomSnapshotter("PlaceholderTestName")

redwood-dom-testing/src/commonMain/kotlin/app/cash/redwood/dom/testing/SnapshotStore.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import kotlinx.coroutines.await
2121
import okio.ByteString
2222
import okio.ByteString.Companion.toByteString
2323
import org.w3c.fetch.RequestInit
24+
import org.w3c.fetch.Response
2425
import org.w3c.files.Blob
2526

2627
internal class SnapshotStore {
@@ -51,7 +52,17 @@ internal class SnapshotStore {
5152
}
5253
}
5354

54-
suspend fun get(fileName: String): ByteString? {
55+
suspend fun getBlob(fileName: String): Blob? {
56+
return getInternal(fileName)?.blob()?.await()
57+
}
58+
59+
suspend fun getByteString(fileName: String): ByteString? {
60+
val response = getInternal(fileName) ?: return null
61+
val bytes: Promise<ByteArray> = response.asDynamic().bytes()
62+
return bytes.await().toByteString()
63+
}
64+
65+
private suspend fun getInternal(fileName: String): Response? {
5566
val response = window.fetch(
5667
input = "/snapshots/$fileName",
5768
).await()
@@ -68,9 +79,7 @@ internal class SnapshotStore {
6879
""".trimMargin(),
6980
)
7081
}
71-
72-
val bytes: Promise<ByteArray> = response.asDynamic().bytes()
73-
return bytes.await().toByteString()
82+
return response
7483
}
7584
}
7685

redwood-dom-testing/src/commonTest/kotlin/app/cash/redwood/dom/testing/DomSnapshotterSampleTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
package app.cash.redwood.dom.testing
1919

2020
import kotlin.test.Test
21+
import kotlin.test.assertFailsWith
2122
import kotlinx.browser.document
2223
import kotlinx.coroutines.test.runTest
2324
import kotlinx.dom.appendElement
2425
import kotlinx.dom.appendText
26+
import kotlinx.dom.clear
2527

2628
/**
2729
* This isn't a proper unit test for [DomSnapshotter], it's just a sample.
@@ -38,4 +40,21 @@ internal class DomSnapshotterSampleTest {
3840
}
3941
snapshotter.snapshot(element, "helloIAmTheSnapshotTest")
4042
}
43+
44+
@Test
45+
fun mismatchedSnapshot() = runTest {
46+
val element = document.documentElement!!
47+
element.appendElement("h1") {
48+
appendText("hello world")
49+
}
50+
snapshotter.snapshot(element, "mismatchedSnapshotTest")
51+
52+
element.clear()
53+
element.appendElement("h2") {
54+
appendText("hello world")
55+
}
56+
assertFailsWith<IllegalStateException> {
57+
snapshotter.snapshot(element, "mismatchedSnapshotTest")
58+
}
59+
}
4160
}

redwood-dom-testing/src/commonTest/kotlin/app/cash/redwood/dom/testing/SnapshotStoreTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@ internal class SnapshotStoreTest {
3030
val store = SnapshotStore()
3131
val data = "Hello World!".encodeUtf8()
3232
store.put("greeting.txt", data)
33-
assertThat(store.get("greeting.txt")).isEqualTo(data)
33+
assertThat(store.getByteString("greeting.txt")).isEqualTo(data)
3434
}
3535

3636
@Test
3737
fun putAndGetFileWithPathHierarchy() = runTest {
3838
val store = SnapshotStore()
3939
val data = "Ahoy, Matey!".encodeUtf8()
4040
store.put("greetings/pirate/greeting.txt", data)
41-
assertThat(store.get("greetings/pirate/greeting.txt")).isEqualTo(data)
41+
assertThat(store.getByteString("greetings/pirate/greeting.txt")).isEqualTo(data)
4242
}
4343

4444
@Test
4545
fun getDirectoryTraversalReturnsNoData() = runTest {
4646
val store = SnapshotStore()
47-
assertThat(store.get("../README.md")).isNull() // 404 Not Found.
47+
assertThat(store.getByteString("../README.md")).isNull() // 404 Not Found.
4848
}
4949

5050
@Test

0 commit comments

Comments
 (0)