Skip to content

Commit 6a1b191

Browse files
committed
Verify snapshot
1 parent b5a6995 commit 6a1b191

File tree

4 files changed

+112
-8
lines changed

4 files changed

+112
-8
lines changed

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

Lines changed: 77 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,75 @@ public class DomSnapshotter @PublishedApi internal constructor(
4050
},
4151
).await()
4252

43-
snapshotStore.put("$path/${name ?: "snapshot"}.png", image)
53+
val fileName = "$path/${name ?: "snapshot"}.png"
54+
55+
snapshotStore.getBlob(fileName)?.let { existing ->
56+
check(existing.contentEquals(image)) {
57+
"Current snapshot does not match the existing file $fileName"
58+
}
59+
} ?: snapshotStore.put(fileName, image)
4460
}
4561

62+
private suspend fun Blob.contentEquals(other: Blob): Boolean {
63+
if (this.size != other.size) return false
64+
65+
val url1 = URL.createObjectURL(this)
66+
val url2 = URL.createObjectURL(other)
67+
68+
try {
69+
val img1 = loadImage(url1)
70+
val img2 = loadImage(url2)
71+
72+
if (img1.width != img2.width || img1.height != img2.height) {
73+
return false
74+
}
75+
76+
val canvas = document.createElement("canvas") as HTMLCanvasElement
77+
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D
78+
79+
canvas.width = img1.width
80+
canvas.height = img1.height
81+
82+
// Get data for first image
83+
ctx.drawImage(img1, 0.0, 0.0)
84+
val data1 = ctx.getImageData(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
85+
86+
// Get data for second image
87+
ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
88+
ctx.drawImage(img2, 0.0, 0.0)
89+
val data2 = ctx.getImageData(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
90+
91+
// Compare pixel by pixel
92+
val pixels1 = data1.data
93+
val pixels2 = data2.data
94+
for (i in 0 until pixels1.length) {
95+
if (pixels1[i] != pixels2[i]) {
96+
return false
97+
}
98+
}
99+
100+
return true
101+
} finally {
102+
URL.revokeObjectURL(url1)
103+
URL.revokeObjectURL(url2)
104+
}
105+
}
106+
107+
private suspend fun loadImage(url: String): HTMLImageElement =
108+
suspendCancellableCoroutine { continuation ->
109+
val img = document.createElement("img") as HTMLImageElement
110+
111+
img.onload = { _ -> continuation.resume(img) }
112+
img.onerror = { _: dynamic, _: String, _: Int, _: Int, _: Any? ->
113+
continuation.resumeWithException(Exception("Failed to load image"))
114+
}
115+
img.src = url
116+
117+
continuation.invokeOnCancellation {
118+
img.src = ""
119+
}
120+
}
121+
46122
public companion object Companion {
47123
public inline operator fun invoke(): DomSnapshotter {
48124
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)