Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.runnect.runnect.presentation.countdown

import androidx.compose.ui.test.assertContentDescriptionEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

class CountDownScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun 카운트다운_배경_숫자_안내문구가_노출된다() {
composeTestRule.setContent {
RunnectTheme {
CountDownContent(count = 3)
}
}

composeTestRule.onNodeWithTag(CountDownScreenTestTags.BACKGROUND).assertIsDisplayed()
composeTestRule.onNodeWithTag(CountDownScreenTestTags.NUMBER)
.assertIsDisplayed()
.assertContentDescriptionEquals("3")
composeTestRule.onNodeWithText("잠시 후 러닝을 시작합니다").assertIsDisplayed()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid hard-coding localized UI copy in the assertion.

This makes the test locale-dependent even though CountDownContent already exposes a DESCRIPTION tag and reads from R.string.count_down_desc. Assert via the tag plus the string resource instead.

Suggested change
+import androidx.compose.ui.test.assertTextEquals
 import androidx.compose.ui.test.assertContentDescriptionEquals
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
+import androidx.test.platform.app.InstrumentationRegistry
+import com.runnect.runnect.R
 import com.runnect.runnect.presentation.ui.theme.RunnectTheme
@@
     fun 카운트다운_배경_숫자_안내문구가_노출된다() {
+        val context = InstrumentationRegistry.getInstrumentation().targetContext
+
         composeTestRule.setContent {
             RunnectTheme {
                 CountDownContent(count = 3)
@@
-        composeTestRule.onNodeWithText("잠시 후 러닝을 시작합니다").assertIsDisplayed()
+        composeTestRule.onNodeWithTag(CountDownScreenTestTags.DESCRIPTION)
+            .assertIsDisplayed()
+            .assertTextEquals(context.getString(R.string.count_down_desc))
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
composeTestRule.onNodeWithText("잠시 후 러닝을 시작합니다").assertIsDisplayed()
val context = InstrumentationRegistry.getInstrumentation().targetContext
composeTestRule.onNodeWithTag(CountDownScreenTestTags.DESCRIPTION)
.assertIsDisplayed()
.assertTextEquals(context.getString(R.string.count_down_desc))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/androidTest/java/com/runnect/runnect/presentation/countdown/CountDownScreenTest.kt`
at line 30, The countdown test is asserting against hard-coded Korean UI text,
which makes it locale-dependent. Update CountDownScreenTest to locate the
countdown description through CountDownContent’s DESCRIPTION tag and verify it
against the R.string.count_down_desc resource instead of onNodeWithText. Keep
the assertion tied to the existing Compose semantics/tag identifiers so the test
remains stable across locales.

}

@Test
fun 카운트다운이_끝나면_완료_콜백이_호출된다() {
var finishedCount = 0
composeTestRule.mainClock.autoAdvance = false

composeTestRule.setContent {
RunnectTheme {
CountDownRoute(
onFinished = { finishedCount += 1 }
)
}
}

composeTestRule.waitForIdle()
repeat(3) {
composeTestRule.mainClock.advanceTimeBy(CountDownStateMachine.TICK_MILLIS)
composeTestRule.waitForIdle()
}
composeTestRule.waitUntil(timeoutMillis = 5_000L) {
finishedCount == 1
}

assertEquals(1, finishedCount)
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
package com.runnect.runnect.presentation.countdown

import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.animation.Animation
import android.view.animation.Animation.AnimationListener
import android.view.animation.AnimationUtils
import androidx.appcompat.content.res.AppCompatResources
import com.runnect.runnect.R
import com.runnect.runnect.binding.BindingActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import com.runnect.runnect.data.dto.CourseData
import com.runnect.runnect.databinding.ActivityCountDownBinding
import com.runnect.runnect.presentation.run.RunActivity
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
import com.runnect.runnect.util.analytics.Analytics
import com.runnect.runnect.util.analytics.EventName
import com.runnect.runnect.util.analytics.EventName.Param
import com.runnect.runnect.util.extension.getCompatibleParcelableExtra
import timber.log.Timber

class CountDownActivity: BindingActivity<ActivityCountDownBinding>(R.layout.activity_count_down) {
class CountDownActivity : AppCompatActivity() {
private val courseData: CourseData? by lazy { intent.getCompatibleParcelableExtra(EXTRA_COURSE_DATA) }

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -29,64 +23,39 @@ class CountDownActivity: BindingActivity<ActivityCountDownBinding>(R.layout.acti
Param.COURSE_ID to courseData?.courseId
)

val intentToRun = Intent(this, RunActivity::class.java)
val numList = arrayListOf(
AppCompatResources.getDrawable(this, R.drawable.anim_num1),
AppCompatResources.getDrawable(this, R.drawable.anim_num2)
)
val anim = AnimationUtils.loadAnimation(this, R.anim.anim_count)
setAnimationListener(anim, numList, intentToRun)
binding.ivCountDown.startAnimation(anim)
setContent {
RunnectTheme {
CountDownRoute(
onFinished = ::moveToRun
)
}
}
}

@Deprecated("Deprecated in Java")
override fun onBackPressed() {
Analytics.logEvent(
EventName.CLICK_CANCEL_COUNTDOWN,
Param.COURSE_ID to courseData?.courseId
)
finish()
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
overridePendingTransition(
com.runnect.runnect.R.anim.slide_in_left,
com.runnect.runnect.R.anim.slide_out_right
)
}

private fun setAnimationListener(
anim: Animation,
numList: ArrayList<Drawable?>,
intentToRun: Intent,
) {
var counter = COUNT_START

anim.setAnimationListener(object : AnimationListener {
override fun onAnimationStart(animation: Animation) {
}

override fun onAnimationEnd(animation: Animation) {
counter -= COUNT_DECREASE_UNIT
if (counter == COUNT_END) {
courseData?.let { courseData ->
intentToRun.apply {
putExtra(
EXTRA_COUNTDOWN_TO_RUN, courseData
)
}
}
startActivity(intentToRun)
finish()
} else {
binding.ivCountDown.post {
binding.ivCountDown.setImageDrawable(numList[counter])
binding.ivCountDown.startAnimation(animation)
}
}
}
override fun onAnimationRepeat(animation: Animation) {}
})
private fun moveToRun() {
val intentToRun = Intent(this, RunActivity::class.java)
courseData?.let { courseData ->
intentToRun.putExtra(EXTRA_COUNTDOWN_TO_RUN, courseData)
}
startActivity(intentToRun)
finish()
}

companion object {
const val COUNT_START = 2
const val COUNT_END = -1
const val COUNT_DECREASE_UNIT = 1
const val EXTRA_COUNTDOWN_TO_RUN = "CountToRunData"
const val EXTRA_COURSE_DATA = "CourseData"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package com.runnect.runnect.presentation.countdown

import androidx.annotation.DrawableRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.runnect.runnect.R
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
import com.runnect.runnect.presentation.ui.theme.White
import kotlinx.coroutines.delay
import kotlin.math.PI
import kotlin.math.cos

object CountDownScreenTestTags {
const val BACKGROUND = "count_down_background"
const val NUMBER = "count_down_number"
const val DESCRIPTION = "count_down_description"
}

object CountDownStateMachine {
const val INITIAL_COUNT = 3
private const val LAST_VISIBLE_COUNT = 1
const val TICK_MILLIS = 1_000L

fun nextCount(currentCount: Int): Int? =
if (currentCount > LAST_VISIBLE_COUNT) currentCount - 1 else null

@DrawableRes
fun numberDrawableRes(count: Int): Int = when (count) {
3 -> R.drawable.anim_num3
2 -> R.drawable.anim_num2
1 -> R.drawable.anim_num1
else -> error("Unsupported countdown number: $count")
}
}

object CountDownAnimationSpec {
const val INITIAL_SCALE = 0.4f
const val TARGET_SCALE = 1f

val AccelerateDecelerateEasing = Easing { fraction ->
(cos((fraction + 1f) * PI).toFloat() / 2f) + 0.5f
}
}

@Composable
fun CountDownRoute(
onFinished: () -> Unit,
modifier: Modifier = Modifier,
) {
var currentCount by remember { mutableIntStateOf(CountDownStateMachine.INITIAL_COUNT) }
var isFinished by remember { mutableStateOf(false) }
val lifecycle = LocalLifecycleOwner.current.lifecycle

LaunchedEffect(lifecycle) {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
while (!isFinished) {
delay(CountDownStateMachine.TICK_MILLIS)
val nextCount = CountDownStateMachine.nextCount(currentCount)
if (nextCount == null) {
isFinished = true
onFinished()
} else {
currentCount = nextCount
}
}
}
}

CountDownContent(
count = currentCount,
modifier = modifier
)
}

@Composable
fun CountDownContent(
count: Int,
modifier: Modifier = Modifier,
) {
val scale = remember { Animatable(CountDownAnimationSpec.INITIAL_SCALE) }

LaunchedEffect(count) {
scale.snapTo(CountDownAnimationSpec.INITIAL_SCALE)
scale.animateTo(
targetValue = CountDownAnimationSpec.TARGET_SCALE,
animationSpec = tween(
durationMillis = CountDownStateMachine.TICK_MILLIS.toInt(),
easing = CountDownAnimationSpec.AccelerateDecelerateEasing
)
)
}

Box(
modifier = modifier.fillMaxSize()
) {
Image(
painter = painterResource(R.drawable.star_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.testTag(CountDownScreenTestTags.BACKGROUND)
)
Image(
painter = painterResource(CountDownStateMachine.numberDrawableRes(count)),
contentDescription = count.toString(),
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = (-350).dp)
.scale(scale.value)
.testTag(CountDownScreenTestTags.NUMBER)
)
Text(
text = stringResource(R.string.count_down_desc),
style = RunnectTheme.textStyle.medium15,
color = White,
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = (-280).dp)
.testTag(CountDownScreenTestTags.DESCRIPTION)
)
}
}
13 changes: 0 additions & 13 deletions app/src/main/res/anim/anim_count.xml

This file was deleted.

43 changes: 0 additions & 43 deletions app/src/main/res/layout/activity_count_down.xml

This file was deleted.

Loading
Loading