בדיקת האנימציות

ב-Compose יש ComposeTestRule שמאפשר לכתוב בדיקות לאנימציות באופן דטרמיניסטי עם שליטה מלאה בשעון הבדיקה. כך אפשר לאמת ערכי אנימציה זמניים. בנוסף, בדיקה יכולה לפעול מהר יותר מהמשך בפועל של האנימציה.

‫ComposeTestRule חושף את שעון הבדיקה שלו בתור mainClock. כדי לשלוט בשעון בקוד הבדיקה, אפשר להגדיר את המאפיין autoAdvance כ-false. אחרי שמפעילים את האנימציה שרוצים לבדוק, אפשר להזיז את השעון קדימה באמצעות advanceTimeBy.

חשוב לציין שהפונקציה advanceTimeBy לא מזיזה את השעון בדיוק לפי משך הזמן שצוין. במקום זאת, הוא מעגל את משך הזמן כלפי מעלה למשך הזמן הקרוב ביותר שהוא כפולה של משך הפריים.

@get:Rule
val rule = createComposeRule()

@Test
fun testAnimationWithClock() {
    // Pause animations
    rule.mainClock.autoAdvance = false
    var enabled by mutableStateOf(false)
    rule.setContent {
        val color by animateColorAsState(
            targetValue = if (enabled) Color.Red else Color.Green,
            animationSpec = tween(durationMillis = 250)
        )
        Box(Modifier.size(64.dp).background(color))
    }

    // Initiate the animation.
    enabled = true

    // Let the animation proceed.
    rule.mainClock.advanceTimeBy(50L)

    // Compare the result with the image showing the expected result.
    // `assertAgainGolden` needs to be implemented in your code.
    rule.onRoot().captureToImage().assertAgainstGolden()
}

בדיקות אופטימיזציה של אנימציה

כשבודקים אנימציות באיכות גבוהה, בדרך כלל צריך להשבית את המעבר האוטומטי ולעבור ידנית בין הפריימים כדי לאשר מצבי ביניים של ממשק המשתמש. בלולאות הספציפיות האלה, שפועלות פריים אחרי פריים, צריך להשתמש ב-method‏ runWithoutImplicitWait כדי להריץ את טענות הנכוֹנוּת (assertions). שאילתות צומת רגילות (כמו onNodeWithTag או fetchSemanticsNode) מפעילות סנכרונים משתמעים שהם מיותרים כששולטים בשעון באופן ידני, כך שמעקף שלהם מאיץ באופן משמעותי את זמני הריצה של הבדיקה.

הנחיות שימוש

  • ניהול ידני של השעון: משתמשים ב-API הזה כש-mainClock.autoAdvance מוגדר ל-false וממשק המשתמש נמצא במצב יציב וידוע בפריים הנוכחי.
  • הפעלה של שרשור UI: כדי לוודא שעץ ממשק המשתמש יציב, צריך להפעיל את runWithoutImplicitWait בשרשור UI, למשל באמצעות runOnUiThread. אם מריצים אותה מחוץ לשרשור ה-UI, הבדיקה חשופה למרוצי תהליכים ולקריאות של מצבים לא עדכניים.
  • טענות נכונות (assertions) לקריאה בלבד: הבלוק צריך להכיל רק טענות נכונות לקריאה בלבד. כל פעולה שמשנה את המצב צריכה להתבצע מחוץ לבלוק הזה.

דוגמה

@Test
fun runWithoutImplicitWaitSample() = runComposeUiTest {
    setContent { MainScreen() }
    mainClock.autoAdvance = false

    // Trigger an animation
    onNodeWithText("Start Animation").performClick()

    // Step through the animation frame-by-frame
    while (hasPendingWork()) {
        mainClock.advanceTimeByFrame()
        waitForIdle()
        runOnUiThread {
            // Suppress implicit synchronization inside this block to avoid redundant
            // waits on each node query, making the frame assertions execute much faster.
            runWithoutImplicitWait {
                val box1 = onNodeWithTag("Box1").fetchSemanticsNode()
                val box2 = onNodeWithTag("Box2").fetchSemanticsNode()
                val box3 = onNodeWithTag("Box3").fetchSemanticsNode()

                // Assert the exact intermediate state of all three properties for this frame
                assert(box1.boundsInRoot.right <= box2.boundsInRoot.left)
                assert(box2.boundsInRoot.right <= box3.boundsInRoot.left)
            }
        }
    }
}