Automation e2e test with maestro

This commit is contained in:
2026-08-18 17:40:02 +07:00
parent c8aac2f6db
commit 0ee4e742c9
12 changed files with 145 additions and 35 deletions
+7
View File
@@ -0,0 +1,7 @@
flows:
- flows/*
executionOrder:
continueOnFailure: false
flowsOrder:
- login
+10
View File
@@ -0,0 +1,10 @@
appId: com.digitoolsolutions.app.torquevaultkmp
---
- runFlow:
file: ../subflows/launch-app.yaml
- runFlow:
file: ../subflows/login-steps.yaml
- runFlow:
file: ../subflows/communicate-steps.yaml
+6
View File
@@ -0,0 +1,6 @@
appId: com.digitoolsolutions.app.torquevaultkmp
---
- runFlow:
file: ../subflows/launch-app.yaml
- runFlow:
file: ../subflows/login-steps.yaml
+6
View File
@@ -0,0 +1,6 @@
appId: com.digitoolsolutions.app.torquevaultkmp
---
#- runFlow:
# file: ../subflows/launch-app.yaml
- runFlow:
file: ../subflows/communicate-steps.yaml
+32
View File
@@ -0,0 +1,32 @@
appId: com.digitoolsolutions.app.torquevaultkmp
---
- tapOn: "Scanner"
- extendedWaitUntil:
visible:
text: "3006"
timeout: 30000 # 30s
- tapOn:
text: "3006"
delay: 1500
- waitForAnimationToEnd:
timeout: 1000
# In case of empty work order
# ---------------------------
#- tapOn:
# text: "Back"
# delay: 500
#
#- assertVisible: "Scanner"
#- waitForAnimationToEnd:
# timeout: 1000
#- extendedWaitUntil:
# visible:
# text: "3006"
# timeout: 30000 # 30s
# In case of work orders
# --------------------------
- tapOn: "Manual Command"
- inputText: "ACCEPT_WO;NG"
- tapOn: "Send"
+16
View File
@@ -0,0 +1,16 @@
appId: com.digitoolsolutions.app.torquevaultkmp
---
- runFlow:
file: ../subflows/launch-app.yaml
- runFlow:
when:
visible: "Enable"
commands:
- tapOn: "Enable"
- runFlow:
when:
visible: "Cho phép"
commands:
- tapOn: "Cho phép"
+6
View File
@@ -0,0 +1,6 @@
appId: com.digitoolsolutions.app.torquevaultkmp
---
- launchApp:
clearState: true
- waitForAnimationToEnd:
timeout: 3000
+24
View File
@@ -0,0 +1,24 @@
appId: com.digitoolsolutions.app.torquevaultkmp
---
#- longPressOn: "Username"
#- tapOn:
# text: "Select All"
# optional: true
#- tapOn:
# text: "Chọn tất cả"
# optional: true
#- eraseText: 1
#- inputText: "admin"
#- tapOn: "App Logo"
#- longPressOn: "Password"
#- tapOn:
# text: "Select All"
# optional: true
#- tapOn:
# text: "Chọn tất cả"
# optional: true
#- eraseText: 1
#- inputText: "admin"
#- tapOn: "App Logo"
- tapOn: "Sign In"
- assertVisible: "Home"
+1 -1
View File
@@ -89,7 +89,7 @@ android {
applicationId = "com.digitoolsolutions.app.torquevaultkmp" applicationId = "com.digitoolsolutions.app.torquevaultkmp"
minSdk = libs.versions.android.minSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 2 versionCode = 3
versionName = "1.0.2" versionName = "1.0.2"
} }
packaging { packaging {
@@ -253,6 +253,9 @@ class ScannerViewModel(
val adv = bleManager.getAdvertisement(deviceId) val adv = bleManager.getAdvertisement(deviceId)
if (adv != null) { if (adv != null) {
connect(adv) connect(adv)
// Remove advertisement from list
advertisementMap.remove(deviceId)
updateDeviceList()
} else { } else {
Napier.e("Cannot connect: Advertisement not found for $deviceId") Napier.e("Cannot connect: Advertisement not found for $deviceId")
} }
@@ -369,22 +372,22 @@ class ScannerViewModel(
_connectedDevices.value -= deviceIdentifier _connectedDevices.value -= deviceIdentifier
} }
} }
private fun fetchWorkOrders(peripheral: Peripheral, woID: String = "0", action: String = "next") { private fun fetchWorkOrders(peripheral: Peripheral, command: Helper.Command, woID: String = "0", action: String = "next") {
viewModelScope.launch { viewModelScope.launch {
try { try {
val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action) val orders = scannerRepository.getAbleWorkOrders(woID = woID, action = action)
_workOrders.value = orders _workOrders.value = orders
sendWorkOrdersToDevice(peripheral,orders) sendWorkOrdersToDevice(peripheral, command.name, orders)
} catch (e: Exception) { } catch (e: Exception) {
Napier.e("Failed to load work orders", e) Napier.e("Failed to load work orders", e)
logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
} }
} }
} }
private fun sendWorkOrdersToDevice(peripheral: Peripheral, workOrders: List<WorkOrder>) { private fun sendWorkOrdersToDevice(peripheral: Peripheral, command: String, workOrders: List<WorkOrder>) {
viewModelScope.launch { viewModelScope.launch {
if (workOrders.isEmpty()) { if (workOrders.isEmpty()) {
sendCommand(peripheral, Helper.CMD_EMPTY_WO) sendCommand(peripheral, Helper.CMD_EMPTY_WO(command))
return@launch return@launch
} }
val formatted = workOrders.joinToString(separator = "\n") { wo -> val formatted = workOrders.joinToString(separator = "\n") { wo ->
@@ -398,7 +401,7 @@ class ScannerViewModel(
} }
private fun acceptWorkOrder(peripheral: Peripheral, type: Int, woId: String, deviceIdentifier: String) { private fun acceptWorkOrder(peripheral: Peripheral, type: Int, woId: String, deviceIdentifier: String) {
viewModelScope.launch { viewModelScope.launch {
var command = Helper.CMD_RES_OK(Helper.Command.ACCEPT_WO.name) var command = Helper.CMD_RES_NG(Helper.Command.ACCEPT_WO.name)
try { try {
val res = scannerRepository.confirmWorkOrder(woId, deviceIdentifier, type) val res = scannerRepository.confirmWorkOrder(woId, deviceIdentifier, type)
if (res) if (res)
@@ -427,39 +430,40 @@ class ScannerViewModel(
) )
Napier.d(">>>>> DTO $dto") Napier.d(">>>>> DTO $dto")
viewModelScope.launch { viewModelScope.launch {
var command = Helper.CMD_RES_NG(Helper.Command.FINISH_WO.name)
when (val act = bleData.firstOrNull()) { when (val act = bleData.firstOrNull()) {
Helper.TorqueAction.TORQUE -> { Helper.TorqueAction.TORQUE -> {
try { val res = scannerRepository.sendMeasurementResult(woId, dto)
scannerRepository.sendMeasurementResult(woId, dto) if (res.isSuccess) {
} catch (e: Exception) { command = Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name)
} else {
val e = res.exceptionOrNull()
Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel") Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel")
logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) logRepository.appendLog(title = "HTTP", content = "${e?.message}".substringBefore("[url="))
} finally {
// Currently, always send UPLOADED_WO regardless of success or failure
sendCommand(peripheral, Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name))
} }
sendCommand(peripheral, command)
} }
Helper.TorqueAction.RE_TORQUE -> { Helper.TorqueAction.RE_TORQUE -> {
try { val res = scannerRepository.sendMeasurementResult(woId, dto, true)
scannerRepository.sendMeasurementResult(woId, dto, true) if (res.isSuccess) {
} catch (e: Exception) { command = Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name)
} else {
val e = res.exceptionOrNull()
Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel") Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel")
logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) logRepository.appendLog(title = "HTTP", content = "${e?.message}".substringBefore("[url="))
} finally {
// Currently, always send UPLOADED_WO regardless of success or failure
sendCommand(peripheral, Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name))
} }
sendCommand(peripheral, command)
} }
Helper.TorqueAction.CANCEL -> { Helper.TorqueAction.CANCEL -> {
try { val res = scannerRepository.sendCancelWorkOrder(woId, dto)
scannerRepository.sendCancelWorkOrder(woId, dto) if (res.isSuccess) {
} catch (e: Exception) { command = Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name)
} else {
val e = res.exceptionOrNull()
Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel") Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel")
logRepository.appendLog(title = "HTTP", content = "${e.message}".substringBefore("[url=")) logRepository.appendLog(title = "HTTP", content = "${e?.message}".substringBefore("[url="))
} finally {
// Currently, always send UPLOADED_WO regardless of success or failure
sendCommand(peripheral, Helper.CMD_RES_OK(Helper.Command.FINISH_WO.name))
} }
sendCommand(peripheral, command)
} }
else -> Napier.w(">>>>> Unknown action: $act") else -> Napier.w(">>>>> Unknown action: $act")
} }
@@ -476,9 +480,9 @@ class ScannerViewModel(
val woId = decoded.woId val woId = decoded.woId
val data = decoded.data val data = decoded.data
when (cmd) { when (cmd) {
Helper.Command.START_WO -> fetchWorkOrders(peripheral) Helper.Command.START_WO -> fetchWorkOrders(peripheral, Helper.Command.START_WO)
Helper.Command.NEXT_WO -> fetchWorkOrders(peripheral, woId, "next") Helper.Command.NEXT_WO -> fetchWorkOrders(peripheral, Helper.Command.NEXT_WO, woId, "next")
Helper.Command.PREVIOUS_WO -> fetchWorkOrders(peripheral, woId, "back") Helper.Command.PREVIOUS_WO -> fetchWorkOrders(peripheral, Helper.Command.PREVIOUS_WO, woId, "back")
Helper.Command.ACCEPT_WO -> acceptWorkOrder(peripheral, type.code, woId, deviceIdentifier) Helper.Command.ACCEPT_WO -> acceptWorkOrder(peripheral, type.code, woId, deviceIdentifier)
Helper.Command.FINISH_WO -> { Helper.Command.FINISH_WO -> {
finishWorkOrder(peripheral, type.code, woId, data, deviceIdentifier) finishWorkOrder(peripheral, type.code, woId, data, deviceIdentifier)
@@ -9,10 +9,9 @@ object Helper {
const val END_LINE_FEED = "#\n" const val END_LINE_FEED = "#\n"
const val SECURITY_CRC16 = "00cr16" const val SECURITY_CRC16 = "00cr16"
/** Command sent to indicate no work orders are available. */
const val CMD_EMPTY_WO = "START_WO;empty"
const val WO_RES = "WO_RES" const val WO_RES = "WO_RES"
/** Generates empty response for a specific command. */
val CMD_EMPTY_WO: (String) -> String = {cmd -> "$cmd;empty"}
/** Generates a success response for a specific command. */ /** Generates a success response for a specific command. */
val CMD_RES_OK: (String) -> String = { cmd -> "$cmd;OK" } val CMD_RES_OK: (String) -> String = { cmd -> "$cmd;OK" }
@@ -27,7 +27,7 @@ class UartCommunicationTest {
assertEquals("cr16", decoded.crc16) assertEquals("cr16", decoded.crc16)
// --- STEP 2: App prepares response (Empty Case) --- // --- STEP 2: App prepares response (Empty Case) ---
val emptyCase = Helper.buildUartCommand(Helper.CMD_EMPTY_WO) val emptyCase = Helper.buildUartCommand(Helper.CMD_EMPTY_WO("START_WO"))
assertEquals($$"$START_WO;empty\n00cr16#\n", emptyCase) assertEquals($$"$START_WO;empty\n00cr16#\n", emptyCase)
// --- STEP 3: App prepares response (Multiple WO Case) --- // --- STEP 3: App prepares response (Multiple WO Case) ---