Defer level validation while editors are open

This commit is contained in:
Joe Tretter
2026-06-26 13:26:56 -05:00
parent 781b5090f7
commit 91913a9de8
3 changed files with 48 additions and 8 deletions

View File

@@ -20,8 +20,8 @@ android {
applicationId = "solutions.tretter.githugandroid" applicationId = "solutions.tretter.githugandroid"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 179 versionCode = 180
versionName = "0.1.178" versionName = "0.1.179"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -3,6 +3,7 @@ package solutions.tretter.githugandroid
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performSemanticsAction import androidx.compose.ui.test.performSemanticsAction
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
@@ -84,6 +85,25 @@ class GitRepositoryRuntimeInstrumentedTest {
} }
} }
@Test
fun squashLevelWaitsForFinalEditorBeforeCompleting() {
launchFreshApp().use {
selectLevel(squashLevel())
submitTerminalCommand("git rebase -i HEAD~4")
waitForGitEditorPath(".git/rebase-merge/git-rebase-todo")
replaceGitEditorContent(squashRebaseTodo(currentGitEditorContent()))
composeRule.onNodeWithTag("git-message-editor-save").performClick()
waitForGitMessageEditor()
composeRule.onNodeWithTag("exercise-title-${squashLevel().id}").assertIsDisplayed()
composeRule.onNodeWithTag("git-message-editor-save").performClick()
waitForLevelCompletion(nextLevel = initLevel())
}
}
@Test @Test
fun configLevelCompletesThroughUiWithArbitraryValues() { fun configLevelCompletesThroughUiWithArbitraryValues() {
launchFreshApp().use { launchFreshApp().use {
@@ -182,12 +202,28 @@ class GitRepositoryRuntimeInstrumentedTest {
composeRule.onNodeWithTag("git-message-editor-path").assertIsDisplayed() composeRule.onNodeWithTag("git-message-editor-path").assertIsDisplayed()
} }
private fun waitForGitMessageEditor() {
composeRule.waitUntil(timeoutMillis = 30_000) {
composeRule.onAllNodesWithTag("git-message-editor-content").fetchSemanticsNodes().isNotEmpty()
}
composeRule.onNodeWithTag("git-message-editor-content").assertIsDisplayed()
}
private fun currentGitEditorContent(): String { private fun currentGitEditorContent(): String {
val node = composeRule.onNodeWithTag("git-message-editor-content").fetchSemanticsNode() val node = composeRule.onNodeWithTag("git-message-editor-content").fetchSemanticsNode()
return node.config.getOrNull(SemanticsProperties.EditableText)?.text return node.config.getOrNull(SemanticsProperties.EditableText)?.text
?: error("Expected Git editor content semantics") ?: error("Expected Git editor content semantics")
} }
private fun squashRebaseTodo(content: String): String {
return content
.lineSequence()
.mapIndexed { index, line ->
if (index > 0 && line.startsWith("pick ")) line.replaceFirst("pick ", "squash ") else line
}
.joinToString("\n")
}
private fun replaceGitEditorContent(content: String) { private fun replaceGitEditorContent(content: String) {
composeRule.onNodeWithTag("git-message-editor-content") composeRule.onNodeWithTag("git-message-editor-content")
.performSemanticsAction(SemanticsActions.SetText) { setText -> .performSemanticsAction(SemanticsActions.SetText) { setText ->

View File

@@ -388,11 +388,16 @@ fun GitHugApp() {
fun applyCommandResult(raw: String, newRepo: RepoState, lines: List<String>, echoCommand: Boolean) { fun applyCommandResult(raw: String, newRepo: RepoState, lines: List<String>, echoCommand: Boolean) {
val startedAt = System.nanoTime() val startedAt = System.nanoTime()
val levelForResult = currentLevel val levelForResult = currentLevel
val solvedAfterCommand = levelForResult.validator(newRepo, raw) val validationBlockedByEditor = editorState != null || gitMessageEditorState != null
val solvedAfterCommand = if (validationBlockedByEditor) {
false
} else {
levelForResult.validator(newRepo, raw)
}
val wasAlreadyCompleted = currentLevel.id in completedLevels val wasAlreadyCompleted = currentLevel.id in completedLevels
AppLog.d( AppLog.d(
"GitHugApp", "GitHugApp",
"Command='$raw' level=${levelForResult.id} solved=$solvedAfterCommand alreadyCompleted=$wasAlreadyCompleted " + "Command='$raw' level=${levelForResult.id} solved=$solvedAfterCommand validationBlockedByEditor=$validationBlockedByEditor alreadyCompleted=$wasAlreadyCompleted " +
"completedBefore=${completedLevels.sorted()} outputLineCount=${lines.size} repo=${newRepo.diagnosticSnapshot()}", "completedBefore=${completedLevels.sorted()} outputLineCount=${lines.size} repo=${newRepo.diagnosticSnapshot()}",
) )
val newOutput = buildList { val newOutput = buildList {
@@ -519,14 +524,13 @@ fun GitHugApp() {
invocation = state.invocation, invocation = state.invocation,
message = state.content, message = state.content,
) )
gitMessageEditorState = null gitMessageEditorState = result.nextEditor?.let { nextEditor ->
applyCommandResult(state.invocation.command, result.repo, result.outputLines, echoCommand = false) GitMessageEditorState(
result.nextEditor?.let { nextEditor ->
gitMessageEditorState = GitMessageEditorState(
invocation = nextEditor.invocation, invocation = nextEditor.invocation,
content = nextEditor.content, content = nextEditor.content,
) )
} }
applyCommandResult(state.invocation.command, result.repo, result.outputLines, echoCommand = false)
} }
fun runCommand() { fun runCommand() {