Possible Duplicate 1
Possible Duplicate 2
I have a custom View which contains textview and edittext. I want to access the Edittext in my custom View for UI test. But I dont want to use custom ViewAction to setEdittext because in that case I wont be able to support the methods like typeText. Here is my test method
#RunWith(AndroidJUnit4::class)
class LoginFragmentTest : BaseTest() {
#Test
#Throws(InterruptedException::class)
fun testLoginForm() {
val navController = TestNavHostController(ApplicationProvider.getApplicationContext())
val loginScenario = launchFragmentInContainer<LoginFragment>()
loginScenario.onFragment { fragment ->
navController.setGraph(R.navigation.nav_login)
Navigation.setViewNavController(fragment.requireView(), navController)
}
onView(allOf(withId(R.id.etForm), isDescendantOfA(withId(R.id.email)))).perform(typeText("user#email.com"))
onView(allOf(withId(R.id.etForm), isDescendantOfA(withId(R.id.password)))).perform(typeText("123456"))
onView(withId(R.id.login)).perform(click())
}
}
I am getting following error
androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching: (with id is <com.example.cicddemo:id/etForm> and is descendant of a: with id is <2131296433>)
Note: I have implemented it by custom ViewAction which is working fine. But I am not able to get typeText functionality.
ViewonView(withId(R.id.email)).perform(setTextEditText(newText = "user#email.com"))
onView(withId(R.id.password)).perform(setTextEditText(newText = "123456"))
Custom ViewAction:
fun setTextEditText(
newText: String ?
): ViewAction {
return object: ViewAction {
override fun getConstraints(): Matcher < View > {
return CoreMatchers.allOf(
ViewMatchers.isDisplayed(),
ViewMatchers.isAssignableFrom(FormView::class.java)
)
}
override fun getDescription(): String {
return "Update the text from the custom EditText"
}
override fun perform(uiController: UiController ? , view : View) {
(view as FormView).setText(newText)
}
}
}
Is it possible to access the actualy edittext inside the Custom view class and pass it for test?
I have solved my problem by retrieving views by tags without using any custom ViewActions. The issue was that in my custom view I was using
etForm.id = View.generateViewId()
As id was changing at runtime so I am setting tags and access by
withTagValue
Here is updated code
#Test
#Throws(InterruptedException::class)
fun testInvalidEmailPassword() {
val emailViewInteraction = onView(allOf(withTagValue(`is`("email" as Any?)), isDescendantOfA(withId(R.id.email))))
val passwordViewInteraction = onView(allOf(withTagValue(`is`("password" as Any?)), isDescendantOfA(withId(R.id.password))))
emailViewInteraction.perform(typeText("user#email.com"))
passwordViewInteraction.perform(typeText("123456"))
onView(withId(R.id.login)).perform(click())
}
If this is AWS FormView, just match the LinearLayout, which it extends
...else you might eventually need to write a custom view-matcher.
Related
I'm trying to test a spinner that should display while loading the information from an API.
The problem is that I can't assert the initial state VISIBLE because it disappear too fast when the results are emitted back thus always having a failing test
Expected: (view has effective visibility <VISIBLE> and view.getGlobalVisibleRect() to return non-empty rectangle)
Got: view.getVisibility() was <GONE>
The first attempt using ui-automator
#Test
fun displayLoaderWhileFetchingPlaylistDetails() {
IdlingRegistry.getInstance().unregister(idlingResource)
uiObjectWithId(R.id.playlist_list).getChild(UiSelector().clickable(true).index(0)).click()
val spinner = uiObjectWithId(R.id.playlist_details_loader)
assertTrue(spinner.exists())
}
Another variant for the test without ui-automator
#Test
fun displayLoaderWhileFetchingPlaylistDetails2() {
IdlingRegistry.getInstance().unregister(idlingResource)
onView(
allOf(
withId(R.id.playlist_image),
isDescendantOfA(withPositionInParent(R.id.playlist_list, 0))
)
)
.perform(click())
assertDisplayed(R.id.playlist_details_loader)
}
ui-automator helper
fun uiObjectWithId(#IdRes id: Int): UiObject {
val resourceId = getTargetContext().resources.getResourceName(id);
val selector = UiSelector().resourceId(resourceId)
return UiDevice.getInstance(getInstrumentation()).findObject(selector)
}
Fragment
private fun observeLoaderState() {
viewModel.playlistLoader.observe(this as LifecycleOwner) { playlistSpinner ->
when (playlistSpinner) {
true -> playlist_details_loader.visibility = View.VISIBLE
else -> playlist_details_loader.visibility = View.GONE
}
}
}
ViewModel
class PlaylistDetailViewModel(
private val repository: PlaylistRepository
) : ViewModel() {
val playlistLoader = MutableLiveData<Boolean>()
fun getPlaylistDetails(playlistId: String) = liveData {
playlistLoader.postValue(true)
emitSource(
repository.getPlaylistDetailsById(playlistId)
.onEach { playlistLoader.postValue(false) }
.asLiveData()
)
}
}
Thanks!
In Android when you set a View's visibility to GONE the renderer does not draw the View object, so the View practically has no dimensions. The same applies if you call any function that searchs in the UI tree for the View that has visibility set to GONE, and will return no match. If your only goal is to pass the test, my suggestions would be to set the View to INVISIBLE instead of GONE or to change the way you test for that specific layout.
From Android documentation:
View.GONE This view is invisible, and it doesn't take any space for
layout purposes.
View.INVISIBLE This view is invisible, but it still takes up space
for layout purposes.
I am rendering a form based on JSON response that I fetch from the server.
My use case involves listening to a click from a radio button, toggling the visibility of certain text fields based on the radioButton selection, and refreshing the layout with the visible textView.
The expected output should be to update the same view with the textView now visible, but I'm now seeing the same form twice, first with default state, and second with updated state.
Have I somehow created an entirely new model_ class and passing it to the controller? I just want to change the boolean field of the existing model and update the view.
My Model Class
#EpoxyModelClass(layout = R.layout.layout_panel_input)
abstract class PanelInputModel(
#EpoxyAttribute var panelInput: PanelInput,
#EpoxyAttribute var isVisible: Boolean,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var context: Context,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var textChangedListener: InputTextChangedListener,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var radioButtonSelectedListener: RadioButtonSelectedListener,
#EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var validationChangedListener: ValidationChangedListener
) : EpoxyModelWithHolder<PanelInputModel.PanelInputHolder>() {
#EpoxyAttribute var imageList = mutableListOf<ImageInput>()
override fun bind(holder: PanelInputHolder) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
generateViews(holder, inflater, panelInput.elements) // Generates textViews, radioButtons, etc, based on ElementType enum inside Panel input
}
fun generateRadioButtonView(element: Element) {
// Created a custom listener and calling its function
radioButtonSelectedListener.radioButtonSelected(chip.id, chip.text.toString())
}
fun generateTextView() {
// Show/hide textView based on isVisible value
}
My Controller Class
class FormInputController(
var context: Context,
var position: Int, // Fragment Position in PagerAdapter
var textChangedListener: InputTextChangedListener,
var radioButtonSelectedListener: RadioButtonSelectedListener,
var validationChangedListener: ValidationChangedListener
) : TypedEpoxyController<FormInput>() {
override fun buildModels(data: FormInput?) {
val panelInputModel = PanelInputModel_(
data as PanelInput,
data.isVisible,
context,
textChangedListener,
radioButtonSelectedListener,
validationChangedListener
)
panelInputModel.id(position)
panelInputModel.addTo(this)
}
}
My fragment implements the on radio button checked listener, modifies the formInput.isVisible = true and calls formInputController.setData(componentList)
Please help me out on this, thanks!
I don't think you are using Epoxy correctly, that's not how it's supposed to be.
First of all, let's start with the Holder: you should not inflate the view inside of bind/unbind, just set your views there. Also, the view is inflated for you from the layout file you are specifying at R.layout.layout_panel_input, so there is no need to inflate at all.
You should copy this into your project:
https://github.com/airbnb/epoxy/blob/master/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/KotlinEpoxyHolder.kt
And create your holder in this way:
class PanelInputHolder : KotlinHolder() {
val textView by bind<TextView>(R.id.your_text_view_id)
val button by bind<Button>(R.id.your_button_id)
}
Let's move to your model class: you should remove these variables from the constructor as they are going to be a reference for the annotation processor to create the actual class.
Also, don't set your layout res from the annotation as that will not be allowed in the future.
Like so:
#EpoxyModelClass
class PanelInputModel : EpoxyModelWithHolder<PanelInputHolder>() {
#EpoxyAttribute
lateinit var text: String
#EpoxyAttribute(DoNotHash)
lateinit var listener: View.OnClickListener
override fun getDefaultLayout(): Int {
return R.layout.layout_panel_input
}
override fun bind(holder: PanelInputHolder) {
// here set your views
holder.textView.text = text
holder.textView.setOnClickListener(listener)
}
override fun unbind(holder: PanelInputHolder) {
// here unset your views
holder.textView.text = null
holder.textView.setOnClickListener(null)
}
}
Loop your data inside the controller not inside the model:
class FormInputController : TypedEpoxyController<FormInput>() {
override fun buildModels(data: FormInput?) {
data?.let {
// do your layout as you want, with the models you specify
// for example a header
PanelInputModel_()
.id(it.id)
.text("Hello WOrld!")
.listener { // do something here }
.addTo(this)
// generate a model per item
it.images.forEach {
ImageModel_()
.id(it.imageId)
.image(it)
.addTo(this)
}
}
}
}
When choosing your id, keep in mind that Epoxy will keep track of those and update if the attrs change, so don't use a position, but a unique id that will not get duplicated.
I'm learning/using RecyclerViews and while my app works (at the moment!), there are two things that I don't understand.
Here are my ViewHolder declarations:
class AAAViewHolder ( view: View, var aaa: AAA? = null) : RecyclerView.ViewHolder (view) {...}
class BBBViewHolder (val view: View, var bbb: BBB? = null) : RecyclerView.ViewHolder (view) {...}
class CCCViewHolder ( view: View, var ccc: CCC? = null) : RecyclerView.ViewHolder (view) {...}
Why does BBBViewHolder have the extra val? If I remove it, then I get an "Unresolved reference: view" compiler error in onBindViewHolder in the ViewAdapter class. Why? And, if I *add the val declaration to AAA and CCC, Android Studio tells me that it's not needed and offers to remove it for me.
Next, there's something odd about the onBindViewHolder functions.
AAAListAdapter.kt (not showing getItemCount or onCreateViewHolder):
class AAAListAdapter : RecyclerView.Adapter<AAAViewHolder>() {
override fun onBindViewHolder(holder: AAAViewHolder, position: Int) {
val aaa = aaaList[position]
holder.itemView.aTextView.text = "AAA"
holder.aaa = aaa
}
}
BBBListAdapter.kt
class BBBListAdapter : RecyclerView.Adapter<BBBViewHolder>() {
override fun onBindViewHolder(holder: BBBViewHolder, position: Int) {
val bbb = bbbList[position]
holder.view.bTextView.text = "BBB"
holder.bbb = bbb
}
}
CCCListAdapter.kt
class CCCListAdapter : RecyclerView.Adapter<CCCViewHolder>() {
override fun onBindViewHolder(holder: CCCViewHolder, position: Int) {
val ccc = cccList[position]
holder.itemView.cTextView.text = "CCC"
holder.ccc = ccc
}
}
The code is almost identical, except why does BBBListAdapter reference holder.view, while the other two reference holder.itemView? Where are those properties declared? Can I control that? I'd much prefer them to be the same.
Seeing how A & C act the same but B is different, I'm guessing the two questions are related, but I don't know.
Firstly you declare val/var inside constructor to use those values somewhere in class without declaring or intializing it anywhere in your class. Let take in example, i want a list in adapter I'll pass it in adapter and in adapter I won't use val/var then and I can't use that unless I create a variable before hand and initialise it inside its default constructor.
class A() {
lateinit var view : View
constructor(view : View) {
this.view = view
}
view.textView.text = "Redundant Code"
}
Now you could have reduced this just by declaring it inside constructor itself.
class A(val view : View) {
view.textView.text = "Easy way"
}
Now coming to your use case, viewholder A and C are identical, and B has view is declared and you are using it, but from the code in adapter I don't think it is necessary, the same logic could have been used in Adapter B, holder.itemView.something, holder.itemView is ultimately is the view object which you're using in A and C, so val view is not need for that particular case.
If you're using it somewhere, then add the whole code, there I might be able to help you out why ViewHolder B is different. But from what you have posted, there is no need for using val inside constructor.
I am working in some end to end test using Espresso.
In the test I need to know the user id (because I need to call one endpoint that mocks some external party).
To get the user id, I was thinking about setting it as a tag in a view and get the tag with Espresso.
Is there a way to do that?
I only find ways to get a view by tag, but not actually getting the content of the tag.
Thanks for your help.
You don't need Espresso to retrieve a View tag - instead, you could simply call findViewById(...) to find your View and then retrieve its tag using getTag() method.
So, assuming, you use ActivityTestRule to launch your Activity, View is visible and has a unique ID within the Activity, you could do it as follows:
...
// make sure the View is there and visible
onView(withId(R.id.someId)).check(matches(isDisplayed()));
// retrieve its tag using ActivityTestRule
String tag = (String) activityRule.getActivity().findViewById(R.id.someId).getTag();
...
You can use the following extension function:
inline fun <reified T : Any> ViewInteraction.getTag(): T? {
var tag: T? = null
perform(object : ViewAction {
override fun getConstraints() = ViewMatchers.isAssignableFrom(View::class.java)
override fun getDescription() = "Get tag from View"
override fun perform(uiController: UiController, view: View) {
when (val viewTag = view.tag) {
is T -> tag = viewTag
else -> error("The tag cannot be casted to the given type!")
}
}
})
return tag
}
To the get tag like:
#Test
fun myTest() {
...
val userId = onView(withId(R.id.myView)).getTag<String>()
...
}
I read that the most benefit of using Anko is its reusability. But i could't find its exact example.
Currently in the new Android layout system, the boiler plate is like below:
DrawerLayout (with some setup)
CoordinatorLayout (with some setup)
AppBarLayout (with some setup)
ToolBar
<The Main Content>
NavigationView (with header inflated)
From the layout structure above, only <The Main Content> is varry. And
in many cases those ceremonial setup duplicated almost in every activity.
So here with Anko im thinking if there is a reusable solution about that issue. Im not expecting it will be reusable for general purpose layout, but et least i can minimize the ceremonial code in the project. Maybe i need something like:
class MainUI: AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View{
return with(ui) {
myCustomRootLayout {
//here is what <The Main Content> will be
}
}
}
}
From the code above im expecting myCustomRootLayout will do all the ceremonial setup for the root layout such as (DrawerLayout, CoordinatorLayout etc etc).
Is that possible?
EDIT
So i think my question is: How to make a custom component which can host other component
One way to reuse the code is to simply extract myCustomRootLayout into a extension method like so:
class MainUI: AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View {
return with(ui) {
myCustomRootLayout {
recyclerView()
}
}
}
}
fun <T> AnkoContext<T>.myCustomRootLayout(customize: AnkoContext<T>.() -> Unit = {}): View {
return relativeLayout {
button("Hello")
textView("myFriend")
customize()
}
}
However as stated in the documentation:
Although you can use the DSL directly (in onCreate() or everywhere
else), without creating any extra classes, it is often convenient to
have UI in the separate class. If you use the provided AnkoComponent
interface, you also you get a DSL layout preview feature for free.
It seems to be a good idea to extract the reusable piece into separate AnkoComponent:
class MainUI : AnkoComponent<MainActivity> {
override fun createView(ui: AnkoContext<MainActivity>): View {
return with(ui) {
MyCustomRootLayout<MainActivity>({
recyclerView()
}).createView(ui)
}
}
}
class MyCustomRootLayout<T : Context>(val customize: AnkoContext<T>.() -> Unit = {}) : AnkoComponent<T> {
override fun createView(ui: AnkoContext<T>) = with(ui) {
relativeLayout {
button("Hello")
textView("myFriend")
customize()
}
}
}
I actually found a way to do this, took me a while to figure it out.
I have a very basic test layout here, the content gets added to a
RelativeLayout.
The key here is to add your custom layout in a delegated AnkoContext that delegates to the immediate parent (the RelativeLayout in my case).
abstract class BaseAnkoComponent<T> : AnkoComponent<T> {
companion object {
val TOOLBAR_ID = View.generateViewId()
val COLLAPSING_ID = View.generateViewId()
val COORDINATOR_ID = View.generateViewId()
val APPBAR_ID = View.generateViewId()
val CONTENT_ID = View.generateViewId()
}
abstract fun <T> AnkoContext<T>.content(ui: AnkoContext<T>): View?
override fun createView(ui: AnkoContext<T>) = with(ui) {
coordinatorLayout {
id = COORDINATOR_ID
lparams(matchParent, matchParent)
appBarLayout(R.style.AppTheme_AppBarOverlay) {
id = APPBAR_ID
lparams(matchParent, wrapContent)
fitsSystemWindows = true
collapsingToolbarLayout {
id = COLLAPSING_ID
val collapsingToolbarLayoutParams = AppBarLayout.LayoutParams(matchParent, matchParent)
collapsingToolbarLayoutParams.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
layoutParams = collapsingToolbarLayoutParams
isTitleEnabled = false
toolbar {
id = TOOLBAR_ID
val toolbarLayoutParams = CollapsingToolbarLayout.LayoutParams(matchParent, dimenAttr(R.attr.actionBarSize))
toolbarLayoutParams.collapseMode = CollapsingToolbarLayout.LayoutParams.COLLAPSE_MODE_PIN
layoutParams = toolbarLayoutParams
minimumHeight = dimenAttr(R.attr.actionBarSize)
background = ColorDrawable(colorAttr(R.attr.colorPrimary))
popupTheme = R.style.AppTheme_PopupOverlay
}
}
}
with(AnkoContext.createDelegate(relativeLayout {
id = CONTENT_ID
val relativeLayoutParams = CoordinatorLayout.LayoutParams(matchParent, matchParent)
relativeLayoutParams.behavior = AppBarLayout.ScrollingViewBehavior()
layoutParams = relativeLayoutParams
})) {
content(ui)
}
}
}
}
And then you can extend the BaseAnkoComponent and build your content in the same way with Anko DSL.
class FooActivityUi : BaseAnkoComponent<FooActivity>() {
override fun <T> AnkoContext<T>.content(): View? {
return verticalLayout {
lparams(width = matchParent, height = matchParent)
button("Hello")
textView("myFriend")
}
}
}
I am sure there is a better way to do this but I have not found it. Kinda new to Kotlin and Anko.