diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt index 41c937fe9..2f9f20589 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteFragmentTest.kt @@ -19,6 +19,7 @@ package org.kiwix.kiwixmobile.note import android.os.Build +import androidx.compose.ui.test.junit4.createComposeRule import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.Lifecycle @@ -33,6 +34,7 @@ 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 com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck import leakcanary.LeakAssertions import org.hamcrest.Matchers.allOf @@ -64,6 +66,9 @@ class NoteFragmentTest : BaseActivityTest() { private lateinit var kiwixMainActivity: KiwixMainActivity + @get:Rule + val composeTestRule = createComposeRule() + @Before override fun waitForIdle() { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { @@ -107,7 +112,8 @@ class NoteFragmentTest : BaseActivityTest() { allOf( matchesCheck(TouchTargetSizeCheck::class.java), matchesViews(withContentDescription("More options")) - ) + ), + matchesCheck(SpeakableTextPresentCheck::class.java) ) ) } @@ -133,16 +139,16 @@ class NoteFragmentTest : BaseActivityTest() { StandardActions.closeDrawer() // close the drawer if open before running the test cases. note { clickOnNoteMenuItem(context) - assertNoteDialogDisplayed() - writeDemoNote() - saveNote() + assertNoteDialogDisplayed(composeTestRule) + writeDemoNote(composeTestRule) + saveNote(composeTestRule) pressBack() openNoteFragment() assertToolbarExist() assertNoteRecyclerViewExist() clickOnSavedNote() clickOnOpenNote() - assertNoteSaved() + assertNoteSaved(composeTestRule) // to close the note dialog. pressBack() // to close the notes fragment. @@ -166,7 +172,7 @@ class NoteFragmentTest : BaseActivityTest() { assertNoteRecyclerViewExist() clickOnSavedNote() clickOnOpenNote() - assertNoteSaved() + assertNoteSaved(composeTestRule) pressBack() } if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) { @@ -182,16 +188,16 @@ class NoteFragmentTest : BaseActivityTest() { note { assertHomePageIsLoadedOfTestZimFile() clickOnNoteMenuItem(context) - assertNoteDialogDisplayed() - writeDemoNote() - saveNote() + assertNoteDialogDisplayed(composeTestRule) + writeDemoNote(composeTestRule) + saveNote(composeTestRule) pressBack() openNoteFragment() assertToolbarExist() assertNoteRecyclerViewExist() clickOnSavedNote() clickOnOpenNote() - assertNoteSaved() + assertNoteSaved(composeTestRule) // to close the note dialog. pressBack() // to close the notes fragment. @@ -205,16 +211,16 @@ class NoteFragmentTest : BaseActivityTest() { loadZimFileInReader("testzim.zim") note { clickOnNoteMenuItem(context) - assertNoteDialogDisplayed() - writeDemoNote() - saveNote() + assertNoteDialogDisplayed(composeTestRule) + writeDemoNote(composeTestRule) + saveNote(composeTestRule) pressBack() openNoteFragment() assertToolbarExist() assertNoteRecyclerViewExist() clickOnSavedNote() clickOnOpenNote() - assertNoteSaved() + assertNoteSaved(composeTestRule) clickOnDeleteIcon() pressBack() assertNoNotesTextDisplayed() @@ -228,9 +234,9 @@ class NoteFragmentTest : BaseActivityTest() { // Save a note. note { clickOnNoteMenuItem(context) - assertNoteDialogDisplayed() - writeDemoNote() - saveNote() + assertNoteDialogDisplayed(composeTestRule) + writeDemoNote(composeTestRule) + saveNote(composeTestRule) pressBack() } // Delete that note from "Note" screen. @@ -238,7 +244,7 @@ class NoteFragmentTest : BaseActivityTest() { // Test the note file is deleted or not. note { clickOnNoteMenuItem(context) - assertNoteDialogDisplayed() + assertNoteDialogDisplayed(composeTestRule) assertNotDoesNotExist() pressBack() } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt index 9a5659c5c..c98a2b54f 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/note/NoteRobot.kt @@ -19,13 +19,16 @@ package org.kiwix.kiwixmobile.note import android.content.Context +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.compose.ui.test.performTextInput import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.closeSoftKeyboard import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu -import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers @@ -41,6 +44,9 @@ 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.core.main.ADD_NOTE_TEXT_FILED_TESTING_TAG +import org.kiwix.kiwixmobile.core.main.SAVE_MENU_BUTTON_TESTING_TAG +import org.kiwix.kiwixmobile.core.ui.components.TOOLBAR_TITLE_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView import org.kiwix.kiwixmobile.utils.StandardActions.openDrawer @@ -49,7 +55,6 @@ fun note(func: NoteRobot.() -> Unit) = NoteRobot().apply(func) class NoteRobot : BaseRobot() { private val noteText = "Test Note" - private val editTextId = R.id.add_note_edit_text fun assertToolbarExist() { isVisible(ViewId(R.id.toolbar)) @@ -71,19 +76,34 @@ class NoteRobot : BaseRobot() { clickOn(TextId(R.string.take_notes)) } - fun assertNoteDialogDisplayed() { + fun assertNoteDialogDisplayed(composeTestRule: ComposeContentTestRule) { pauseForBetterTestPerformance() - isVisible(TextId(R.string.note)) + testFlakyView({ + composeTestRule.onNodeWithTag(TOOLBAR_TITLE_TESTING_TAG) + .assertTextEquals(context.getString(R.string.note)) + }) } - fun writeDemoNote() { - onView(withId(editTextId)).perform(clearText(), typeText(noteText)) - closeSoftKeyboard() + fun writeDemoNote(composeTestRule: ComposeContentTestRule) { + pauseForBetterTestPerformance() + testFlakyView({ + // Click on the TextField to focus it + composeTestRule.onNodeWithTag(ADD_NOTE_TEXT_FILED_TESTING_TAG) + .assertExists("TextField not found in dialog") + .performClick() + .performTextInput(noteText) + + // Close the keyboard after typing + closeSoftKeyboard() + }) } - fun saveNote() { + fun saveNote(composeTestRule: ComposeContentTestRule) { pauseForBetterTestPerformance() - clickOn(ViewId(R.id.save_note)) + testFlakyView({ + composeTestRule.onNodeWithTag(SAVE_MENU_BUTTON_TESTING_TAG) + .performClick() + }) } fun openNoteFragment() { @@ -106,10 +126,13 @@ class NoteRobot : BaseRobot() { testFlakyView({ clickOn(Text("OPEN NOTE")) }) } - fun assertNoteSaved() { + fun assertNoteSaved(composeTestRule: ComposeContentTestRule) { // This is flaky since it is shown in a dialog and sometimes // UIDevice does not found the view immediately due to rendering process. - testFlakyView({ isVisible(Text(noteText)) }) + testFlakyView({ + composeTestRule.onNodeWithTag(ADD_NOTE_TEXT_FILED_TESTING_TAG) + .assertTextEquals(noteText) + }) } fun assertNotDoesNotExist() { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/SnackbarHostStateExtension.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/SnackbarHostStateExtension.kt index 5b9374cd2..2cc75c3c7 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/SnackbarHostStateExtension.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/SnackbarHostStateExtension.kt @@ -18,41 +18,11 @@ package org.kiwix.kiwixmobile.core.extensions -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult -import androidx.compose.runtime.Composable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.kiwix.kiwixmobile.core.ui.theme.DenimBlue400 - -/** - * A custom SnackbarHost for displaying snackbars with theme-aware action button colors. - * - * This function ensures that the action button color follows the app's theme: - * - In **light mode**, the action button color is `DenimBlue400`. - * - In **dark mode**, the action button color is `surface`, similar to the XML-based styling. - * - * @param snackbarHostState The state that controls the Snackbar display. - */ -@Composable -fun KiwixSnackbarHost(snackbarHostState: SnackbarHostState) { - val actionColor = if (isSystemInDarkTheme()) { - MaterialTheme.colorScheme.surface - } else { - DenimBlue400 - } - SnackbarHost(hostState = snackbarHostState) { snackbarData -> - Snackbar( - snackbarData = snackbarData, - actionColor = actionColor - ) - } -} fun SnackbarHostState.snack( message: String, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.kt index bcdd14efc..f09c5435e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.kt @@ -209,19 +209,22 @@ class AddNoteDialog : DialogFragment() { Vector(Icons.Default.Delete), R.string.delete, { deleteNote() }, - isEnabled = false + isEnabled = false, + testingTag = DELETE_MENU_BUTTON_TESTING_TAG ), ActionMenuItem( Vector(Icons.Default.Share), R.string.share, { shareNote() }, - isEnabled = false + isEnabled = false, + testingTag = SHARE_MENU_BUTTON_TESTING_TAG ), ActionMenuItem( Drawable(R.drawable.ic_save), R.string.save, { saveNote() }, - isEnabled = false + isEnabled = false, + testingTag = SAVE_MENU_BUTTON_TESTING_TAG ) ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialogScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialogScreen.kt index 2fe4cc291..354e1453c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialogScreen.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialogScreen.kt @@ -18,7 +18,6 @@ package org.kiwix.kiwixmobile.core.main -import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -26,9 +25,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Share import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -46,19 +42,15 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview import org.kiwix.kiwixmobile.core.R -import org.kiwix.kiwixmobile.core.extensions.KiwixSnackbarHost import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar -import org.kiwix.kiwixmobile.core.ui.components.NavigationIcon +import org.kiwix.kiwixmobile.core.ui.components.KiwixSnackbarHost import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem -import org.kiwix.kiwixmobile.core.ui.models.IconItem -import org.kiwix.kiwixmobile.core.ui.models.IconItem.Drawable -import org.kiwix.kiwixmobile.core.ui.models.IconItem.Vector import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP @@ -66,6 +58,11 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.MINIMUM_HEIGHT_OF_NOTE_TEX import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TEN_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWENTY_DP +const val ADD_NOTE_TEXT_FILED_TESTING_TAG = "addNoteTextFiledTestingTag" +const val SAVE_MENU_BUTTON_TESTING_TAG = "saveMenuButtonTestingTag" +const val SHARE_MENU_BUTTON_TESTING_TAG = "shareMenuButtonTestingTag" +const val DELETE_MENU_BUTTON_TESTING_TAG = "deleteMenuButtonTestingTag" + @Suppress("ComposableLambdaParameterNaming") @Composable fun AddNoteDialogScreen( @@ -138,7 +135,8 @@ private fun NoteTextField( .heightIn(min = MINIMUM_HEIGHT_OF_NOTE_TEXT_FILED) .padding(bottom = TEN_DP) .padding(horizontal = FOUR_DP) - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .testTag(ADD_NOTE_TEXT_FILED_TESTING_TAG), placeholder = { Text(text = stringResource(R.string.note)) }, singleLine = false, shape = RectangleShape, @@ -158,42 +156,3 @@ private fun NoteTextField( ) ) } - -@Preview -@Preview(name = "Night mode", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -fun PreviewAddNoteDialog() { - AddNoteDialogScreen( - "Demo note", - navigationIcon = { - NavigationIcon( - iconItem = IconItem.Drawable(R.drawable.ic_close_white_24dp), - onClick = {} - ) - }, - noteText = TextFieldValue(""), - actionMenuItems = listOf( - ActionMenuItem( - Vector(Icons.Default.Delete), - R.string.delete, - { }, - isEnabled = false - ), - ActionMenuItem( - Vector(Icons.Default.Share), - R.string.share, - { }, - isEnabled = false - ), - ActionMenuItem( - Drawable(R.drawable.ic_save), - R.string.save, - { }, - isEnabled = false - ) - ), - onTextChange = { text -> }, - isNoteFileExist = true, - snackBarHostState = SnackbarHostState() - ) -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt index c9c4b77bd..468f31013 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixAppBar.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight.Companion.SemiBold @@ -44,6 +45,8 @@ import org.kiwix.kiwixmobile.core.ui.theme.White import org.kiwix.kiwixmobile.core.utils.ComposeDimens.KIWIX_APP_BAR_HEIGHT import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP +const val TOOLBAR_TITLE_TESTING_TAG = "toolbarTitle" + @Composable fun KiwixAppBar( @StringRes titleId: Int, @@ -80,7 +83,7 @@ private fun AppBarTitle( text = stringResource(titleId), color = White, style = MaterialTheme.typography.titleLarge.copy(fontWeight = SemiBold), - modifier = Modifier.padding(horizontal = SIXTEEN_DP) + modifier = Modifier.padding(horizontal = SIXTEEN_DP).testTag(TOOLBAR_TITLE_TESTING_TAG) ) } @@ -90,7 +93,8 @@ private fun ActionMenu(actionMenuItems: List) { actionMenuItems.forEach { menuItem -> IconButton( enabled = menuItem.isEnabled, - onClick = menuItem.onClick + onClick = menuItem.onClick, + modifier = Modifier.testTag(menuItem.testingTag) ) { Icon( painter = when (val icon = menuItem.icon) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSnackbarHost.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSnackbarHost.kt new file mode 100644 index 000000000..f5ef0a449 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSnackbarHost.kt @@ -0,0 +1,51 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.ui.components + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import org.kiwix.kiwixmobile.core.ui.theme.DenimBlue400 + +/** + * A custom SnackbarHost for displaying snackbars with theme-aware action button colors. + * + * This function ensures that the action button color follows the app's theme: + * - In **light mode**, the action button color is `DenimBlue400`. + * - In **dark mode**, the action button color is `surface`, similar to the XML-based styling. + * + * @param snackbarHostState The state that controls the Snackbar display. + */ +@Composable +fun KiwixSnackbarHost(snackbarHostState: SnackbarHostState) { + val actionColor = if (isSystemInDarkTheme()) { + MaterialTheme.colorScheme.surface + } else { + DenimBlue400 + } + SnackbarHost(hostState = snackbarHostState) { snackbarData -> + Snackbar( + snackbarData = snackbarData, + actionColor = actionColor + ) + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt index 0f0860fa0..6ed601275 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt @@ -27,5 +27,6 @@ data class ActionMenuItem( @StringRes val contentDescription: Int, val onClick: () -> Unit, val iconTint: Color = White, - val isEnabled: Boolean = true + val isEnabled: Boolean = true, + val testingTag: String )