Skip to content

Commit

Permalink
Change isIdle in tests to return true when there exist delayed (e…
Browse files Browse the repository at this point in the history
….g. LaunchedEffect tasks.
  • Loading branch information
m-sasha committed Sep 11, 2024
1 parent d85a139 commit 340a155
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal class TooltipAreaTest {
onNodeWithTag("elementWithTooltip").performMouseInput {
moveTo(Offset(30f, 40f))
}

mainClock.advanceTimeBy(TooltipDelayMillis + 1L)
onNodeWithTag("tooltip").assertExists()
}

Expand All @@ -64,6 +64,7 @@ internal class TooltipAreaTest {
onNodeWithTag("elementWithTooltip").performMouseInput {
moveTo(Offset(30f, 40f))
}
mainClock.advanceTimeBy(TooltipDelayMillis + 1L)
onNodeWithTag("tooltip").assertExists()

onNodeWithTag("elementWithTooltip").performMouseInput {
Expand All @@ -84,6 +85,7 @@ internal class TooltipAreaTest {
onNodeWithTag("elementWithTooltip").performMouseInput {
moveTo(Offset(30f, 40f))
}
mainClock.advanceTimeBy(TooltipDelayMillis + 1L)
onNodeWithTag("tooltip").assertExists()

onNodeWithTag("elementWithTooltip").performMouseInput {
Expand Down Expand Up @@ -122,6 +124,7 @@ internal class TooltipAreaTest {
onNodeWithTag("elementWithTooltip").performMouseInput {
moveTo(Offset(30f, 40f))
}
mainClock.advanceTimeBy(TooltipDelayMillis + 1L)

// Move into the tooltip, but still inside the area
onNodeWithTag("tooltip").let {
Expand Down Expand Up @@ -178,6 +181,7 @@ internal class TooltipAreaTest {
onNodeWithTag("elementWithTooltip").performMouseInput {
moveTo(Offset(30f, 40f))
}
mainClock.advanceTimeBy(TooltipDelayMillis + 1L)
onNodeWithTag("tooltip").assertExists()

onNodeWithTag("elementWithTooltip").performMouseInput {
Expand All @@ -189,14 +193,15 @@ internal class TooltipAreaTest {
release()
moveBy(Offset(10f, 10f))
}
mainClock.advanceTimeBy(TooltipDelayMillis + 1L)
onNodeWithTag("tooltip").assertExists()
}

@Composable
private fun SimpleTooltipArea(
areaSize: Dp = 100.dp,
tooltipSize: Dp = 20.dp,
delayMillis: Int = 500
delayMillis: Int = TooltipDelayMillis
) {
TooltipArea(
tooltip = {
Expand All @@ -207,4 +212,6 @@ internal class TooltipAreaTest {
Box(Modifier.size(areaSize).testTag("elementWithTooltip"))
}
}
}
}

private const val TooltipDelayMillis = 500
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,43 @@

package androidx.compose.material

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.launch
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test


class SnackbarTest {
@get:Rule
val rule = createComposeRule()

@OptIn(ExperimentalTestApi::class)
@Test
fun testQueueing() {
fun testQueueing() = runComposeUiTest {
var snackbarsShown = 0

rule.setContent {
val scope = rememberCoroutineScope()
setContent {
val state = remember { SnackbarHostState() }

Scaffold(
snackbarHost = { SnackbarHost(state) }
) {
scope.launch {
(1..4).forEach {
state.showSnackbar(it.toString())
snackbarsShown = it
SnackbarHost(state)

LaunchedEffect(Unit) {
repeat(4) {
launch {
state.showSnackbar(snackbarsShown.toString())
snackbarsShown++
}
}
}
}

assertEquals(4, snackbarsShown)
// The snackbars are shown sequentially. The coroutines calling `showSnackbar` are blocked
// and released by a `LaunchedEffect` (in the composition) after the snackbar duration
// completes. That's why each pair of `advanceTimeBy` and `waitForIdle` only shows one
// snackbar.
(1..4).forEach {
mainClock.advanceTimeBy(60_000)
waitForIdle()
assertEquals(it, snackbarsShown)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,37 @@ class TestBasicsTest {
rule.unregisterIdlingResource(idlingResource)
test(expectedValue = "first")
}

@Test(timeout = 500)
fun infiniteLoopInLaunchedEffectDoesNotHang() = runComposeUiTest {
setContent {
LaunchedEffect(Unit) {
while (true) {
delay(1000)
}
}
}
}

@Test(timeout = 500)
fun delayInLaunchedEffectIsExecutedAfterAdvancingClock() = runComposeUiTest {
var value = 0
mainClock.autoAdvance = false
setContent {
LaunchedEffect(Unit) {
repeat(5) {
delay(1000)
value = it+1
}
}
}

assertEquals(0, value)
mainClock.advanceTimeBy(999, ignoreFrameDuration = true)
assertEquals(0, value)
mainClock.advanceTimeBy(2, ignoreFrameDuration = true)
assertEquals(1, value)
mainClock.advanceTimeBy(2000, ignoreFrameDuration = true)
assertEquals(3, value)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class FlushCoroutineDispatcherTest {
}

assertEquals((0 until 10000).toList(), actualNumbers)
assertFalse(dispatcher.hasTasks())
assertFalse(dispatcher.hasImmediateTasks())
}

// Needs JVM APIs to test (and can't test in a single-threaded (JS) environment anyway)
Expand All @@ -79,7 +79,7 @@ class FlushCoroutineDispatcherTest {

try {
jobExchanger.exchange(Unit) // Wait for the task to run
assertTrue(dispatcher.hasTasks(), "hasTasks == false while executing tasks")
assertTrue(dispatcher.hasImmediateTasks(), "hasTasks == false while executing tasks")
} finally {
// Allow the task to complete, even if `assertTrue` threw an exception
jobExchanger.exchange(Unit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,15 @@ internal class FlushCoroutineDispatcher(
}

/**
* Whether the dispatcher has any tasks scheduled or currently running.
* Whether the dispatcher has immediate tasks pending.
*/
fun hasTasks() = synchronized(tasksLock) {
immediateTasks.isNotEmpty() || delayedTasks.isNotEmpty()
} || isPerformingRun
fun hasImmediateTasks() = isPerformingRun ||
synchronized(tasksLock) { immediateTasks.isNotEmpty() }

/**
* Whether the dispatcher has delayed tasks pending.
*/
fun hasDelayedTasks() = synchronized(tasksLock) { delayedTasks.isNotEmpty() }

/**
* Perform all scheduled tasks and wait for the tasks which are already
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ internal class ComposeSceneRecomposer(
*/
val hasPendingWork: Boolean
get() = recomposer.hasPendingWork ||
effectDispatcher.hasTasks() ||
recomposeDispatcher.hasTasks()
effectDispatcher.hasImmediateTasks() ||
recomposeDispatcher.hasImmediateTasks()

val compositionContext: CompositionContext
get() = recomposer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class FlushCoroutineDispatcherTest {
actualNumbers.add(3)
}

while (dispatcher.hasTasks()) {
while (dispatcher.hasImmediateTasks()) {
dispatcher.flush()
}

Expand All @@ -71,7 +71,7 @@ class FlushCoroutineDispatcherTest {
testScheduler.advanceUntilIdle()

assertEquals(listOf(1, 2, 3), actualNumbers)
assertFalse(dispatcher.hasTasks())
assertFalse(dispatcher.hasImmediateTasks())
}

@Test
Expand All @@ -81,10 +81,10 @@ class FlushCoroutineDispatcherTest {
val job = launch(dispatcher) {
delay(Long.MAX_VALUE/2)
}
assertTrue(dispatcher.hasTasks())
assertTrue(dispatcher.hasDelayedTasks())
job.cancel()
assertTrue(
!dispatcher.hasTasks(),
assertFalse(
dispatcher.hasDelayedTasks(),
"FlushCoroutineDispatcher has a delayed task that has been cancelled"
)
}
Expand Down Expand Up @@ -112,13 +112,15 @@ class FlushCoroutineDispatcherTest {
ignoreDispatch = true
delay(Long.MAX_VALUE/2)
}
assertTrue(dispatcher.hasTasks())
// Needed because the cancellation notification is itself dispatched with the coroutine
// dispatcher.
// dispatcher. Additionally, it's needed *before* the assertion, because if the assertion
// fails while ignoreDispatch is true, the cancellation of the coroutine will be ignored and
// the test will be stuck.
ignoreDispatch = false
assertTrue(dispatcher.hasDelayedTasks())
job.cancel()
assertTrue(
actual = !dispatcher.hasTasks(),
assertFalse(
actual = dispatcher.hasDelayedTasks(),
message = "FlushCoroutineDispatcher has a delayed task that has been cancelled"
)
}
Expand Down

0 comments on commit 340a155

Please sign in to comment.