Status Update
Comments
ma...@google.com <ma...@google.com>
je...@google.com <je...@google.com> #2
Hi. Thanks for reporting this. Fixed in alpha-04
je...@google.com <je...@google.com> #3
Branch: androidx-main
commit e782987543a9f8ccd485e970ddc74564b24378db
Author: Vighnesh Raut <vighnesh.raut13@gmail.com>
Date: Mon Jan 02 15:27:40 2023
fix: tab row crashes when only 1 tab is added
Bug:
Test: Added unit test
Change-Id: I6381dbac304fc1d69d3708c6655f8b595668e93f
M tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt
M tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt
me...@thomaskeller.biz <me...@thomaskeller.biz> #4
je...@google.com <je...@google.com> #5
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.tv:tv-material:1.0.0-alpha04
me...@thomaskeller.biz <me...@thomaskeller.biz> #6
Yes, for example I have a Fragment with a Compose UI, set up like this:
class FooFragment : Fragment() {
private lateinit var binding: FragmentFooBinding
private val viewModel: FooViewModel by viewModels()
private val args: FooFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.composeView.setContent {
MdcTheme {
FooScreen(viewModel)
}
}
viewModel.onAction(FooActions.OnViewCreated(args))
}
override fun onPause() {
viewModel.onAction(FooActions.OnPause)
super.onPause()
}
}
What I'm doing right now in my Compose test is that I manually trigger the specific actions to either feed the arguments to my ViewModel or to manually trigger the OnPause
lifecycle action. This however doesn't test the real implementation! Since the fragment code isn't run, I cannot verify that I trigger the specific actions in the right lifecycle methods (or, that anybody moved things around).
Similar issues arise once I want to trigger and test for jetpack navigation events via a TestNavController
, i.e. testing regular nav graphs in conjunction with the VM / Fragment calling code.
me...@thomaskeller.biz <me...@thomaskeller.biz> #7
Hi, is this still a Won't Fix for you? This issue is still present and needs awkward workarounds.
je...@google.com <je...@google.com> #8
Apologies, this slipped off my radar.
If you add the FooFragment to a test activity and then use createAndroidComposeRule<YourTestActivity>()
, it should execute everything necessary to get the activity in the resumed state, which includes onViewCreated
.
For onPause
, however, you need a little more control. What you can do there is create an "empty" compose test rule, one that does not start an activity for you at all, and move your fragment through all states manually (like you would do for View-based UIs). For example:
@get:Rule
val composeRule = createEmptyComposeRule()
@Test
fun test() {
with(launchFragmentInContainer<FooFragment>()) {
// composeRule.onNode(..).assert(..), etc
moveToState(State.STARTED)
// assertions, etc.
}
}
me...@thomaskeller.biz <me...@thomaskeller.biz> #9
The problem is really interaction. The OnViewCreated
action would kick-off a change that would result in a change of the downstream state that FooScreen
would subscribe to. I assume that even though I'd add FooFragment
to the Activity-under-test, I'd still have to call composeRule.setContent {}
, because the Fragment's setContent
call on the ComposeView
still knows nothing of the Recomposer
from the test rule, so these two things would still be detached from one another.
That's what I was referring to - if the current implementation of AndroidComposeTestRule
would be a bit more open and would also allow the wrapping of ComposeView
intances (from Activity
s, Fragment
s or else), it would be of much help, especially for cases where developers weren't able to convert a complete screen to Compose yet, but start bottom up with small building blocks, like single views or fragments.
My go-to-solution is now to have a separate Fragment
test that mocks out the ViewModel and tests the interactions in isolation, and then have another Compose+ViewModel test which does the other part.
je...@google.com <je...@google.com> #10
Ah, I think I see the misunderstanding now.
You don't need to call AndroidComposeTestRule.setContent
if your Activity or Fragment already sets the content. If you look closely to the return type of createEmptyComposeRule()
, you'll find that it doesn't even have a setContent
method.
The test rule takes care of wiring everything together. If you're interested to see how, take a look at
@get:Rule(order = 0)
val activityScenarioRule = ActivityScenarioRule<CustomActivity>()
@get:Rule(order = 1)
val composeTestRule = createEmptyComposeRule()
Please take a look at
ap...@google.com <ap...@google.com> #11
Branch: androidx-main
commit ff4469e78695de2436bdbe8822722f7c5e22de29
Author: Jelle Fresen <jellefresen@google.com>
Date: Tue Feb 15 14:22:51 2022
Test Compose when content set by a Fragment
This shows we can test Compose content if it is set by a Fragment
Fix: 199631334
Test: ./gradlew compose:ui:ui-test-junit4:cC
Change-Id: I9c79a1e240734be481c54d24c14111750c916f45
M compose/ui/ui-test-junit4/build.gradle
A compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeInFragmentTest.kt
je...@google.com <je...@google.com>
me...@thomaskeller.biz <me...@thomaskeller.biz> #12
Many thanks for the example, will try that out!
me...@thomaskeller.biz <me...@thomaskeller.biz> #13
A small addition - when my Fragment is in Lifecycle.State.STARTED
, compose will tell me this:
java.lang.IllegalStateException: No compose views found in the app. Is your Activity resumed?
at androidx.compose.ui.test.TestContext.getAllSemanticsNodes$ui_test_release(TestOwner.kt:96)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNodes$ui_test_release(SemanticsNodeInteraction.kt:82)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrDie(SemanticsNodeInteraction.kt:155)
at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNode(SemanticsNodeInteraction.kt:106)
at androidx.compose.ui.test.AndroidAssertions_androidKt.checkIsDisplayed(AndroidAssertions.android.kt:29)
at androidx.compose.ui.test.AssertionsKt.assertIsDisplayed(Assertions.kt:33)
If I move my Fragment to Lifecycle.State.RESUMED
, this error is gone, which is fine for me; I don't need to test things before onResume
.
I do call setContent
on the ComposeView
in onViewCreated
, as per your example.
je...@google.com <je...@google.com> #14
when my Fragment is in Lifecycle.State.STARTED, compose will tell me this
That is by design. If you open multiple activities we only want to find Compose elements in the RESUMED activity as that is the one the user is looking at.
If there is a good reason why the test harness should look in STARTED activities (/fragments) as well, then please open a new bug and I'll take a look.
me...@thomaskeller.biz <me...@thomaskeller.biz> #15
What I also figured is that each test that does anything with composeRule
runs quite slow, i.e. each test method needs about two extra seconds to run. I looked at the profile, but cannot say for sure whats causing this. What I can see however is that a test with the Activity-based rule takes about 150 to 250ms to execute with Robolectric, when I use the empty test rule the test takes about 2100 to 2300ms. Unfortunately I couldn't reproduce it yet with a smaller example. Will further have to look into this.
me...@thomaskeller.biz <me...@thomaskeller.biz> #16
That is by design. If you open multiple activities we only want to find Compose elements in the RESUMED activity as that is the one the user is looking at.
No, this is totally fine. The error message could only a tad bit better by saying "Is your Activity or Fragment resumed?", other than that this is fine.
me...@thomaskeller.biz <me...@thomaskeller.biz> #17
I created a follow-up DialogFragment
-related crash.
ap...@google.com <ap...@google.com> #18
Branch: androidx-main
commit 0fa0145d802d8effe0e833f0fe996dbeec3ab23e
Author: Jelle Fresen <jellefresen@google.com>
Date: Thu Feb 24 14:40:50 2022
Disallow overwriting content with test content
This adds a check in AndroidComposeTest.setContent that makes sure that
the Activity on which the content is set doesn't have content yet. If
there is content already, it is a signal that the tester is probably
unintentionally doing something wrong, so help them by throwing.
Bug: 199631334
Test: ./gradlew bOS
Relnote: "`ComposeContentTestRule.setContent` will now throw an
IllegalStateException if you try to set content when there already is
content."
Change-Id: I888a5054c27d7884110415a812d0ac748be3f869
M compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/BitmapCapturingTest.kt
M compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
M compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/CustomActivityTest.kt
M compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/accessibility/CollectionInfoTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ClickTestRuleTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/IsDisplayedTest.kt
M compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/ActivityWithActionBar.kt
M compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/ComposeTest.android.kt
Description
Previously with plain XML UIs, one was able to test ViewModel and UI (Fragment/Activity + XML) in combination. With Compose and the
AndroidComposeTestRule
this is no longer possible:AndroidComposeTestRule
needs a test rule that provides anActivity
to the underlying compose infrastructure, while one can give it aFragmentScenarioRule
the fragment part of this cannot actually be re-used and the content of the whole Activity is replaced by the Compose UI.ComposeView.setContent()
of theFragment
(orActivity
, for that matter) isn't run when theAndroidComposeTestRule
'ssetContent
is used instead. Even worse, depending on the lifecycle state in which anActivity
is in (in which state it is started), a previous rendering could or could not have happened, leading to possible side effects with an actualViewModel
interfacing implementation.ComposeView
is not the root view of the controller component.I understand that a special
Recomposer
setup and a lot of other custom tooling is needed to make Jetpack Compose testable in an Robolectric / Android Instrumentation test, however I wish the test architecture would first and foremost provide an API to hook in / replace the existing functionality, e.g. by making it easy to inject a custom test composer in a running fragment / activity instance or by at least making this composer and possibly other utilities available for certain testing purposes.