Refactored the ZimHostFragmentTest according to compose.

This commit is contained in:
MohitMaliFtechiz 2025-03-15 16:10:36 +05:30
parent 6e370a1e74
commit a72d021e95
5 changed files with 134 additions and 95 deletions

View File

@ -53,12 +53,14 @@ class ErrorActivityRobot : BaseRobot() {
} }
fun assertCheckBoxesDisplayed(composeTestRule: ComposeContentTestRule) { fun assertCheckBoxesDisplayed(composeTestRule: ComposeContentTestRule) {
composeTestRule.onNodeWithText(context.getString(R.string.crash_checkbox_language)) composeTestRule.apply {
.assertIsDisplayed() onNodeWithText(context.getString(R.string.crash_checkbox_language))
composeTestRule.onNodeWithText(context.getString(R.string.crash_checkbox_logs)) .assertIsDisplayed()
.assertIsDisplayed() onNodeWithText(context.getString(R.string.crash_checkbox_logs))
composeTestRule.onNodeWithText(context.getString(R.string.crash_checkbox_zimfiles)) .assertIsDisplayed()
.assertIsDisplayed() onNodeWithText(context.getString(R.string.crash_checkbox_zimfiles))
.assertIsDisplayed()
}
} }
fun clickOnSendDetailsButton(composeTestRule: ComposeContentTestRule) { fun clickOnSendDetailsButton(composeTestRule: ComposeContentTestRule) {

View File

@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.webserver
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.accessibility.AccessibilityChecks import androidx.test.espresso.accessibility.AccessibilityChecks
@ -31,7 +32,6 @@ import androidx.test.uiautomator.UiDevice
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheck import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesCheck
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils.matchesViews
import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck
import leakcanary.LeakAssertions
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -54,6 +54,9 @@ class ZimHostFragmentTest {
@JvmField @JvmField
var retryRule = RetryRule() var retryRule = RetryRule()
@get:Rule
val composeTestRule = createComposeRule()
private lateinit var sharedPreferenceUtil: SharedPreferenceUtil private lateinit var sharedPreferenceUtil: SharedPreferenceUtil
private lateinit var activityScenario: ActivityScenario<KiwixMainActivity> private lateinit var activityScenario: ActivityScenario<KiwixMainActivity>
@ -150,53 +153,52 @@ class ZimHostFragmentTest {
openZimHostFragment() openZimHostFragment()
// Check if server is already started // Check if server is already started
stopServerIfAlreadyStarted() stopServerIfAlreadyStarted(composeTestRule)
// Check if both zim file are selected or not to properly run our test case // Check if both zim file are selected or not to properly run our test case
selectZimFileIfNotAlreadySelected() selectZimFileIfNotAlreadySelected(composeTestRule)
clickOnTestZim() clickOnTestZim(composeTestRule)
// Start the server with one ZIM file // Start the server with one ZIM file
startServer() startServer(composeTestRule)
assertServerStarted() assertServerStarted(composeTestRule)
// Check that only one ZIM file is hosted on the server // Check that only one ZIM file is hosted on the server
assertItemHostedOnServer(1) assertItemHostedOnServer(1, composeTestRule)
// Check QR code shown // Check QR code shown
assertQrShown() assertQrShown(composeTestRule)
// Stop the server // Stop the server
stopServer() stopServer(composeTestRule)
assertServerStopped() assertServerStopped(composeTestRule)
// Check QR code not shown after stopping the server // Check QR code not shown after stopping the server
assertQrNotShown() assertQrNotShown(composeTestRule)
// Select the test ZIM file to host on the server // Select the test ZIM file to host on the server
clickOnTestZim() clickOnTestZim(composeTestRule)
// Start the server with two ZIM files // Start the server with two ZIM files
startServer() startServer(composeTestRule)
assertServerStarted() assertServerStarted(composeTestRule)
// Check that both ZIM files are hosted on the server // Check that both ZIM files are hosted on the server
assertItemHostedOnServer(2) assertItemHostedOnServer(2, composeTestRule)
// Unselect the test ZIM to test restarting server functionality // Unselect the test ZIM to test restarting server functionality
clickOnTestZim() clickOnTestZim(composeTestRule)
// Check if the server is running // Check if the server is running
assertServerStarted() assertServerStarted(composeTestRule)
// Check that only one ZIM file is hosted on the server after unselecting // Check that only one ZIM file is hosted on the server after unselecting
assertItemHostedOnServer(1) assertItemHostedOnServer(1, composeTestRule)
// finally close the server at the end of test case // finally close the server at the end of test case
stopServer() stopServer(composeTestRule)
} }
LeakAssertions.assertNoLeaks()
} }
} }

View File

@ -18,30 +18,28 @@
package org.kiwix.kiwixmobile.webserver package org.kiwix.kiwixmobile.webserver
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import applyWithViewHierarchyPrinting import applyWithViewHierarchyPrinting
import com.adevinta.android.barista.interaction.BaristaSleepInteractions import com.adevinta.android.barista.interaction.BaristaSleepInteractions
import com.adevinta.android.barista.interaction.BaristaSwipeRefreshInteractions.refresh import com.adevinta.android.barista.interaction.BaristaSwipeRefreshInteractions.refresh
import junit.framework.AssertionFailedError import junit.framework.AssertionFailedError
import org.hamcrest.CoreMatchers
import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.Findable.StringId.TextId
import org.kiwix.kiwixmobile.Findable.Text import org.kiwix.kiwixmobile.Findable.Text
import org.kiwix.kiwixmobile.Findable.ViewId
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.R.id
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView
import org.kiwix.kiwixmobile.utils.RecyclerViewItemCount import org.kiwix.kiwixmobile.ui.BOOK_ITEM_CHECKBOX_TESTING_TAG
import org.kiwix.kiwixmobile.utils.RecyclerViewMatcher
import org.kiwix.kiwixmobile.utils.RecyclerViewSelectedCheckBoxCountAssertion
import org.kiwix.kiwixmobile.utils.StandardActions.openDrawer import org.kiwix.kiwixmobile.utils.StandardActions.openDrawer
fun zimHost(func: ZimHostRobot.() -> Unit) = ZimHostRobot().applyWithViewHierarchyPrinting(func) fun zimHost(func: ZimHostRobot.() -> Unit) = ZimHostRobot().applyWithViewHierarchyPrinting(func)
@ -66,15 +64,20 @@ class ZimHostRobot : BaseRobot() {
clickOn(TextId(R.string.menu_wifi_hotspot)) clickOn(TextId(R.string.menu_wifi_hotspot))
} }
fun clickOnTestZim() { fun clickOnTestZim(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() testFlakyView({
clickOn(Text("Test_Zim")) composeTestRule.apply {
waitForIdle()
onNodeWithTag("${BOOK_ITEM_CHECKBOX_TESTING_TAG}2").performClick()
}
})
} }
fun startServer() { fun startServer(composeTestRule: ComposeContentTestRule) {
// stop the server if it is already running. // stop the server if it is already running.
stopServerIfAlreadyStarted() stopServerIfAlreadyStarted(composeTestRule)
clickOn(ViewId(id.startServerButton)) composeTestRule.onNodeWithTag(START_SERVER_BUTTON_TESTING_TAG)
.performClick()
assetWifiDialogDisplayed() assetWifiDialogDisplayed()
testFlakyView({ onView(withText("PROCEED")).perform(click()) }) testFlakyView({ onView(withText("PROCEED")).perform(click()) })
} }
@ -83,24 +86,30 @@ class ZimHostRobot : BaseRobot() {
testFlakyView({ isVisible(Text("WiFi connection detected")) }) testFlakyView({ isVisible(Text("WiFi connection detected")) })
} }
fun assertServerStarted() { fun assertServerStarted(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() pauseForBetterTestPerformance()
// starting server takes a bit so sometimes it fails to find this view. // starting server takes a bit so sometimes it fails to find this view.
// which makes this view flaky so we are testing this with FlakyView. // which makes this view flaky so we are testing this with FlakyView.
testFlakyView({ isVisible(Text("STOP SERVER")) }) testFlakyView({
composeTestRule.apply {
waitForIdle()
onNodeWithTag(START_SERVER_BUTTON_TESTING_TAG)
.assertTextEquals(context.getString(R.string.stop_server_label).uppercase())
}
})
} }
fun stopServerIfAlreadyStarted() { fun stopServerIfAlreadyStarted(composeTestRule: ComposeContentTestRule) {
try { try {
// Check if the "START SERVER" button is visible because, in most scenarios, // Check if the "START SERVER" button is visible because, in most scenarios,
// this button will appear when the server is already stopped. // this button will appear when the server is already stopped.
// This will expedite our test case, as verifying the visibility of // This will expedite our test case, as verifying the visibility of
// non-visible views takes more time due to the try mechanism needed // non-visible views takes more time due to the try mechanism needed
// to properly retrieve the view. // to properly retrieve the view.
assertServerStopped() assertServerStopped(composeTestRule)
} catch (exception: Exception) { } catch (_: Exception) {
// if "START SERVER" button is not visible it means server is started so close it. // if "START SERVER" button is not visible it means server is started so close it.
stopServer() stopServer(composeTestRule)
Log.i( Log.i(
"ZIM_HOST_FRAGMENT", "ZIM_HOST_FRAGMENT",
"Stopped the server to perform our test case since it was already running" "Stopped the server to perform our test case since it was already running"
@ -108,66 +117,76 @@ class ZimHostRobot : BaseRobot() {
} }
} }
fun selectZimFileIfNotAlreadySelected() { fun selectZimFileIfNotAlreadySelected(composeTestRule: ComposeContentTestRule) {
try { try {
// check both files are selected. // check both files are selected.
assertItemHostedOnServer(2) assertItemHostedOnServer(2, composeTestRule)
} catch (assertionFailedError: AssertionFailedError) { } catch (_: AssertionFailedError) {
try { try {
val recyclerViewItemsCount = selectZimFile(1, composeTestRule)
RecyclerViewItemCount(id.recyclerViewZimHost).checkRecyclerViewCount() selectZimFile(2, composeTestRule)
(0 until recyclerViewItemsCount) } catch (_: AssertionFailedError) {
.asSequence()
.filter { it != 0 }
.forEach(::selectZimFile)
} catch (assertionFailedError: AssertionFailedError) {
Log.i("ZIM_HOST_FRAGMENT", "Failed to select the zim file, probably it is already selected") Log.i("ZIM_HOST_FRAGMENT", "Failed to select the zim file, probably it is already selected")
} }
} }
} }
private fun selectZimFile(position: Int) { private fun selectZimFile(position: Int, composeTestRule: ComposeContentTestRule) {
try { try {
onView( composeTestRule.onNodeWithTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG$position")
RecyclerViewMatcher(id.recyclerViewZimHost).atPositionOnView( .assertIsOn()
position, } catch (_: AssertionFailedError) {
R.id.itemBookCheckbox composeTestRule.onNodeWithTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG$position")
) .performClick()
).check(matches(ViewMatchers.isChecked()))
} catch (assertionError: AssertionFailedError) {
onView(
RecyclerViewMatcher(id.recyclerViewZimHost).atPositionOnView(
position,
R.id.itemBookCheckbox
)
).perform(click())
} }
} }
fun assertItemHostedOnServer(itemCount: Int) { fun assertItemHostedOnServer(itemCount: Int, composeTestRule: ComposeContentTestRule) {
val checkedCheckboxCount = for (i in 0 until itemCount) {
RecyclerViewSelectedCheckBoxCountAssertion( composeTestRule.onNodeWithTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG${i + 1}")
id.recyclerViewZimHost, .assertIsOn()
R.id.itemBookCheckbox }
).countCheckedCheckboxes()
assertThat(checkedCheckboxCount, CoreMatchers.`is`(itemCount))
} }
fun stopServer() { fun stopServer(composeTestRule: ComposeContentTestRule) {
testFlakyView({ onView(withId(id.startServerButton)).perform(click()) }) testFlakyView(
{
composeTestRule.apply {
waitForIdle()
onNodeWithTag(START_SERVER_BUTTON_TESTING_TAG).performClick()
}
}
)
} }
fun assertServerStopped() { fun assertServerStopped(composeTestRule: ComposeContentTestRule) {
pauseForBetterTestPerformance() testFlakyView({
isVisible(Text("START SERVER")) composeTestRule.apply {
waitForIdle()
onNodeWithTag(START_SERVER_BUTTON_TESTING_TAG)
.assertTextEquals(context.getString(R.string.start_server_label).uppercase())
}
})
} }
fun assertQrShown() { fun assertQrShown(composeTestRule: ComposeContentTestRule) {
isVisible(ViewId(id.serverQrCode)) testFlakyView({
composeTestRule.apply {
waitForIdle()
onNodeWithTag(QR_IMAGE_TESTING_TAG)
.assertIsDisplayed()
}
})
} }
fun assertQrNotShown() { fun assertQrNotShown(composeTestRule: ComposeContentTestRule) {
isNotVisible(ViewId(id.serverQrCode)) testFlakyView({
composeTestRule.apply {
waitForIdle()
onNodeWithTag(QR_IMAGE_TESTING_TAG)
.assertIsNotDisplayed()
}
})
} }
private fun pauseForBetterTestPerformance() { private fun pauseForBetterTestPerformance() {

View File

@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
@ -56,9 +57,12 @@ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.ArticleCount
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
const val BOOK_ITEM_CHECKBOX_TESTING_TAG = "bookItemCheckboxTestingTag"
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun BookItem( fun BookItem(
index: Int,
bookOnDisk: BookOnDisk, bookOnDisk: BookOnDisk,
onClick: ((BookOnDisk) -> Unit)? = null, onClick: ((BookOnDisk) -> Unit)? = null,
onLongClick: ((BookOnDisk) -> Unit)? = null, onLongClick: ((BookOnDisk) -> Unit)? = null,
@ -87,7 +91,7 @@ fun BookItem(
elevation = CardDefaults.elevatedCardElevation(), elevation = CardDefaults.elevatedCardElevation(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
) { ) {
BookContent(bookOnDisk, selectionMode, onMultiSelect, onClick) BookContent(bookOnDisk, selectionMode, onMultiSelect, onClick, index)
} }
} }
} }
@ -98,6 +102,7 @@ private fun BookContent(
selectionMode: SelectionMode, selectionMode: SelectionMode,
onMultiSelect: ((BookOnDisk) -> Unit)?, onMultiSelect: ((BookOnDisk) -> Unit)?,
onClick: ((BookOnDisk) -> Unit)?, onClick: ((BookOnDisk) -> Unit)?,
index: Int,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -106,7 +111,7 @@ private fun BookContent(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (selectionMode == SelectionMode.MULTI) { if (selectionMode == SelectionMode.MULTI) {
BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick) BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick, index)
} }
BookIcon(bookOnDisk.book.faviconToPainter()) BookIcon(bookOnDisk.book.faviconToPainter())
BookDetails(Modifier.weight(1f), bookOnDisk) BookDetails(Modifier.weight(1f), bookOnDisk)
@ -118,7 +123,8 @@ private fun BookCheckbox(
bookOnDisk: BookOnDisk, bookOnDisk: BookOnDisk,
selectionMode: SelectionMode, selectionMode: SelectionMode,
onMultiSelect: ((BookOnDisk) -> Unit)?, onMultiSelect: ((BookOnDisk) -> Unit)?,
onClick: ((BookOnDisk) -> Unit)? onClick: ((BookOnDisk) -> Unit)?,
index: Int
) { ) {
Checkbox( Checkbox(
checked = bookOnDisk.isSelected, checked = bookOnDisk.isSelected,
@ -127,7 +133,8 @@ private fun BookCheckbox(
SelectionMode.MULTI -> onMultiSelect?.invoke(bookOnDisk) SelectionMode.MULTI -> onMultiSelect?.invoke(bookOnDisk)
SelectionMode.NORMAL -> onClick?.invoke(bookOnDisk) SelectionMode.NORMAL -> onClick?.invoke(bookOnDisk)
} }
} },
modifier = Modifier.testTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG$index")
) )
} }

View File

@ -43,6 +43,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
@ -65,6 +66,9 @@ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDis
import org.kiwix.kiwixmobile.ui.BookItem import org.kiwix.kiwixmobile.ui.BookItem
import org.kiwix.kiwixmobile.ui.ZimFilesLanguageHeader import org.kiwix.kiwixmobile.ui.ZimFilesLanguageHeader
const val START_SERVER_BUTTON_TESTING_TAG = "startServerButtonTestingTag"
const val QR_IMAGE_TESTING_TAG = "qrImageTestingTag"
@Suppress("ComposableLambdaParameterNaming", "LongParameterList") @Suppress("ComposableLambdaParameterNaming", "LongParameterList")
@Composable @Composable
fun ZimHostScreen( fun ZimHostScreen(
@ -104,7 +108,10 @@ fun ZimHostScreen(
KiwixButton( KiwixButton(
startServerButtonItem.first, startServerButtonItem.first,
startServerButtonItem.third, startServerButtonItem.third,
modifier = Modifier.fillMaxWidth().padding(FOUR_DP), modifier = Modifier
.fillMaxWidth()
.padding(FOUR_DP)
.testTag(START_SERVER_BUTTON_TESTING_TAG),
buttonBackgroundColor = startServerButtonItem.second buttonBackgroundColor = startServerButtonItem.second
) )
} }
@ -157,7 +164,8 @@ private fun QRImage(qrImageItem: Pair<Boolean, IconItem>) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = MINIMUM_HEIGHT_OF_QR_CODE, max = MAXIMUM_HEIGHT_OF_QR_CODE) .heightIn(min = MINIMUM_HEIGHT_OF_QR_CODE, max = MAXIMUM_HEIGHT_OF_QR_CODE)
.padding(horizontal = SIXTEEN_DP), .padding(horizontal = SIXTEEN_DP)
.testTag(QR_IMAGE_TESTING_TAG),
contentScale = ContentScale.Fit contentScale = ContentScale.Fit
) )
} }
@ -181,7 +189,7 @@ private fun BookItemList(
item { item {
QRImage(qrImageItem) QRImage(qrImageItem)
} }
itemsIndexed(booksList) { _, bookItem -> itemsIndexed(booksList) { index, bookItem ->
when (bookItem) { when (bookItem) {
is BooksOnDiskListItem.LanguageItem -> { is BooksOnDiskListItem.LanguageItem -> {
ZimFilesLanguageHeader(bookItem) ZimFilesLanguageHeader(bookItem)
@ -189,6 +197,7 @@ private fun BookItemList(
is BookOnDisk -> { is BookOnDisk -> {
BookItem( BookItem(
index = index,
bookOnDisk = bookItem, bookOnDisk = bookItem,
selectionMode = selectionMode, selectionMode = selectionMode,
onClick = onClick, onClick = onClick,