From 54371df7568b122bc8ada6819720b9ef20b8a089 Mon Sep 17 00:00:00 2001 From: Bober1337IT Date: Mon, 30 Mar 2026 19:27:22 +0200 Subject: [PATCH 1/2] add Hilt instrumentation testing setup and NoteScreen UI test --- app/build.gradle.kts | 11 ++++- .../bober/notesapp/ExampleInstrumentedTest.kt | 24 ----------- .../java/com/bober/notesapp/HiltTestRunner.kt | 18 +++++++++ .../com/bober/notesapp/di/AppModuleTest.kt | 34 ++++++++++++++++ .../presentation/notes/NoteScreenTest.kt | 40 +++++++++++++++++++ .../com/bober/notesapp/core/util/TestTags.kt | 6 +++ .../notesapp/presentation/notes/NoteScreen.kt | 5 ++- 7 files changed, 112 insertions(+), 26 deletions(-) delete mode 100644 app/src/androidTest/java/com/bober/notesapp/ExampleInstrumentedTest.kt create mode 100644 app/src/androidTest/java/com/bober/notesapp/HiltTestRunner.kt create mode 100644 app/src/androidTest/java/com/bober/notesapp/di/AppModuleTest.kt create mode 100644 app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt create mode 100644 app/src/main/java/com/bober/notesapp/core/util/TestTags.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5d33560..cdb8c03 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,16 @@ android { versionCode = 1 versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.bober.notesapp.HiltTestRunner" + } + + packaging { + resources { + excludes += "META-INF/LICENSE.md" + excludes += "META-INF/LICENSE-notice.md" + excludes += "META-INF/LICENSE*" + excludes += "META-INF/NOTICE*" + } } buildTypes { diff --git a/app/src/androidTest/java/com/bober/notesapp/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/bober/notesapp/ExampleInstrumentedTest.kt deleted file mode 100644 index acb0d34..0000000 --- a/app/src/androidTest/java/com/bober/notesapp/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.bober.notesapp - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.bober.notesapp", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/bober/notesapp/HiltTestRunner.kt b/app/src/androidTest/java/com/bober/notesapp/HiltTestRunner.kt new file mode 100644 index 0000000..dfb25ef --- /dev/null +++ b/app/src/androidTest/java/com/bober/notesapp/HiltTestRunner.kt @@ -0,0 +1,18 @@ +package com.bober.notesapp + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + + +class HiltTestRunner: AndroidJUnitRunner() { + + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application? { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/bober/notesapp/di/AppModuleTest.kt b/app/src/androidTest/java/com/bober/notesapp/di/AppModuleTest.kt new file mode 100644 index 0000000..ba21349 --- /dev/null +++ b/app/src/androidTest/java/com/bober/notesapp/di/AppModuleTest.kt @@ -0,0 +1,34 @@ +package com.bober.notesapp.di + +import android.app.Application +import androidx.room.Room +import com.bober.notesapp.data.local.NoteDatabase +import com.bober.notesapp.data.repository.FakeNoteRepository +import com.bober.notesapp.data.repository.NoteRepositoryImpl +import com.bober.notesapp.domain.repository.NoteRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModuleTest { + + @Provides + @Singleton + fun provideNoteDatabase(app: Application): NoteDatabase { + return Room.inMemoryDatabaseBuilder( + app, + NoteDatabase::class.java + ).build() + } + + @Provides + @Singleton + fun provideNoteRepository(db : NoteDatabase): NoteRepository{ + return NoteRepositoryImpl(db.noteDao()) + // return FakeNoteRepository() + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt b/app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt new file mode 100644 index 0000000..fb67474 --- /dev/null +++ b/app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt @@ -0,0 +1,40 @@ +package com.bober.notesapp.presentation.notes + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import com.bober.notesapp.core.util.TestTags +import com.bober.notesapp.di.AppModule +import com.bober.notesapp.presentation.MainActivity +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@HiltAndroidTest +@UninstallModules(AppModule::class) +class NoteScreenTest { + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + val composeRule = createAndroidComposeRule() + + @Before + fun setUp(){ + hiltRule.inject() + } + + @Test + fun clickToggleOrderSection_isVisible() { + composeRule.onNodeWithTag(TestTags.ORDER_SECTION).assertDoesNotExist() + composeRule.onNodeWithContentDescription("Sort").performClick() + composeRule.onNodeWithTag(TestTags.ORDER_SECTION).assertIsDisplayed() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bober/notesapp/core/util/TestTags.kt b/app/src/main/java/com/bober/notesapp/core/util/TestTags.kt new file mode 100644 index 0000000..64faea6 --- /dev/null +++ b/app/src/main/java/com/bober/notesapp/core/util/TestTags.kt @@ -0,0 +1,6 @@ +package com.bober.notesapp.core.util + +object TestTags { + + const val ORDER_SECTION = "ORDER_SECTION" +} \ No newline at end of file diff --git a/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt b/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt index 58cd452..f99d779 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/notes/NoteScreen.kt @@ -35,10 +35,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import com.bober.notesapp.core.util.TestTags import com.bober.notesapp.domain.model.Note import com.bober.notesapp.presentation.notes.components.NoteItem import com.bober.notesapp.presentation.notes.components.OrderSection @@ -115,7 +117,8 @@ fun NoteScreenContent( OrderSection( modifier = Modifier .fillMaxWidth() - .padding(vertical = 16.dp), + .padding(vertical = 16.dp) + .testTag(TestTags.ORDER_SECTION), noteOrder = state.noteOrder, onOrderChange = { onEvent(NotesEvent.Order(it)) From c535b1358a81839a8f9d4eb72bf51c892957842e Mon Sep 17 00:00:00 2001 From: Bober1337IT Date: Mon, 30 Mar 2026 20:14:14 +0200 Subject: [PATCH 2/2] add test tags and instrumentation tests for note ordering and navigation --- .../presentation/notes/NoteScreenTest.kt | 31 ++++++++++++++++++- .../com/bober/notesapp/core/util/TestTags.kt | 1 + .../presentation/notes/components/NoteItem.kt | 2 +- .../notes/components/OrderSection.kt | 5 ++- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt b/app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt index fb67474..8e99f7b 100644 --- a/app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt +++ b/app/src/androidTest/java/com/bober/notesapp/presentation/notes/NoteScreenTest.kt @@ -1,9 +1,15 @@ package com.bober.notesapp.presentation.notes import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasClickAction import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onChildren import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import com.bober.notesapp.core.util.TestTags import com.bober.notesapp.di.AppModule @@ -11,7 +17,6 @@ import com.bober.notesapp.presentation.MainActivity import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules -import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test @@ -28,6 +33,7 @@ class NoteScreenTest { @Before fun setUp(){ + hiltRule.inject() } @@ -37,4 +43,27 @@ class NoteScreenTest { composeRule.onNodeWithContentDescription("Sort").performClick() composeRule.onNodeWithTag(TestTags.ORDER_SECTION).assertIsDisplayed() } + + @Test + fun clickToggleOrderSectionAndChangeToTitle_titleIsClicked() { + composeRule.onNodeWithTag(TestTags.ORDER_SECTION).assertDoesNotExist() + composeRule.onNodeWithContentDescription("Sort").performClick() + + val titleRadioButton = composeRule + .onNodeWithTag(TestTags.TITLE_RADIO_BUTTON, useUnmergedTree = true) + .onChildren() + .filterToOne(hasClickAction()) + + titleRadioButton.assertIsNotSelected() + composeRule.onNodeWithText("Title").assertIsDisplayed() + titleRadioButton.performClick() + titleRadioButton.assertIsSelected() + } + + @Test + fun clickAddNote_navigatesToAddEditNoteScreen() { + composeRule.onNodeWithText("Your note").assertIsDisplayed() + composeRule.onNodeWithContentDescription("Add Note").performClick() + composeRule.onNodeWithText("Enter title...").assertIsDisplayed() + } } \ No newline at end of file diff --git a/app/src/main/java/com/bober/notesapp/core/util/TestTags.kt b/app/src/main/java/com/bober/notesapp/core/util/TestTags.kt index 64faea6..ddadd61 100644 --- a/app/src/main/java/com/bober/notesapp/core/util/TestTags.kt +++ b/app/src/main/java/com/bober/notesapp/core/util/TestTags.kt @@ -3,4 +3,5 @@ package com.bober.notesapp.core.util object TestTags { const val ORDER_SECTION = "ORDER_SECTION" + const val TITLE_RADIO_BUTTON = "TITLE_RADIO_BUTTON" } \ No newline at end of file diff --git a/app/src/main/java/com/bober/notesapp/presentation/notes/components/NoteItem.kt b/app/src/main/java/com/bober/notesapp/presentation/notes/components/NoteItem.kt index 6c0ccd8..75ab6d3 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/notes/components/NoteItem.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/notes/components/NoteItem.kt @@ -99,7 +99,7 @@ fun NoteItem( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Delete note", + contentDescription = "Delete note ${note.title}", tint = Color.Black ) } diff --git a/app/src/main/java/com/bober/notesapp/presentation/notes/components/OrderSection.kt b/app/src/main/java/com/bober/notesapp/presentation/notes/components/OrderSection.kt index c9e1edf..51ff264 100644 --- a/app/src/main/java/com/bober/notesapp/presentation/notes/components/OrderSection.kt +++ b/app/src/main/java/com/bober/notesapp/presentation/notes/components/OrderSection.kt @@ -8,7 +8,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import com.bober.notesapp.core.util.TestTags import com.bober.notesapp.domain.util.NoteOrder import com.bober.notesapp.domain.util.OrderType @@ -29,7 +31,8 @@ fun OrderSection( selected = noteOrder is NoteOrder.Title, onSelect = { onOrderChange(NoteOrder.Title(noteOrder.orderType)) - } + }, + Modifier.testTag(TestTags.TITLE_RADIO_BUTTON) ) Spacer(modifier = Modifier.width(8.dp)) DefaultRadioButton(