시맨틱 트리(Semantics tree)는 항상 관련 정보만 표시하여 최대한 간결하게 만들려고 한다.

예를 들어, TopAppBar에서는 아이콘과 레이블이 다른 노드일 필요가 없다. “Overview” 노드를 살펴보자.

d20c96207c30e44a.png
|-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'

이 노드에는 selectable 컴포넌트에 대해 특별히 정의된 속성(예: SelectedRole)과 전체 탭에 대한 콘텐츠 설명(content description)이 있다. 이는 간단한 테스트에 매우 유용한 고수준(high-level) 속성이다. 아이콘이나 텍스트에 대한 세부정보는 중복되어 표시되지 않는다.

컴포즈는 Text와 같은 일부 컴포저블에서 이러한 Semantics 속성을 자동으로 노출한다. 하나 또는 여러 하위 항목으로 구성된 단일 컴포넌트를 나타내도록 커스터마이징하고 병합할 수도 있다. 예를 들어, Text 컴포저블을 포함하는 Button을 나타낼 수 있다. MergeDescendants = ‘true’ 속성은 이 노드에 하위 항목이 있지만 병합되었음을 알려준다. 테스트에서 우리는 종종 모든 노드에 액세스해야 한다.

탭 안의 Text가 표시되는지 여부를 확인하기 위해, 병합되지 않은 시맨틱 트리를 쿼리하여 useUnmergedTree = true를 onRoot라는 finder에 전달할 수 있다.

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")

}

지금 로그캣에 출력된 내용은 약간 더 길다.

Printing with useUnmergedTree = 'true'
    Node #1 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
     |-Node #2 at (l=0.0, t=63.0, r=1080.0, b=210.0)px
       [SelectableGroup]
       MergeDescendants = 'true'
        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]
        |-Node #11 at (l=552.0, t=105.0, r=615.0, b=168.0)px
          Role = 'Tab'
          Selected = 'false'
          StateDescription = 'Not selected'
          ContentDescription = 'Bills'
          Actions = [OnClick]
          MergeDescendants = 'true'
          ClearAndSetSemantics = 'true'

노드 #3에는 여전히 하위 항목이 없다.

        |-Node #3 at (l=42.0, t=105.0, r=105.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'false'
        | StateDescription = 'Not selected'
        | ContentDescription = 'Overview'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        | ClearAndSetSemantics = 'true'

그러나 선택한 탭인 노드 6에는 하나가 있으며, 이제 ‘Text’ 속성을 볼 수 있다.

        |-Node #6 at (l=189.0, t=105.0, r=468.0, b=168.0)px
        | Role = 'Tab'
        | Selected = 'true'
        | StateDescription = 'Selected'
        | ContentDescription = 'Accounts'
        | Actions = [OnClick]
        | MergeDescendants = 'true'
        |  |-Node #9 at (l=284.0, t=105.0, r=468.0, b=154.0)px
        |    Text = 'ACCOUNTS'
        |    Actions = [GetTextLayoutResult]

우리가 원하는 대로 올바른 동작을 확인하기 위해 “ACCOUNTS”라는 텍스트가 있는 노드 하나를 찾는 matcher를 작성하자. 이 노드는 콘텐츠 설명(content description) “Accounts”를 가진 상위 노드다.

Compose Testing Cheat Sheet를 다시 확인하고, 해당 matcher를 작성하는 방법을 찾아보자. andor와 같은 Boolean 연산자를 matcher와 함께 사용할 수 있다.

모든 finder에는 useUnmergedTree라는 매개변수가 있다. 병합되지 않은 트리를 사용하려면 true로 설정하자.

최종 코드(Solution)을 보지 않고 테스트를 한번 작성해보자.

최종 코드

@Test
fun rallyTopAppBarTest_currentLabelExists() {
    val allScreens = RallyScreen.values().toList()
    composeTestRule.setContent {
        RallyTopAppBar(
            allScreens = allScreens,
            onTabSelected = { },
            currentScreen = RallyScreen.Accounts
        )
    }

    composeTestRule
        .onNode(
            hasText(RallyScreen.Accounts.name.uppercase(Locale.getDefault())) and
            hasParent(
                hasContentDescription(RallyScreen.Accounts.name)
            ),
            useUnmergedTree = true
        )
        .assertExists()
}

Note: 이 경우 매우 격리된 테스트이기 때문에, 엄격하게 matcher에 상위요소를 추가할 필요가 없다. 그러나 더 큰 테스트에서 실패할 수 있는 광범위한 finder(예: hasText)를 단독으로 사용하는 것은 피하는 것이 좋다(텍스트의 다른 인스턴스를 찾을 수 있는 경우).

이제 테스트를 실행해보도록 하자.

377261a7b0db6354.png

축하! 이 단계에서는 속성 병합과 병합 및 병합 해제 시맨틱 트리에 대해 배웠다.

카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.