diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd85143c14..411c45e8f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ junit = "5.10.2" kotlin = "1.8.22" kotlinResult = "1.1.18" ktor = "2.3.6" +mockk = "1.12.0" mozillaRhino = "1.7.14" picocli = "4.6.3" selenium = "4.13.0" @@ -100,6 +101,7 @@ ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" } ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" } ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "mozillaRhino" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" } diff --git a/maestro-utils/build.gradle.kts b/maestro-utils/build.gradle.kts index 7a6e96c372..fea9bf58cf 100644 --- a/maestro-utils/build.gradle.kts +++ b/maestro-utils/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { api(libs.square.okio) + testImplementation(libs.mockk) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) testImplementation(libs.google.truth) diff --git a/maestro-utils/src/test/kotlin/CollectionsTest.kt b/maestro-utils/src/test/kotlin/CollectionsTest.kt new file mode 100644 index 0000000000..bf1f3ba295 --- /dev/null +++ b/maestro-utils/src/test/kotlin/CollectionsTest.kt @@ -0,0 +1,61 @@ +import maestro.utils.isRegularFile +import maestro.utils.isSingleFile +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.nio.file.Files + +class CollectionsTest { + + @Test + fun `isSingleFile should return true for a single regular file`() { + val file = Files.createTempFile("testFile", ".txt").toFile() + val files = listOf(file) + assertTrue(files.isSingleFile) + file.delete() + } + + @Test + fun `isSingleFile should return false for multiple files`() { + val file1 = Files.createTempFile("testFile1", ".txt").toFile() + val file2 = Files.createTempFile("testFile2", ".txt").toFile() + val files = listOf(file1, file2) + assertFalse(files.isSingleFile) + file1.delete() + file2.delete() + } + + @Test + fun `isSingleFile should return false for a single directory`() { + val dir = Files.createTempDirectory("testDir").toFile() + val files = listOf(dir) + assertFalse(files.isSingleFile) + dir.delete() + } + + @Test + fun `isRegularFile should return true for a single regular file`() { + val file = Files.createTempFile("testFile", ".txt") + val paths = listOf(file) + assertTrue(paths.isRegularFile) + Files.delete(file) + } + + @Test + fun `isRegularFile should return false for multiple files`() { + val file1 = Files.createTempFile("testFile1", ".txt") + val file2 = Files.createTempFile("testFile2", ".txt") + val paths = listOf(file1, file2) + assertFalse(paths.isRegularFile) + Files.delete(file1) + Files.delete(file2) + } + + @Test + fun `isRegularFile should return false for a single directory`() { + val dir = Files.createTempDirectory("testDir") + val paths = listOf(dir) + assertFalse(paths.isRegularFile) + Files.delete(dir) + } +} \ No newline at end of file diff --git a/maestro-utils/src/test/kotlin/DepthTrackerTest.kt b/maestro-utils/src/test/kotlin/DepthTrackerTest.kt new file mode 100644 index 0000000000..b61aa6177c --- /dev/null +++ b/maestro-utils/src/test/kotlin/DepthTrackerTest.kt @@ -0,0 +1,27 @@ +import maestro.utils.DepthTracker +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class DepthTrackerTest { + + @BeforeEach + fun setUp() { + DepthTracker.trackDepth(0) + } + + @Test + fun `trackDepth should update currentDepth and maxDepth`() { + DepthTracker.trackDepth(10) + assertEquals(10, DepthTracker.getMaxDepth()) + } + + @Test + fun `getMaxDepth should return the maximum depth tracked`() { + DepthTracker.trackDepth(3) + DepthTracker.trackDepth(2) + DepthTracker.trackDepth(5) + + assertEquals(5, DepthTracker.getMaxDepth()) + } +} \ No newline at end of file diff --git a/maestro-utils/src/test/kotlin/InsightTest.kt b/maestro-utils/src/test/kotlin/InsightTest.kt new file mode 100644 index 0000000000..b7b40c44c4 --- /dev/null +++ b/maestro-utils/src/test/kotlin/InsightTest.kt @@ -0,0 +1,43 @@ +import maestro.utils.Insight +import maestro.utils.Insights +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class InsightsTest { + + @Test + fun `report should update insight and notify listeners`() { + val insight = Insight("Test message", Insight.Level.INFO) + var notifiedInsight: Insight? = null + + Insights.onInsightsUpdated { notifiedInsight = it } + Insights.report(insight) + + assertEquals(insight, notifiedInsight) + } + + @Test + fun `onInsightsUpdated should add a listener`() { + val insight = Insight("Test message", Insight.Level.INFO) + var notified = false + + Insights.onInsightsUpdated { notified = true } + Insights.report(insight) + + assertTrue(notified) + } + + @Test + fun `unregisterListener should remove a listener`() { + val insight = Insight("Test message", Insight.Level.INFO) + var notified = false + val listener: (Insight) -> Unit = { notified = true } + + Insights.onInsightsUpdated(listener) + Insights.unregisterListener(listener) + Insights.report(insight) + + assertTrue(!notified) + } +} \ No newline at end of file diff --git a/maestro-utils/src/test/kotlin/MaestroTimerTest.kt b/maestro-utils/src/test/kotlin/MaestroTimerTest.kt new file mode 100644 index 0000000000..59166beb9b --- /dev/null +++ b/maestro-utils/src/test/kotlin/MaestroTimerTest.kt @@ -0,0 +1,70 @@ +import maestro.utils.MaestroTimer +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class MaestroTimerTest { + + @BeforeEach + fun setUp() { + MaestroTimer.setTimerFunc { _, ms -> Thread.sleep(ms) } + } + + @Test + fun `withTimeout should return result within timeout`() { + val result = MaestroTimer.withTimeout(1000) { + "Success" + } + + assertEquals("Success", result) + } + + @Test + fun `withTimeout should return null if body is null`() { + val result = MaestroTimer.withTimeout(1000) { + null + } + + assertNull(result) + } + + @Test + fun `retryUntilTrue should return true if block succeeds within timeout`() { + val result = MaestroTimer.retryUntilTrue(1000) { + true + } + + assertTrue(result) + } + + @Test + fun `retryUntilTrue should return false if block fails within timeout`() { + val result = MaestroTimer.retryUntilTrue(100) { + false + } + + assertFalse(result) + } + + @Test + fun `retryUntilTrue should handle exceptions and continue retrying`() { + var attempts = 0 + val result = MaestroTimer.retryUntilTrue(1000, 100, { }) { + attempts++ + if (attempts < 3) throw Exception("Test exception") + true + } + + assertTrue(result) + assertEquals(3, attempts) + } + + @Test + fun `setTimerFunc should change the sleep function`() { + var sleepCalled = false + MaestroTimer.setTimerFunc { _, _ -> sleepCalled = true } + MaestroTimer.sleep(MaestroTimer.Reason.BUFFER, 100) + + assertTrue(sleepCalled) + } +} \ No newline at end of file diff --git a/maestro-utils/src/test/kotlin/SocketUtilsTest.kt b/maestro-utils/src/test/kotlin/SocketUtilsTest.kt new file mode 100644 index 0000000000..605a745884 --- /dev/null +++ b/maestro-utils/src/test/kotlin/SocketUtilsTest.kt @@ -0,0 +1,55 @@ +import io.mockk.every +import io.mockk.mockkStatic +import maestro.utils.SocketUtils +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.net.Inet4Address +import java.net.InetAddress +import java.net.NetworkInterface +import java.util.* + +class SocketUtilsTest { + + private fun List.toEnumeration(): Enumeration = Collections.enumeration(this) + + @Test + fun `nextFreePort should return a free port within the specified range`() { + val from = 5000 + val to = 5100 + val port = SocketUtils.nextFreePort(from, to) + + assertTrue(port in from..to) + } + + @Test + fun `nextFreePort should throw IllegalStateException when no ports are available in the range`() { + val from = 100000 + val to = 100010 + + assertThrows(IllegalStateException::class.java) { + SocketUtils.nextFreePort(from, to) + } + } + + @Test + fun `localIp should return local IP address`() { + val ip = SocketUtils.localIp() + + assertNotNull(ip) + assertTrue(ip.startsWith("192") || ip.startsWith("10") || ip.startsWith("172") || ip.startsWith("127")) + assertTrue(InetAddress.getByName(ip) is Inet4Address) + } + + @Test + fun `localIp should return localhost address if no network interfaces are available`() { + mockkStatic(NetworkInterface::class) + every { NetworkInterface.getNetworkInterfaces() } returns listOf().toEnumeration() + + val ip = SocketUtils.localIp() + + assertEquals(InetAddress.getLocalHost().hostAddress, ip) + } +} \ No newline at end of file diff --git a/maestro-utils/src/test/kotlin/StringsTest.kt b/maestro-utils/src/test/kotlin/StringsTest.kt new file mode 100644 index 0000000000..410be2f198 --- /dev/null +++ b/maestro-utils/src/test/kotlin/StringsTest.kt @@ -0,0 +1,42 @@ +import maestro.utils.chunkStringByWordCount +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class StringsTest { + + @Test + fun `chunkStringByWordCount should return empty list for empty string`() { + val result = "".chunkStringByWordCount(2) + assertEquals(emptyList(), result) + } + + @Test + fun `chunkStringByWordCount should return single chunk for string with fewer words than chunk size`() { + val result = "hello world".chunkStringByWordCount(3) + assertEquals(listOf("hello world"), result) + } + + @Test + fun `chunkStringByWordCount should return multiple chunks for string with more words than chunk size`() { + val result = "hello world this is a test".chunkStringByWordCount(2) + assertEquals(listOf("hello world", "this is", "a test"), result) + } + + @Test + fun `chunkStringByWordCount should handle exact chunk size`() { + val result = "hello world this is a test".chunkStringByWordCount(5) + assertEquals(listOf("hello world this is a", "test"), result) + } + + @Test + fun `chunkStringByWordCount should handle trailing spaces`() { + val result = " hello world ".chunkStringByWordCount(1) + assertEquals(listOf("hello", "world"), result) + } + + @Test + fun `chunkStringByWordCount should handle multiple spaces between words`() { + val result = "hello world this is a test".chunkStringByWordCount(2) + assertEquals(listOf("hello world", "this is", "a test"), result) + } +} \ No newline at end of file diff --git a/maestro-utils/src/test/kotlin/network/ErrorsTest.kt b/maestro-utils/src/test/kotlin/network/ErrorsTest.kt new file mode 100644 index 0000000000..db7b67a3ff --- /dev/null +++ b/maestro-utils/src/test/kotlin/network/ErrorsTest.kt @@ -0,0 +1,78 @@ +package network + +import maestro.utils.network.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class ErrorsTest { + + @Test + fun `InputFieldNotFound should have correct message`() { + assertThrows("Unable to find focused input field") { + throw InputFieldNotFound() + } + } + + @Test + fun `UnknownFailure should have correct message`() { + val errorMessage = "An unknown error occurred" + + assertThrows(errorMessage) { + throw UnknownFailure(errorMessage) + } + } + + @Test + fun `XCUITestServerResult Success should contain data`() { + val data = "Test Data" + val result = XCUITestServerResult.Success(data) + + assertEquals(data, result.data) + } + + @Test + fun `XCUITestServerResult Failure should contain error`() { + val error = XCUITestServerError.UnknownFailure("Error") + val result = XCUITestServerResult.Failure(error) + + assertEquals(error, result.errors) + } + + @Test + fun `XCUITestServerError UnknownFailure should have correct message`() { + val errorMessage = "Unknown error" + + assertThrows(errorMessage) { + throw XCUITestServerError.UnknownFailure(errorMessage) + } + } + + @Test + fun `XCUITestServerError NetworkError should have correct message`() { + val errorMessage = "Network error" + + assertThrows(errorMessage) { + throw XCUITestServerError.NetworkError(errorMessage) + } + } + + @Test + fun `XCUITestServerError AppCrash should have correct message`() { + val errorMessage = "App crashed" + + assertThrows(errorMessage) { + throw XCUITestServerError.AppCrash(errorMessage) + } + } + + @Test + fun `XCUITestServerError BadRequest should have correct messages`() { + val errorMessage = "Bad request" + val clientMessage = "Client error" + + assertThrows(errorMessage) { + throw XCUITestServerError.BadRequest(errorMessage, clientMessage) + } + } +} \ No newline at end of file