Move view tests to common
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 35 KiB |
BIN
core/assets/test/components/CanvasTest.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
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])
|
||||
|
||||
37
core/src/test/common/org/isoron/platform/io/StringsTest.kt
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
63
core/src/test/common/org/isoron/uhabits/BaseViewTest.kt
Normal file
@@ -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 fun createCanvas(width: Int, height: Int): Canvas {
|
||||
val canvasElement = document.getElementById("canvas") as HTMLCanvasElement
|
||||
canvasElement.style.width = "${width}px"
|
||||
canvasElement.style.height = "${height}px"
|
||||
return HtmlCanvas(canvasElement)
|
||||
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 exportCanvas(canvas: Canvas, filename: String) {
|
||||
override fun createCanvas(width: Int, height: Int): Canvas {
|
||||
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"
|
||||
canvasElement.className = "canvasTest"
|
||||
document.body?.appendChild(canvasElement)
|
||||
return JsCanvas(canvasElement, 2.0)
|
||||
}
|
||||
|
||||
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 fun createCanvas(width: Int, height: Int): Canvas {
|
||||
val image = BufferedImage(width, height, BufferedImage.TYPE_INT_RGB)
|
||||
return JavaCanvas(image, pixelScale = 1.0)
|
||||
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)
|
||||
}
|
||||
|
||||
override fun exportCanvas(canvas: Canvas, filename: String) {
|
||||
val javaCanvas = canvas as JavaCanvas
|
||||
ImageIO.write(javaCanvas.image, "png", File("/tmp/$filename"))
|
||||
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 widthPx = width * 2
|
||||
val heightPx = height * 2
|
||||
val image = BufferedImage(widthPx, heightPx, BufferedImage.TYPE_INT_ARGB)
|
||||
return JavaCanvas(image, pixelScale = 2.0)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||