Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recyclerview 0 children, can't test viewholder click #3747

Open
JustinTullgren opened this issue Feb 9, 2018 · 9 comments
Open

Recyclerview 0 children, can't test viewholder click #3747

JustinTullgren opened this issue Feb 9, 2018 · 9 comments

Comments

@JustinTullgren
Copy link

Description

Calling notifyDataSetChanged() and calling ActivityController.visible() does not update the recyclerview childcount or returns null for recycler.findViewHolderForAdapterPosition(0)

I tried follow steps in this Stackoverflow post which mentioned the visible method. The other results didn't work either.

The activity works and loads fine.

I am not sure its a bug or just a misunderstanding on my part. I tried looking at the github samples and articles on line but they all had similar answers (draw the recycler manually) or were very shallow.

Steps to Reproduce: Code below(modified to not show domain details)

Activity

class TheActivity : BaseActivity<IView, IPresenter>(), IView {

	private val theAdapter = TheAdapter { presenter.select(it) }
	@Inject lateinit var presenter: IPresenter

	override fun onInject() {
		component?.inject(this)
	}

	override fun getIPresenter(): IPresenter = presenter
	override fun getIView(): IView = this

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_example)
		with(activity_example_recyclerview) {
			adapter = theAdapter
			layoutManager = LinearLayoutManager(this@TheActivity)
		}

	}

	override fun setItems(items: List<String>) {
		theAdapter.setItems(items)
	}

	private class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

	private class TheAdapter(private val itemClick: (String) -> Unit) : RecyclerView.Adapter<ViewHolder>() {
		private var list = listOf<String>()
		fun setItems(list: List<String>) {
			this.list = list
			notifyDataSetChanged()
		}
		override fun onBindViewHolder(holder: ViewHolder, position: Int) {
			val item = list[position]
			holder.itemView.setOnClickListener { itemClick(item) }
			val text = holder.itemView as TextView
			text.text = item
		}

		override fun getItemCount(): Int = list.size

		override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = ViewHolder(TextView(parent.context))
	}
}

Test

@RunWith(RobolectricTestRunner::class)
class TheActivityUnitTest {
	private lateinit var subject:TheActivity
	private val controller = Robolectric.buildActivity(TheActivity::class.java)
	private val mockExistingItems = listOf("foo", "bar", "baz")
	private val mockPresenter = mock<IPresenter>()
	private val mockComponent = mock<AppCompatActivityComponent> {
		on { inject(any<TheActivity>()) }.doAnswer {
			with(it.arguments[0] as TheActivity) {
				presenter = mockPresenter
			}
			return@doAnswer null
		}
	}

	@Before
	fun setup() {
		subject = controller.get()
		subject.component = mockComponent
	}

	@Test
	fun selectItemFromList() {
		controller.create().start().visible()
		subject.setItems(mockExistingItems) // Invokes notifyDataSetChanged()
		controller.visible()
		subject.findViewById<RecyclerView>(R.id.activity_example_recyclerview)
				.findViewHolderForItemId(0)
				// THIS WILL FAIL as findViewHolderForItemId returns null
				.itemView
				.performClick()
		// OR Alternatively 
		/*
		subject.findViewById<RecyclerView>(R.id.activity_example_recyclerview)
				.findViewHolderForAdapterPosition(0)
				.itemView
				.performClick()
		 */
		
		// OR Alternatively 
		/*
		subject.findViewById<RecyclerView>(R.id.activity_example_recyclerview)
				.getChildAt(0)
				.performClick()
		 */
		verify(mockPresenter).select(mockExistingItems[0])
	}
}

Robolectric & Android Version

Robolectric: 3.6.1
Android compileSdkVersion: 26,
android minSdkVersion : 21,
android targetSdkVersion : 26

@JustinTullgren
Copy link
Author

Doing the measure manually fixed the issue. It would still be nice if this was not required.

val recycler = subject.findViewById<RecyclerView>(R.id.activity_example_recyclerview)
recycler.measure(0,0)
recycler.layout(0,0,100,1000)

@amrfarid140
Copy link

Getting the same issue on Robolectric 4.1, @JustinTullgren suggested solution works.

@brettchabot
Copy link
Contributor

I'm hoping this issue will be fixed by the fix for #4153 which I've been working on.

Robolectric currently performs layout assuming the window is zero-size, which leads to a bunch of inconsistencies when sizing TextViews, RecyclerViews etc.

@lupsyn
Copy link

lupsyn commented Feb 19, 2019

Same issue on Roboelectric 4.2

@PaulKlauser
Copy link

Still seeing this issue on Robolectric 4.3.1

@calvarez-ov
Copy link

Just calling recycler.measure(0,0) was enough to fix the issue in my case.

@bhuva13
Copy link

bhuva13 commented Jul 9, 2021

Just calling recycler.measure(0,0) was enough to fix the issue in my case.

What is the Robolectric version you are using?

@mnlin0905
Copy link

@bhuva13 @PaulKlauser @lupsyn

This is how I handle it here

testImplementation "org.robolectric:robolectric:4.5.1"
testImplementation 'androidx.test.espresso:espresso-core:3.3.0'

test code

   @Test
    fun `should show all buttons and digit text content is empty when launch OplusDialpadFragment with open screen`() {
        // GIVEN
        val screenWidth = 1080
        val screenHeight = 1920

        // Manually load the soft keyboard interface into the activity
        activityRule.scenario.onActivity {
            it.supportFragmentManager
                .beginTransaction()
                .add(R.id.vp_screen_center, OplusDialpadFragment(), OplusDialpadFragment::class.simpleName)
                .commitNow()
        }

        // WHEN
        activityRule.scenario.onActivity {
            it.supportFragmentManager
                .findFragmentByTag(OplusDialpadFragment::class.simpleName)
                ?.view
                ?.findViewById<RecyclerView>(R.id.rv_dialpad_buttons)
                ?.run {
                    measure(
                        makeMeasureSpec(screenWidth, View.MeasureSpec.EXACTLY),
                        makeMeasureSpec(screenHeight, View.MeasureSpec.AT_MOST),
                    )
                    layout(0, 0, screenWidth, screenHeight)
                }
        }

        Shadows.shadowOf(Looper.getMainLooper()).idle()

        // THEN
        for ((number, _) in (DialpadViewModel.DIALPAD_NUMBERS_LETTERS)) {
            onView(withText(number.toString())).check(matches(isEnabled()))
        }
    }

The key code is here

measure(
      makeMeasureSpec(screenWidth, View.MeasureSpec.EXACTLY),
      makeMeasureSpec(screenHeight, View.MeasureSpec.AT_MOST),
  )
  layout(0, 0, screenWidth, screenHeight)

Finally ensure that the test cases can cover the source code

image

@mnlin0905
Copy link

@JustinTullgren

Perhaps, you can refer to this article

Robolectric Tips: Testing RecyclerViews

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants