diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt index a11b3f738..66a1b2b32 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/error/ErrorActivityRobot.kt @@ -53,12 +53,14 @@ class ErrorActivityRobot : BaseRobot() { } fun assertCheckBoxesDisplayed(composeTestRule: ComposeContentTestRule) { - composeTestRule.onNodeWithText(context.getString(R.string.crash_checkbox_language)) - .assertIsDisplayed() - composeTestRule.onNodeWithText(context.getString(R.string.crash_checkbox_logs)) - .assertIsDisplayed() - composeTestRule.onNodeWithText(context.getString(R.string.crash_checkbox_zimfiles)) - .assertIsDisplayed() + composeTestRule.apply { + onNodeWithText(context.getString(R.string.crash_checkbox_language)) + .assertIsDisplayed() + onNodeWithText(context.getString(R.string.crash_checkbox_logs)) + .assertIsDisplayed() + onNodeWithText(context.getString(R.string.crash_checkbox_zimfiles)) + .assertIsDisplayed() + } } fun clickOnSendDetailsButton(composeTestRule: ComposeContentTestRule) { diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt index 282ee9a3b..69d714e77 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt @@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.webserver import android.Manifest import android.content.Context import android.os.Build +import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario 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.matchesViews import com.google.android.apps.common.testing.accessibility.framework.checks.DuplicateClickableBoundsCheck -import leakcanary.LeakAssertions import org.hamcrest.Matchers.allOf import org.junit.After import org.junit.Before @@ -54,6 +54,9 @@ class ZimHostFragmentTest { @JvmField var retryRule = RetryRule() + @get:Rule + val composeTestRule = createComposeRule() + private lateinit var sharedPreferenceUtil: SharedPreferenceUtil private lateinit var activityScenario: ActivityScenario @@ -150,53 +153,52 @@ class ZimHostFragmentTest { openZimHostFragment() // Check if server is already started - stopServerIfAlreadyStarted() + stopServerIfAlreadyStarted(composeTestRule) // 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 - startServer() - assertServerStarted() + startServer(composeTestRule) + assertServerStarted(composeTestRule) // Check that only one ZIM file is hosted on the server - assertItemHostedOnServer(1) + assertItemHostedOnServer(1, composeTestRule) // Check QR code shown - assertQrShown() + assertQrShown(composeTestRule) // Stop the server - stopServer() - assertServerStopped() + stopServer(composeTestRule) + assertServerStopped(composeTestRule) // Check QR code not shown after stopping the server - assertQrNotShown() + assertQrNotShown(composeTestRule) // Select the test ZIM file to host on the server - clickOnTestZim() + clickOnTestZim(composeTestRule) // Start the server with two ZIM files - startServer() - assertServerStarted() + startServer(composeTestRule) + assertServerStarted(composeTestRule) // Check that both ZIM files are hosted on the server - assertItemHostedOnServer(2) + assertItemHostedOnServer(2, composeTestRule) // Unselect the test ZIM to test restarting server functionality - clickOnTestZim() + clickOnTestZim(composeTestRule) // Check if the server is running - assertServerStarted() + assertServerStarted(composeTestRule) // 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 - stopServer() + stopServer(composeTestRule) } - LeakAssertions.assertNoLeaks() } } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt index 9c56880e7..992f6edb3 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostRobot.kt @@ -18,30 +18,28 @@ 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.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 applyWithViewHierarchyPrinting import com.adevinta.android.barista.interaction.BaristaSleepInteractions import com.adevinta.android.barista.interaction.BaristaSwipeRefreshInteractions.refresh import junit.framework.AssertionFailedError -import org.hamcrest.CoreMatchers import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.Findable.Text -import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.core.R -import org.kiwix.kiwixmobile.R.id import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView -import org.kiwix.kiwixmobile.utils.RecyclerViewItemCount -import org.kiwix.kiwixmobile.utils.RecyclerViewMatcher -import org.kiwix.kiwixmobile.utils.RecyclerViewSelectedCheckBoxCountAssertion +import org.kiwix.kiwixmobile.ui.BOOK_ITEM_CHECKBOX_TESTING_TAG import org.kiwix.kiwixmobile.utils.StandardActions.openDrawer fun zimHost(func: ZimHostRobot.() -> Unit) = ZimHostRobot().applyWithViewHierarchyPrinting(func) @@ -66,15 +64,20 @@ class ZimHostRobot : BaseRobot() { clickOn(TextId(R.string.menu_wifi_hotspot)) } - fun clickOnTestZim() { - pauseForBetterTestPerformance() - clickOn(Text("Test_Zim")) + fun clickOnTestZim(composeTestRule: ComposeContentTestRule) { + testFlakyView({ + 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. - stopServerIfAlreadyStarted() - clickOn(ViewId(id.startServerButton)) + stopServerIfAlreadyStarted(composeTestRule) + composeTestRule.onNodeWithTag(START_SERVER_BUTTON_TESTING_TAG) + .performClick() assetWifiDialogDisplayed() testFlakyView({ onView(withText("PROCEED")).perform(click()) }) } @@ -83,24 +86,30 @@ class ZimHostRobot : BaseRobot() { testFlakyView({ isVisible(Text("WiFi connection detected")) }) } - fun assertServerStarted() { + fun assertServerStarted(composeTestRule: ComposeContentTestRule) { pauseForBetterTestPerformance() // 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. - 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 { // Check if the "START SERVER" button is visible because, in most scenarios, // this button will appear when the server is already stopped. // This will expedite our test case, as verifying the visibility of // non-visible views takes more time due to the try mechanism needed // to properly retrieve the view. - assertServerStopped() - } catch (exception: Exception) { + assertServerStopped(composeTestRule) + } catch (_: Exception) { // if "START SERVER" button is not visible it means server is started so close it. - stopServer() + stopServer(composeTestRule) Log.i( "ZIM_HOST_FRAGMENT", "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 { // check both files are selected. - assertItemHostedOnServer(2) - } catch (assertionFailedError: AssertionFailedError) { + assertItemHostedOnServer(2, composeTestRule) + } catch (_: AssertionFailedError) { try { - val recyclerViewItemsCount = - RecyclerViewItemCount(id.recyclerViewZimHost).checkRecyclerViewCount() - (0 until recyclerViewItemsCount) - .asSequence() - .filter { it != 0 } - .forEach(::selectZimFile) - } catch (assertionFailedError: AssertionFailedError) { + selectZimFile(1, composeTestRule) + selectZimFile(2, composeTestRule) + } catch (_: AssertionFailedError) { 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 { - onView( - RecyclerViewMatcher(id.recyclerViewZimHost).atPositionOnView( - position, - R.id.itemBookCheckbox - ) - ).check(matches(ViewMatchers.isChecked())) - } catch (assertionError: AssertionFailedError) { - onView( - RecyclerViewMatcher(id.recyclerViewZimHost).atPositionOnView( - position, - R.id.itemBookCheckbox - ) - ).perform(click()) + composeTestRule.onNodeWithTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG$position") + .assertIsOn() + } catch (_: AssertionFailedError) { + composeTestRule.onNodeWithTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG$position") + .performClick() } } - fun assertItemHostedOnServer(itemCount: Int) { - val checkedCheckboxCount = - RecyclerViewSelectedCheckBoxCountAssertion( - id.recyclerViewZimHost, - R.id.itemBookCheckbox - ).countCheckedCheckboxes() - assertThat(checkedCheckboxCount, CoreMatchers.`is`(itemCount)) + fun assertItemHostedOnServer(itemCount: Int, composeTestRule: ComposeContentTestRule) { + for (i in 0 until itemCount) { + composeTestRule.onNodeWithTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG${i + 1}") + .assertIsOn() + } } - fun stopServer() { - testFlakyView({ onView(withId(id.startServerButton)).perform(click()) }) + fun stopServer(composeTestRule: ComposeContentTestRule) { + testFlakyView( + { + composeTestRule.apply { + waitForIdle() + onNodeWithTag(START_SERVER_BUTTON_TESTING_TAG).performClick() + } + } + ) } - fun assertServerStopped() { - pauseForBetterTestPerformance() - isVisible(Text("START SERVER")) + fun assertServerStopped(composeTestRule: ComposeContentTestRule) { + testFlakyView({ + composeTestRule.apply { + waitForIdle() + onNodeWithTag(START_SERVER_BUTTON_TESTING_TAG) + .assertTextEquals(context.getString(R.string.start_server_label).uppercase()) + } + }) } - fun assertQrShown() { - isVisible(ViewId(id.serverQrCode)) + fun assertQrShown(composeTestRule: ComposeContentTestRule) { + testFlakyView({ + composeTestRule.apply { + waitForIdle() + onNodeWithTag(QR_IMAGE_TESTING_TAG) + .assertIsDisplayed() + } + }) } - fun assertQrNotShown() { - isNotVisible(ViewId(id.serverQrCode)) + fun assertQrNotShown(composeTestRule: ComposeContentTestRule) { + testFlakyView({ + composeTestRule.apply { + waitForIdle() + onNodeWithTag(QR_IMAGE_TESTING_TAG) + .assertIsNotDisplayed() + } + }) } private fun pauseForBetterTestPerformance() { diff --git a/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt b/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt index f48bc720c..34c43980e 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/ui/BookItem.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow 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.adapter.BooksOnDiskListItem.BookOnDisk +const val BOOK_ITEM_CHECKBOX_TESTING_TAG = "bookItemCheckboxTestingTag" + @OptIn(ExperimentalFoundationApi::class) @Composable fun BookItem( + index: Int, bookOnDisk: BookOnDisk, onClick: ((BookOnDisk) -> Unit)? = null, onLongClick: ((BookOnDisk) -> Unit)? = null, @@ -87,7 +91,7 @@ fun BookItem( elevation = CardDefaults.elevatedCardElevation(), 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, onMultiSelect: ((BookOnDisk) -> Unit)?, onClick: ((BookOnDisk) -> Unit)?, + index: Int, ) { Row( modifier = Modifier @@ -106,7 +111,7 @@ private fun BookContent( verticalAlignment = Alignment.CenterVertically ) { if (selectionMode == SelectionMode.MULTI) { - BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick) + BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick, index) } BookIcon(bookOnDisk.book.faviconToPainter()) BookDetails(Modifier.weight(1f), bookOnDisk) @@ -118,7 +123,8 @@ private fun BookCheckbox( bookOnDisk: BookOnDisk, selectionMode: SelectionMode, onMultiSelect: ((BookOnDisk) -> Unit)?, - onClick: ((BookOnDisk) -> Unit)? + onClick: ((BookOnDisk) -> Unit)?, + index: Int ) { Checkbox( checked = bookOnDisk.isSelected, @@ -127,7 +133,8 @@ private fun BookCheckbox( SelectionMode.MULTI -> onMultiSelect?.invoke(bookOnDisk) SelectionMode.NORMAL -> onClick?.invoke(bookOnDisk) } - } + }, + modifier = Modifier.testTag("$BOOK_ITEM_CHECKBOX_TESTING_TAG$index") ) } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostScreen.kt index 918a72bbf..a2aa093a6 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostScreen.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/webserver/ZimHostScreen.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource 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.ZimFilesLanguageHeader +const val START_SERVER_BUTTON_TESTING_TAG = "startServerButtonTestingTag" +const val QR_IMAGE_TESTING_TAG = "qrImageTestingTag" + @Suppress("ComposableLambdaParameterNaming", "LongParameterList") @Composable fun ZimHostScreen( @@ -104,7 +108,10 @@ fun ZimHostScreen( KiwixButton( startServerButtonItem.first, startServerButtonItem.third, - modifier = Modifier.fillMaxWidth().padding(FOUR_DP), + modifier = Modifier + .fillMaxWidth() + .padding(FOUR_DP) + .testTag(START_SERVER_BUTTON_TESTING_TAG), buttonBackgroundColor = startServerButtonItem.second ) } @@ -157,7 +164,8 @@ private fun QRImage(qrImageItem: Pair) { modifier = Modifier .fillMaxWidth() .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 ) } @@ -181,7 +189,7 @@ private fun BookItemList( item { QRImage(qrImageItem) } - itemsIndexed(booksList) { _, bookItem -> + itemsIndexed(booksList) { index, bookItem -> when (bookItem) { is BooksOnDiskListItem.LanguageItem -> { ZimFilesLanguageHeader(bookItem) @@ -189,6 +197,7 @@ private fun BookItemList( is BookOnDisk -> { BookItem( + index = index, bookOnDisk = bookItem, selectionMode = selectionMode, onClick = onClick,