Move view tests to common

pull/498/head
Alinson S. Xavier 7 years ago
parent defa2f9431
commit a3bfc05068

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

@ -22,17 +22,22 @@ package org.isoron.platform.io
interface Log {
fun info(tag: String, msg: String)
fun debug(tag: String, msg: String)
fun warn(tag: String, msg: String)
}
/**
* A Log that prints to the standard output.
*/
class StandardLog : Log {
override fun warn(tag: String, msg: String) {
println(sprintf("W %-20s %s", tag, msg))
}
override fun info(tag: String, msg: String) {
println("I/$tag $msg")
println(sprintf("I %-20s %s", tag, msg))
}
override fun debug(tag: String, msg: String) {
println("D/$tag $msg")
println(sprintf("D %-20s %s", tag, msg))
}
}

@ -19,18 +19,27 @@
package org.isoron.platform.gui
import kotlinx.coroutines.*
import org.w3c.dom.*
import kotlin.browser.*
import kotlin.js.*
import kotlin.math.*
class HtmlCanvas(val canvas: HTMLCanvasElement) : Canvas {
class JsCanvas(val element: HTMLCanvasElement,
val pixelScale: Double) : Canvas {
val ctx = canvas.getContext("2d") as CanvasRenderingContext2D
val ctx = element.getContext("2d") as CanvasRenderingContext2D
var fontSize = 12.0
var fontWeight = ""
var fontFamily = "sans-serif"
var fontFamily = "NotoRegular"
var align = CanvasTextAlign.CENTER
private fun toPixel(x: Double): Double {
return pixelScale * x
}
private fun toDp(x: Int): Double {
return x / pixelScale
}
override fun setColor(color: Color) {
val c = "rgb(${color.red * 255}, ${color.green * 255}, ${color.blue * 255})"
ctx.fillStyle = c;
@ -39,45 +48,54 @@ class HtmlCanvas(val canvas: HTMLCanvasElement) : Canvas {
override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) {
ctx.beginPath()
ctx.moveTo(x1 + 0.5, y1 + 0.5)
ctx.lineTo(x2 + 0.5, y2 + 0.5)
ctx.moveTo(toPixel(x1), toPixel(y1))
ctx.lineTo(toPixel(x2), toPixel(y2))
ctx.stroke()
}
override fun drawText(text: String, x: Double, y: Double) {
ctx.font = "${fontWeight} ${fontSize}px ${fontFamily}"
ctx.font = "${fontSize}px ${fontFamily}"
ctx.textAlign = align
ctx.textBaseline = CanvasTextBaseline.MIDDLE
ctx.fillText(text, x, y)
ctx.fillText(text, toPixel(x), toPixel(y + fontSize * 0.05))
}
override fun fillRect(x: Double, y: Double, width: Double, height: Double) {
ctx.fillRect(x - 0.5, y - 0.5, width + 1.0, height + 1.0)
ctx.fillRect(toPixel(x),
toPixel(y),
toPixel(width),
toPixel(height))
}
override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
ctx.strokeRect(x - 0.5, y - 0.5, width + 1.0, height + 1.0)
ctx.strokeRect(toPixel(x),
toPixel(y),
toPixel(width),
toPixel(height))
}
override fun getHeight(): Double {
return canvas.height.toDouble()
return toDp(element.height)
}
override fun getWidth(): Double {
return canvas.width.toDouble()
return toDp(element.width)
}
override fun setFont(font: Font) {
fontWeight = if (font == Font.BOLD) "bold" else ""
fontFamily = if (font == Font.FONT_AWESOME) "FontAwesome" else "sans-serif"
fontFamily = when(font) {
Font.REGULAR -> "NotoRegular"
Font.BOLD -> "NotoBold"
Font.FONT_AWESOME -> "FontAwesome"
}
}
override fun setFontSize(size: Double) {
fontSize = size
fontSize = size * pixelScale
}
override fun setStrokeWidth(size: Double) {
ctx.lineWidth = size
ctx.lineWidth = size * pixelScale
}
override fun fillArc(centerX: Double,
@ -85,18 +103,24 @@ class HtmlCanvas(val canvas: HTMLCanvasElement) : Canvas {
radius: Double,
startAngle: Double,
swipeAngle: Double) {
val x = toPixel(centerX)
val y = toPixel(centerY)
val from = startAngle / 180 * PI
val to = (startAngle + swipeAngle) / 180 * PI
ctx.beginPath()
ctx.moveTo(centerX, centerY)
ctx.arc(centerX, centerY, radius, -from, -to, swipeAngle >= 0)
ctx.lineTo(centerX, centerY)
ctx.moveTo(x, y)
ctx.arc(x, y, toPixel(radius), -from, -to, swipeAngle >= 0)
ctx.lineTo(x, y)
ctx.fill()
}
override fun fillCircle(centerX: Double, centerY: Double, radius: Double) {
ctx.beginPath()
ctx.arc(centerX, centerY, radius, 0.0, 2 * PI)
ctx.arc(toPixel(centerX),
toPixel(centerY),
toPixel(radius),
0.0,
2 * PI)
ctx.fill()
}
@ -107,4 +131,15 @@ class HtmlCanvas(val canvas: HTMLCanvasElement) : Canvas {
TextAlign.RIGHT -> CanvasTextAlign.RIGHT
}
}
suspend fun loadImage(src: String) {
Promise<Int> { resolve, reject ->
val img = Image()
img.onload = {
ctx.drawImage(img, 0.0, 0.0)
resolve(0)
}
img.src = src
}.await()
}
}

@ -24,6 +24,9 @@ import org.w3c.xhr.*
import kotlin.js.*
class JsFileStorage {
private val TAG = "JsFileStorage"
private val log = StandardLog()
private val indexedDB = eval("indexedDB")
private var db: dynamic = null
@ -31,16 +34,16 @@ class JsFileStorage {
private val OS_NAME = "Files"
suspend fun init() {
console.log("Initializing JsFileStorage...")
log.info(TAG, "Initializing")
Promise<Int> { resolve, reject ->
val req = indexedDB.open(DB_NAME, 2)
req.onerror = { reject(Exception("could not open IndexedDB")) }
req.onupgradeneeded = {
console.log("Creating document store for JsFileStorage...")
log.info(TAG, "Creating document store")
req.result.createObjectStore(OS_NAME)
}
req.onsuccess = {
console.log("JsFileStorage is ready.")
log.info(TAG, "Ready")
db = req.result
resolve(0)
}

@ -20,5 +20,5 @@
package org.isoron.platform.io
actual fun sprintf(format: String, vararg args: Any?): String {
TODO()
return js("vsprintf")(format, args)
}

@ -19,6 +19,7 @@
package org.isoron.platform.gui
import kotlinx.coroutines.*
import org.isoron.platform.io.*
import java.awt.*
import java.awt.RenderingHints.*
@ -26,14 +27,6 @@ import java.awt.font.*
import java.awt.image.*
import kotlin.math.*
fun createFont(path: String): java.awt.Font {
val file = JavaFileOpener().openResourceFile(path) as JavaResourceFile
return java.awt.Font.createFont(0, file.stream())
}
private val NOTO_REGULAR_FONT = createFont("fonts/NotoSans-Regular.ttf")
private val NOTO_BOLD_FONT = createFont("fonts/NotoSans-Bold.ttf")
private val FONT_AWESOME_FONT = createFont("fonts/FontAwesome.ttf")
class JavaCanvas(val image: BufferedImage,
val pixelScale: Double = 2.0) : Canvas {
@ -46,6 +39,10 @@ class JavaCanvas(val image: BufferedImage,
val heightPx = image.height
val g2d = image.createGraphics()
private val NOTO_REGULAR_FONT = createFont("fonts/NotoSans-Regular.ttf")
private val NOTO_BOLD_FONT = createFont("fonts/NotoSans-Bold.ttf")
private val FONT_AWESOME_FONT = createFont("fonts/FontAwesome.ttf")
init {
g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON);
@ -73,6 +70,7 @@ class JavaCanvas(val image: BufferedImage,
}
override fun drawText(text: String, x: Double, y: Double) {
updateFont()
val bounds = g2d.font.getStringBounds(text, frc)
val bWidth = bounds.width.roundToInt()
val bHeight = bounds.height.roundToInt()
@ -122,7 +120,7 @@ class JavaCanvas(val image: BufferedImage,
}
override fun setStrokeWidth(size: Double) {
g2d.setStroke(BasicStroke(size.toFloat()))
g2d.stroke = BasicStroke((size * pixelScale).toFloat())
}
private fun updateFont() {
@ -158,4 +156,10 @@ class JavaCanvas(val image: BufferedImage,
override fun setTextAlign(align: TextAlign) {
this.textAlign = align
}
private fun createFont(path: String) = runBlocking<java.awt.Font> {
val file = JavaFileOpener().openResourceFile(path) as JavaResourceFile
if (!file.exists()) throw RuntimeException("File not found: ${file.path}")
java.awt.Font.createFont(0, file.stream())
}
}

@ -22,7 +22,7 @@ package org.isoron.platform.io
import java.io.*
import java.nio.file.*
class JavaResourceFile(private val path: String) : ResourceFile {
class JavaResourceFile(val path: String) : ResourceFile {
private val javaPath: Path
get() {
val mainPath = Paths.get("assets/main/$path")
@ -41,7 +41,9 @@ class JavaResourceFile(private val path: String) : ResourceFile {
override suspend fun copyTo(dest: UserFile) {
if (dest.exists()) dest.delete()
Files.copy(javaPath, (dest as JavaUserFile).path)
val destPath = (dest as JavaUserFile).path
destPath.toFile().parentFile?.mkdirs()
Files.copy(javaPath, destPath)
}
fun stream(): InputStream {

@ -29,7 +29,8 @@ enum class Locale {
interface CanvasHelper {
fun createCanvas(width: Int, height: Int): Canvas
fun exportCanvas(canvas: Canvas, filename: String)
suspend fun exportCanvas(canvas: Canvas, filename: String)
suspend fun compare(imageFile: ResourceFile, canvas: Canvas): Double
}
expect object DependencyResolver {

@ -20,12 +20,13 @@
package org.isoron.platform.gui
import org.isoron.*
import org.isoron.uhabits.*
import kotlin.test.*
class CanvasTest {
class CanvasTest: BaseViewTest() {
@Test
fun run() {
if (!DependencyResolver.supportsCanvasTests) return
fun run() = asyncTest{
if (!DependencyResolver.supportsCanvasTests) return@asyncTest
val helper = DependencyResolver.getCanvasHelper()
val canvas = helper.createCanvas(500, 400)
@ -67,6 +68,6 @@ class CanvasTest {
canvas.setFont(Font.FONT_AWESOME)
canvas.drawText(FontAwesome.CHECK, 250.0, 300.0)
helper.exportCanvas(canvas, "CanvasTest.png")
assertRenders("components/CanvasTest.png", canvas)
}
}

@ -40,7 +40,7 @@ class FilesTest() {
assertEquals("Hello World!", lines[0])
assertEquals("This is a resource.", lines[1])
val helloCopy = fileOpener.openUserFile("hello-copy.txt")
val helloCopy = fileOpener.openUserFile("copies/hello.txt")
hello.copyTo(helloCopy)
lines = helloCopy.lines()
assertEquals("Hello World!", lines[0])

@ -0,0 +1,37 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.io
import kotlin.test.*
class StringsTest {
@Test
fun testSprintf() {
assertEquals(" 5", sprintf("%3d", 5))
assertEquals("005", sprintf("%03d", 5))
assertEquals("005", sprintf("%03d", 5))
assertEquals(" 45", sprintf("%3d", 45))
assertEquals("145", sprintf("%3d", 145))
assertEquals(" 9 9", sprintf("%3d%3d", 9, 9))
assertEquals(" 13.42", sprintf("%8.2f", 13.419187263))
assertEquals("00013.42", sprintf("%08.2f", 13.419187263))
assertEquals("13.42 ", sprintf("%-8.2f", 13.419187263))
}
}

@ -0,0 +1,63 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits
import org.isoron.*
import org.isoron.platform.gui.*
import org.isoron.uhabits.components.*
import kotlin.test.*
var SIMILARITY_THRESHOLD = 5.0
open class BaseViewTest {
var theme = LightTheme()
suspend fun assertRenders(width: Int,
height: Int,
expectedPath: String,
component: Component) {
val helper = DependencyResolver.getCanvasHelper()
val canvas = helper.createCanvas(width, height)
component.draw(canvas)
assertRenders(expectedPath, canvas)
}
suspend fun assertRenders(expectedPath: String,
canvas: Canvas) {
val helper = DependencyResolver.getCanvasHelper()
val fileOpener = DependencyResolver.getFileOpener()
val expectedFile = fileOpener.openResourceFile(expectedPath)
val actualPath = "/failed/${expectedPath}"
if (expectedFile.exists()) {
val d = helper.compare(expectedFile, canvas)
if (d >= SIMILARITY_THRESHOLD) {
helper.exportCanvas(canvas, actualPath)
val expectedCopy = expectedPath.replace(".png", ".expected.png")
expectedFile.copyTo(fileOpener.openUserFile("/failed/$expectedCopy"))
fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
}
} else {
helper.exportCanvas(canvas, actualPath)
fail("Expected file is missing. Actual render saved to $actualPath")
}
}
}

@ -19,32 +19,30 @@
package org.isoron.uhabits.components
import org.isoron.*
import org.isoron.platform.time.*
import org.isoron.uhabits.*
import org.junit.*
import java.util.*
import kotlin.test.*
class CalendarChartTest : BaseViewTest() {
val base = "components/CalendarChart"
@Test
fun testDraw() {
fun testDraw() = asyncTest {
val fmt = DependencyResolver.getDateFormatter(Locale.US)
val component = CalendarChart(LocalDate(2015, 1, 25),
theme.color(4),
theme,
JavaLocalDateFormatter(Locale.US))
fmt)
component.series = listOf(1.0, // today
0.2, 0.5, 0.7, 0.0, 0.3, 0.4, 0.6,
0.6, 0.0, 0.3, 0.6, 0.5, 0.8, 0.0,
0.0, 0.0, 0.0, 0.6, 0.5, 0.7, 0.7,
0.5, 0.5, 0.8, 0.9, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.2)
assertRenders(800, 400, "$base/base.png", component)
assertRenders(400, 200, "$base/base.png", component)
component.scrollPosition = 2
assertRenders(800, 400, "$base/scroll.png", component)
component.dateFormatter = JavaLocalDateFormatter(Locale.JAPAN)
assertRenders(800, 400, "$base/base-jp.png", component)
assertRenders(400, 200, "$base/scroll.png", component)
}
}

@ -19,27 +19,28 @@
package org.isoron.uhabits.components
import org.isoron.*
import org.isoron.uhabits.*
import org.junit.*
import kotlin.test.*
class CheckmarkButtonTest : BaseViewTest() {
val base = "components/CheckmarkButton"
@Test
fun testDrawExplicit() {
fun testDrawExplicit() = asyncTest {
val component = CheckmarkButton(2, theme.color(8), theme)
assertRenders(96, 96, "$base/explicit.png", component)
assertRenders(48, 48, "$base/explicit.png", component)
}
@Test
fun testDrawImplicit() {
fun testDrawImplicit() = asyncTest {
val component = CheckmarkButton(1, theme.color(8), theme)
assertRenders(96, 96, "$base/implicit.png", component)
assertRenders(48, 48, "$base/implicit.png", component)
}
@Test
fun testDrawUnchecked() {
fun testDrawUnchecked() = asyncTest {
val component = CheckmarkButton(0, theme.color(8), theme)
assertRenders(96, 96, "$base/unchecked.png", component)
assertRenders(48, 48, "$base/unchecked.png", component)
}
}

@ -19,20 +19,16 @@
package org.isoron.uhabits.components
import org.isoron.*
import org.isoron.platform.time.*
import org.isoron.uhabits.*
import org.junit.*
import java.util.*
import kotlin.test.*
class HabitListHeaderTest : BaseViewTest() {
@Test
fun testDraw() {
val header = HabitListHeader(LocalDate(2019, 3, 25),
5,
theme,
JavaLocalDateFormatter(Locale.US))
assertRenders(1200, 96,
"components/HabitListHeader/light.png",
header)
fun testDraw() = asyncTest {
val fmt = DependencyResolver.getDateFormatter(Locale.US)
val header = HabitListHeader(LocalDate(2019, 3, 25), 5, theme, fmt)
assertRenders(600, 48, "components/HabitListHeader/light.png", header)
}
}

@ -0,0 +1,64 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.*
import org.isoron.uhabits.*
import kotlin.test.*
class NumberButtonTest : BaseViewTest() {
val base = "components/NumberButton"
@Test
fun testFormatValue() = asyncTest{
assertEquals("0.12", 0.1235.toShortString())
assertEquals("0.1", 0.1000.toShortString())
assertEquals("5", 5.0.toShortString())
assertEquals("5.25", 5.25.toShortString())
assertEquals("12.3", 12.3456.toShortString())
assertEquals("123", 123.123.toShortString())
assertEquals("321", 321.2.toShortString())
assertEquals("4.3k", 4321.2.toShortString())
assertEquals("54.3k", 54321.2.toShortString())
assertEquals("654k", 654321.2.toShortString())
assertEquals("7.7M", 7654321.2.toShortString())
assertEquals("87.7M", 87654321.2.toShortString())
assertEquals("988M", 987654321.2.toShortString())
assertEquals("2.0G", 1987654321.2.toShortString())
}
@Test
fun testRenderAbove() = asyncTest {
val btn = NumberButton(theme.color(8), 500.0, 100.0, "steps", theme)
assertRenders(48, 48, "$base/render_above.png", btn)
}
@Test
fun testRenderBelow() = asyncTest {
val btn = NumberButton(theme.color(8), 99.0, 100.0, "steps", theme)
assertRenders(48, 48, "$base/render_below.png", btn)
}
@Test
fun testRenderZero() = asyncTest {
val btn = NumberButton(theme.color(8), 0.0, 100.0, "steps", theme)
assertRenders(48, 48, "$base/render_zero.png", btn)
}
}

@ -19,20 +19,21 @@
package org.isoron.uhabits.components
import org.isoron.*
import org.isoron.uhabits.*
import org.junit.*
import kotlin.test.*
class RingTest : BaseViewTest() {
val base = "components/Ring"
@Test
fun testDraw() {
fun testDraw() = asyncTest {
val component = Ring(theme.color(8),
percentage = 0.30,
thickness = 5.0,
radius = 30.0,
theme = theme,
label = true)
assertRenders(120, 120, "$base/draw1.png", component)
assertRenders(60, 60, "$base/draw1.png", component)
}
}

@ -23,8 +23,10 @@ import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import org.isoron.platform.time.*
import org.isoron.uhabits.*
import org.khronos.webgl.*
import org.w3c.dom.*
import kotlin.browser.*
import kotlin.math.*
actual object DependencyResolver {
actual val supportsDatabaseTests = true
@ -52,7 +54,7 @@ actual object DependencyResolver {
}
actual fun getDateFormatter(locale: Locale): LocalDateFormatter {
return when(locale) {
return when (locale) {
Locale.US -> JsDateFormatter("en-US")
Locale.JAPAN -> JsDateFormatter("ja-JP")
}
@ -60,14 +62,65 @@ actual object DependencyResolver {
}
class JsCanvasHelper : CanvasHelper {
override suspend fun compare(imageFile: ResourceFile,
canvas: Canvas): Double {
canvas as JsCanvas
imageFile as JsResourceFile
val width = canvas.element.width
val height = canvas.element.height
val expectedCanvasElement = document.createElement("canvas") as HTMLCanvasElement
expectedCanvasElement.width = width
expectedCanvasElement.height = height
expectedCanvasElement.style.width = canvas.element.style.width
expectedCanvasElement.style.height = canvas.element.style.height
expectedCanvasElement.className = "canvasTest"
document.body?.appendChild(expectedCanvasElement)
val expectedCanvas = JsCanvas(expectedCanvasElement, 1.0)
expectedCanvas.loadImage("../assets/${imageFile.filename}")
val actualData = canvas.ctx.getImageData(0.0,
0.0,
width.toDouble(),
height.toDouble()).data
val expectedData = expectedCanvas.ctx.getImageData(0.0,
0.0,
width.toDouble(),
height.toDouble()).data
var distance = 0.0;
for (x in 0 until width) {
for (y in 0 until height) {
val k = (y * width + x) * 4
distance += abs(actualData[k] - expectedData[k])
distance += abs(actualData[k + 1] - expectedData[k + 1])
distance += abs(actualData[k + 2] - expectedData[k + 2])
distance += abs(actualData[k + 3] - expectedData[k + 3])
}
}
val adjustedDistance = distance / 255.0 / 4 / 1000
if (adjustedDistance > SIMILARITY_THRESHOLD) {
expectedCanvasElement.style.display = "block"
canvas.element.style.display = "block"
}
return adjustedDistance
}
override fun createCanvas(width: Int, height: Int): Canvas {
val canvasElement = document.getElementById("canvas") as HTMLCanvasElement
val canvasElement = document.createElement("canvas") as HTMLCanvasElement
canvasElement.width = width * 2
canvasElement.height = height * 2
canvasElement.style.width = "${width}px"
canvasElement.style.height = "${height}px"
return HtmlCanvas(canvasElement)
canvasElement.className = "canvasTest"
document.body?.appendChild(canvasElement)
return JsCanvas(canvasElement, 2.0)
}
override fun exportCanvas(canvas: Canvas, filename: String) {
override suspend fun exportCanvas(canvas: Canvas, filename: String) {
// do nothing
}
}

@ -25,6 +25,8 @@ import org.isoron.platform.time.*
import org.isoron.uhabits.*
import java.awt.image.*
import java.io.*
import java.lang.Math.*
import java.nio.file.*
import javax.imageio.*
actual object DependencyResolver {
@ -47,7 +49,7 @@ actual object DependencyResolver {
}
actual fun getDateFormatter(locale: Locale): LocalDateFormatter {
return when(locale) {
return when (locale) {
Locale.US -> JavaLocalDateFormatter(java.util.Locale.US)
Locale.JAPAN -> JavaLocalDateFormatter(java.util.Locale.JAPAN)
}
@ -55,13 +57,42 @@ actual object DependencyResolver {
}
class JavaCanvasHelper : CanvasHelper {
override suspend fun compare(imageFile: ResourceFile, canvas: Canvas): Double {
val actual = (canvas as JavaCanvas).image
val expected = ImageIO.read((imageFile as JavaResourceFile).stream())
return compare(expected, actual)
}
private fun compare(expected: BufferedImage,
actual: BufferedImage): Double {
if (actual.width != expected.width) return Double.POSITIVE_INFINITY
if (actual.height != expected.height) return Double.POSITIVE_INFINITY
var distance = 0.0;
for (x in 0 until actual.width) {
for (y in 0 until actual.height) {
val p1 = Color(actual.getRGB(x, y))
val p2 = Color(expected.getRGB(x, y))
distance += abs(p1.red - p2.red)
distance += abs(p1.green - p2.green)
distance += abs(p1.blue - p2.blue)
}
}
return distance / 4.0
}
override fun createCanvas(width: Int, height: Int): Canvas {
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
return JavaCanvas(image, pixelScale = 1.0)
val widthPx = width * 2
val heightPx = height * 2
val image = BufferedImage(widthPx, heightPx, BufferedImage.TYPE_INT_ARGB)
return JavaCanvas(image, pixelScale = 2.0)
}
override fun exportCanvas(canvas: Canvas, filename: String) {
val javaCanvas = canvas as JavaCanvas
ImageIO.write(javaCanvas.image, "png", File("/tmp/$filename"))
override suspend fun exportCanvas(canvas: Canvas, filename: String) {
val file = File("/tmp/$filename")
file.parentFile.mkdirs()
ImageIO.write((canvas as JavaCanvas).image, "png", file)
}
}

@ -1,85 +0,0 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits
import kotlinx.coroutines.*
import org.isoron.platform.gui.*
import org.isoron.platform.io.*
import org.isoron.uhabits.components.*
import java.awt.image.*
import java.io.*
import javax.imageio.*
import kotlin.math.*
open class BaseViewTest {
val theme = LightTheme()
private fun distance(actual: BufferedImage,
expected: BufferedImage): Double {
if (actual.width != expected.width) return Double.POSITIVE_INFINITY
if (actual.height != expected.height) return Double.POSITIVE_INFINITY
var distance = 0.0;
for (x in 0 until actual.width) {
for (y in 0 until actual.height) {
val p1 = Color(actual.getRGB(x, y))
val p2 = Color(expected.getRGB(x, y))
distance += abs(p1.red - p2.red)
distance += abs(p1.green - p2.green)
distance += abs(p1.blue - p2.blue)
}
}
return 255 * distance / (actual.width * actual.height)
}
fun assertRenders(width: Int,
height: Int,
expectedPath: String,
component: Component,
threshold: Double = 1e-3) {
val actual = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
val canvas = JavaCanvas(actual)
val expectedFile: JavaResourceFile
val actualPath = "/tmp/${expectedPath}"
component.draw(canvas)
expectedFile = JavaFileOpener().openResourceFile(expectedPath) as JavaResourceFile
runBlocking<Unit> {
if (expectedFile.exists()) {
val expected = ImageIO.read(expectedFile.stream())
val d = distance(actual, expected)
if (d >= threshold) {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
ImageIO.write(expected, "png", File(actualPath.replace(".png", ".expected.png")))
//fail("Images differ (distance=${d}). Actual rendered saved to ${actualPath}.")
}
} else {
File(actualPath).parentFile.mkdirs()
ImageIO.write(actual, "png", File(actualPath))
//fail("Expected file is missing. Actual render saved to $actualPath")
}
}
}
}

@ -1,65 +0,0 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.hamcrest.CoreMatchers.*
import org.isoron.uhabits.*
import org.junit.*
import org.junit.Assert.*
class NumberButtonTest : BaseViewTest() {
val base = "components/NumberButton/"
@Test
fun testFormatValue() {
assertThat(0.1235.toShortString(), equalTo("0.12"))
assertThat(0.1000.toShortString(), equalTo("0.1"))
assertThat(5.0.toShortString(), equalTo("5"))
assertThat(5.25.toShortString(), equalTo("5.25"))
assertThat(12.3456.toShortString(), equalTo("12.3"))
assertThat(123.123.toShortString(), equalTo("123"))
assertThat(321.2.toShortString(), equalTo("321"))
assertThat(4321.2.toShortString(), equalTo("4.3k"))
assertThat(54321.2.toShortString(), equalTo("54.3k"))
assertThat(654321.2.toShortString(), equalTo("654k"))
assertThat(7654321.2.toShortString(), equalTo("7.7M"))
assertThat(87654321.2.toShortString(), equalTo("87.7M"))
assertThat(987654321.2.toShortString(), equalTo("988M"))
assertThat(1987654321.2.toShortString(), equalTo("2.0G"))
}
@Test
fun testRenderAbove() {
val btn = NumberButton(theme.color(8), 500.0, 100.0, "steps", theme)
assertRenders(96, 96, "$base/render_above.png", btn)
}
@Test
fun testRenderBelow() {
val btn = NumberButton(theme.color(8), 99.0, 100.0, "steps", theme)
assertRenders(96, 96, "$base/render_below.png", btn)
}
@Test
fun testRenderZero() {
val btn = NumberButton(theme.color(8), 0.0, 100.0, "steps", theme)
assertRenders(96, 96, "$base/render_zero.png", btn)
}
}

@ -20,6 +20,7 @@ $(test_bundle): src/test/index.js core
cp node_modules/mocha/mocha.css build/lib
cp node_modules/mocha/mocha.js build/lib
cp node_modules/sql.js/js/sql.js build/lib
cp node_modules/sprintf-js/dist/sprintf.min.js build/lib
serve:
npx serve build/

@ -315,6 +315,14 @@
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
},
"dependencies": {
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
}
}
},
"arr-diff": {
@ -5111,10 +5119,9 @@
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
"dev": true
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
},
"sql.js": {
"version": "0.5.0",

@ -21,6 +21,7 @@
"kotlin": "^1.3.21",
"kotlin-test": "^1.3.21",
"kotlinx-coroutines-core": "^1.1.1",
"sprintf-js": "^1.1.2",
"sql.js": "^0.5.0"
},
"devDependencies": {

@ -3,20 +3,47 @@
<head>
<title>Mocha Tests</title>
<link rel="stylesheet" href="../lib/mocha.css">
<script src="../lib/sprintf.min.js"></script>
<script src="../lib/sql.js"></script>
<script src="../lib/mocha.js"></script>
<style>
@font-face {
font-family: "FontAwesome";
src: url(../assets/fonts/FontAwesome.ttf) format("truetype");
}
@font-face {
font-family: "NotoRegular";
src: url(../assets/fonts/NotoSans-Regular.ttf) format("truetype");
}
@font-face {
font-family: "NotoBold";
src: url(../assets/fonts/NotoSans-Bold.ttf) format("truetype");
}
.canvasTest {
border: 1px solid #000;
display: none;
margin: 10px auto;
}
</style>
</head>
<body>
<canvas id="canvas" width=500 height=400 style="display: none"></canvas>
<!-- Preload fonts for canvas. See https://stackoverflow.com/questions/2756575/ -->
<span style="font-family: FontAwesome">&nbsp;</span>
<span style="font-family: NotoRegular">&nbsp;</span>
<span style="font-family: NotoBold">&nbsp;</span>
<div id="mocha"></div>
<script src="../lib/mocha.js"></script>
<script>mocha.setup('bdd')</script>
<script src="../test.js"></script>
<script>mocha.run();</script>
<script>
mocha.setup('bdd');
testElement = document.createElement("script");
testElement.src = "../test.js";
testElement.onload = function() {
mocha.run();
}
document.body.appendChild(testElement);
</script>
</body>
</html>

Loading…
Cancel
Save