From b1947aa3432829f641c915da793b1dcfae059313 Mon Sep 17 00:00:00 2001 From: Akshay Nandwana Date: Thu, 22 Aug 2024 11:05:14 +0530 Subject: [PATCH] Added tests for OfflineAreasFragment (#2639) --- .../offlineareas/OfflineAreasFragmentTest.kt | 100 ++++++++++++++++++ .../ground/util/drwable/DrawableMatcher.kt | 89 ++++++++++++++++ .../util/recyclerview/RecyclerViewMatcher.kt | 57 ++++++++++ 3 files changed, 246 insertions(+) create mode 100644 ground/src/test/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragmentTest.kt create mode 100644 ground/src/test/java/com/google/android/ground/util/drwable/DrawableMatcher.kt create mode 100644 ground/src/test/java/com/google/android/ground/util/recyclerview/RecyclerViewMatcher.kt diff --git a/ground/src/test/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragmentTest.kt b/ground/src/test/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragmentTest.kt new file mode 100644 index 0000000000..d3b95a724f --- /dev/null +++ b/ground/src/test/java/com/google/android/ground/ui/offlineareas/OfflineAreasFragmentTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.ground.ui.offlineareas + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import com.google.android.ground.BaseHiltTest +import com.google.android.ground.R +import com.google.android.ground.launchFragmentInHiltContainer +import com.google.android.ground.persistence.local.stores.LocalOfflineAreaStore +import com.google.android.ground.util.recyclerview.atPositionOnView +import com.sharedtest.FakeData.OFFLINE_AREA +import dagger.hilt.android.testing.HiltAndroidTest +import javax.inject.Inject +import kotlinx.coroutines.test.advanceUntilIdle +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@HiltAndroidTest +@RunWith(RobolectricTestRunner::class) +class OfflineAreasFragmentTest : BaseHiltTest() { + + private lateinit var fragment: OfflineAreasFragment + @Inject lateinit var localOfflineAreaStore: LocalOfflineAreaStore + + @Before + override fun setUp() { + super.setUp() + launchFragmentInHiltContainer { fragment = this as OfflineAreasFragment } + } + + @Test + fun `Toolbar text is displayed`() { + onView(withId(R.id.offline_areas_toolbar)) + .check(matches(hasDescendant(withText(fragment.getString(R.string.offline_map_imagery))))) + } + + @Test + fun `Heading text is displayed`() { + onView(withId(R.id.offline_areas_list_title)) + .check(matches(withText(fragment.getString(R.string.offline_downloaded_areas)))) + onView(withId(R.id.offline_areas_list_tip)) + .check(matches(withText(fragment.getString(R.string.offline_area_list_tip)))) + onView(withId(R.id.no_areas_downloaded_message)) + .check(matches(withText(fragment.getString(R.string.no_basemaps_downloaded)))) + } + + @Test + fun `List is Displayed`() = runWithTestDispatcher { + localOfflineAreaStore.insertOrUpdate(OFFLINE_AREA) + + advanceUntilIdle() + onView(withId(R.id.offline_areas_list)).check(matches(isDisplayed())) + + verifyTextOnOfflineAreasListItemAtPosition( + itemPosition = 0, + targetView = R.id.offline_area_list_item_name, + stringToMatch = "Test Area", + ) + verifyTextOnOfflineAreasListItemAtPosition( + itemPosition = 0, + targetView = R.id.offline_area_list_item_size, + stringToMatch = "<1\u00A0MB", + ) + } + + private fun verifyTextOnOfflineAreasListItemAtPosition( + itemPosition: Int, + targetView: Int, + stringToMatch: String, + ) { + onView( + atPositionOnView( + recyclerViewId = R.id.offline_areas_list, + position = itemPosition, + targetViewId = targetView, + ) + ) + .check(matches(withText(stringToMatch))) + } +} diff --git a/ground/src/test/java/com/google/android/ground/util/drwable/DrawableMatcher.kt b/ground/src/test/java/com/google/android/ground/util/drwable/DrawableMatcher.kt new file mode 100644 index 0000000000..7fd29be169 --- /dev/null +++ b/ground/src/test/java/com/google/android/ground/util/drwable/DrawableMatcher.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.ground.util.drwable + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.view.View +import android.widget.ImageView +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + +// https://github.com/dbottillo/Blog/blob/espresso_match_imageview/app/src/androidTest/java/com/danielebottillo/blog/config/DrawableMatcher.java +/** + * This class mainly provides a custom matcher to test whether the drawable-image is correctly shown + * in ImageView. + */ +private class DrawableMatcher(private val expectedId: Int) : + TypeSafeMatcher(View::class.java) { + + private var resourceName: String? = null + + override fun matchesSafely(target: View): Boolean { + if (target !is ImageView) { + return false + } + if (expectedId == NONE) { + return target.drawable == null + } + if (expectedId == ANY) { + return target.drawable != null + } + val resources = target.context.resources + val expectedDrawable = resources.getDrawable(expectedId, target.context.theme) + resourceName = resources.getResourceEntryName(expectedId) + + if (expectedDrawable == null) { + return false + } + + val targetBitmap = getBitmap(target.drawable) + val expectedBitmap = getBitmap(expectedDrawable) + return targetBitmap.sameAs(expectedBitmap) + } + + private fun getBitmap(drawable: Drawable): Bitmap { + val targetBitmap = + Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888, + ) + val canvas = Canvas(targetBitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return targetBitmap + } + + override fun describeTo(description: Description) { + description.appendText("with drawable from resource id: ") + description.appendValue(expectedId) + if (resourceName != null) { + description.appendText("[") + description.appendText(resourceName) + description.appendText("]") + } + } + + companion object { + const val NONE = -1 + const val ANY = -2 + } +} + +fun withDrawable(resourceId: Int): Matcher = DrawableMatcher(resourceId) diff --git a/ground/src/test/java/com/google/android/ground/util/recyclerview/RecyclerViewMatcher.kt b/ground/src/test/java/com/google/android/ground/util/recyclerview/RecyclerViewMatcher.kt new file mode 100644 index 0000000000..ce1462408d --- /dev/null +++ b/ground/src/test/java/com/google/android/ground/util/recyclerview/RecyclerViewMatcher.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.ground.util.recyclerview + +import android.content.res.Resources +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + +fun atPositionOnView(recyclerViewId: Int, position: Int, targetViewId: Int): Matcher = + object : TypeSafeMatcher() { + var resources: Resources? = null + var childView: View? = null + + override fun describeTo(description: Description) { + var idDescription = recyclerViewId.toString() + if (this.resources != null) { + idDescription = + try { + this.resources!!.getResourceName(recyclerViewId) + } catch (exception: Resources.NotFoundException) { + "$recyclerViewId (resource name not found) \n ${exception.message}" + } + } + description.appendText("with id: $idDescription") + } + + public override fun matchesSafely(view: View): Boolean { + this.resources = view.resources + if (childView == null) { + val recyclerView = view.rootView.findViewById(recyclerViewId) as? RecyclerView + if (recyclerView?.id == recyclerViewId) { + childView = recyclerView.findViewHolderForAdapterPosition(position)?.itemView + } else return false + } + return if (targetViewId == -1) { + view === childView + } else { + view === childView?.findViewById(targetViewId) + } + } + }