test(cron): verify phone editor save/edit/run over real HTTP; clarify server timezone

This commit is contained in:
yilsem committed 2026-10-08 04:28:34 +03:00
1 parent dc01c225f7
commit 6c17c3f04b
4 files changed
+119 -2

No files matched your search

@@ -0,0 +1,115 @@
package com.m57.hermescontrol.ui.cron
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.test.platform.app.InstrumentationRegistry
import com.m57.hermescontrol.BuildConfig
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient
import java.net.URL
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
/** Opt-in test against a fresh, isolated real cron backend, never a user server. */
class ScheduleHttpAcceptanceTest {
@get:Rule val compose = createComposeRule()
@Test
fun editorCreatesEditsAndScheduledJobExecutesViaHttp() {
val args = InstrumentationRegistry.getArguments()
assumeTrue(args.getString("isolatedCronAcceptance") == "true")
val context = InstrumentationRegistry.getInstrumentation().targetContext
check(BuildConfig.DEBUG && context.packageName.endsWith(".dev"))
val url = "http://127.0.0.1:18765"
AuthManager.init(context)
kotlinx.coroutines.runBlocking { AuthManager.awaitInitialization() }
val oldUrl = AuthManager.getBaseUrl()
val oldToken = AuthManager.getToken()
val store = ViewModelStore()
val owner = object : ViewModelStoreOwner { override val viewModelStore = store }
try {
AuthManager.setBaseUrl(url)
AuthManager.setToken(null)
ApiClient.rebuild()
val vm = ViewModelProvider(owner)[CronJobsViewModel::class.java]
compose.setContent {
val state by vm.uiState.collectAsState()
MaterialTheme {
if (state.editorState.isOpen) {
CronJobEditorDialog(
state = state.editorState,
onFieldChange = vm::updateEditorField,
onToggleNoAgent = vm::toggleNoAgent,
onToggleRunContinuity = vm::toggleRunContinuity,
onSetMonitorMode = vm::setMonitorMode,
onSelectBlueprint = vm::selectBlueprint,
onBlueprintFieldChange = vm::updateBlueprintValue,
onSave = vm::saveEditor,
onDismiss = vm::closeEditor,
onClearToast = vm::clearEditorToast,
)
}
}
}
// Harmless execution fixture. Schedule is changed using the real clock below.
compose.runOnIdle {
vm.openNewJobDialog()
vm.updateEditorField("name", "Android HTTP clock acceptance")
vm.updateEditorField("script", "probe.py")
vm.updateEditorField("schedule", "59 23 * * *")
vm.toggleNoAgent()
}
compose.onNodeWithText("23:59").performScrollTo().performClick()
compose.onNodeWithText("59").performClick()
compose.onNode(hasContentDescription("55", substring = true)).performClick()
compose.onNodeWithText(context.getString(R.string.action_ok)).performClick()
compose.onNodeWithText(context.getString(R.string.cron_edit_action_save)).performClick()
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isOpen }
compose.waitUntil(30_000) { vm.uiState.value.jobs.size == 1 }
val id = vm.uiState.value.jobs.single().id
compose.runOnIdle { vm.openEditJobDialog(id) }
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isLoading }
compose.runOnIdle { assertEquals("55 23 * * *", vm.uiState.value.editorState.schedule) }
compose.onNodeWithText("23:55").performScrollTo()
compose.onNodeWithText(context.getString(R.string.cron_repeat_label)).performClick()
compose.onAllNodesWithText(context.getString(R.string.cron_repeat_minutely))[0].performClick()
compose.onNodeWithText(context.getString(R.string.cron_edit_action_save)).performClick()
compose.waitUntil(30_000) { !vm.uiState.value.editorState.isOpen }
val deadline = android.os.SystemClock.elapsedRealtime() + 90_000
var evidence = JSONObject()
while (android.os.SystemClock.elapsedRealtime() < deadline) {
evidence = JSONObject(URL("$url/acceptance/evidence").readText())
if (evidence.optString("result") == "PASS") break
android.os.SystemClock.sleep(500)
}
assertEquals(evidence.toString(), "PASS", evidence.optString("result"))
assertEquals(id, evidence.getString("job_id"))
assertEquals(1, evidence.getInt("runs"))
val requests = evidence.getJSONArray("requests")
assertEquals(2, requests.length())
assertEquals("55 23 * * *", requests.getJSONObject(0).getJSONObject("body").getString("schedule"))
assertEquals("* * * * *", requests.getJSONObject(1).getJSONObject("body").getJSONObject("updates").getString("schedule"))
assertTrue(requests.getJSONObject(1).getString("path").endsWith(id))
} finally {
compose.runOnIdle { store.clear() }
AuthManager.setBaseUrl(oldUrl)
AuthManager.setToken(oldToken)
ApiClient.rebuild()
}
}
}
+1 -1
View File
@@ -1950,5 +1950,5 @@
<string name="cron_repeat_calendar_note">Seçilen günün olmadığı ay veya yıllarda çalışmaz.</string>
<string name="cron_repeat_count">Aralık</string>
<string name="cron_repeat_unit">Birim</string>
<string name="cron_schedule_timezone_note">Saatler bağlı AITURK sunucusunda ayarlı saat dilimine göredir.</string>
<string name="cron_schedule_timezone_note">Saatler bağlı AITURK sunucusunun saat dilimine göredir. Sunucunun dilimi burada okunamıyor; telefonunuzdan farklı olabilir.</string>
</resources>
+1 -1
View File
@@ -1950,5 +1950,5 @@
<string name="cron_repeat_calendar_note">Skips months or years without the selected date.</string>
<string name="cron_repeat_count">Interval</string>
<string name="cron_repeat_unit">Unit</string>
<string name="cron_schedule_timezone_note">Times use the time zone configured on the connected AITURK server.</string>
<string name="cron_schedule_timezone_note">Times use the connected AITURK server’s time zone. Its zone cannot be read here and may differ from your phone’s.</string>
</resources>
@@ -15,6 +15,8 @@ class ScheduleSelectionTest {
"59 23 29 2 *" to ScheduleMode.YEARLY,
"0 0 1 1 *" to ScheduleMode.YEARLY,
"every 120m" to ScheduleMode.INTERVAL,
"every 2h" to ScheduleMode.INTERVAL,
"every 3d" to ScheduleMode.INTERVAL,
)
cases.forEach { (expression, mode) ->
val parsed = parseSchedule(expression)