Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@
*/
package app.cash.redwood.dom.testing

import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.browser.document
import kotlinx.coroutines.await
import kotlinx.coroutines.suspendCancellableCoroutine
import org.khronos.webgl.get
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.Element
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.HTMLImageElement
import org.w3c.dom.url.URL
import org.w3c.files.Blob

public class DomSnapshotter @PublishedApi internal constructor(
private val path: String,
Expand All @@ -40,9 +50,75 @@ public class DomSnapshotter @PublishedApi internal constructor(
},
).await()

snapshotStore.put("$path/${name ?: "snapshot"}.png", image)
val fileName = "$path/${name ?: "snapshot"}.png"

snapshotStore.getBlob(fileName)?.let { existing ->
check(existing.contentEquals(image)) {
"Current snapshot does not match the existing file $fileName"
}
} ?: snapshotStore.put(fileName, image)
}

private suspend fun Blob.contentEquals(other: Blob): Boolean {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may have to implement more advanced strategies in the future if the variation across machines turns out to be a problem. Something like https://zschuessler.github.io/DeltaE/ maybe.

if (this.size != other.size) return false

val url1 = URL.createObjectURL(this)
val url2 = URL.createObjectURL(other)

try {
val img1 = loadImage(url1)
val img2 = loadImage(url2)

if (img1.width != img2.width || img1.height != img2.height) {
return false
}

val canvas = document.createElement("canvas") as HTMLCanvasElement
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D

canvas.width = img1.width
canvas.height = img1.height

// Get data for first image
ctx.drawImage(img1, 0.0, 0.0)
val data1 = ctx.getImageData(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())

// Get data for second image
ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
ctx.drawImage(img2, 0.0, 0.0)
val data2 = ctx.getImageData(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())

// Compare pixel by pixel
val pixels1 = data1.data
val pixels2 = data2.data
for (i in 0 until pixels1.length) {
if (pixels1[i] != pixels2[i]) {
return false
}
}

return true
} finally {
URL.revokeObjectURL(url1)
URL.revokeObjectURL(url2)
}
}

private suspend fun loadImage(url: String): HTMLImageElement =
suspendCancellableCoroutine { continuation ->
val img = document.createElement("img") as HTMLImageElement

img.onload = { _ -> continuation.resume(img) }
img.onerror = { _: dynamic, _: String, _: Int, _: Int, _: Any? ->
continuation.resumeWithException(Exception("Failed to load image"))
}
img.src = url

continuation.invokeOnCancellation {
img.src = ""
}
}

public companion object Companion {
public inline operator fun invoke(): DomSnapshotter {
return DomSnapshotter("PlaceholderTestName")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import kotlinx.coroutines.await
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.w3c.fetch.RequestInit
import org.w3c.fetch.Response
import org.w3c.files.Blob

internal class SnapshotStore {
Expand Down Expand Up @@ -51,7 +52,17 @@ internal class SnapshotStore {
}
}

suspend fun get(fileName: String): ByteString? {
suspend fun getBlob(fileName: String): Blob? {
return getInternal(fileName)?.blob()?.await()
}

suspend fun getByteString(fileName: String): ByteString? {
val response = getInternal(fileName) ?: return null
val bytes: Promise<ByteArray> = response.asDynamic().bytes()
return bytes.await().toByteString()
}

private suspend fun getInternal(fileName: String): Response? {
val response = window.fetch(
input = "/snapshots/$fileName",
).await()
Expand All @@ -68,9 +79,7 @@ internal class SnapshotStore {
""".trimMargin(),
)
}

val bytes: Promise<ByteArray> = response.asDynamic().bytes()
return bytes.await().toByteString()
return response
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
package app.cash.redwood.dom.testing

import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlinx.browser.document
import kotlinx.coroutines.test.runTest
import kotlinx.dom.appendElement
import kotlinx.dom.appendText
import kotlinx.dom.clear

/**
* This isn't a proper unit test for [DomSnapshotter], it's just a sample.
Expand All @@ -38,4 +40,21 @@ internal class DomSnapshotterSampleTest {
}
snapshotter.snapshot(element, "helloIAmTheSnapshotTest")
}

@Test
fun mismatchedSnapshot() = runTest {
val element = document.documentElement!!
element.appendElement("h1") {
appendText("hello world")
}
snapshotter.snapshot(element, "mismatchedSnapshotTest")

element.clear()
element.appendElement("h2") {
appendText("hello world")
}
assertFailsWith<IllegalStateException> {
snapshotter.snapshot(element, "mismatchedSnapshotTest")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ internal class SnapshotStoreTest {
val store = SnapshotStore()
val data = "Hello World!".encodeUtf8()
store.put("greeting.txt", data)
assertThat(store.get("greeting.txt")).isEqualTo(data)
assertThat(store.getByteString("greeting.txt")).isEqualTo(data)
}

@Test
fun putAndGetFileWithPathHierarchy() = runTest {
val store = SnapshotStore()
val data = "Ahoy, Matey!".encodeUtf8()
store.put("greetings/pirate/greeting.txt", data)
assertThat(store.get("greetings/pirate/greeting.txt")).isEqualTo(data)
assertThat(store.getByteString("greetings/pirate/greeting.txt")).isEqualTo(data)
}

@Test
fun getDirectoryTraversalReturnsNoData() = runTest {
val store = SnapshotStore()
assertThat(store.get("../README.md")).isNull() // 404 Not Found.
assertThat(store.getByteString("../README.md")).isNull() // 404 Not Found.
}

@Test
Expand Down
Loading