diff --git a/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt b/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt index 6b64e56fbe..22f8cfd3a4 100644 --- a/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt +++ b/maestro-orchestra-models/src/main/java/maestro/orchestra/Commands.kt @@ -22,6 +22,7 @@ package maestro.orchestra import maestro.KeyCode import maestro.Point import maestro.orchestra.util.Env.injectEnv +import maestro.orchestra.util.InputRandomTextHelper sealed interface Command { @@ -381,6 +382,37 @@ class ClearKeychainCommand : Command { } +enum class InputRandomType { + NUMBER, TEXT, TEXT_EMAIL_ADDRESS, TEXT_PERSON_NAME, +} + +data class InputRandomCommand( + val inputType: InputRandomType? = InputRandomType.TEXT, + val length: Int? = 8, +) : Command { + + fun genRandomString(): String { + val lengthNonNull = length ?: 8 + val finalLength = if (lengthNonNull <= 0) 8 else lengthNonNull + + return when (inputType) { + InputRandomType.NUMBER -> InputRandomTextHelper.getRandomNumber(finalLength) + InputRandomType.TEXT -> InputRandomTextHelper.getRandomText(finalLength) + InputRandomType.TEXT_EMAIL_ADDRESS -> InputRandomTextHelper.randomEmail() + InputRandomType.TEXT_PERSON_NAME -> InputRandomTextHelper.randomPersonName() + else -> InputRandomTextHelper.getRandomText(finalLength) + } + } + + override fun description(): String { + return "Input text random $inputType" + } + + override fun injectEnv(env: Map): InputRandomCommand { + return this + } +} + data class RunFlowCommand( val commands: List, val condition: Condition? = null, diff --git a/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt b/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt index 55be7b87a6..a71d571c66 100644 --- a/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt +++ b/maestro-orchestra-models/src/main/java/maestro/orchestra/MaestroCommand.kt @@ -33,6 +33,7 @@ data class MaestroCommand( val backPressCommand: BackPressCommand? = null, val assertCommand: AssertCommand? = null, val inputTextCommand: InputTextCommand? = null, + val inputRandomTextCommand: InputRandomCommand? = null, val launchAppCommand: LaunchAppCommand? = null, val applyConfigurationCommand: ApplyConfigurationCommand? = null, val openLinkCommand: OpenLinkCommand? = null, @@ -56,6 +57,7 @@ data class MaestroCommand( backPressCommand = command as? BackPressCommand, assertCommand = command as? AssertCommand, inputTextCommand = command as? InputTextCommand, + inputRandomTextCommand = command as? InputRandomCommand, launchAppCommand = command as? LaunchAppCommand, applyConfigurationCommand = command as? ApplyConfigurationCommand, openLinkCommand = command as? OpenLinkCommand, @@ -79,6 +81,7 @@ data class MaestroCommand( backPressCommand != null -> backPressCommand assertCommand != null -> assertCommand inputTextCommand != null -> inputTextCommand + inputRandomTextCommand != null -> inputRandomTextCommand launchAppCommand != null -> launchAppCommand applyConfigurationCommand != null -> applyConfigurationCommand openLinkCommand != null -> openLinkCommand diff --git a/maestro-orchestra-models/src/main/java/maestro/orchestra/util/InputRandomTextHelper.kt b/maestro-orchestra-models/src/main/java/maestro/orchestra/util/InputRandomTextHelper.kt new file mode 100644 index 0000000000..b2133cb6a4 --- /dev/null +++ b/maestro-orchestra-models/src/main/java/maestro/orchestra/util/InputRandomTextHelper.kt @@ -0,0 +1,71 @@ +package maestro.orchestra.util + +object InputRandomTextHelper { + private const val CHARSET_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789" + private const val CHARSET_NUMBER = "0123456789" + private const val CHARSET_NUMBER_WITHOUT_ZERO = "123456789" + private val LIST_POPULAR_LAST_NAME = arrayOf("Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez") + private val LIST_POPULAR_FIST_NAME = arrayOf( + "Liam", + "Olivia", + "Noah", + "Emma", + "Oliver", + "Charlotte", + "Elijah", + "Amelia", + "James", + "Ava", + "William", + "Sophia", + "Benjamin", + "Isabella", + "Lucas", + "Mia", + "Henry", + "Evelyn", + "Theodore", + "Harper" + ) + private val LIST_POPULAR_EMAIL_DOMAIN = arrayOf("gmail.com", "yahoo.com", "hotmail.com", "aol.com", "msn.com", "outlook.com") + + /** + * Returns random person name format: FistName LastName + */ + fun randomPersonName() = String.format( + "%s %s", + LIST_POPULAR_FIST_NAME.random(), LIST_POPULAR_LAST_NAME.random() + ) + + /** + * Returns random email address with format: fistName_lastName_randomTex@emailDomain + */ + fun randomEmail() = String.format( + "%s_%s_%s@%s", + LIST_POPULAR_FIST_NAME.random(), + LIST_POPULAR_LAST_NAME.random(), + getRandomText(length = 4), + LIST_POPULAR_EMAIL_DOMAIN.random(), + ).lowercase() + + /** + * Returns random number with [length]. + */ + fun getRandomNumber(length: Int): String { + val randomNum = (1..length) + .map { CHARSET_NUMBER.random() } + .joinToString("") + return if (randomNum.startsWith("0")) { + CHARSET_NUMBER_WITHOUT_ZERO.random() + randomNum.substring(1) + } else randomNum + } + + /** + * Returns random text with [length]. + */ + fun getRandomText(length: Int): String { + return (1..length) + .map { CHARSET_TEXT.random() } + .joinToString("") + } +} diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt index ff399a1c01..6485f2d730 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt @@ -132,6 +132,7 @@ class Orchestra( is SwipeCommand -> swipeCommand(command) is AssertCommand -> assertCommand(command) is InputTextCommand -> inputTextCommand(command) + is InputRandomCommand -> inputTextRandomCommand(command) is LaunchAppCommand -> launchAppCommand(command) is OpenLinkCommand -> openLinkCommand(command) is PressKeyCommand -> pressKeyCommand(command) @@ -260,6 +261,10 @@ class Orchestra( maestro.inputText(command.text) } + private fun inputTextRandomCommand(command: InputRandomCommand) { + inputTextCommand(InputTextCommand(text = command.genRandomString())) + } + private fun assertCommand(command: AssertCommand) { command.visible?.let { assertVisible(it, command.timeout) } command.notVisible?.let { assertNotVisible(it, command.timeout) } diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt index f558167313..ca3a4045ce 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt @@ -32,6 +32,8 @@ import maestro.orchestra.EraseTextCommand import maestro.orchestra.HideKeyboardCommand import maestro.orchestra.ClipboardPasteCommand import maestro.orchestra.InputTextCommand +import maestro.orchestra.InputRandomCommand +import maestro.orchestra.InputRandomType import maestro.orchestra.LaunchAppCommand import maestro.orchestra.MaestroCommand import maestro.orchestra.OpenLinkCommand @@ -57,6 +59,10 @@ data class YamlFluentCommand( val assertNotVisible: YamlElementSelectorUnion? = null, val action: String? = null, val inputText: String? = null, + val inputRandomText: YamlInputRandomText? = null, + val inputRandomNumber: YamlInputRandomNumber? = null, + val inputRandomEmail: YamlInputRandomEmail? = null, + val inputRandomPersonName: YamlInputRandomPersonName? = null, val launchApp: YamlLaunchApp? = null, val swipe: YamlElementSelectorUnion? = null, val openLink: String? = null, @@ -79,6 +85,10 @@ data class YamlFluentCommand( assertVisible != null -> listOf(MaestroCommand(AssertCommand(visible = toElementSelector(assertVisible)))) assertNotVisible != null -> listOf(MaestroCommand(AssertCommand(notVisible = toElementSelector(assertNotVisible)))) inputText != null -> listOf(MaestroCommand(InputTextCommand(inputText))) + inputRandomText != null -> listOf(MaestroCommand(InputRandomCommand(inputType = InputRandomType.TEXT, length = inputRandomText.length))) + inputRandomNumber != null -> listOf(MaestroCommand(InputRandomCommand(inputType = InputRandomType.NUMBER, length = inputRandomNumber.length))) + inputRandomEmail != null -> listOf(MaestroCommand(InputRandomCommand(inputType = InputRandomType.TEXT_EMAIL_ADDRESS))) + inputRandomPersonName != null -> listOf(MaestroCommand(InputRandomCommand(inputType = InputRandomType.TEXT_PERSON_NAME))) swipe != null -> listOf(swipeCommand(swipe)) openLink != null -> listOf(MaestroCommand(OpenLinkCommand(openLink))) pressKey != null -> listOf(MaestroCommand(PressKeyCommand(code = KeyCode.getByName(pressKey) ?: throw SyntaxError("Unknown key name: $pressKey")))) @@ -364,6 +374,26 @@ data class YamlFluentCommand( eraseText = YamlEraseText(charactersToErase = 50) ) + "inputRandomText" -> YamlFluentCommand( + inputRandomText = YamlInputRandomText(length = 8), + ) + + "inputRandomText" -> YamlFluentCommand( + inputRandomText = YamlInputRandomText(length = 8), + ) + + "inputRandomNumber" -> YamlFluentCommand( + inputRandomNumber = YamlInputRandomNumber(length = 8), + ) + + "inputRandomEmail" -> YamlFluentCommand( + inputRandomEmail = YamlInputRandomEmail(), + ) + + "inputRandomPersonName" -> YamlFluentCommand( + inputRandomPersonName = YamlInputRandomPersonName(), + ) + "back" -> YamlFluentCommand( action = "back" ) diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlInputRandomText.kt b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlInputRandomText.kt new file mode 100644 index 0000000000..96e8ccc2e8 --- /dev/null +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlInputRandomText.kt @@ -0,0 +1,32 @@ +/* + * + * Copyright (c) 2022 mobile.dev inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + */ + +package maestro.orchestra.yaml + +data class YamlInputRandomText( + val length: Int?, +) + +data class YamlInputRandomNumber( + val length: Int?, +) + +class YamlInputRandomEmail + +class YamlInputRandomPersonName diff --git a/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt b/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt index 3e3b80d0b1..be098fa400 100644 --- a/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt +++ b/maestro-orchestra/src/test/java/maestro/orchestra/MaestroCommandSerializationTest.kt @@ -395,6 +395,114 @@ internal class MaestroCommandSerializationTest { .isEqualTo(command) } + @Test + fun `serialize InputRandomCommand with text`() { + // given + val command = MaestroCommand( + InputRandomCommand(InputRandomType.TEXT, 2) + ) + + // when + val serializedCommandJson = command.toJson() + val deserializedCommand = objectMapper.readValue(serializedCommandJson, MaestroCommand::class.java) + + // then + @Language("json") + val expectedJson = """ + { + "inputRandomTextCommand" : { + "inputType" : "TEXT", + "length" : 2 + } + } + """.trimIndent() + assertThat(serializedCommandJson) + .isEqualTo(expectedJson) + assertThat(deserializedCommand) + .isEqualTo(command) + } + + @Test + fun `serialize InputRandomCommand with number`() { + // given + val command = MaestroCommand( + InputRandomCommand(InputRandomType.NUMBER, 3) + ) + + // when + val serializedCommandJson = command.toJson() + val deserializedCommand = objectMapper.readValue(serializedCommandJson, MaestroCommand::class.java) + + // then + @Language("json") + val expectedJson = """ + { + "inputRandomTextCommand" : { + "inputType" : "NUMBER", + "length" : 3 + } + } + """.trimIndent() + assertThat(serializedCommandJson) + .isEqualTo(expectedJson) + assertThat(deserializedCommand) + .isEqualTo(command) + } + + @Test + fun `serialize InputRandomCommand with email`() { + // given + val command = MaestroCommand( + InputRandomCommand(InputRandomType.TEXT_EMAIL_ADDRESS) + ) + + // when + val serializedCommandJson = command.toJson() + val deserializedCommand = objectMapper.readValue(serializedCommandJson, MaestroCommand::class.java) + + // then + @Language("json") + val expectedJson = """ + { + "inputRandomTextCommand" : { + "inputType" : "TEXT_EMAIL_ADDRESS", + "length" : 8 + } + } + """.trimIndent() + assertThat(serializedCommandJson) + .isEqualTo(expectedJson) + assertThat(deserializedCommand) + .isEqualTo(command) + } + + @Test + fun `serialize InputRandomCommand with person name`() { + // given + val command = MaestroCommand( + InputRandomCommand(InputRandomType.TEXT_PERSON_NAME) + ) + + // when + val serializedCommandJson = command.toJson() + val deserializedCommand = objectMapper.readValue(serializedCommandJson, MaestroCommand::class.java) + + // then + @Language("json") + val expectedJson = """ + { + "inputRandomTextCommand" : { + "inputType" : "TEXT_PERSON_NAME", + "length" : 8 + } + } + """.trimIndent() + assertThat(serializedCommandJson) + .isEqualTo(expectedJson) + assertThat(deserializedCommand) + .isEqualTo(command) + } + private fun MaestroCommand.toJson(): String = objectMapper .writerWithDefaultPrettyPrinter() diff --git a/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt b/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt index f1f592fc38..274c72d1b7 100644 --- a/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt +++ b/maestro-test/src/main/kotlin/maestro/test/drivers/FakeDriver.kt @@ -254,6 +254,14 @@ class FakeDriver : Driver { } } + fun assertAnyEvent(condition: ((event: Event) -> Boolean)) { + assertThat(events.any { condition(it) }).isTrue() + } + + fun assertAllEvent(condition: ((event: Event) -> Boolean)) { + assertThat(events.all { condition(it) }).isTrue() + } + fun assertNoInteraction() { if (events.isNotEmpty()) { throw AssertionError("Expected no interaction, but got: $events") diff --git a/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt b/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt index daba7d3b9e..84e5ac5f57 100644 --- a/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt +++ b/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt @@ -1456,6 +1456,44 @@ class IntegrationTest { ) } + @Test + fun `Case 052 - Input random`() { + // Given + val commands = readCommands("052_text_random") + + val driver = driver { + } + + // When + Maestro(driver).use { + orchestra(it).runFlow(commands) + } + + // Then + // No test failure + driver.assertAllEvent(condition = { + ((it as? Event.InputText?)?.text?.length ?: -1) >= 5 + }) + driver.assertAnyEvent(condition = { + val number = try { + (it as? Event.InputText?)?.text?.toInt() ?: -1 + } catch (e: NumberFormatException) { + -1 + } + number in 10000..99999 + }) + + driver.assertAnyEvent(condition = { + val text = (it as? Event.InputText?)?.text ?: "" + text.contains("@") + }) + + driver.assertAnyEvent(condition = { + val text = (it as? Event.InputText?)?.text ?: "" + text.contains(" ") + }) + } + private fun orchestra(it: Maestro) = Orchestra(it, lookupTimeoutMs = 0L, optionalLookupTimeoutMs = 0L) private fun driver(builder: FakeLayoutElement.() -> Unit): FakeDriver { diff --git a/maestro-test/src/test/resources/052_text_random.yaml b/maestro-test/src/test/resources/052_text_random.yaml new file mode 100644 index 0000000000..3d221d9598 --- /dev/null +++ b/maestro-test/src/test/resources/052_text_random.yaml @@ -0,0 +1,10 @@ +appId: com.example.app +--- +- inputRandomText +- inputRandomNumber +- inputRandomText: + length: 5 +- inputRandomNumber: + length: 5 +- inputRandomEmail +- inputRandomPersonName