266 lines
9.9 KiB
Kotlin
266 lines
9.9 KiB
Kotlin
package solutions.tretter.githugandroid
|
|
|
|
import androidx.compose.ui.test.assertIsDisplayed
|
|
import androidx.compose.ui.test.hasText
|
|
import androidx.compose.ui.test.junit4.createEmptyComposeRule
|
|
import androidx.compose.ui.test.onAllNodesWithTag
|
|
import androidx.compose.ui.test.onNodeWithTag
|
|
import androidx.compose.ui.test.performSemanticsAction
|
|
import androidx.compose.ui.test.performClick
|
|
import androidx.compose.ui.test.performImeAction
|
|
import androidx.compose.ui.test.performScrollTo
|
|
import androidx.compose.ui.test.performTextClearance
|
|
import androidx.compose.ui.test.performTextInput
|
|
import androidx.compose.ui.semantics.SemanticsActions
|
|
import androidx.compose.ui.semantics.SemanticsProperties
|
|
import androidx.compose.ui.semantics.getOrNull
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import androidx.test.core.app.ActivityScenario
|
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
import androidx.test.platform.app.InstrumentationRegistry
|
|
import java.io.File
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.junit.Assert.assertTrue
|
|
import org.junit.Rule
|
|
import org.junit.Test
|
|
import org.junit.runner.RunWith
|
|
|
|
@RunWith(AndroidJUnit4::class)
|
|
class GitRepositoryRuntimeInstrumentedTest {
|
|
@get:Rule
|
|
val composeRule = createEmptyComposeRule()
|
|
|
|
@Test
|
|
fun nativeGitRuntimeCompletesInitLevelOnDevice() {
|
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
val runtime = GitRepositoryRuntime(context)
|
|
val level = initLevel()
|
|
|
|
assertTrue(runtime.unavailableMessage(), runtime.isNativeGitAvailable())
|
|
|
|
val repo = runtime.prepareLevel(level)
|
|
val (updatedRepo, _) = runtime.execute(level, repo, "git init")
|
|
|
|
assertTrue(updatedRepo.initialized)
|
|
assertTrue(level.validator(updatedRepo, "git init"))
|
|
}
|
|
|
|
@Test
|
|
fun allLevelsCompleteThroughAppTerminalUi() {
|
|
launchFreshApp().use {
|
|
allGithugLevels().forEachIndexed { index, level ->
|
|
waitForExercise(level)
|
|
if (level.id == renameCommitLevel().id) {
|
|
completeRenameCommitWithEditor()
|
|
} else {
|
|
level.testCases.first().commands.forEach(::submitTerminalCommand)
|
|
}
|
|
waitForLevelCompletion(nextLevel = allGithugLevels().getOrNull(index + 1))
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun renameCommitLevelCompletesThroughUiEditor() {
|
|
launchFreshApp().use {
|
|
selectLevel(renameCommitLevel())
|
|
|
|
completeRenameCommitWithEditor()
|
|
|
|
waitForLevelCompletion(nextLevel = initLevel())
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun renameCommitLevelCompletesThroughMessageOptionAfterReword() {
|
|
launchFreshApp().use {
|
|
selectLevel(renameCommitLevel())
|
|
|
|
startRenameCommitReword()
|
|
composeRule.onNodeWithTag("git-message-editor-dismiss").performClick()
|
|
submitTerminalCommand("git commit --amend -m \"First commit\"")
|
|
submitTerminalCommand("git rebase --continue")
|
|
|
|
waitForLevelCompletion(nextLevel = initLevel())
|
|
}
|
|
}
|
|
|
|
@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
|
|
fun configLevelCompletesThroughUiWithArbitraryValues() {
|
|
launchFreshApp().use {
|
|
submitTerminalCommand("git init")
|
|
waitForExercise(configLevel())
|
|
|
|
submitTerminalCommand("git config user.name githug")
|
|
submitTerminalCommand("git config user.email xxx@yy.com")
|
|
|
|
waitForLevelCompletion(nextLevel = addLevel())
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun visualFileEditorStartsWithActionsVisible() {
|
|
launchFreshApp().use {
|
|
submitTerminalCommand("edit notes.txt")
|
|
|
|
waitForTextEditorActions()
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun selectingLevelFromLevelsPaneUpdatesExerciseImmediately() {
|
|
launchFreshApp().use {
|
|
composeRule.onNodeWithTag("level-submodule")
|
|
.performScrollTo()
|
|
.performClick()
|
|
|
|
composeRule.waitUntil(timeoutMillis = 1_500) {
|
|
composeRule.onAllNodes(hasText(submoduleLevel().title, substring = true))
|
|
.fetchSemanticsNodes()
|
|
.isNotEmpty()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun launchFreshApp(): ActivityScenario<MainActivity> {
|
|
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
File(context.filesDir, "githug-sandboxes").deleteRecursively()
|
|
File(context.applicationInfo.dataDir, "cache").deleteRecursively()
|
|
runBlocking {
|
|
GameProgressStore(context.applicationContext).saveProgress(emptySet(), activeLevelId = null)
|
|
GameProgressStore(context.applicationContext).saveShowHelpOnStart(showHelpOnStart = true)
|
|
}
|
|
val scenario = ActivityScenario.launch(MainActivity::class.java)
|
|
waitForExercise(initLevel())
|
|
return scenario
|
|
}
|
|
|
|
private fun waitForExercise(level: Level) {
|
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
|
composeRule.onAllNodes(hasText(level.title, substring = true)).fetchSemanticsNodes().isNotEmpty()
|
|
}
|
|
composeRule.onNodeWithTag("exercise-title-${level.id}").assertIsDisplayed()
|
|
}
|
|
|
|
private fun waitForLevelCompletion(nextLevel: Level?) {
|
|
if (nextLevel == null) {
|
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
|
composeRule.onAllNodes(hasText("All Githug levels completed", substring = true)).fetchSemanticsNodes().isNotEmpty()
|
|
}
|
|
return
|
|
}
|
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
|
composeRule.onAllNodes(hasText(nextLevel.title, substring = true)).fetchSemanticsNodes().isNotEmpty()
|
|
}
|
|
composeRule.onNodeWithTag("exercise-title-${nextLevel.id}").assertIsDisplayed()
|
|
}
|
|
|
|
private fun selectLevel(level: Level) {
|
|
composeRule.onNodeWithTag("level-${level.id}")
|
|
.performScrollTo()
|
|
.performClick()
|
|
waitForExercise(level)
|
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
|
composeRule.onAllNodes(hasText("Loaded level: ${level.title}", substring = true))
|
|
.fetchSemanticsNodes()
|
|
.isNotEmpty()
|
|
}
|
|
}
|
|
|
|
private fun completeRenameCommitWithEditor() {
|
|
startRenameCommitReword()
|
|
waitForGitEditorPath(".git/COMMIT_EDITMSG")
|
|
replaceGitEditorContent(
|
|
currentGitEditorContent().replaceFirst("First coommit", "First commit"),
|
|
)
|
|
composeRule.onNodeWithTag("git-message-editor-save").performClick()
|
|
}
|
|
|
|
private fun startRenameCommitReword() {
|
|
submitTerminalCommand("git rebase -i HEAD~2")
|
|
waitForGitEditorPath(".git/rebase-merge/git-rebase-todo")
|
|
replaceGitEditorContent(
|
|
currentGitEditorContent().replaceFirst(Regex("(?m)^pick "), "reword "),
|
|
)
|
|
composeRule.onNodeWithTag("git-message-editor-save").performClick()
|
|
}
|
|
|
|
private fun waitForGitEditorPath(path: String) {
|
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
|
composeRule.onAllNodes(hasText(path, substring = true)).fetchSemanticsNodes().isNotEmpty()
|
|
}
|
|
composeRule.onNodeWithTag("git-message-editor-path").assertIsDisplayed()
|
|
waitForGitEditorActions()
|
|
}
|
|
|
|
private fun waitForGitMessageEditor() {
|
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
|
composeRule.onAllNodesWithTag("git-message-editor-content").fetchSemanticsNodes().isNotEmpty()
|
|
}
|
|
composeRule.onNodeWithTag("git-message-editor-content").assertIsDisplayed()
|
|
waitForGitEditorActions()
|
|
}
|
|
|
|
private fun waitForGitEditorActions() {
|
|
composeRule.onNodeWithTag("git-message-editor-save").assertIsDisplayed()
|
|
composeRule.onNodeWithTag("git-message-editor-dismiss").assertIsDisplayed()
|
|
}
|
|
|
|
private fun waitForTextEditorActions() {
|
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
|
composeRule.onAllNodesWithTag("text-editor-content").fetchSemanticsNodes().isNotEmpty()
|
|
}
|
|
composeRule.onNodeWithTag("text-editor-save").assertIsDisplayed()
|
|
composeRule.onNodeWithTag("text-editor-dismiss").assertIsDisplayed()
|
|
}
|
|
|
|
private fun currentGitEditorContent(): String {
|
|
val node = composeRule.onNodeWithTag("git-message-editor-content").fetchSemanticsNode()
|
|
return node.config.getOrNull(SemanticsProperties.EditableText)?.text
|
|
?: 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) {
|
|
composeRule.onNodeWithTag("git-message-editor-content")
|
|
.performSemanticsAction(SemanticsActions.SetText) { setText ->
|
|
setText(AnnotatedString(content))
|
|
}
|
|
}
|
|
|
|
private fun submitTerminalCommand(command: String) {
|
|
val input = composeRule.onNodeWithTag("terminal-command-input")
|
|
input.performClick()
|
|
input.performTextClearance()
|
|
input.performTextInput(command)
|
|
input.performImeAction()
|
|
}
|
|
}
|