Slide 1

Slide 1 text

stewemetal istvanjuhos.dev How to Test Your Compose UI István Juhos

Slide 2

Slide 2 text

#fosdem23 – @stewemetal istvanjuhos.dev Source of examples github.com/stewemetal/composehydrationtracker

Slide 3

Slide 3 text

#fosdem23 – @stewemetal istvanjuhos.dev Testing Views - Recap ☕ • View(Group) objects with rendering and behavior • Views are inflated from XML descriptors or instantiated in code • Views are directly referrable by View IDs

Slide 4

Slide 4 text

#fosdem23 – @stewemetal istvanjuhos.dev Testing Compose UI 🧪

Slide 5

Slide 5 text

#fosdem23 – @stewemetal istvanjuhos.dev Testing Compose UI • No direct access to the Composition • No UI objects to look up directly • No UI element IDs • Not every Composable emits UI ❌ ❌ ❌ ❌ 🧪

Slide 6

Slide 6 text

#fosdem23 – @stewemetal istvanjuhos.dev @Composable fun EntryList( entries: List = emptyList(), ) { LazyColumn { items(entries) { item -> HydrationItem(item = item) } } } @Composable fun HydrationItem(item: HydrationEntry) { Row( modifier = Modifier .wrapContentHeight( align = Alignment.CenterVertically, Composables to test: EntryList #fosdem23 – @stewemetal EntryList HydrationItem

Slide 7

Slide 7 text

#fosdem23 – @stewemetal istvanjuhos.dev HydrationItem(item = item) } } } @Composable fun HydrationItem(item: HydrationEntry) { Row( modifier = Modifier .wrapContentHeight( align = Alignment.CenterVertically, ) .padding(16.dp) ) { Text( "${item.milliliters} ml", modifier = Modifier.weight(1f), ) Text("${item.dateTime}") } } Composables to test: HydrationItem HydrationItem EntryList #fosdem23 – @stewemetal

Slide 8

Slide 8 text

#fosdem23 – @stewemetal istvanjuhos.dev Setting up Compose UI tests

Slide 9

Slide 9 text

#fosdem23 – @stewemetal istvanjuhos.dev "// Test rules and transitive dependencies: androidTestImplementation "androidx.compose.ui:ui-test-junit4" "// Needed for createComposeRule, but not createAndroidComposeRule: debugImplementation "androidx.compose.ui:ui-test-manifest" Setting up Compose UI tests implementation platform("androidx.compose:compose-bom:$compose_bom_version")

Slide 10

Slide 10 text

#fosdem23 – @stewemetal istvanjuhos.dev Setting up Compose UI tests @RunWith(AndroidJUnit4!::class) class EntryListTest { !// !!... } create ComposeRule () Android @get:Rule val composeTestRule =

Slide 11

Slide 11 text

#fosdem23 – @stewemetal istvanjuhos.dev Setting up Compose UI tests @RunWith(AndroidJUnit4!::class) class EntryListTest { !// !!... } create ComposeRule () Android @get:Rule val composeTestRule =

Slide 12

Slide 12 text

#fosdem23 – @stewemetal istvanjuhos.dev Setting up Compose UI tests @RunWith(AndroidJUnit4!::class) class EntryListTest { !// !!... } create ComposeRule () Android @get:Rule val composeTestRule =

Slide 13

Slide 13 text

#fosdem23 – @stewemetal istvanjuhos.dev Setting up Compose UI tests @RunWith(AndroidJUnit4!::class) class EntryListTest { !// !!... } createComposeRule() @get:Rule val composeTestRule =

Slide 14

Slide 14 text

#fosdem23 – @stewemetal istvanjuhos.dev @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } } A simple UI test createComposeRule() @get:Rule val composeTestRule =

Slide 15

Slide 15 text

#fosdem23 – @stewemetal istvanjuhos.dev A simple UI test @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } }

Slide 16

Slide 16 text

#fosdem23 – @stewemetal istvanjuhos.dev A simple UI test @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } }

Slide 17

Slide 17 text

#fosdem23 – @stewemetal istvanjuhos.dev A simple UI test @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } }

Slide 18

Slide 18 text

#fosdem23 – @stewemetal istvanjuhos.dev A simple UI test @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } }

Slide 19

Slide 19 text

#fosdem23 – @stewemetal istvanjuhos.dev A simple UI test @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } } private val entries = listOf( HydrationEntry(…), HydrationEntry(…), HydrationEntry(…), )

Slide 20

Slide 20 text

#fosdem23 – @stewemetal istvanjuhos.dev A simple UI test @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } } So, how can we act or assert on EntryList? 🤔

Slide 21

Slide 21 text

#fosdem23 – @stewemetal istvanjuhos.dev The semantics tree 🌳

Slide 22

Slide 22 text

#fosdem23 – @stewemetal istvanjuhos.dev The semantics tree 🌳 • A parallel tree next to the Composition 🌲🌳 • Used for accessibility and testing • Composables (can) contribute to the semantics tree

Slide 23

Slide 23 text

#fosdem23 – @stewemetal istvanjuhos.dev The semantics tree 🌳 composeTestRule.apply { setContent { Text("100 ml") } } Semantics tree root node Semantic node with text "100 ml"

Slide 24

Slide 24 text

#fosdem23 – @stewemetal istvanjuhos.dev The semantics tree 🌳 composeTestRule.apply { setContent { Text("100 ml") } } Root Text = '[100 ml]'

Slide 25

Slide 25 text

#fosdem23 – @stewemetal istvanjuhos.dev The semantics tree 🌳 composeTestRule.apply { setContent { Row { Text("100 ml") } } } Root Text = '[100 ml]'

Slide 26

Slide 26 text

#fosdem23 – @stewemetal istvanjuhos.dev The semantics tree 🌳 composeTestRule.apply { setContent { Row { Text("100 ml") Text("2023-02-04") } } } Text = '[100 ml]' Text = '[2023-02-04]' Root

Slide 27

Slide 27 text

#fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 Text = '[100 ml]' Text = '[2023-02-04]' Root Row { Text("100 ml") Text("2023-02-04") }

Slide 28

Slide 28 text

#fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 • In Compose tests onRoot() .printToLog("RowAndTexts") Text = '[100 ml]' Text = '[2023-02-04]' Root

Slide 29

Slide 29 text

#fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 • In Compose tests onRoot() .printToLog("RowAndTexts") D/RowAndTexts: printToLog: Printing with useUnmergedTree = 'false' Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px | Text = '[100 ml]' | Actions = [GetTextLayoutResult] |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px Text = '[2023-02-04]' Actions = [GetTextLayoutResult] Text = '[100 ml]' Text = '[2023-02-04]' Root

Slide 30

Slide 30 text

#fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 • In Compose tests onRoot() .printToLog("RowAndTexts") D/RowAndTexts: printToLog: Printing with useUnmergedTree = 'false' Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px | Text = '[100 ml]' | Actions = [GetTextLayoutResult] |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px Text = '[2023-02-04]' Actions = [GetTextLayoutResult] Text = '[100 ml]' Text = '[2023-02-04]' Root

Slide 31

Slide 31 text

#fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 • In Compose tests onRoot() .printToLog("RowAndTexts") D/RowAndTexts: printToLog: Printing with useUnmergedTree = 'false' Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px | Text = '[100 ml]' | Actions = [GetTextLayoutResult] |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px Text = '[2023-02-04]' Actions = [GetTextLayoutResult] Text = '[100 ml]' Text = '[2023-02-04]' Root

Slide 32

Slide 32 text

#fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 • In Compose tests onRoot() .printToLog("RowAndTexts") D/RowAndTexts: printToLog: Printing with useUnmergedTree = 'false' Node #1 at (l=0.0, t=66.0, r=224.0, b=125.0)px |-Node #2 at (l=0.0, t=66.0, r=136.0, b=125.0)px | Text = '[100 ml]' | Actions = [GetTextLayoutResult] |-Node #3 at (l=0.0, t=66.0, r=224.0, b=125.0)px Text = '[2023-02-04]' Actions = [GetTextLayoutResult] Text = '[100 ml]' Text = '[2023-02-04]' Root

Slide 33

Slide 33 text

#fosdem23 – @stewemetal istvanjuhos.dev Visualizing the semantics tree 🌳👀 • In Android Studio • Layout Inspector

Slide 34

Slide 34 text

#fosdem23 – @stewemetal istvanjuhos.dev Modifying the semantics tree 🌳 Row( modifier = Modifier .semantics { !// !!... } ) { Text("100 ml") Text("2023-02-04") }

Slide 35

Slide 35 text

#fosdem23 – @stewemetal istvanjuhos.dev Root Text = '[100 ml]' Text = '[2023-02-04]' Modifying the semantics tree 🌳 Row( modifier = Modifier .semantics { !// !!... } ) { Text("100 ml") Text("2023-02-04") }

Slide 36

Slide 36 text

#fosdem23 – @stewemetal istvanjuhos.dev Modifying the semantics tree 🌳 Row( modifier = Modifier .semantics { contentDescription = "List item" } ) { Text("100 ml") Text("2023-02-04") } Root Text = '[100 ml]' Text = '[2023-02-04]' ContentDescription = '[List item]'

Slide 37

Slide 37 text

#fosdem23 – @stewemetal istvanjuhos.dev Modifying the semantics tree 🌳 Row( modifier = Modifier .semantics { testTag = "List item" } ) { Text("100 ml") Text("2023-02-04") } Root Text = '[100 ml]' Text = '[2023-02-04]' Tag = '[List item]'

Slide 38

Slide 38 text

#fosdem23 – @stewemetal istvanjuhos.dev Using the semantics tree... @get:Rule val composeTestRule = createComposeRule() @Test fun entriesExist_entriesDisplayed() { composeTestRule.apply { setContent { HydrationTrackerTheme { EntryList( entries = entries ) } } "// ""... } } !!...we can test Compose UI 🤩

Slide 39

Slide 39 text

#fosdem23 – @stewemetal istvanjuhos.dev Compose testing API

Slide 40

Slide 40 text

#fosdem23 – @stewemetal istvanjuhos.dev Select nodes - Finders • onRoot onRoot() .printToLog(”TAG") Root Text = '[100 ml, 2023-02-04]' Tag = '[List item]' ContentDescription = '[100 ml on 2023-02-04]'

Slide 41

Slide 41 text

#fosdem23 – @stewemetal istvanjuhos.dev Select nodes - Finders • onNodeWithTag onNodeWithTag("List item") Root Text = '[100 ml, 2023-02-04]' Tag = '[List item]' ContentDescription = '[100 ml on 2023-02-04]'

Slide 42

Slide 42 text

#fosdem23 – @stewemetal istvanjuhos.dev Select nodes - Finders • onNodeWithContentDescription onNodeWithContentDescription( "100 ml on 2023-02-04", ) Root Text = '[100 ml, 2023-02-04]' Tag = '[List item]' ContentDescription = '[100 ml on 2023-02-04]'

Slide 43

Slide 43 text

#fosdem23 – @stewemetal istvanjuhos.dev Select nodes - Finders • onNodeWithText onNodeWithText( "100 ml", ) Root Text = '[100 ml, 2023-02-04]' Tag = '[List item]' ContentDescription = '[100 ml on 2023-02-04]'

Slide 44

Slide 44 text

#fosdem23 – @stewemetal istvanjuhos.dev Select nodes - Finders • onAllNodesWith... Tag = '[entries_list]' onAllNodesNodeWithTag("List item") Tag = '[List item]' Text = '[100 ml, 2023-02-04]' Tag = '[List item]' Text = '[500 ml, 2022-07-04]' !!...

Slide 45

Slide 45 text

#fosdem23 – @stewemetal istvanjuhos.dev Actions • performClick • performScrollTo... • performTextInput • performGesture • performKeyPress • ... onNodeWithText("100 ml") .performClick()

Slide 46

Slide 46 text

#fosdem23 – @stewemetal istvanjuhos.dev Actions • performClick • performScrollTo... • performTextInput • performGesture • performKeyPress • ... Role = 'Button' Focused = 'false' Text = '[100 ml]' Actions = [OnClick, GetTextLayoutResult] MergeDescendants = 'true' onNodeWithText("100 ml") .performClick()

Slide 47

Slide 47 text

#fosdem23 – @stewemetal istvanjuhos.dev Compose testing cheatseet developer.android.com/jetpack/compose/testing-cheatsheet

Slide 48

Slide 48 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing

Slide 49

Slide 49 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing • Adding Compose content to existing View-based layouts • Adding existing Views to the composition • Compose & Espresso in the same tests 🪟 🪟 ☕ &

Slide 50

Slide 50 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView

Slide 51

Slide 51 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView "

Slide 52

Slide 52 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView "

Slide 53

Slide 53 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView "

Slide 54

Slide 54 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView class ComposeViewDemoActivity : AppCompatActivity() { private lateinit var binding: ActivityComposeViewDemoBinding "// ""... } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityComposeViewDemoBinding.inflate(layoutInflater) setContentView(binding.root) }

Slide 55

Slide 55 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView binding.toolbar.title = "ComposeView Demo Title" binding.composeView.apply { setViewCompositionStrategy(DisposeOnDetachedFromWindow) setContent { CustomButton( text = "Demo Button", modifier = Modifier .padding(horizontal = 8.dp) .testTag("demo_custom_button"), ) {} } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityComposeViewDemoBinding.inflate(layoutInflater) setContentView(binding.root) }

Slide 56

Slide 56 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView binding.toolbar.title = "ComposeView Demo Title" binding.composeView.apply { setViewCompositionStrategy(DisposeOnDetachedFromWindow) setContent { CustomButton( text = "Demo Button", modifier = Modifier .padding(horizontal = 8.dp) .testTag("demo_custom_button"), ) {} } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityComposeViewDemoBinding.inflate(layoutInflater) setContentView(binding.root) }

Slide 57

Slide 57 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView @RunWith(AndroidJUnit4"::class) class DemoComposeViewTest { @get:Rule val androidComposeTestRule = createAndroidComposeRule() @Test fun hybrid_ui_customComposeButton_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Demo Button").assertIsDisplayed() } } }

Slide 58

Slide 58 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView @RunWith(AndroidJUnit4"::class) class DemoComposeViewTest { @get:Rule val androidComposeTestRule = createAndroidComposeRule() @Test fun hybrid_ui_customComposeButton_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Demo Button").assertIsDisplayed() } } }

Slide 59

Slide 59 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView @RunWith(AndroidJUnit4"::class) class DemoComposeViewTest { @get:Rule val androidComposeTestRule = createAndroidComposeRule() @Test fun hybrid_ui_customComposeButton_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Demo Button").assertIsDisplayed() } } } ✅

Slide 60

Slide 60 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing – ComposeView @RunWith(AndroidJUnit4"::class) class DemoComposeViewTest { @get:Rule val androidComposeTestRule = createAndroidComposeRule() @Test fun hybrid_ui_customComposeButton_isVisible() { androidComposeTestRule.apply { Espresso.onView(withText("ComposeView Demo Title")) .check(matches(isDisplayed())) onNodeWithText("Demo Button").assertIsDisplayed() } } } ✅ ✅

Slide 61

Slide 61 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView

Slide 62

Slide 62 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView class CustomButtonView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : ConstraintLayout(context, attrs, defStyleAttr) { "// ""... LayoutInflater.from(context).inflate(R.layout.view_custom_button, this, true) "// ""... fun setText(text: String) { button.text = text } override fun setOnClickListener(listener: OnClickListener?) { button.setOnClickListener { listener"?.onClick(button) } } }

Slide 63

Slide 63 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView @Composable fun AndroidViewDemo( onButtonClick: () -> Unit, ) { Scaffold( topBar = { TopAppBar(title = { Text("AndroidView Demo Title") }) }, content = { padding -> AndroidView( modifier = Modifier.padding(padding).wrapContentHeight(), factory = { context -> CustomButtonView(context).apply { setText("Demo Button") setOnClickListener { onButtonClick() } } }, ) } ) }

Slide 64

Slide 64 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView @Composable fun AndroidViewDemo( onButtonClick: () -> Unit, ) { Scaffold( topBar = { TopAppBar(title = { Text("AndroidView Demo Title") }) }, content = { padding -> AndroidView( modifier = Modifier.padding(padding).wrapContentHeight(), factory = { context -> CustomButtonView(context).apply { setText("Demo Button") setOnClickListener { onButtonClick() } } }, ) } ) }

Slide 65

Slide 65 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView @get:Rule val composeTestRule = createComposeRule() @Test fun composable_withAndroidView() { composeTestRule.apply { "// ""... } } var buttonClicked = false setContent { HydrationTrackerTheme { AndroidViewDemo { buttonClicked = true } } }

Slide 66

Slide 66 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView onNodeWithText("AndroidView Demo Title").assertIsDisplayed() Espresso.onView(withText("Demo Button")).check(matches(isDisplayed())) Espresso.onView(withText("Demo Button")).perform(click()) assertEquals(true, buttonClicked) var buttonClicked = false setContent { HydrationTrackerTheme { AndroidViewDemo { buttonClicked = true } } }

Slide 67

Slide 67 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView var buttonClicked = false setContent { HydrationTrackerTheme { AndroidViewDemo { buttonClicked = true } } } onNodeWithText("AndroidView Demo Title").assertIsDisplayed() Espresso.onView(withText("Demo Button")).check(matches(isDisplayed())) Espresso.onView(withText("Demo Button")).perform(click()) assertEquals(true, buttonClicked) ✅ ✅

Slide 68

Slide 68 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView var buttonClicked = false setContent { HydrationTrackerTheme { AndroidViewDemo { buttonClicked = true } } } onNodeWithText("AndroidView Demo Title").assertIsDisplayed() Espresso.onView(withText("Demo Button")).check(matches(isDisplayed())) assertEquals(true, buttonClicked) ✅ ✅ ✅❓ Espresso.onView(withText("Demo Button")).perform(click())

Slide 69

Slide 69 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView var buttonClicked = false setContent { HydrationTrackerTheme { AndroidViewDemo { buttonClicked = true } } } onNodeWithText("AndroidView Demo Title").assertIsDisplayed() Espresso.onView(withText("Demo Button")).check(matches(isDisplayed())) assertEquals(true, buttonClicked) ✅ ✅ ✅❓ ❌ Espresso.onView(withText("Demo Button")).perform(click())

Slide 70

Slide 70 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView Doesn’t work as of Jetpack Compose BOM 2023.01.00 🙁 https:!//issuetracker.google.com/issues/215231631 Espresso.onView(withText("Demo Button")).perform(click())

Slide 71

Slide 71 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView Espresso.onView(withText("Demo Button")).check { view, _ -> view.performClick() } Unstable workaround https:!//issuetracker.google.com/issues/215231631

Slide 72

Slide 72 text

#fosdem23 – @stewemetal istvanjuhos.dev Hybrid UI testing - AndroidView var buttonClicked = false setContent { HydrationTrackerTheme { AndroidViewDemo { buttonClicked = true } } } onNodeWithText("AndroidView Demo Title").assertIsDisplayed() Espresso.onView(withText("Demo Button")).check(matches(isDisplayed())) assertEquals(true, buttonClicked) Espresso.onView(withText("Demo Button")).check { view, _ -> view.performClick() } ✅ ✅ ✅ ✅❓

Slide 73

Slide 73 text

#fosdem23 – @stewemetal istvanjuhos.dev Topics to check out • Testing animations in Compose 👀 • Synchronization and IdlingResources 🕞 • Screenshot testing with Paparazzi, Showkase, etc. 📸

Slide 74

Slide 74 text

#fosdem23 – @stewemetal istvanjuhos.dev Resources • developer.android.com/jetpack/compose/testing • developer.android.com/jetpack/compose/testing-cheatsheet • developer.android.com/jetpack/compose/accessibility • developer.android.com/jetpack/compose/semantics • youtu.be/kdwofTaEHrs • adbackstage.libsyn.com/episode-171-compose-testing • github.com/android/compose-samples • adavis.info/2021/09/testing-hybrid-jetpack-compose-apps.html • github.com/stewemetal/composehydrationtracker

Slide 75

Slide 75 text

#fosdem23 – @stewemetal istvanjuhos.dev Resources • developer.android.com/jetpack/compose/testing • developer.android.com/jetpack/compose/testing-cheatsheet • developer.android.com/jetpack/compose/accessibility • developer.android.com/jetpack/compose/semantics • youtu.be/kdwofTaEHrs • adbackstage.libsyn.com/episode-171-compose-testing • github.com/android/compose-samples • adavis.info/2021/09/testing-hybrid-jetpack-compose-apps.html • github.com/stewemetal/composehydrationtracker istvanjuhos.dev

Slide 76

Slide 76 text

stewemetal istvanjuhos.dev How to Test Your Compose UI István Juhos • Testing based on the semantics tree • Planned accessibility = good testability • Hybrid UI testing is supported out of the box (although there are issues currently) Cover photo by Rob Mulally on Unsplash