Added several unit tests and improved testing infra.

This commit is contained in:
Daan Vanden Bosch 2020-11-08 14:27:02 +01:00
parent 25f015dfbb
commit 346a2cb4f9
57 changed files with 572 additions and 187 deletions

View File

@ -10,12 +10,18 @@ abstract class TrackedDisposable : Disposable {
init { init {
disposableCount++ disposableCount++
if (trackPrecise) {
@Suppress("LeakingThis")
disposables.add(this)
}
} }
final override fun dispose() { final override fun dispose() {
if (!disposed) { if (!disposed) {
disposed = true disposed = true
disposableCount-- disposableCount--
disposables.remove(this)
internalDispose() internalDispose()
} }
} }
@ -25,16 +31,45 @@ abstract class TrackedDisposable : Disposable {
} }
companion object { companion object {
const val DISPOSABLE_PRINT_COUNT = 10
var disposables: MutableSet<Disposable> = mutableSetOf()
var trackPrecise = false
var disposableCount: Int = 0 var disposableCount: Int = 0
private set private set
fun checkNoLeaks(block: () -> Unit) { inline fun checkNoLeaks(trackPrecise: Boolean = false, block: () -> Unit) {
val count = disposableCount val initialCount = disposableCount
val initialTrackPrecise = this.trackPrecise
val initialDisposables = disposables
this.trackPrecise = trackPrecise
disposables = mutableSetOf()
try { try {
block() block()
checkLeaks(disposableCount - initialCount)
} finally { } finally {
check(count == disposableCount) { "TrackedDisposables were leaked." } this.trackPrecise = initialTrackPrecise
disposables = initialDisposables
}
}
fun checkLeaks(leakCount: Int) {
buildString {
append("$leakCount TrackedDisposables were leaked")
if (trackPrecise) {
append(": ")
disposables.take(DISPOSABLE_PRINT_COUNT).joinTo(this) {
it::class.simpleName ?: "Anonymous"
}
if (disposables.size > DISPOSABLE_PRINT_COUNT) {
append(",..")
}
}
append(".")
} }
} }
} }

View File

@ -48,6 +48,7 @@ kotlin {
dependencies { dependencies {
implementation(kotlin("test-common")) implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common")) implementation(kotlin("test-annotations-common"))
implementation(project(":test-utils"))
} }
} }

View File

@ -11,6 +11,20 @@ class QuestNpc(
override var areaId: Int, override var areaId: Int,
val data: Buffer, val data: Buffer,
) : QuestEntity<NpcType> { ) : QuestEntity<NpcType> {
constructor(
type: NpcType,
episode: Episode,
areaId: Int,
wave: Int,
) : this(episode, areaId, Buffer.withSize(NPC_BYTE_SIZE)) {
this.type = type
// TODO: Set default data.
// Set area_id after type, because you might want to overwrite the area_id that type has
// determined.
this.areaId = areaId
// TODO: Set wave properties.
}
var typeId: Short var typeId: Short
get() = data.getShort(0) get() = data.getShort(0)
set(value) { set(value) {

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.assembly package world.phantasmal.lib.assembly
import world.phantasmal.core.Success import world.phantasmal.core.Success
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class AssemblyTests { class AssemblyTests : LibTestSuite() {
@Test @Test
fun assemble_basic_script() { fun assemble_basic_script() {
val result = assemble(""" val result = assemble("""

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis package world.phantasmal.lib.assembly.dataFlowAnalysis
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.toInstructions import world.phantasmal.lib.test.toInstructions
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class ControlFlowGraphTests { class ControlFlowGraphTests : LibTestSuite() {
@Test @Test
fun single_instruction() { fun single_instruction() {
val im = toInstructions(""" val im = toInstructions("""

View File

@ -1,13 +1,14 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis package world.phantasmal.lib.assembly.dataFlowAnalysis
import world.phantasmal.lib.assembly.* import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.toInstructions import world.phantasmal.lib.test.toInstructions
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
private const val MAX_REGISTER_VALUES_SIZE: Long = 1L shl 32 private const val MAX_REGISTER_VALUES_SIZE: Long = 1L shl 32
class GetRegisterValueTests { class GetRegisterValueTests : LibTestSuite() {
@Test @Test
fun when_no_instruction_sets_the_register_zero_is_returned() { fun when_no_instruction_sets_the_register_zero_is_returned() {
val im = toInstructions(""" val im = toInstructions("""

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.assembly.dataFlowAnalysis package world.phantasmal.lib.assembly.dataFlowAnalysis
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class ValueSetTests { class ValueSetTests : LibTestSuite() {
@Test @Test
fun empty_set_has_size_0() { fun empty_set_has_size_0() {
val vs = ValueSet.empty() val vs = ValueSet.empty()

View File

@ -1,11 +1,12 @@
package world.phantasmal.lib.buffer package world.phantasmal.lib.buffer
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class BufferTests { class BufferTests : LibTestSuite() {
@Test @Test
fun withCapacity() { fun withCapacity() {
withCapacity(Endianness.Little) withCapacity(Endianness.Little)

View File

@ -2,11 +2,12 @@ package world.phantasmal.lib.compression.prs
import world.phantasmal.lib.buffer.Buffer import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.cursor import world.phantasmal.lib.cursor.cursor
import world.phantasmal.lib.test.LibTestSuite
import kotlin.random.Random import kotlin.random.Random
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class PrsCompressTests { class PrsCompressTests : LibTestSuite() {
@Test @Test
fun edge_case_0_bytes() { fun edge_case_0_bytes() {
val compressed = prsCompress(Buffer.withSize(0).cursor()) val compressed = prsCompress(Buffer.withSize(0).cursor())

View File

@ -3,13 +3,13 @@ package world.phantasmal.lib.compression.prs
import world.phantasmal.lib.buffer.Buffer import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.cursor import world.phantasmal.lib.cursor.cursor
import world.phantasmal.lib.test.asyncTest import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile import world.phantasmal.lib.test.readFile
import kotlin.random.Random import kotlin.random.Random
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class PrsDecompressTests { class PrsDecompressTests : LibTestSuite() {
@Test @Test
fun edge_case_0_bytes() { fun edge_case_0_bytes() {
testWithBuffer(Buffer.withSize(0)) testWithBuffer(Buffer.withSize(0))

View File

@ -1,6 +1,7 @@
package world.phantasmal.lib.cursor package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -8,7 +9,7 @@ import kotlin.test.assertEquals
* Test suite for all [Cursor] implementations. There is a subclass of this suite for every [Cursor] * Test suite for all [Cursor] implementations. There is a subclass of this suite for every [Cursor]
* implementation. * implementation.
*/ */
abstract class CursorTests { abstract class CursorTests : LibTestSuite() {
abstract fun createCursor( abstract fun createCursor(
bytes: ByteArray, bytes: ByteArray,
endianness: Endianness, endianness: Endianness,

View File

@ -2,7 +2,7 @@ package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer import world.phantasmal.lib.buffer.Buffer
import kotlin.math.abs import world.phantasmal.testUtils.assertCloseTo
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -144,8 +144,8 @@ abstract class WritableCursorTests : CursorTests() {
// The read floats won't be exactly the same as the written floats in Kotlin JS, because // The read floats won't be exactly the same as the written floats in Kotlin JS, because
// they're backed by numbers (64-bit floats). // they're backed by numbers (64-bit floats).
assertTrue(abs(1337.9001f - cursor.float()) < 0.001) assertCloseTo(1337.9001f, cursor.float(), epsilon = 0.001f)
assertTrue(abs(103.502f - cursor.float()) < 0.001) assertCloseTo(103.502f, cursor.float(), epsilon = 0.001f)
assertEquals(8, cursor.position) assertEquals(8, cursor.position)
} }

View File

@ -0,0 +1,28 @@
package world.phantasmal.lib.fileFormats
import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile
import world.phantasmal.testUtils.assertCloseTo
import kotlin.test.Test
import kotlin.test.assertEquals
class AreaCollisionGeometryTests : LibTestSuite() {
@Test
fun parse_forest_1() = asyncTest {
val obj = parseAreaCollisionGeometry(readFile("/map_forest01c.rel"))
assertEquals(69, obj.meshes.size)
assertEquals(11, obj.meshes[0].vertices.size)
assertCloseTo(-589.5195f, obj.meshes[0].vertices[0].x)
assertCloseTo(16.7166f, obj.meshes[0].vertices[0].y)
assertCloseTo(-218.6852f, obj.meshes[0].vertices[0].z)
assertEquals(12, obj.meshes[0].triangles.size)
assertEquals(0b100000001, obj.meshes[0].triangles[0].flags)
assertEquals(5, obj.meshes[0].triangles[0].index1)
assertEquals(0, obj.meshes[0].triangles[0].index2)
assertEquals(7, obj.meshes[0].triangles[0].index3)
assertCloseTo(0.0137f, obj.meshes[0].triangles[0].normal.x)
assertCloseTo(0.9994f, obj.meshes[0].triangles[0].normal.y)
assertCloseTo(-0.0307f, obj.meshes[0].triangles[0].normal.z)
}
}

View File

@ -1,13 +1,13 @@
package world.phantasmal.lib.fileFormats.ninja package world.phantasmal.lib.fileFormats.ninja
import world.phantasmal.core.Success import world.phantasmal.core.Success
import world.phantasmal.lib.test.asyncTest import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile import world.phantasmal.lib.test.readFile
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class NinjaTests { class NinjaTests : LibTestSuite() {
@Test @Test
fun can_parse_rag_rappy_model() = asyncTest { fun can_parse_rag_rappy_model() = asyncTest {
val result = parseNj(readFile("/RagRappy.nj")) val result = parseNj(readFile("/RagRappy.nj"))

View File

@ -1,11 +1,11 @@
package world.phantasmal.lib.fileFormats.quest package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.lib.test.asyncTest import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile import world.phantasmal.lib.test.readFile
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class BinTests { class BinTests : LibTestSuite() {
@Test @Test
fun parse_quest_towards_the_future() = asyncTest { fun parse_quest_towards_the_future() = asyncTest {
val bin = parseBin(readFile("/quest118_e_decompressed.bin")) val bin = parseBin(readFile("/quest118_e_decompressed.bin"))

View File

@ -5,11 +5,12 @@ import world.phantasmal.lib.assembly.InstructionSegment
import world.phantasmal.lib.assembly.OP_BB_MAP_DESIGNATE import world.phantasmal.lib.assembly.OP_BB_MAP_DESIGNATE
import world.phantasmal.lib.assembly.OP_SET_EPISODE import world.phantasmal.lib.assembly.OP_SET_EPISODE
import world.phantasmal.lib.buffer.Buffer import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.test.LibTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class ByteCodeTests { class ByteCodeTests : LibTestSuite() {
@Test @Test
fun minimal() { fun minimal() {
val buffer = Buffer.fromByteArray(ubyteArrayOf( val buffer = Buffer.fromByteArray(ubyteArrayOf(

View File

@ -1,11 +1,11 @@
package world.phantasmal.lib.fileFormats.quest package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.lib.test.asyncTest import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile import world.phantasmal.lib.test.readFile
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class DatTests { class DatTests : LibTestSuite() {
@Test @Test
fun parse_quest_towards_the_future() = asyncTest { fun parse_quest_towards_the_future() = asyncTest {
val dat = parseDat(readFile("/quest118_e_decompressed.dat")) val dat = parseDat(readFile("/quest118_e_decompressed.dat"))

View File

@ -1,14 +1,14 @@
package world.phantasmal.lib.fileFormats.quest package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.lib.test.asyncTest import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile import world.phantasmal.lib.test.readFile
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class QstTests { class QstTests : LibTestSuite() {
@Test @Test
fun parse_a_GC_quest() = asyncTest{ fun parse_a_GC_quest() = asyncTest {
val cursor = readFile("/lost_heat_sword_gc.qst") val cursor = readFile("/lost_heat_sword_gc.qst")
val qst = parseQst(cursor).unwrap() val qst = parseQst(cursor).unwrap()

View File

@ -2,13 +2,13 @@ package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.core.Success import world.phantasmal.core.Success
import world.phantasmal.lib.assembly.* import world.phantasmal.lib.assembly.*
import world.phantasmal.lib.test.asyncTest import world.phantasmal.lib.test.LibTestSuite
import world.phantasmal.lib.test.readFile import world.phantasmal.lib.test.readFile
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class QuestTests { class QuestTests : LibTestSuite() {
@Test @Test
fun parseBinDatToQuest_with_towards_the_future() = asyncTest { fun parseBinDatToQuest_with_towards_the_future() = asyncTest {
val result = parseBinDatToQuest(readFile("/quest118_e.bin"), readFile("/quest118_e.dat")) val result = parseBinDatToQuest(readFile("/quest118_e.bin"), readFile("/quest118_e.dat"))

View File

@ -0,0 +1,9 @@
package world.phantasmal.lib.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
import world.phantasmal.testUtils.TestContext
abstract class LibTestSuite : AbstractTestSuite<TestContext>() {
override fun createContext(disposer: Disposer) = TestContext(disposer)
}

View File

@ -6,14 +6,6 @@ import world.phantasmal.lib.assembly.assemble
import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.cursor.Cursor
import kotlin.test.assertTrue import kotlin.test.assertTrue
/**
* Ensure you return the value of this function in your test function. On Kotlin/JS this function
* actually returns a Promise. If this promise is not returned from the test function, the testing
* framework won't wait for its completion. This is a workaround for issue
* [https://youtrack.jetbrains.com/issue/KT-22228].
*/
expect fun asyncTest(block: suspend () -> Unit)
expect suspend fun readFile(path: String): Cursor expect suspend fun readFile(path: String): Cursor
fun toInstructions(assembly: String): List<InstructionSegment> { fun toInstructions(assembly: String): List<InstructionSegment> {

Binary file not shown.

View File

@ -1,15 +1,11 @@
package world.phantasmal.lib.test package world.phantasmal.lib.test
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.await import kotlinx.coroutines.await
import kotlinx.coroutines.promise
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.cursor.ArrayBufferCursor import world.phantasmal.lib.cursor.ArrayBufferCursor
import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.cursor.Cursor
actual fun asyncTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() }
actual suspend fun readFile(path: String): Cursor { actual suspend fun readFile(path: String): Cursor {
return window.fetch(path) return window.fetch(path)
.then { .then {

View File

@ -2,15 +2,10 @@
package world.phantasmal.lib.test package world.phantasmal.lib.test
import kotlinx.coroutines.runBlocking
import world.phantasmal.lib.buffer.Buffer import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.cursor import world.phantasmal.lib.cursor.cursor
actual fun asyncTest(block: suspend () -> Unit) {
runBlocking { block() }
}
actual suspend fun readFile(path: String): Cursor { actual suspend fun readFile(path: String): Cursor {
val stream = {}::class.java.getResourceAsStream(path) val stream = {}::class.java.getResourceAsStream(path)
?: error("""Couldn't load resource "$path".""") ?: error("""Couldn't load resource "$path".""")

View File

@ -1,6 +1,6 @@
package world.phantasmal.observable package world.phantasmal.observable
import world.phantasmal.testUtils.TestSuite import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -10,7 +10,7 @@ typealias ObservableAndEmit = Pair<Observable<*>, () -> Unit>
* Test suite for all [Observable] implementations. There is a subclass of this suite for every * Test suite for all [Observable] implementations. There is a subclass of this suite for every
* [Observable] implementation. * [Observable] implementation.
*/ */
abstract class ObservableTests : TestSuite() { abstract class ObservableTests : ObservableTestSuite() {
protected abstract fun create(): ObservableAndEmit protected abstract fun create(): ObservableAndEmit
@Test @Test

View File

@ -0,0 +1,9 @@
package world.phantasmal.observable.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
import world.phantasmal.testUtils.TestContext
abstract class ObservableTestSuite : AbstractTestSuite<TestContext>() {
override fun createContext(disposer: Disposer) = TestContext(disposer)
}

View File

@ -1,10 +1,10 @@
package world.phantasmal.observable.value package world.phantasmal.observable.value
import world.phantasmal.testUtils.TestSuite import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class StaticValTests : TestSuite() { class StaticValTests : ObservableTestSuite() {
@Test @Test
fun observing_StaticVal_should_never_create_leaks() = test { fun observing_StaticVal_should_never_create_leaks() = test {
val static = StaticVal("test value") val static = StaticVal("test value")

View File

@ -1,9 +1,9 @@
package world.phantasmal.observable.value package world.phantasmal.observable.value
import world.phantasmal.testUtils.TestSuite import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.* import kotlin.test.*
class ValCreationTests : TestSuite() { class ValCreationTests : ObservableTestSuite() {
@Test @Test
fun test_value() = test { fun test_value() = test {
assertEquals(7, value(7).value) assertEquals(7, value(7).value)

View File

@ -1,10 +1,10 @@
package world.phantasmal.observable.value.list package world.phantasmal.observable.value.list
import world.phantasmal.testUtils.TestSuite import world.phantasmal.observable.test.ObservableTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class StaticListValTests : TestSuite() { class StaticListValTests : ObservableTestSuite() {
@Test @Test
fun observing_StaticListVal_should_never_create_leaks() = test { fun observing_StaticListVal_should_never_create_leaks() = test {
val static = StaticListVal(listOf(1, 2, 3)) val static = StaticListVal(listOf(1, 2, 3))

View File

@ -9,6 +9,8 @@ kotlin {
browser {} browser {}
} }
jvm()
sourceSets { sourceSets {
commonMain { commonMain {
dependencies { dependencies {
@ -24,5 +26,11 @@ kotlin {
api(kotlin("test-js")) api(kotlin("test-js"))
} }
} }
named("jvmMain") {
dependencies {
api(kotlin("test"))
}
}
} }
} }

View File

@ -0,0 +1,28 @@
package world.phantasmal.testUtils
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.TrackedDisposable
abstract class AbstractTestSuite<Ctx : TestContext> {
fun test(testBlock: Ctx.() -> Unit) {
TrackedDisposable.checkNoLeaks(trackPrecise = true) {
val disposer = Disposer()
testBlock(createContext(disposer))
disposer.dispose()
}
}
fun asyncTest(testBlock: suspend Ctx.() -> Unit) = world.phantasmal.testUtils.asyncTest {
TrackedDisposable.checkNoLeaks(trackPrecise = true) {
val disposer = Disposer()
testBlock(createContext(disposer))
disposer.dispose()
}
}
protected abstract fun createContext(disposer: Disposer): Ctx
}

View File

@ -0,0 +1,12 @@
package world.phantasmal.testUtils
import kotlin.math.abs
import kotlin.test.assertTrue
fun assertCloseTo(expected: Double, actual: Double, epsilon: Double = 0.001) {
assertTrue(abs(expected - actual) <= epsilon)
}
fun assertCloseTo(expected: Float, actual: Float, epsilon: Float = 0.001f) {
assertTrue(abs(expected - actual) <= epsilon)
}

View File

@ -0,0 +1,9 @@
package world.phantasmal.testUtils
/**
* Ensure you return the value of this function in your test function. On Kotlin/JS this function
* actually returns a Promise. If this promise is not returned from the test function, the testing
* framework won't wait for its completion. This is a workaround for issue
* [https://youtrack.jetbrains.com/issue/KT-22228].
*/
expect fun asyncTest(block: suspend () -> Unit)

View File

@ -0,0 +1,11 @@
package world.phantasmal.testUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import world.phantasmal.core.disposable.Disposer
open class TestContext(val disposer: Disposer) {
val scope: CoroutineScope = object : CoroutineScope {
override val coroutineContext = Job()
}
}

View File

@ -1,26 +0,0 @@
package world.phantasmal.testUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.TrackedDisposable
import kotlin.test.assertEquals
abstract class TestSuite {
fun test(block: TestContext.() -> Unit) {
val initialDisposableCount = TrackedDisposable.disposableCount
val disposer = Disposer()
block(TestContext(disposer))
disposer.dispose()
val leakCount = TrackedDisposable.disposableCount - initialDisposableCount
assertEquals(0, leakCount, "TrackedDisposables were leaked")
}
class TestContext(val disposer: Disposer) {
val scope: CoroutineScope = object : CoroutineScope {
override val coroutineContext = Job()
}
}
}

View File

@ -0,0 +1,6 @@
package world.phantasmal.testUtils
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
actual fun asyncTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() }

View File

@ -0,0 +1,9 @@
@file:JvmName("AsyncTestJvm")
package world.phantasmal.testUtils
import kotlinx.coroutines.runBlocking
actual fun asyncTest(block: suspend () -> Unit) {
runBlocking { block() }
}

View File

@ -13,8 +13,8 @@ import world.phantasmal.web.questEditor.controllers.QuestInfoController
import world.phantasmal.web.questEditor.loading.AreaAssetLoader import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.EntityAssetLoader import world.phantasmal.web.questEditor.loading.EntityAssetLoader
import world.phantasmal.web.questEditor.loading.QuestLoader import world.phantasmal.web.questEditor.loading.QuestLoader
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
import world.phantasmal.web.questEditor.rendering.EntityManipulator import world.phantasmal.web.questEditor.rendering.EntityManipulator
import world.phantasmal.web.questEditor.rendering.QuestEditorMeshManager
import world.phantasmal.web.questEditor.rendering.QuestRenderer import world.phantasmal.web.questEditor.rendering.QuestRenderer
import world.phantasmal.web.questEditor.stores.AreaStore import world.phantasmal.web.questEditor.stores.AreaStore
import world.phantasmal.web.questEditor.stores.QuestEditorStore import world.phantasmal.web.questEditor.stores.QuestEditorStore
@ -63,7 +63,7 @@ class QuestEditor(
// Main Widget // Main Widget
return QuestEditorWidget( return QuestEditorWidget(
scope, scope,
QuestEditorToolbar(scope, toolbarController), { s -> QuestEditorToolbar(s, toolbarController) },
{ s -> QuestInfoWidget(s, questInfoController) }, { s -> QuestInfoWidget(s, questInfoController) },
{ s -> NpcCountsWidget(s, npcCountsController) }, { s -> NpcCountsWidget(s, npcCountsController) },
{ s -> QuestEditorRendererWidget(s, canvas, renderer) } { s -> QuestEditorRendererWidget(s, canvas, renderer) }

View File

@ -39,17 +39,13 @@ class EntityManipulator(
init { init {
state = IdleState(questEditorStore, renderer, enabled) state = IdleState(questEditorStore, renderer, enabled)
observe(questEditorStore.selectedEntity, ::selectedEntityChanged) observe(questEditorStore.selectedEntity) { state.cancel() }
addDisposables( addDisposables(
disposableListener(renderer.canvas, "pointerdown", ::onPointerDown) disposableListener(renderer.canvas, "pointerdown", ::onPointerDown)
) )
} }
private fun selectedEntityChanged(entity: QuestEntityModel<*, *>?) {
state.cancel()
}
private fun onPointerDown(e: PointerEvent) { private fun onPointerDown(e: PointerEvent) {
processPointerEvent(e) processPointerEvent(e)

View File

@ -17,9 +17,12 @@ private class TestWidget(scope: CoroutineScope) : Widget(scope) {
} }
} }
/**
* Takes ownership of the widgets created by the given createWidget functions.
*/
class QuestEditorWidget( class QuestEditorWidget(
scope: CoroutineScope, scope: CoroutineScope,
private val toolbar: Widget, private val createToolbar: (CoroutineScope) -> Widget,
private val createQuestInfoWidget: (CoroutineScope) -> Widget, private val createQuestInfoWidget: (CoroutineScope) -> Widget,
private val createNpcCountsWidget: (CoroutineScope) -> Widget, private val createNpcCountsWidget: (CoroutineScope) -> Widget,
private val createQuestRendererWidget: (CoroutineScope) -> Widget, private val createQuestRendererWidget: (CoroutineScope) -> Widget,
@ -28,7 +31,7 @@ class QuestEditorWidget(
div { div {
className = "pw-quest-editor-quest-editor" className = "pw-quest-editor-quest-editor"
addChild(toolbar) addChild(createToolbar(scope))
addChild(DockWidget( addChild(DockWidget(
scope, scope,
item = DockedRow( item = DockedRow(

View File

@ -31,6 +31,11 @@ class Viewer(
val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas))) val renderer = addDisposable(MeshRenderer(viewerStore, canvas, createEngine(canvas)))
// Main Widget // Main Widget
return ViewerWidget(scope, ViewerToolbar(scope, viewerToolbarController), canvas, renderer) return ViewerWidget(
scope,
{ s -> ViewerToolbar(s, viewerToolbarController) },
canvas,
renderer
)
} }
} }

View File

@ -8,9 +8,12 @@ import world.phantasmal.web.core.widgets.RendererWidget
import world.phantasmal.webui.dom.div import world.phantasmal.webui.dom.div
import world.phantasmal.webui.widgets.Widget import world.phantasmal.webui.widgets.Widget
/**
* Takes ownership of the widget returned by [createToolbar].
*/
class ViewerWidget( class ViewerWidget(
scope: CoroutineScope, scope: CoroutineScope,
private val toolbar: Widget, private val createToolbar: (CoroutineScope) -> Widget,
private val canvas: HTMLCanvasElement, private val canvas: HTMLCanvasElement,
private val renderer: Renderer, private val renderer: Renderer,
) : Widget(scope) { ) : Widget(scope) {
@ -18,7 +21,7 @@ class ViewerWidget(
div { div {
className = "pw-viewer-viewer" className = "pw-viewer-viewer"
addChild(toolbar) addChild(createToolbar(scope))
div { div {
className = "pw-viewer-viewer-container" className = "pw-viewer-viewer-container"

View File

@ -1,41 +1,26 @@
package world.phantasmal.web.application package world.phantasmal.web.application
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.Disposer import world.phantasmal.core.disposable.Disposer
import world.phantasmal.core.disposable.disposable
import world.phantasmal.core.disposable.use import world.phantasmal.core.disposable.use
import world.phantasmal.testUtils.TestSuite import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test import kotlin.test.Test
class ApplicationTests : TestSuite() { class ApplicationTests : WebTestSuite() {
@Test @Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test { fun initialization_and_shutdown_should_succeed_without_throwing() = test {
(listOf(null) + PwTool.values().toList()).forEach { tool -> (listOf(null) + PwToolType.values().toList()).forEach { tool ->
Disposer().use { disposer -> Disposer().use { disposer ->
val httpClient = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}
disposer.add(disposable { httpClient.cancel() })
val appUrl = TestApplicationUrl(if (tool == null) "" else "/${tool.slug}") val appUrl = TestApplicationUrl(if (tool == null) "" else "/${tool.slug}")
disposer.add( disposer.add(
Application( Application(
scope, scope,
rootElement = document.body!!, rootElement = document.body!!,
assetLoader = AssetLoader(basePath = "", httpClient), assetLoader = components.assetLoader,
applicationUrl = appUrl, applicationUrl = appUrl,
createEngine = { Engine(it) } createEngine = { Engine(it) }
) )

View File

@ -1,14 +1,15 @@
package world.phantasmal.web.core.controllers package world.phantasmal.web.core.controllers
import world.phantasmal.testUtils.TestSuite import world.phantasmal.testUtils.TestContext
import world.phantasmal.web.core.PwTool import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
class PathAwareTabControllerTests : TestSuite() { class PathAwareTabControllerTests : WebTestSuite() {
@Test @Test
fun activeTab_is_initialized_correctly() = test { fun activeTab_is_initialized_correctly() = test {
setup { ctrl, appUrl -> setup { ctrl, appUrl ->
@ -23,7 +24,7 @@ class PathAwareTabControllerTests : TestSuite() {
setup { ctrl, appUrl -> setup { ctrl, appUrl ->
ctrl.setActiveTab(ctrl.tabs[2]) ctrl.setActiveTab(ctrl.tabs[2])
assertEquals("/${PwTool.HuntOptimizer.slug}/c", appUrl.url.value) assertEquals("/${PwToolType.HuntOptimizer.slug}/c", appUrl.url.value)
assertEquals(1, appUrl.historyEntries) assertEquals(1, appUrl.historyEntries)
assertFalse(appUrl.canGoForward) assertFalse(appUrl.canGoForward)
} }
@ -32,7 +33,7 @@ class PathAwareTabControllerTests : TestSuite() {
@Test @Test
fun activeTab_changes_when_applicationUrl_changes() = test { fun activeTab_changes_when_applicationUrl_changes() = test {
setup { ctrl, applicationUrl -> setup { ctrl, applicationUrl ->
applicationUrl.pushUrl("/${PwTool.HuntOptimizer.slug}/c") applicationUrl.pushUrl("/${PwToolType.HuntOptimizer.slug}/c")
assertEquals("/c", ctrl.activeTab.value?.path) assertEquals("/c", ctrl.activeTab.value?.path)
} }
@ -44,7 +45,7 @@ class PathAwareTabControllerTests : TestSuite() {
val uiStore = disposer.add(UiStore(scope, appUrl)) val uiStore = disposer.add(UiStore(scope, appUrl))
disposer.add( disposer.add(
PathAwareTabController(scope, uiStore, PwTool.HuntOptimizer, listOf( PathAwareTabController(uiStore, PwToolType.HuntOptimizer, listOf(
PathAwareTab("A", "/a"), PathAwareTab("A", "/a"),
PathAwareTab("B", "/b"), PathAwareTab("B", "/b"),
PathAwareTab("C", "/c"), PathAwareTab("C", "/c"),
@ -55,11 +56,11 @@ class PathAwareTabControllerTests : TestSuite() {
assertFalse(appUrl.canGoForward) assertFalse(appUrl.canGoForward)
assertEquals("/${uiStore.defaultTool.slug}", appUrl.url.value) assertEquals("/${uiStore.defaultTool.slug}", appUrl.url.value)
uiStore.setCurrentTool(PwTool.HuntOptimizer) uiStore.setCurrentTool(PwToolType.HuntOptimizer)
assertEquals(1, appUrl.historyEntries) assertEquals(1, appUrl.historyEntries)
assertFalse(appUrl.canGoForward) assertFalse(appUrl.canGoForward)
assertEquals("/${PwTool.HuntOptimizer.slug}", appUrl.url.value) assertEquals("/${PwToolType.HuntOptimizer.slug}", appUrl.url.value)
appUrl.back() appUrl.back()
@ -69,12 +70,12 @@ class PathAwareTabControllerTests : TestSuite() {
private fun TestContext.setup( private fun TestContext.setup(
block: (PathAwareTabController<PathAwareTab>, applicationUrl: TestApplicationUrl) -> Unit, block: (PathAwareTabController<PathAwareTab>, applicationUrl: TestApplicationUrl) -> Unit,
) { ) {
val applicationUrl = TestApplicationUrl("/${PwTool.HuntOptimizer.slug}/b") val applicationUrl = TestApplicationUrl("/${PwToolType.HuntOptimizer.slug}/b")
val uiStore = disposer.add(UiStore(scope, applicationUrl)) val uiStore = disposer.add(UiStore(scope, applicationUrl))
uiStore.setCurrentTool(PwTool.HuntOptimizer) uiStore.setCurrentTool(PwToolType.HuntOptimizer)
val ctrl = disposer.add( val ctrl = disposer.add(
PathAwareTabController(scope, uiStore, PwTool.HuntOptimizer, listOf( PathAwareTabController(uiStore, PwToolType.HuntOptimizer, listOf(
PathAwareTab("A", "/a"), PathAwareTab("A", "/a"),
PathAwareTab("B", "/b"), PathAwareTab("B", "/b"),
PathAwareTab("C", "/c"), PathAwareTab("C", "/c"),

View File

@ -1,20 +1,20 @@
package world.phantasmal.web.core.store package world.phantasmal.web.core.store
import world.phantasmal.testUtils.TestSuite import world.phantasmal.web.core.PwToolType
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
class UiStoreTests : TestSuite() { class UiStoreTests : WebTestSuite() {
@Test @Test
fun applicationUrl_is_initialized_correctly() = test { fun applicationUrl_is_initialized_correctly() = test {
val applicationUrl = TestApplicationUrl("/") val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl)) val uiStore = disposer.add(UiStore(scope, applicationUrl))
assertEquals(PwTool.Viewer, uiStore.currentTool.value) assertEquals(PwToolType.Viewer, uiStore.currentTool.value)
assertEquals("/${PwTool.Viewer.slug}", applicationUrl.url.value) assertEquals("/${PwToolType.Viewer.slug}", applicationUrl.url.value)
} }
@Test @Test
@ -22,7 +22,7 @@ class UiStoreTests : TestSuite() {
val applicationUrl = TestApplicationUrl("/") val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl)) val uiStore = disposer.add(UiStore(scope, applicationUrl))
PwTool.values().forEach { tool -> PwToolType.values().forEach { tool ->
uiStore.setCurrentTool(tool) uiStore.setCurrentTool(tool)
assertEquals(tool, uiStore.currentTool.value) assertEquals(tool, uiStore.currentTool.value)
@ -35,13 +35,13 @@ class UiStoreTests : TestSuite() {
val applicationUrl = TestApplicationUrl("/") val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl)) val uiStore = disposer.add(UiStore(scope, applicationUrl))
assertEquals(PwTool.Viewer, uiStore.currentTool.value) assertEquals(PwToolType.Viewer, uiStore.currentTool.value)
assertEquals("/${PwTool.Viewer.slug}", applicationUrl.url.value) assertEquals("/${PwToolType.Viewer.slug}", applicationUrl.url.value)
listOf("/models", "/textures", "/animations").forEach { prefix -> listOf("/models", "/textures", "/animations").forEach { prefix ->
uiStore.setPathPrefix(prefix, replace = false) uiStore.setPathPrefix(prefix, replace = false)
assertEquals("/${PwTool.Viewer.slug}${prefix}", applicationUrl.url.value) assertEquals("/${PwToolType.Viewer.slug}${prefix}", applicationUrl.url.value)
} }
} }
@ -50,7 +50,7 @@ class UiStoreTests : TestSuite() {
val applicationUrl = TestApplicationUrl("/") val applicationUrl = TestApplicationUrl("/")
val uiStore = disposer.add(UiStore(scope, applicationUrl)) val uiStore = disposer.add(UiStore(scope, applicationUrl))
PwTool.values().forEach { tool -> PwToolType.values().forEach { tool ->
listOf("/a", "/b", "/c").forEach { path -> listOf("/a", "/b", "/c").forEach { path ->
applicationUrl.url.value = "/${tool.slug}$path" applicationUrl.url.value = "/${tool.slug}$path"
@ -67,13 +67,13 @@ class UiStoreTests : TestSuite() {
assertEquals("/${uiStore.defaultTool.slug}", appUrl.url.value) assertEquals("/${uiStore.defaultTool.slug}", appUrl.url.value)
uiStore.setCurrentTool(PwTool.HuntOptimizer) uiStore.setCurrentTool(PwToolType.HuntOptimizer)
assertEquals("/${PwTool.HuntOptimizer.slug}", appUrl.url.value) assertEquals("/${PwToolType.HuntOptimizer.slug}", appUrl.url.value)
uiStore.setPathPrefix("/prefix", replace = true) uiStore.setPathPrefix("/prefix", replace = true)
assertEquals("/${PwTool.HuntOptimizer.slug}/prefix", appUrl.url.value) assertEquals("/${PwToolType.HuntOptimizer.slug}/prefix", appUrl.url.value)
appUrl.back() appUrl.back()
@ -81,6 +81,6 @@ class UiStoreTests : TestSuite() {
appUrl.forward() appUrl.forward()
assertEquals("/${PwTool.HuntOptimizer.slug}/prefix", appUrl.url.value) assertEquals("/${PwToolType.HuntOptimizer.slug}/prefix", appUrl.url.value)
} }
} }

View File

@ -1,37 +1,18 @@
package world.phantasmal.web.huntOptimizer package world.phantasmal.web.huntOptimizer
import io.ktor.client.* import world.phantasmal.web.core.PwToolType
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.disposable
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.core.PwTool
import world.phantasmal.web.core.stores.UiStore import world.phantasmal.web.core.stores.UiStore
import world.phantasmal.web.test.TestApplicationUrl import world.phantasmal.web.test.TestApplicationUrl
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test import kotlin.test.Test
class HuntOptimizerTests : TestSuite() { class HuntOptimizerTests : WebTestSuite() {
@Test @Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test { fun initialization_and_shutdown_should_succeed_without_throwing() = test {
val httpClient = HttpClient { val uiStore =
install(JsonFeature) { disposer.add(UiStore(scope, TestApplicationUrl("/${PwToolType.HuntOptimizer}")))
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}
disposer.add(disposable { httpClient.cancel() })
val uiStore = disposer.add(UiStore(scope, TestApplicationUrl("/${PwTool.HuntOptimizer}"))) val huntOptimizer = disposer.add(HuntOptimizer(components.assetLoader, uiStore))
disposer.add(huntOptimizer.initialize(scope))
disposer.add(
HuntOptimizer(
scope,
AssetLoader(basePath = "", httpClient),
uiStore
)
)
} }
} }

View File

@ -1,33 +1,15 @@
package world.phantasmal.web.questEditor package world.phantasmal.web.questEditor
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.disposable
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.externals.babylon.Engine import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test import kotlin.test.Test
class QuestEditorTests : TestSuite() { class QuestEditorTests : WebTestSuite() {
@Test @Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test { fun initialization_and_shutdown_should_succeed_without_throwing() = test {
val httpClient = HttpClient { val questEditor = disposer.add(
install(JsonFeature) { QuestEditor(components.assetLoader, createEngine = { Engine(it) })
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}
disposer.add(disposable { httpClient.cancel() })
disposer.add(
QuestEditor(
scope,
AssetLoader(basePath = "", httpClient),
createEngine = { Engine(it) }
)
) )
disposer.add(questEditor.initialize(scope))
} }
} }

View File

@ -0,0 +1,42 @@
package world.phantasmal.web.questEditor.controllers
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.NpcType
import world.phantasmal.web.test.WebTestSuite
import world.phantasmal.web.test.createQuestModel
import world.phantasmal.web.test.createQuestNpcModel
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class NpcCountsControllerTests : WebTestSuite() {
@Test
fun exposes_correct_model_before_and_after_a_quest_is_loaded() = test {
val store = components.questEditorStore
val ctrl = disposer.add(NpcCountsController(store))
assertTrue(ctrl.unavailable.value)
store.setCurrentQuest(createQuestModel(
episode = Episode.I,
npcs = listOf(
createQuestNpcModel(NpcType.Scientist, Episode.I),
createQuestNpcModel(NpcType.Nurse, Episode.I),
createQuestNpcModel(NpcType.Nurse, Episode.I),
createQuestNpcModel(NpcType.Principal, Episode.I),
createQuestNpcModel(NpcType.Nurse, Episode.I),
createQuestNpcModel(NpcType.Scientist, Episode.I),
)
))
assertFalse(ctrl.unavailable.value)
assertEquals(3, ctrl.npcCounts.value.size)
assertEquals("Principal", ctrl.npcCounts.value[0].name)
assertEquals("1", ctrl.npcCounts.value[0].count)
assertEquals("Scientist", ctrl.npcCounts.value[1].name)
assertEquals("2", ctrl.npcCounts.value[1].count)
assertEquals("Nurse", ctrl.npcCounts.value[2].name)
assertEquals("3", ctrl.npcCounts.value[2].count)
}
}

View File

@ -0,0 +1,35 @@
package world.phantasmal.web.questEditor.controllers
import org.w3c.files.File
import world.phantasmal.core.Failure
import world.phantasmal.core.Severity
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class QuestEditorToolbarControllerTests : WebTestSuite() {
@Test
fun a_failure_is_exposed_when_openFiles_fails() = asyncTest {
val ctrl = disposer.add(QuestEditorToolbarController(
components.questLoader,
components.areaStore,
components.questEditorStore
))
assertNull(ctrl.result.value)
ctrl.openFiles(listOf(File(arrayOf(), "unknown.extension")))
val result = ctrl.result.value
assertTrue(result is Failure)
assertEquals(1, result.problems.size)
assertEquals(Severity.Error, result.problems.first().severity)
assertEquals(
"Please select a .qst file or one .bin and one .dat file.",
result.problems.first().uiMessage
)
}
}

View File

@ -0,0 +1,36 @@
package world.phantasmal.web.questEditor.controllers
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.web.test.WebTestSuite
import world.phantasmal.web.test.createQuestModel
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class QuestInfoControllerTests : WebTestSuite() {
@Test
fun exposes_correct_model_before_and_after_a_quest_is_loaded() = test {
val store = components.questEditorStore
val ctrl = disposer.add(QuestInfoController(store))
assertTrue(ctrl.unavailable.value)
assertTrue(ctrl.disabled.value)
store.setCurrentQuest(createQuestModel(
id = 25,
name = "A Quest",
shortDescription = "A short description.",
longDescription = "A long description.",
episode = Episode.II
))
assertFalse(ctrl.unavailable.value)
assertFalse(ctrl.disabled.value)
assertEquals("II", ctrl.episode.value)
assertEquals(25, ctrl.id.value)
assertEquals("A Quest", ctrl.name.value)
assertEquals("A short description.", ctrl.shortDescription.value)
assertEquals("A long description.", ctrl.longDescription.value)
}
}

View File

@ -0,0 +1,100 @@
package world.phantasmal.web.test
import io.ktor.client.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.coroutines.cancel
import world.phantasmal.core.disposable.Disposable
import world.phantasmal.core.disposable.disposable
import world.phantasmal.testUtils.TestContext
import world.phantasmal.web.core.loading.AssetLoader
import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.externals.babylon.Scene
import world.phantasmal.web.questEditor.loading.AreaAssetLoader
import world.phantasmal.web.questEditor.loading.QuestLoader
import world.phantasmal.web.questEditor.stores.AreaStore
import world.phantasmal.web.questEditor.stores.QuestEditorStore
import kotlin.reflect.KProperty
/**
* Assigning a disposable to any of the properties in this class will add the assigned value to
* [ctx]'s disposer.
*/
class TestComponents(private val ctx: TestContext) {
var httpClient: HttpClient by default {
HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
})
}
}.also {
ctx.disposer.add(disposable { it.cancel() })
}
}
// Babylon.js
var scene: Scene by default { Scene(Engine(null)) }
// Asset Loaders
var assetLoader: AssetLoader by default { AssetLoader(basePath = "", httpClient) }
var areaAssetLoader: AreaAssetLoader by default {
AreaAssetLoader(ctx.scope, assetLoader, scene)
}
var questLoader: QuestLoader by default { QuestLoader(ctx.scope, assetLoader) }
// Stores
var areaStore: AreaStore by default { AreaStore(ctx.scope, areaAssetLoader) }
var questEditorStore: QuestEditorStore by default {
QuestEditorStore(ctx.scope, areaStore)
}
private fun <T> default(defaultValue: () -> T) = LazyDefault {
val value = defaultValue()
if (value is Disposable) {
ctx.disposer.add(value)
}
value
}
private inner class LazyDefault<T>(private val defaultValue: () -> T) {
private var initialized = false
private var value: T? = null
operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
if (!initialized) {
val value = defaultValue()
if (value is Disposable) {
ctx.disposer.add(value)
}
this.value = value
initialized = true
}
return value.unsafeCast<T>()
}
operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: T) {
require(initialized) {
"Property ${prop.name} is already initialized."
}
if (value is Disposable) {
ctx.disposer.add(value)
}
this.value = value
initialized = true
}
}
}

View File

@ -0,0 +1,32 @@
package world.phantasmal.web.test
import world.phantasmal.lib.fileFormats.quest.Episode
import world.phantasmal.lib.fileFormats.quest.NpcType
import world.phantasmal.lib.fileFormats.quest.QuestNpc
import world.phantasmal.web.questEditor.models.QuestModel
import world.phantasmal.web.questEditor.models.QuestNpcModel
import world.phantasmal.web.questEditor.models.QuestObjectModel
fun createQuestModel(
id: Int = 1,
name: String = "Test",
shortDescription: String = name,
longDescription: String = name,
episode: Episode = Episode.I,
npcs: List<QuestNpcModel> = emptyList(),
objects: List<QuestObjectModel> = emptyList(),
): QuestModel =
QuestModel(
id,
language = 1,
name,
shortDescription,
longDescription,
episode,
emptyMap(),
npcs.toMutableList(),
objects.toMutableList(),
) { _, _, _ -> null }
fun createQuestNpcModel(type: NpcType, episode: Episode): QuestNpcModel =
QuestNpcModel(QuestNpc(type, episode, areaId = 0, wave = 0), wave = null)

View File

@ -0,0 +1,9 @@
package world.phantasmal.web.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.TestContext
open class WebTestContext(disposer: Disposer) : TestContext(disposer) {
@Suppress("LeakingThis")
val components = TestComponents(this)
}

View File

@ -0,0 +1,8 @@
package world.phantasmal.web.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
abstract class WebTestSuite : AbstractTestSuite<WebTestContext>() {
override fun createContext(disposer: Disposer) = WebTestContext(disposer)
}

View File

@ -0,0 +1,15 @@
package world.phantasmal.web.viewer
import world.phantasmal.web.externals.babylon.Engine
import world.phantasmal.web.test.WebTestSuite
import kotlin.test.Test
class ViewerTests : WebTestSuite() {
@Test
fun initialization_and_shutdown_should_succeed_without_throwing() = test {
val viewer = disposer.add(
Viewer(createEngine = { Engine(it) })
)
disposer.add(viewer.initialize(scope))
}
}

View File

@ -0,0 +1,9 @@
package world.phantasmal.webui.test
import world.phantasmal.core.disposable.Disposer
import world.phantasmal.testUtils.AbstractTestSuite
import world.phantasmal.testUtils.TestContext
abstract class WebuiTestSuite : AbstractTestSuite<TestContext>() {
override fun createContext(disposer: Disposer) = TestContext(disposer)
}

View File

@ -6,14 +6,13 @@ import world.phantasmal.observable.value.Val
import world.phantasmal.observable.value.falseVal import world.phantasmal.observable.value.falseVal
import world.phantasmal.observable.value.mutableVal import world.phantasmal.observable.value.mutableVal
import world.phantasmal.observable.value.trueVal import world.phantasmal.observable.value.trueVal
import world.phantasmal.testUtils.TestSuite
import world.phantasmal.webui.dom.div import world.phantasmal.webui.dom.div
import world.phantasmal.webui.test.WebuiTestSuite
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail
class WidgetTests : TestSuite() { class WidgetTests : WebuiTestSuite() {
@Test @Test
fun ancestorHidden_and_selfOrAncestorHidden_should_update_when_hidden_changes() = test { fun ancestorHidden_and_selfOrAncestorHidden_should_update_when_hidden_changes() = test {
val parentHidden = mutableVal(false) val parentHidden = mutableVal(false)