From 2aaeaff22c1f8c176611ee0cd2c360b1c492efda Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Sat, 26 Aug 2023 21:45:15 +0100 Subject: [PATCH 1/4] - LegacyDatabaseTest.kt added --- .../farmr/data/legacydb/LegacyDatabaseTest.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt new file mode 100644 index 0000000..e539bdd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt @@ -0,0 +1,91 @@ +package com.appttude.h_mal.farmr.data.legacydb + +import androidx.test.rule.provider.ProviderTestRule +import com.appttude.h_mal.farmr.model.Shift +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class LegacyDatabaseTest { + @get:Rule + val providerRule: ProviderTestRule = ProviderTestRule + .Builder(ShiftProvider::class.java, ShiftsContract.CONTENT_AUTHORITY) + .build() + + private lateinit var database: LegacyDatabase + + @Before + fun setup() { + database = LegacyDatabase(providerRule.resolver) + } + + @After + fun tearDown() { + database.deleteAllShiftsInDatabase() + } + + @Test + fun insertShift_readShift_successfulRead() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + + // Act + database.insertShiftDataIntoDatabase(shift) + val retrievedShift = database.readShiftsFromDatabase()?.first() + + // Assert + assertEquals(retrievedShift?.description, shift.description) + assertEquals(retrievedShift?.date, shift.date) + assertEquals(retrievedShift?.units, shift.units) + assertEquals(retrievedShift?.rateOfPay, shift.rateOfPay) + } + + @Test + fun insertShift_updateShift_successfulRead() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f) + + // Act + database.insertShiftDataIntoDatabase(shift) + val id = database.readShiftsFromDatabase()?.first()!!.id + database.updateShiftDataIntoDatabase( + id = id, + typeString = updateShift.type.type, + descriptionString = updateShift.description, + dateString = updateShift.date, + timeInString = updateShift.timeIn ?: "", + timeOutString = updateShift.timeOut ?: "", + duration = updateShift.duration ?: 0f, + breaks = updateShift.breakMins ?: 0, + units = updateShift.units!!, + payRate = updateShift.rateOfPay, + totalPay = updateShift.totalPay + ) + val retrievedShift = database.readSingleShiftWithId(id) + + // Assert + assertEquals(retrievedShift?.description, updateShift.description) + assertEquals(retrievedShift?.date, updateShift.date) + assertEquals(retrievedShift?.units, updateShift.units) + assertEquals(retrievedShift?.rateOfPay, updateShift.rateOfPay) + } + + @Test + fun insertShift_deleteShift_databaseEmpty() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f) + + // Act + database.insertShiftDataIntoDatabase(shift) + database.insertShiftDataIntoDatabase(updateShift) + val id = database.readShiftsFromDatabase()?.first()!!.id + database.deleteSingleShift(id) + + // Assert + assertEquals(database.readShiftsFromDatabase()?.size, 1) + } +} \ No newline at end of file From 68f02b8917630e98a930a462f16ef9ac7ee538f4 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Sat, 26 Aug 2023 22:23:19 +0100 Subject: [PATCH 2/4] - RepositoryImplTest.kt added --- app/build.gradle | 28 +++++++++- .../appttude/h_mal/farmr/ExampleUnitTest.java | 17 ------ .../h_mal/farmr/data/RepositoryImplTest.kt | 52 +++++++++++++++++++ .../appttude/h_mal/farmr/utils/testUtils.kt | 39 ++++++++++++++ 4 files changed, 117 insertions(+), 19 deletions(-) delete mode 100644 app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 8a92332..e7edb61 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,11 +34,33 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.preference:preference:1.2.1' - implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2' - testImplementation 'junit:junit:4.12' + / * Unit testing * / + testImplementation 'junit:junit:4.13.2' + androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' + / * mockito and livedata testing * / + testImplementation 'org.mockito:mockito-inline:2.13.0' + implementation 'androidx.arch.core:core-testing:2.1.0' + / * MockK * / + def mockk_ver = "1.10.5" + testImplementation "io.mockk:mockk:$mockk_ver" + androidTestImplementation "io.mockk:mockk-android:$mockk_ver" + / * Android Espresso * / + def testJunitVersion = "1.1.5" + def testRunnerVersion = "1.5.2" + def espressoVersion = "3.5.1" + androidTestImplementation "androidx.test.ext:junit:$testJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion" + implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion" + androidTestImplementation "androidx.test:runner:$testRunnerVersion" + androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" + androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" + androidTestImplementation "org.hamcrest:hamcrest:2.2" / * Room database * / def room_version = "2.4.3" implementation "androidx.room:room-runtime:$room_version" @@ -48,4 +70,6 @@ dependencies { def kodein_version = "6.2.1" implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" + / * SQLite to excel */ + implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2' } diff --git a/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java b/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java deleted file mode 100644 index cfb3bfd..0000000 --- a/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.appttude.h_mal.farmr; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt new file mode 100644 index 0000000..4c53925 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt @@ -0,0 +1,52 @@ +package com.appttude.h_mal.farmr.data + +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyLong +import java.util.UUID +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class RepositoryImplTest { + + private lateinit var repository: RepositoryImpl + + @MockK + lateinit var db: LegacyDatabase + + @MockK + lateinit var prefs: PreferenceProvider + + @Before + fun setUp() { + MockKAnnotations.init(this) + repository = RepositoryImpl(db, prefs) + } + + @Test + fun readDatabase_validResponse() { + // Arrange + val elements = listOf( + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() } + ) + + //Act + every { db.readShiftsFromDatabase() } returns elements + + // Assert + val result = repository.readShiftsFromDatabase() + assertIs>(result) + assertEquals(result.first().id, anyLong()) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt new file mode 100644 index 0000000..4d64b8d --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt @@ -0,0 +1,39 @@ +package com.appttude.h_mal.farmr.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + + this.observeForever(observer) + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} + +fun sleep(millis: Long = 1000) { + runBlocking(Dispatchers.Default) { delay(millis) } +} \ No newline at end of file From 07d7e6cbe730ef6a780cef8e371c157a791d6ac2 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Mon, 28 Aug 2023 21:13:43 +0100 Subject: [PATCH 3/4] - mid commit --- app/build.gradle | 2 +- .../appttude/h_mal/farmr/data/ui/BaseTest.kt | 91 +++++ .../h_mal/farmr/data/ui/BaseTestRobot.kt | 161 ++++++++ .../data/ui/robots/AddItemScreenRobot.kt | 41 ++ .../farmr/data/ui/robots/FilterScreenRobot.kt | 29 ++ .../farmr/data/ui/robots/HomeScreenRobot.kt | 26 ++ .../data/ui/robots/ViewItemScreenRobot.kt | 33 ++ .../h_mal/farmr/data/ui/tests/ShiftTests.kt | 77 ++++ .../h_mal/farmr/data/ui/utils/Constants.kt | 1 + .../farmr/data/ui/utils/EspressoHelper.kt | 123 ++++++ .../h_mal/farmr/data/ui/utils/TestUtils.kt | 32 ++ .../appttude/h_mal/farmr/base/BaseActivity.kt | 18 +- .../appttude/h_mal/farmr/base/BaseFragment.kt | 17 +- .../farmr/data/prefs/PreferencesProvider.kt | 12 +- .../h_mal/farmr/ui/FilterDataFragment.kt | 46 ++- .../h_mal/farmr/ui/FragmentAddItem.kt | 4 +- .../appttude/h_mal/farmr/ui/FragmentMain.kt | 3 +- .../h_mal/farmr/ui/FurtherInfoFragment.kt | 85 ++-- .../appttude/h_mal/farmr/ui/MainActivity.kt | 2 +- .../appttude/h_mal/farmr/utils/Formatting.kt | 10 + .../viewmodel/ApplicationViewModelFactory.kt | 3 + .../h_mal/farmr/viewmodel/FilterViewModel.kt | 21 + .../h_mal/farmr/viewmodel/InfoViewModel.kt | 40 ++ .../h_mal/farmr/viewmodel/MainViewModel.kt | 371 +----------------- .../h_mal/farmr/viewmodel/ShiftViewModel.kt | 52 +++ .../farmr/viewmodel/SubmissionViewModel.kt | 308 +++++++++++++++ .../farmr/viewmodel/MainViewModelTest.kt | 239 +++++++++++ .../farmr/viewmodel/ShiftViewModelTest.kt | 171 ++++++++ .../viewmodel/SubmissionViewModelTest.kt | 85 ++++ 29 files changed, 1646 insertions(+), 457 deletions(-) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt create mode 100644 app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt diff --git a/app/build.gradle b/app/build.gradle index d030c25..86f0e87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,7 +44,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.4.0' / * mockito and livedata testing * / testImplementation 'org.mockito:mockito-inline:2.13.0' - implementation 'androidx.arch.core:core-testing:2.1.0' + testImplementation 'androidx.arch.core:core-testing:2.1.0' / * MockK * / def mockk_ver = "1.10.5" testImplementation "io.mockk:mockk:$mockk_ver" diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt new file mode 100644 index 0000000..87e8ed5 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt @@ -0,0 +1,91 @@ +package com.appttude.h_mal.farmr.data.ui + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.RootMatchers.withDecorView +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import com.appttude.h_mal.farmr.di.ShiftApplication +import kotlinx.coroutines.runBlocking +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Rule + +@Suppress("EmptyMethod") +open class BaseTest( + private val activity: Class, + private val intentBundle: Bundle? = null, +) { + + lateinit var scenario: ActivityScenario + private lateinit var testApp: ShiftApplication + private lateinit var testActivity: Activity + private lateinit var decorView: View + + @get:Rule + var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) + + @Before + fun setUp() { + val startIntent = + Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity) + if (intentBundle != null) { + startIntent.replaceExtras(intentBundle) + } + + testApp = + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ShiftApplication + runBlocking { + beforeLaunch() + } + + scenario = ActivityScenario.launch(startIntent) + scenario.onActivity { + decorView = it.window.decorView + testActivity = it + } + afterLaunch() + } + + fun getActivity() = testActivity + + @After + fun tearDown() { + testFinished() + } + + open fun beforeLaunch() {} + open fun afterLaunch() {} + open fun testFinished() {} + + fun waitFor(delay: Long) { + Espresso.onView(ViewMatchers.isRoot()).perform(object : ViewAction { + override fun getConstraints(): Matcher = ViewMatchers.isRoot() + override fun getDescription(): String = "wait for $delay milliseconds" + override fun perform(uiController: UiController, v: View?) { + uiController.loopMainThreadForAtLeast(delay) + } + }) + } + + @Suppress("DEPRECATION") + fun checkToastMessage(message: String) { + Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView))) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + waitFor(3500) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt new file mode 100644 index 0000000..d864acb --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt @@ -0,0 +1,161 @@ +package com.appttude.h_mal.farmr.data.ui + +import android.content.res.Resources +import android.widget.DatePicker +import android.widget.TimePicker +import androidx.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.test.espresso.Espresso.onData +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.PickerActions +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.* +import com.appttude.h_mal.farmr.data.ui.utils.EspressoHelper.waitForView +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.anything +import org.hamcrest.CoreMatchers.equalTo + +@SuppressWarnings("unused") +open class BaseTestRobot { + + fun fillEditText(resId: Int, text: String?): ViewInteraction = + onView(withId(resId)).perform( + ViewActions.replaceText(text), + ViewActions.closeSoftKeyboard() + ) + + fun clickButton(resId: Int): ViewInteraction = + onView((withId(resId))).perform(click()) + +// fun clickMenu(menuId: Int): ViewInteraction = onView() + + fun matchView(resId: Int): ViewInteraction = onView(withId(resId)) + + fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId)) + + fun matchDisplayed(resId: Int): ViewInteraction = matchView(resId).check(matches(isDisplayed())) + + fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction + .check(matches(withText(text))) + + fun matchText(viewId: Int, textId: Int): ViewInteraction = onView(withId(viewId)) + .check(matches(withText(textId))) + + fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text) + + fun clickListItem(listRes: Int, position: Int) { + onData(anything()) + .inAdapterView(allOf(withId(listRes))) + .atPosition(position).perform(click()) + } + + fun scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(text)) + ) + ) + } + + fun scrollToRecyclerItem( + recyclerId: Int, + resIdForString: Int + ): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(resIdForString)) + ) + ) + } + + fun scrollToRecyclerItemByPosition( + recyclerId: Int, + position: Int + ): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position) + ) + } + + fun clickViewInRecycler(recyclerId: Int, text: String) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click()) + ) + } + + fun clickViewInRecycler(recyclerId: Int, resIdForString: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.actionOnItem( + hasDescendant(withText(resIdForString)), + click() + ) + ) + } + + fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position), + RecyclerViewActions.actionOnItemAtPosition(position, click()) + ) + } + + fun clickOnRecyclerItemWithText(recyclerId: Int, text: String) { + scrollToRecyclerItem(recyclerId, text) + ?.perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.actionOnItem( + withChild(withText(text)), click() + ) + ) + } + + fun swipeDown(resId: Int): ViewInteraction = + onView(withId(resId)).perform(swipeDown()) + + fun getStringFromResource(@StringRes resId: Int): String = + Resources.getSystem().getString(resId) + + fun pullToRefresh(resId: Int) { + onView(allOf(withId(resId), isDisplayed())).perform(swipeDown()) + } + + fun selectDateInPicker(year: Int, month: Int, day: Int) { + onView(withClassName(equalTo(DatePicker::class.java.name))).perform( + PickerActions.setDate( + year, + month, + day + ) + ) + } + + fun selectTextInSpinner(id: Int, text:String) { + clickButton(id) + onView(withSpinnerText(text)).perform(click()) + } + + fun selectTimeInPicker(hours: Int, minutes: Int) { + onView(withClassName(equalTo(TimePicker::class.java.name))).perform( + PickerActions.setTime( + hours, minutes + ) + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt new file mode 100644 index 0000000..f2149b2 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt @@ -0,0 +1,41 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.model.ShiftType + +fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() } +class AddItemScreenRobot : BaseTestRobot() { + + fun clickShiftType(type: ShiftType) { + when (type) { + ShiftType.HOURLY -> clickButton(R.id.hourly) + ShiftType.PIECE -> clickButton(R.id.piecerate) + } + } + + fun setDescription(text: String?) = fillEditText(R.id.locationEditText, text) + fun setDate(year: Int, month: Int, day: Int) { + clickButton(R.id.dateEditText) + selectDateInPicker(year, month, day) + } + + fun setTimeIn(hour: Int, minutes: Int) { + clickButton(R.id.timeInEditText) + selectTimeInPicker(hour, minutes) + } + + fun setTimeOut(hour: Int, minutes: Int) { + clickButton(R.id.timeOutEditText) + selectTimeInPicker(hour, minutes) + } + + fun setBreakTime(mins: Int) = fillEditText(R.id.breakEditText, mins.toString()) + fun setUnits(units: Float) = fillEditText(R.id.unitET, units.toString()) + fun setRateOfPay(rateOfPay: Float) = fillEditText(R.id.payrateET, rateOfPay.toString()) + fun submit() = clickButton(R.id.submit) + + fun assertTotalPay(pay: String) = matchText(R.id.totalpayval, pay) + fun assertDuration(duration: String) = matchText(R.id.ShiftDuration, duration) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt new file mode 100644 index 0000000..22e36ed --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt @@ -0,0 +1,29 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.model.ShiftType + +fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() } +class FilterScreenRobot : BaseTestRobot() { + + fun setDescription(text: String?) = fillEditText(R.id.filterLocationEditText, text) + + fun setDateIn(year: Int, month: Int, day: Int) { + clickButton(R.id.fromdateInEditText) + selectDateInPicker(year, month, day) + } + + fun setDateOut(year: Int, month: Int, day: Int) { + clickButton(R.id.filterDateOutEditText) + selectDateInPicker(year, month, day) + } + + fun setType(type: ShiftType?) = when(type) { + ShiftType.HOURLY -> selectTextInSpinner(R.id.TypeFilterEditText, type.type) + ShiftType.PIECE -> selectTextInSpinner(R.id.TypeFilterEditText, type.type) + null -> selectTextInSpinner(R.id.TypeFilterEditText, "") + } + fun submit() = clickButton(R.id.submitFiltered) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt new file mode 100644 index 0000000..4f5c5e6 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt @@ -0,0 +1,26 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot + +fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } +class HomeScreenRobot : BaseTestRobot() { + + fun clickOnItem(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position) + fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text) + fun clickOnEdit(position: Int) = clickViewInRecycler(R.id.list_item_view, R.id.imageView) + fun clickFab() = clickButton(R.id.fab1) + fun clickOnInfo() = clickButton(R.id.action_favorite) +// fun clearFilter() = +// fun applySort() = + + +// fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location) +// fun refresh() = pullToRefresh(R.id.swipe_refresh) +// fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4) +// fun verifyUnableToRetrieve() { +// matchText(R.id.header_text, R.string.retrieve_warning) +// matchText(R.id.body_text, R.string.empty_retrieve_warning) +// } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt new file mode 100644 index 0000000..9585ec9 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt @@ -0,0 +1,33 @@ +package com.appttude.h_mal.farmr.data.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.model.ShiftType + +fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() } +class ViewItemScreenRobot : BaseTestRobot() { + + fun matchShiftType(type: ShiftType) { + when (type) { + ShiftType.HOURLY -> matchText(R.id.details_shift, type.type) + ShiftType.PIECE -> matchText(R.id.details_shift, type.type) + } + } + + fun matchDescription(text: String) = matchText(R.id.details_desc, text) + fun matchDate(date: String) { + matchText(R.id.details_date, date) + } + + fun matchTime(timeIn: String, timeOut: String) { + matchText(R.id.details_time, "$timeIn-$timeOut") + } + + fun matchBreakTime(mins: Int) = matchText(R.id.details_breaks, mins.toString()) + fun matchUnits(units: Float) = fillEditText(R.id.details_units, units.toString()) + fun matchRateOfPay(rateOfPay: Float) = fillEditText(R.id.details_pay_rate, rateOfPay.toString()) + fun matchTotalPay(pay: String) = matchText(R.id.details_totalpay, pay) + fun matchDuration(duration: String) = matchText(R.id.details_duration, duration) + + fun clickEdit() = clickButton(R.id.details_edit) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt new file mode 100644 index 0000000..53997bd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt @@ -0,0 +1,77 @@ +package com.appttude.h_mal.farmr.data.ui.tests + +import com.appttude.h_mal.farmr.data.ui.BaseTest +import com.appttude.h_mal.farmr.data.ui.robots.addScreen +import com.appttude.h_mal.farmr.data.ui.robots.homeScreen +import com.appttude.h_mal.farmr.data.ui.robots.viewScreen +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.MainActivity +import org.junit.Test + +class ShiftTests: BaseTest(MainActivity::class.java) { + + // Add a shift successfully + @Test + fun test1() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.HOURLY) + setTimeIn(12,0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(10f) + assertDuration("2.0 hours") + assertTotalPay("£20.00") + submit() + } + homeScreen { + sc("This is a description") + } + } + + // Edit a shift successfully + @Test + fun test2() { + homeScreen { + clickOnItemWithText("Edit this shift") + } + addScreen { + setRateOfPay(20f) + assertDuration("2.0 hours") + assertTotalPay("£40.00") + submit() + } + homeScreen { + clickOnItemWithText("Edit this shift") + } + viewScreen { + matchDescription("Edit this shift") + matchDuration("2 Hours 0 minutes") + matchTotalPay("2.0 hours @ £20.00 per Hour\nEquals:£40.00") + } + } + + // filter the list with date from + @Test + fun test3() {} + + // filter the list with date to + @Test + fun test4() {} + + // Add a shift as piece rate + @Test + fun test5() {} + + // Validate the details screen + @Test + fun test6() {} + + // filter, sort, order and then reset + @Test + fun test7() {} +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt new file mode 100644 index 0000000..e18ec2c --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt @@ -0,0 +1 @@ +package com.appttude.h_mal.farmr.data.ui.utils diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt new file mode 100644 index 0000000..e008cfe --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt @@ -0,0 +1,123 @@ +package com.appttude.h_mal.farmr.data.ui.utils + +import android.os.SystemClock.sleep +import android.view.View +import android.widget.CheckBox +import android.widget.Checkable +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.NoMatchingViewException +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.util.TreeIterables +import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.isA +import org.hamcrest.Description +import org.hamcrest.Matcher + + +object EspressoHelper { + + /** + * Perform action of waiting for a certain view within a single root view + * @param viewMatcher Generic Matcher used to find our view + */ + fun searchFor(viewMatcher: Matcher): ViewAction { + + return object : ViewAction { + + override fun getConstraints(): Matcher = isRoot() + override fun getDescription(): String { + return "searching for view $this in the root view" + } + + override fun perform(uiController: UiController, view: View) { + var tries = 0 + val childViews: Iterable = TreeIterables.breadthFirstViewTraversal(view) + + // Look for the match in the tree of child views + childViews.forEach { + tries++ + if (viewMatcher.matches(it)) { + // found the view + return + } + } + + throw NoMatchingViewException.Builder() + .withRootView(view) + .withViewMatcher(viewMatcher) + .build() + } + } + } + + /** + * Performs an action to check/uncheck a checkbox + * + */ + fun setChecked(checked: Boolean): ViewAction { + return object : ViewAction { + override fun getConstraints(): BaseMatcher { + return object : BaseMatcher() { + override fun describeTo(description: Description?) {} + + override fun matches(actual: Any?): Boolean { + return isA(CheckBox::class.java).matches(actual) + } + } + } + + override fun getDescription(): String { + return "" + } + + override fun perform(uiController: UiController, view: View) { + val checkableView = view as Checkable + checkableView.isChecked = checked + } + } + } + + /** + * Perform action of implicitly waiting for a certain view. + * This differs from EspressoExtensions.searchFor in that, + * upon failure to locate an element, it will fetch a new root view + * in which to traverse searching for our @param match + * + * @param viewMatcher ViewMatcher used to find our view + */ + fun waitForView( + viewMatcher: Matcher, + waitMillis: Int = 5000, + waitMillisPerTry: Long = 100 + ): ViewInteraction { + + // Derive the max tries + val maxTries = waitMillis / waitMillisPerTry.toInt() + + var tries = 0 + + for (i in 0..maxTries) + try { + // Track the amount of times we've tried + tries++ + + // Search the root for the view + onView(isRoot()).perform(searchFor(viewMatcher)) + + // If we're here, we found our view. Now return it + return onView(viewMatcher) + + } catch (e: Exception) { + + if (tries == maxTries) { + throw e + } + sleep(waitMillisPerTry) + } + + throw Exception("Error finding a view matching $viewMatcher") + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt new file mode 100644 index 0000000..2a8c143 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt @@ -0,0 +1,32 @@ +package com.appttude.h_mal.farmr.data.ui.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + + this.observeForever(observer) + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt index 3960cf5..6ddc382 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt @@ -1,26 +1,10 @@ package com.appttude.h_mal.farmr.base import android.content.Intent -import androidx.activity.ComponentActivity import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelLazy import com.appttude.h_mal.farmr.utils.displayToast -import com.appttude.h_mal.farmr.utils.getGenericClassAt -import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory -import org.kodein.di.KodeinAware -import org.kodein.di.android.kodein -import org.kodein.di.generic.instance -abstract class BaseActivity : AppCompatActivity(), KodeinAware { - - override val kodein by kodein() - private val factory by instance() - - val viewModel: V by getViewModel() - - private fun getViewModel(): Lazy = - ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore }, - factoryProducer = { factory } ) +abstract class BaseActivity : AppCompatActivity() { /** diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt index a939028..3a25089 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt @@ -5,11 +5,13 @@ import android.view.View import androidx.annotation.LayoutRes import androidx.fragment.app.Fragment import androidx.fragment.app.createViewModelLazy +import androidx.lifecycle.ViewModelLazy import com.appttude.h_mal.farmr.model.ViewState import com.appttude.h_mal.farmr.utils.getGenericClassAt import com.appttude.h_mal.farmr.utils.popBackStack import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein import org.kodein.di.android.x.kodein import org.kodein.di.generic.instance import kotlin.properties.Delegates @@ -21,14 +23,13 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) override val kodein by kodein() private val factory by instance() - val viewModel: V by getActivityViewModel() + val viewModel: V by getViewModel() - private fun getActivityViewModel() = createViewModelLazy( - getGenericClassAt(0), - { requireActivity().viewModelStore }, - { factory }) + private fun getViewModel(): Lazy = + ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore }, + factoryProducer = { factory } ) - var mActivity: BaseActivity<*>? = null + var mActivity: BaseActivity? = null private var shortAnimationDuration by Delegates.notNull() @@ -39,7 +40,7 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - mActivity = requireActivity() as BaseActivity<*> + mActivity = requireActivity() as BaseActivity configureObserver() } @@ -75,7 +76,7 @@ abstract class BaseFragment(@LayoutRes contentLayoutId: Int) } fun setTitle(title: String) { - (requireActivity() as BaseActivity<*>).setTitleInActionBar(title) + (requireActivity() as BaseActivity).setTitleInActionBar(title) } fun popBackStack() = mActivity?.popBackStack() diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt index 99004a6..403d5e3 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt @@ -13,8 +13,8 @@ const val SORT = "SORT" const val ORDER = "ORDER" const val DESCRIPTION = "DESCRIPTION" -const val TIME_IN = "TIME_IN" -const val TIME_OUT = "TIME_OUT" +const val DATE_IN = "TIME_IN" +const val DATE_OUT = "TIME_OUT" const val TYPE = "TYPE" class PreferenceProvider( @@ -47,8 +47,8 @@ class PreferenceProvider( ) { preference.edit() .putString(DESCRIPTION, description) - .putString(TIME_IN, timeIn) - .putString(TIME_OUT, timeOut) + .putString(DATE_IN, timeIn) + .putString(DATE_OUT, timeOut) .putString(TYPE, type) .apply() } @@ -56,8 +56,8 @@ class PreferenceProvider( fun getFilteringDetails(): Map { return mapOf( Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)), - Pair(TIME_IN, preference.getString(TIME_IN, null)), - Pair(TIME_OUT, preference.getString(TIME_OUT, null)), + Pair(DATE_IN, preference.getString(DATE_IN, null)), + Pair(DATE_OUT, preference.getString(DATE_OUT, null)), Pair(TYPE, preference.getString(TYPE, null)) ) } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt index 745fb68..400c5a4 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt @@ -14,9 +14,9 @@ import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.setDatePicker -import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.appttude.h_mal.farmr.viewmodel.FilterViewModel -class FilterDataFragment : BaseFragment(R.layout.fragment_filter_data), +class FilterDataFragment : BaseFragment(R.layout.fragment_filter_data), AdapterView.OnItemSelectedListener, OnClickListener { private val spinnerList: Array = arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type) @@ -26,10 +26,10 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ private lateinit var dateToET: EditText private lateinit var typeSpinner: Spinner - private var description: String? = null - private var dateFrom: String? = null - private var dateTo: String? = null - private var type: String? = null + private var descriptionString: String? = null + private var dateFromString: String? = null + private var dateToString: String? = null + private var typeString: String? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -47,21 +47,29 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ val filterDetails = viewModel.getFiltrationDetails() - filterDetails.let { - LocationET.setText(it.description) - dateFromET.setText(it.dateFrom) - dateToET.setText(it.dateTo) - - it.type?.let { t -> - val spinnerPosition: Int = adapter.getPosition(t) + filterDetails.run { + description?.let { + LocationET.setText(it) + descriptionString = it + } + dateFrom?.let { + dateFromET.setText(it) + dateFromString = it + } + dateTo?.let { + dateToET.setText(it) + dateToString = it + } + type?.let { + typeString = it + val spinnerPosition: Int = adapter.getPosition(it) typeSpinner.setSelection(spinnerPosition) } - } - LocationET.doAfterTextChanged { description = it.toString() } - dateFromET.setDatePicker { dateFrom = it } - dateToET.setDatePicker { dateTo = it } + LocationET.doAfterTextChanged { descriptionString = it.toString() } + dateFromET.setDatePicker { dateFromString = it } + dateToET.setDatePicker { dateToString = it } typeSpinner.onItemSelectedListener = this submit.setOnClickListener(this) @@ -73,7 +81,7 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ position: Int, id: Long ) { - type = when (position) { + typeString = when (position) { 1 -> ShiftType.HOURLY.type 2 -> ShiftType.PIECE.type else -> return @@ -83,7 +91,7 @@ class FilterDataFragment : BaseFragment(R.layout.fragment_filter_ override fun onNothingSelected(parentView: AdapterView<*>?) {} private fun submitFiltrationDetails() { - viewModel.setFiltrationDetails(description, dateFrom, dateTo, type) + viewModel.applyFilters(descriptionString, dateFromString, dateToString, typeString) } override fun onClick(p0: View?) { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index eb74f10..c0a09b9 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -26,8 +26,9 @@ import com.appttude.h_mal.farmr.utils.setTimePicker import com.appttude.h_mal.farmr.utils.show import com.appttude.h_mal.farmr.utils.validateField import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel -class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), +class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), RadioGroup.OnCheckedChangeListener, BackPressedListener { private lateinit var mHourlyRadioButton: RadioButton @@ -262,7 +263,6 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), StringBuilder().append(mDuration).append(" hours").toString() mDuration!! * mPayRate } - ShiftType.PIECE -> { (mUnits ?: 0f) * mPayRate } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index f083778..e0f1cbd 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -70,7 +70,6 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr override fun onStart() { super.onStart() - viewModel.refreshLiveData() } @@ -112,7 +111,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr } R.id.clear_filter -> { - viewModel.setFiltrationDetails(null, null, null, null) + viewModel.clearFilters() return true } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt index bda6dfd..e536e10 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt @@ -11,13 +11,14 @@ import com.appttude.h_mal.farmr.base.BaseFragment import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.utils.CURRENCY -import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString +import com.appttude.h_mal.farmr.utils.formatToTwoDpString import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.navigateToFragment import com.appttude.h_mal.farmr.utils.show -import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.appttude.h_mal.farmr.viewmodel.InfoViewModel -class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher_info) { +class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher_info) { private lateinit var typeTV: TextView private lateinit var descriptionTV: TextView private lateinit var dateTV: TextView @@ -52,60 +53,50 @@ class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher hourlyDetailHolder = view.findViewById(R.id.details_hourly_details) unitsHolder = view.findViewById(R.id.details_units_holder) - val id = arguments!!.getLong(ID) - editButton.setOnClickListener { navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!) } - setupView(id) + viewModel.retrieveData(arguments) } - private fun setupView(id: Long) { - viewModel.getCurrentShift(id)?.run { - typeTV.text = type - descriptionTV.text = description - dateTV.text = date - payRateTV.text = rateOfPay.toString() - totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString() + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is ShiftObject) data.setupView() + } - when (ShiftType.getEnumByType(type)) { - ShiftType.HOURLY -> { - hourlyDetailHolder.show() - unitsHolder.hide() - times.text = StringBuilder(timeIn).append("-").append(timeOut).toString() - breakTV.text = StringBuilder(breakMins).append("mins").toString() - durationTV.text = buildDurationSummary(this) - val paymentSummary = - StringBuilder().append(duration).append(" Hours @ ").append(CURRENCY) - .append(rateOfPay).append(" per Hour").append("\n") - .append("Equals: ").append(CURRENCY).append(totalPay) - totalPayTV.text = paymentSummary - } + private fun ShiftObject.setupView() { + typeTV.text = type + descriptionTV.text = description + dateTV.text = date + payRateTV.text = rateOfPay.toString() + totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString() - ShiftType.PIECE -> { - hourlyDetailHolder.hide() - unitsHolder.show() - unitsTV.text = units.toString() + when (ShiftType.getEnumByType(type)) { + ShiftType.HOURLY -> { + hourlyDetailHolder.show() + unitsHolder.hide() + times.text = StringBuilder(timeIn).append("-").append(timeOut).toString() + breakTV.text = StringBuilder().append(breakMins).append(" mins").toString() + durationTV.text = viewModel.buildDurationSummary(this) + val paymentSummary = + StringBuilder().append(duration).append(" Hours @ ") + .append(rateOfPay.formatAsCurrencyString()).append(" per Hour").append("\n") + .append("Equals: ").append(totalPay.formatAsCurrencyString()) + totalPayTV.text = paymentSummary + } - val paymentSummary = - StringBuilder().append(units).append(" Units @ ").append(CURRENCY) - .append(rateOfPay).append(" per Unit").append("\n") - .append("Equals: ").append(CURRENCY).append(totalPay) - totalPayTV.text = paymentSummary - } + ShiftType.PIECE -> { + hourlyDetailHolder.hide() + unitsHolder.show() + unitsTV.text = units.toString() + + val paymentSummary = + StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ") + .append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n") + .append("Equals: ").append(totalPay.formatAsCurrencyString()) + totalPayTV.text = paymentSummary } } } - - private fun buildDurationSummary(shiftObject: ShiftObject): String { - val time = shiftObject.getHoursMinutesPairFromDuration() - - val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second) - .append(" Minutes ") - if (shiftObject.breakMins > 0) { - stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)") - } - return stringBuilder.toString() - } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt index 187d4cd..245b54f 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt @@ -19,7 +19,7 @@ import com.appttude.h_mal.farmr.utils.popBackStack import com.appttude.h_mal.farmr.viewmodel.MainViewModel import kotlin.system.exitProcess -class MainActivity : BaseActivity() { +class MainActivity : BaseActivity() { private lateinit var toolbar: Toolbar override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt index 00f9119..5b007e1 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt @@ -1,8 +1,10 @@ package com.appttude.h_mal.farmr.utils import java.io.IOException +import java.text.NumberFormat import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Currency import java.util.Date import java.util.Locale @@ -16,6 +18,14 @@ fun Float.formatToTwoDp(): Float { return formattedString.toFloat() } +fun Float.formatAsCurrencyString(): String? { + val format: NumberFormat = NumberFormat.getCurrencyInstance() + format.maximumFractionDigits = 2 + format.currency = Currency.getInstance("GBP") + + return format.format(this) +} + fun Float.formatToTwoDpString(): String { return formatToTwoDp().toString() } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt index 16883c7..73cc067 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt @@ -14,6 +14,9 @@ class ApplicationViewModelFactory( with(modelClass) { return when { isAssignableFrom(MainViewModel::class.java) -> MainViewModel(repository) + isAssignableFrom(SubmissionViewModel::class.java) -> SubmissionViewModel(repository) + isAssignableFrom(InfoViewModel::class.java) -> InfoViewModel(repository) + isAssignableFrom(FilterViewModel::class.java) -> FilterViewModel(repository) else -> throw IllegalArgumentException("Unknown ViewModel class") } as T } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt new file mode 100644 index 0000000..87bb2bd --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt @@ -0,0 +1,21 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.model.Success + + +class FilterViewModel( + repository: Repository +) : ShiftViewModel(repository) { + + fun applyFilters( + description: String?, + dateFrom: String?, + dateTo: String?, + type: String? + ) { + super.setFiltrationDetails(description, dateFrom, dateTo, type) + onSuccess(Success("Filter(s) have been applied")) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt new file mode 100644 index 0000000..dea7585 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt @@ -0,0 +1,40 @@ +package com.appttude.h_mal.farmr.viewmodel + +import android.os.Bundle +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.utils.ID + + +class InfoViewModel( + repository: Repository +) : ShiftViewModel(repository) { + + fun retrieveData(bundle: Bundle?) { + val id = bundle?.getLong(ID) + if (id == null) { + onError("Failed to retrieve shift") + return + } + + val shift = getCurrentShift(id) + if (shift == null) { + onError("Failed to retrieve shift") + return + } + + onSuccess(shift) + } + + fun buildDurationSummary(shiftObject: ShiftObject): String { + val time = shiftObject.getHoursMinutesPairFromDuration() + + val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second) + .append(" Minutes ") + if (shiftObject.breakMins > 0) { + stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)") + } + return stringBuilder.toString() + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt index bec425b..7a99874 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -1,13 +1,10 @@ package com.appttude.h_mal.farmr.viewmodel import android.Manifest.permission.WRITE_EXTERNAL_STORAGE -import android.os.Build -import android.os.Environment import androidx.annotation.RequiresPermission import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer -import com.appttude.h_mal.farmr.base.BaseViewModel import com.appttude.h_mal.farmr.data.Repository import com.appttude.h_mal.farmr.data.legacydb.ShiftObject import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK @@ -21,24 +18,14 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID -import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION -import com.appttude.h_mal.farmr.data.prefs.TIME_IN -import com.appttude.h_mal.farmr.data.prefs.TIME_OUT -import com.appttude.h_mal.farmr.data.prefs.TYPE -import com.appttude.h_mal.farmr.model.FilterStore import com.appttude.h_mal.farmr.model.Order -import com.appttude.h_mal.farmr.model.Shift import com.appttude.h_mal.farmr.model.ShiftType import com.appttude.h_mal.farmr.model.Sortable import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.CURRENCY -import com.appttude.h_mal.farmr.utils.calculateDuration import com.appttude.h_mal.farmr.utils.convertDateString -import com.appttude.h_mal.farmr.utils.dateStringIsValid -import com.appttude.h_mal.farmr.utils.formatToTwoDp -import com.appttude.h_mal.farmr.utils.getTimeString +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString import com.appttude.h_mal.farmr.utils.sortedByOrder -import com.appttude.h_mal.farmr.utils.timeStringIsValid import jxl.Workbook import jxl.WorkbookSettings import jxl.write.Label @@ -46,25 +33,24 @@ import jxl.write.WritableWorkbook import jxl.write.WriteException import java.io.File import java.io.IOException -import java.util.Calendar import java.util.Locale class MainViewModel( private val repository: Repository -) : BaseViewModel() { +) : ShiftViewModel(repository) { private val _shiftLiveData = MutableLiveData>() - val shiftLiveData: LiveData> = _shiftLiveData + private val shiftLiveData: LiveData> = _shiftLiveData private var mSort: Sortable = Sortable.ID private var mOrder: Order = Order.ASCENDING - private var mFilterStore: FilterStore? = null - private val observer = Observer> { - val result = it.applyFilters().sortList(mSort, mOrder) - onSuccess(result) + it?.let { + val result = it.applyFilters().sortList(mSort, mOrder) + onSuccess(result) + } } init { @@ -148,8 +134,9 @@ class MainViewModel( var countOfTypeP = 0 var totalUnits = 0f var totalPay = 0f - val lines = _shiftLiveData.value?.size ?: 0 - _shiftLiveData.value?.forEach { + var lines = 0 + _shiftLiveData.value?.applyFilters()?.forEach { + lines += 1 totalDuration += it.duration when (ShiftType.getEnumByType(it.type)) { ShiftType.HOURLY -> countOfTypeH += 1 @@ -169,161 +156,6 @@ class MainViewModel( ) } - fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id) - - fun insertHourlyShift( - description: String, - date: String, - rateOfPay: Float, - timeIn: String?, - timeOut: String?, - breakMins: Int?, - ) { - // Validate inputs from the edit texts - (description.length > 3).validateField { - onError("Description length should be longer") - return - } - date.dateStringIsValid().validateField { - onError("Date format is invalid") - return - } - (rateOfPay >= 0.00).validateField { - onError("Rate of pay is invalid") - return - } - timeIn?.timeStringIsValid()?.validateField { - onError("Time in format is in correct") - return - } - timeOut?.timeStringIsValid()?.validateField { - onError("Time out format is in correct") - return - } - breakMins?.let { it > 0 }?.validateField { - onError("Break in minutes is invalid") - return - } - - doTry { - val result = insertShiftIntoDatabase( - ShiftType.HOURLY, - description, - date, - rateOfPay.formatToTwoDp(), - timeIn, - timeOut, - breakMins, - null - ) - - if (result) onSuccess(Success("Shift successfully added")) - } - - } - - fun insertPieceRateShift( - description: String, - date: String, - units: Float, - rateOfPay: Float - ) { - // Validate inputs from the edit texts - (description.length > 3).validateField { - onError("Description length should be longer") - return - } - date.dateStringIsValid().validateField { - onError("Date format is invalid") - return - } - (rateOfPay >= 0.00).validateField { - onError("Rate of pay is invalid") - return - } - (units.toInt() >= 0).validateField { - onError("Units cannot be below zero") - return - } - - doTry { - val result = insertShiftIntoDatabase( - type = ShiftType.PIECE, - description = description, - date = date, - rateOfPay = rateOfPay.formatToTwoDp(), - null, - null, - null, - units = units - ) - if (result) onSuccess(Success("New shift successfully added")) - } - } - - fun updateShift( - id: Long, - type: String? = null, - description: String? = null, - date: String? = null, - rateOfPay: Float? = null, - timeIn: String? = null, - timeOut: String? = null, - breakMins: Int? = null, - units: Float? = null, - ) { - description?.let { - (it.length > 3).validateField { - onError("Description length should be longer") - return - } - } - date?.dateStringIsValid()?.validateField { - onError("Date format is invalid") - return - } - rateOfPay?.let { - (it >= 0.00).validateField { - onError("Rate of pay is invalid") - return - } - } - units?.let { - (it.toInt() >= 0).validateField { - onError("Units cannot be below zero") - return - } - } - timeIn?.timeStringIsValid()?.validateField { - onError("Time in format is in correct") - return - } - timeOut?.timeStringIsValid()?.validateField { - onError("Time out format is in correct") - return - } - breakMins?.let { it >= 0 }?.validateField { - onError("Break in minutes is invalid") - return - } - - doTry { - val result = updateShiftInDatabase( - id, - type = type?.let { ShiftType.getEnumByType(it) }, - description = description, - date = date, - rateOfPay = rateOfPay, - timeIn = timeIn, - timeOut = timeOut, - breakMins = breakMins, - units = units - ) - - if (result) onSuccess(Success("Shift successfully updated")) - } - } - fun deleteShift(id: Long) { if (!repository.deleteSingleShiftFromDatabase(id)) { onError("Failed to delete shift") @@ -340,134 +172,6 @@ class MainViewModel( } } - private fun updateShiftInDatabase( - id: Long, - type: ShiftType? = null, - description: String? = null, - date: String? = null, - rateOfPay: Float? = null, - timeIn: String? = null, - timeOut: String? = null, - breakMins: Int? = null, - units: Float? = null, - ): Boolean { - val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() - ?: throw IOException("Cannot update shift as it does not exist") - - val shift = when (type) { - ShiftType.HOURLY -> { - // Shift type has changed so mandatory fields for hourly shift are now required as well - val insertTimeIn = - (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted") - val insertTimeOut = - (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, - breakMins = breakMins ?: currentShift.breakMins, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - - ShiftType.PIECE -> { - // Shift type has changed so mandatory fields for piece rate shift are now required as well - val insertUnits = (units ?: currentShift.units) - ?: throw IOException("Units must be inserted for piece rate shifts") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - units = insertUnits, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - - else -> { - if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { - // Updates to description or date field - currentShift.copy( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - ) - } else { - // Updating shifts where shift type has remained the same - when (currentShift.type) { - ShiftType.HOURLY -> { - val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException( - "No time in inserted" - ) - val insertTimeOut = (timeOut ?: currentShift.timeOut) - ?: throw IOException("No time out inserted") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, - breakMins = breakMins ?: currentShift.breakMins, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - - ShiftType.PIECE -> { - val insertUnits = (units ?: currentShift.units) - ?: throw IOException("Units must be inserted for piece rate shifts") - Shift( - description = description ?: currentShift.description, - date = date ?: currentShift.date, - units = insertUnits, - rateOfPay = rateOfPay ?: currentShift.rateOfPay - ) - } - } - } - } - } - - return repository.updateShiftIntoDatabase(id, shift) - } - - private fun insertShiftIntoDatabase( - type: ShiftType, - description: String, - date: String, - rateOfPay: Float, - timeIn: String?, - timeOut: String?, - breakMins: Int?, - units: Float?, - ): Boolean { - val shift = when (type) { - ShiftType.HOURLY -> { - if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null") - val calendar by lazy { Calendar.getInstance() } - val insertTimeIn = timeIn ?: calendar.getTimeString() - val insertTimeOut = timeOut ?: calendar.getTimeString() - - Shift( - description = description, - date = date, - timeIn = insertTimeIn, - timeOut = insertTimeOut, - breakMins = breakMins, - rateOfPay = rateOfPay - ) - } - - ShiftType.PIECE -> { - Shift( - description = description, - date = date, - units = units!!, - rateOfPay = rateOfPay, - ) - } - } - - return repository.insertShiftIntoDatabase(shift) - } - - private fun buildInfoString( totalDuration: Float, countOfHourly: Int, @@ -488,63 +192,21 @@ class MainViewModel( stringBuilder.append("Total Units: ").append(totalUnits).append("\n") } if (totalPay != 0f) { - stringBuilder.append("Total Pay: ").append(CURRENCY).append(totalPay).append("\n") + stringBuilder.append("Total Pay: ").append(totalPay.formatAsCurrencyString()) } return stringBuilder.toString() } fun refreshLiveData() { - _shiftLiveData.postValue(repository.readShiftsFromDatabase()) + repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) } } - private inline fun Boolean.validateField(failureCallback: () -> Unit) { - if (!this) failureCallback.invoke() - } - - /** - * Lambda function that will invoke onError(...) on failure - * but update live data when successful - */ - private inline fun doTry(operation: () -> Unit) { - try { - operation.invoke() - refreshLiveData() - } catch (e: Exception) { - onError(e) - } - } - - fun setFiltrationDetails( - description: String?, - dateFrom: String?, - dateTo: String?, - type: String? - ) { - repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) - onSuccess(Success("Filter(s) successfully applied")) + fun clearFilters() { + super.setFiltrationDetails(null, null, null, null) + onSuccess(Success("Filters have been cleared")) refreshLiveData() } - fun getFiltrationDetails(): FilterStore { - val prefs = repository.retrieveFilteringDetailsInPrefs() - mFilterStore = FilterStore( - prefs[DESCRIPTION], - prefs[TIME_IN], - prefs[TIME_OUT], - prefs[TYPE] - ) - return mFilterStore!! - } - - fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { - try { - return calculateDuration(mTimeIn, mTimeOut, mBreaks) - } catch (e: IOException) { - onError(e) - } - return null - } - @RequiresPermission(WRITE_EXTERNAL_STORAGE) fun createExcelSheet(file: File): File? { val wbSettings = WorkbookSettings().apply { @@ -574,7 +236,8 @@ class MainViewModel( return null } val sortAndOrder = getSortAndOrder() - val data = shiftLiveData.value!!.applyFilters().sortList(sortAndOrder.first, sortAndOrder.second) + val data = shiftLiveData.value!!.applyFilters() + .sortList(sortAndOrder.first, sortAndOrder.second) var currentRow = 0 val cells = data.mapIndexed { index, shift -> currentRow += 1 diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt new file mode 100644 index 0000000..33c7ea5 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt @@ -0,0 +1,52 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.base.BaseViewModel +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION +import com.appttude.h_mal.farmr.data.prefs.DATE_IN +import com.appttude.h_mal.farmr.data.prefs.DATE_OUT +import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.model.FilterStore + + +open class ShiftViewModel( + private val repository: Repository +) : BaseViewModel() { + + /* + * Add Item & Further info + */ + fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id) + + /** + * Lambda function that will invoke onError(...) on failure + * but update live data when successful + */ + private inline fun doTry(operation: () -> Unit) { + try { + operation.invoke() + } catch (e: Exception) { + onError(e) + } + } + + open fun setFiltrationDetails( + description: String?, + dateFrom: String?, + dateTo: String?, + type: String? + ) { + repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) + } + + open fun getFiltrationDetails(): FilterStore { + val prefs = repository.retrieveFilteringDetailsInPrefs() + return FilterStore( + prefs[DESCRIPTION], + prefs[DATE_IN], + prefs[DATE_OUT], + prefs[TYPE] + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt new file mode 100644 index 0000000..322ca40 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt @@ -0,0 +1,308 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.calculateDuration +import com.appttude.h_mal.farmr.utils.dateStringIsValid +import com.appttude.h_mal.farmr.utils.formatToTwoDp +import com.appttude.h_mal.farmr.utils.getTimeString +import com.appttude.h_mal.farmr.utils.timeStringIsValid +import java.io.IOException +import java.util.Calendar + + +class SubmissionViewModel( + private val repository: Repository +) : ShiftViewModel(repository) { + + fun insertHourlyShift( + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + ) { + // Validate inputs from the edit texts + (description.length > 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it >= 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + val result = insertShiftIntoDatabase( + ShiftType.HOURLY, + description, + date, + rateOfPay.formatToTwoDp(), + timeIn, + timeOut, + breakMins, + null + ) + + if (result) onSuccess(Success("New shift successfully added")) + else onError("Cannot insert shift") + } + + fun insertPieceRateShift( + description: String, + date: String, + units: Float, + rateOfPay: Float + ) { + // Validate inputs from the edit texts + (description.length > 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + (units.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + + val result = insertShiftIntoDatabase( + type = ShiftType.PIECE, + description = description, + date = date, + rateOfPay = rateOfPay.formatToTwoDp(), + null, + null, + null, + units = units + ) + if (result) onSuccess(Success("New shift successfully added")) + else onError("Cannot insert shift") + } + + fun updateShift( + id: Long, + type: String? = null, + description: String? = null, + date: String? = null, + rateOfPay: Float? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: Int? = null, + units: Float? = null, + ) { + description?.let { + (it.length > 3).validateField { + onError("Description length should be longer") + return + } + } + date?.dateStringIsValid()?.validateField { + onError("Date format is invalid") + return + } + rateOfPay?.let { + (it >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + } + units?.let { + (it.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it >= 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + val result = updateShiftInDatabase( + id, + type = type?.let { ShiftType.getEnumByType(it) }, + description = description, + date = date, + rateOfPay = rateOfPay, + timeIn = timeIn, + timeOut = timeOut, + breakMins = breakMins, + units = units + ) + + if (result) onSuccess(Success("Shift successfully updated")) + else onError("Cannot update shift") + } + + private fun updateShiftInDatabase( + id: Long, + type: ShiftType? = null, + description: String? = null, + date: String? = null, + rateOfPay: Float? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: Int? = null, + units: Float? = null, + ): Boolean { + val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() + ?: throw IOException("Cannot update shift as it does not exist") + + val shift = when (type) { + ShiftType.HOURLY -> { + // Shift type has changed so mandatory fields for hourly shift are now required as well + val insertTimeIn = + (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted") + val insertTimeOut = + (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + // Shift type has changed so mandatory fields for piece rate shift are now required as well + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + else -> { + if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { + // Updates to description or date field + currentShift.copy( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + ) + } else { + // Updating shifts where shift type has remained the same + when (currentShift.type) { + ShiftType.HOURLY -> { + val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException( + "No time in inserted" + ) + val insertTimeOut = (timeOut ?: currentShift.timeOut) + ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + } + } + } + } + + return repository.updateShiftIntoDatabase(id, shift) + } + + private fun insertShiftIntoDatabase( + type: ShiftType, + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + units: Float?, + ): Boolean { + val shift = when (type) { + ShiftType.HOURLY -> { + if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null") + val calendar by lazy { Calendar.getInstance() } + val insertTimeIn = timeIn ?: calendar.getTimeString() + val insertTimeOut = timeOut ?: calendar.getTimeString() + Shift( + description = description, + date = date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins, + rateOfPay = rateOfPay + ) + } + + ShiftType.PIECE -> { + Shift( + description = description, + date = date, + units = units!!, + rateOfPay = rateOfPay, + ) + } + } + + return repository.insertShiftIntoDatabase(shift) + } + + private inline fun Boolean.validateField(failureCallback: () -> Unit) { + if (!this) failureCallback.invoke() + } + + fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { + try { + return calculateDuration(mTimeIn, mTimeOut, mBreaks) + } catch (e: IOException) { + onError(e) + } + return null + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt new file mode 100644 index 0000000..127dab4 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt @@ -0,0 +1,239 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.DATE_IN +import com.appttude.h_mal.farmr.data.prefs.DATE_OUT +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION +import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getOrAwaitValue +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyList +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import java.util.concurrent.TimeoutException +import kotlin.test.assertEquals + +class MainViewModelTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + private lateinit var repository: Repository + private lateinit var viewModel: MainViewModel + + @Before + fun setUp() { + repository = mockk() + every { repository.readShiftsFromDatabase() }.returns(null) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) + viewModel = MainViewModel(repository) + } + + @Test + fun initViewModel_liveDataIsEmpty() { + // Assert + assertThrows(TimeoutException::class.java) { viewModel.uiState.getOrAwaitValue() } + } + + @Test + fun getShiftsFromRepository_liveDataIsShown() { + // Arrange + val listOfShifts = anyList() + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + viewModel.refreshLiveData() + + // Assert + assertEquals(retrieveCurrentData(), listOfShifts) + } + + @Test + fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() { + // Arrange + val listOfShifts = getShifts() + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + viewModel.refreshLiveData() + val retrievedShifts = retrieveCurrentData() + val description = viewModel.getInformation() + + // Assert + assertEquals(retrievedShifts, listOfShifts) + assertEquals( + description, "8 Shifts\n" + + " (4 Hourly/4 Piece Rate)\n" + + "Total Hours: 4.0\n" + + "Total Units: 4.0\n" + + "Total Pay: £70.00" + ) + } + + @Test + fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() { + // Arrange + val listOfShifts = getShifts() + val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type } + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type)) + viewModel.refreshLiveData() + val retrievedShifts = retrieveCurrentData() + val description = viewModel.getInformation() + + every { repository.setFilteringDetailsInPrefs(null, null, null, null) }.returns(Unit) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) + viewModel.clearFilters() + val descriptionAfterClearedFilter = viewModel.getInformation() + + // Assert + assertEquals(retrievedShifts, filteredShifts) + assertEquals( + description, "4 Shifts\n" + + "Total Hours: 4.0\n" + + "Total Pay: £30.00" + ) + assertEquals( + descriptionAfterClearedFilter, "8 Shifts\n" + + " (4 Hourly/4 Piece Rate)\n" + + "Total Hours: 4.0\n" + + "Total Units: 4.0\n" + + "Total Pay: £70.00" + ) + } + + private fun retrieveCurrentData() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasData<*>).data + + private fun getFilter( + description: String? = null, + type: String? = null, + dateIn: String? = null, + dateOut: String? = null + ): Map = + mapOf( + Pair(DESCRIPTION, description), + Pair(DATE_IN, dateIn), + Pair(DATE_OUT, dateOut), + Pair(TYPE, type) + ) + + private fun getShifts() = listOf( + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + anyInt(), + anyFloat(), + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + anyInt(), + anyFloat(), + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + anyFloat(), + 10f, + 5f + ), + ShiftObject( + anyLong(), + ShiftType.HOURLY.type, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + anyFloat(), + 10f, + 5f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day six", + "2023-08-06", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day seven", + "2023-08-07", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + anyLong(), + ShiftType.PIECE.type, + "Day eight", + "2023-08-08", + anyString(), + anyString(), + anyFloat(), + anyInt(), + 1f, + 10f, + 10f + ), + ) +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt new file mode 100644 index 0000000..1106249 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt @@ -0,0 +1,171 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getOrAwaitValue +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.RelaxedMockK +import org.junit.Before +import org.junit.Rule +import org.mockito.ArgumentMatchers + +open class ShiftViewModelTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + @RelaxedMockK + lateinit var repository: Repository + + @InjectMockKs + lateinit var viewModel: V + + @Before + fun setUp() { + MockKAnnotations.init(this) + } + + fun retrieveCurrentData() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasData<*>).data + + fun retrieveCurrentError() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasError<*>).error + + fun getHourlyShift() = ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ) + + fun getPieceRateShift() = ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ) + + fun getShifts() = listOf( + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day six", + "2023-08-06", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day seven", + "2023-08-07", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day eight", + "2023-08-08", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ) +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt new file mode 100644 index 0000000..2f6244b --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt @@ -0,0 +1,85 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.model.Success +import io.mockk.every +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.test.assertIs + +class SubmissionViewModelTest : ShiftViewModelTest() { + + @Test + fun insertHourlyShifts_validParameters_successfulInsertions() { + // Arrange + val hourly = getHourlyShift() + + // Act + every { repository.insertShiftIntoDatabase(hourly.copyToShift()) }.returns(true) + hourly.run { + viewModel.insertHourlyShift(description, date, rateOfPay, timeIn, timeOut, breakMins) + } + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + (retrieveCurrentData() as Success).successMessage, + "New shift successfully added" + ) + } + + @Test + fun insertPieceShifts_validParameters_successfulInsertions() { + // Arrange + val piece = getPieceRateShift() + + // Act + every { repository.insertShiftIntoDatabase(piece.copyToShift()) }.returns(true) + piece.run { + viewModel.insertPieceRateShift(description, date, units, rateOfPay) + } + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + (retrieveCurrentData() as Success).successMessage, + "New shift successfully added" + ) + } + + @Test + fun insertHourlyShifts_validParameters_unsuccessfulInsertions() { + // Arrange + val hourly = getHourlyShift() + + // Act + every { repository.insertShiftIntoDatabase(hourly.copyToShift()) }.returns(false) + hourly.run { + viewModel.insertHourlyShift(description, date, rateOfPay, timeIn, timeOut, breakMins) + } + + // Assert + assertEquals( + retrieveCurrentError(), + "Cannot insert shift" + ) + } + + @Test + fun insertPieceShifts_validParameters_unsuccessfulInsertions() { + // Arrange + val piece = getPieceRateShift() + + // Act + every { repository.insertShiftIntoDatabase(piece.copyToShift()) }.returns(false) + piece.run { + viewModel.insertPieceRateShift(description, date, units, rateOfPay) + } + + // Assert + assertEquals( + retrieveCurrentError(), + "Cannot insert shift" + ) + } + +} \ No newline at end of file From ce5be162321c30ac0511f7b4db93447239a49ed5 Mon Sep 17 00:00:00 2001 From: "h.malik144@gmail.com" Date: Tue, 29 Aug 2023 14:02:42 +0100 Subject: [PATCH 4/4] - Test suite expanded - config.yml updated - --- .circleci/config.yml | 140 ++++++++++++++--- .gitignore | 2 +- .idea/androidTestResultsUserPreferences.xml | 22 --- .idea/assetWizardSettings.xml | 127 ---------------- .idea/caches/build_file_checksums.ser | Bin 537 -> 0 bytes .idea/caches/gradle_models.ser | Bin 128383 -> 0 bytes .idea/compiler.xml | 6 - .idea/gradle.xml | 21 --- .idea/jarRepositories.xml | 30 ---- .idea/misc.xml | 47 ------ .idea/modules.xml | 12 -- .idea/vcs.xml | 6 - app/build.gradle | 3 +- .../h_mal/farmr/application/TestAppClass.kt | 38 +++++ .../h_mal/farmr/application/TestRunner.kt | 21 +++ .../h_mal/farmr/data/ShiftProviderTest.kt | 6 + .../farmr/data/ui/robots/HomeScreenRobot.kt | 26 ---- .../h_mal/farmr/data/ui/tests/ShiftTests.kt | 77 ---------- .../h_mal/farmr/data/ui/utils/Constants.kt | 1 - .../h_mal/farmr/{data => }/ui/BaseTest.kt | 27 +++- .../farmr/{data => }/ui/BaseTestRobot.kt | 96 +++++++++--- .../ui/robots/AddItemScreenRobot.kt | 4 +- .../{data => }/ui/robots/FilterScreenRobot.kt | 4 +- .../h_mal/farmr/ui/robots/HomeScreenRobot.kt | 29 ++++ .../ui/robots/ViewItemScreenRobot.kt | 4 +- .../h_mal/farmr/ui/tests/ShiftTests.kt | 142 ++++++++++++++++++ .../h_mal/farmr/ui/utils/Constants.kt | 1 + .../h_mal/farmr/ui/utils/DataHelper.kt | 103 +++++++++++++ .../{data => }/ui/utils/EspressoHelper.kt | 2 +- .../farmr/{data => }/ui/utils/TestUtils.kt | 2 +- .../h_mal/farmr/base/BaseApplication.kt | 31 ++++ .../farmr/data/legacydb/ShiftsContract.kt | 3 +- .../farmr/data/prefs/PreferencesProvider.kt | 4 + .../h_mal/farmr/di/ShiftApplication.kt | 20 +-- .../appttude/h_mal/farmr/model/Sortable.kt | 4 + .../h_mal/farmr/ui/FragmentAddItem.kt | 7 +- .../appttude/h_mal/farmr/ui/FragmentMain.kt | 2 +- .../appttude/h_mal/farmr/ui/SplashScreen.kt | 8 +- 38 files changed, 631 insertions(+), 447 deletions(-) delete mode 100644 .idea/androidTestResultsUserPreferences.xml delete mode 100644 .idea/assetWizardSettings.xml delete mode 100644 .idea/caches/build_file_checksums.ser delete mode 100644 .idea/caches/gradle_models.ser delete mode 100644 .idea/compiler.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/jarRepositories.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt delete mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt delete mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt delete mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/BaseTest.kt (77%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/BaseTestRobot.kt (65%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/robots/AddItemScreenRobot.kt (92%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/robots/FilterScreenRobot.kt (90%) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/robots/ViewItemScreenRobot.kt (92%) create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt create mode 100644 app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/utils/EspressoHelper.kt (98%) rename app/src/androidTest/java/com/appttude/h_mal/farmr/{data => }/ui/utils/TestUtils.kt (94%) create mode 100644 app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt diff --git a/.circleci/config.yml b/.circleci/config.yml index 4175da6..342196f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,26 +1,130 @@ # Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/configuration-reference +# See: https://circleci.com/docs/2.0/configuration-reference +# For a detailed guide to building and testing on Android, read the docs: +# https://circleci.com/docs/2.0/language-android/ for more details. version: 2.1 -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/configuration-reference/#jobs -jobs: - say-hello: - # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/configuration-reference/#executor-job - docker: - - image: cimg/base:stable - # Add steps to the job - # See: https://circleci.com/docs/configuration-reference/#steps +# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. +# See: https://circleci.com/docs/2.0/orb-intro/ +orbs: + android: circleci/android@2.3.0 + +commands: + setup_repo: + description: checkout repo and android dependencies steps: - checkout - run: - name: "Say hello" - command: "echo Hello, World!" - -# Orchestrate jobs using workflows -# See: https://circleci.com/docs/configuration-reference/#workflows + name: Give gradle permissions + command: | + sudo chmod +x ./gradlew + - android/restore-gradle-cache + run_tests: + description: run tests for flavour specified + steps: + # The next step will run the unit tests + - run: + name: Run local unit tests + command: | + ./gradlew testDebugUnitTest + - android/save-gradle-cache + - store_artifacts: + path: app/build/reports + destination: reports + - store_test_results: + path: app/build/test-results + run_ui_tests: + description: run instrumentation and espresso tests + steps: + - android/start-emulator-and-run-tests: + post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest + test-command: ./gradlew connectedDebugAndroidTest --continue + system-image: system-images;android-26;google_apis;x86 + # store screenshots for failed ui tests + - when: + condition: on_fail + steps: + - store_artifacts: + path: app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected + destination: connected_android_test + # store test reports + - store_artifacts: + path: app/build/reports/androidTests/connected + destination: reports + - store_test_results: + path: app/build/outputs/androidTest-results/connected + deploy_to_play_store: + description: deploy to playstore + steps: + # The next step will run the unit tests + - android/decode-keystore: + keystore-location: "./app/keystore.jks" + - run: + name: Setup playstore key + command: | + echo "$GOOGLE_PLAY_KEY" > "google-play-key.json" + - run: + name: Run fastlane command to deploy to playstore + command: | + pwd + bundle exec fastlane deploy + - store_test_results: + path: fastlane/report.xml +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/2.0/configuration-reference/#jobs +jobs: + # Below is the definition of your job to build and test your app, you can rename and customize it as you want. + build-and-test: + # These next lines define the Android machine image executor. + # See: https://circleci.com/docs/2.0/executor-types/ + executor: + name: android/android-machine + tag: 2023.05.1 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + - setup_repo + - run_tests + run_instrumentation_test: + # These next lines define the Android machine image executor. + # See: https://circleci.com/docs/2.0/executor-types/ + executor: + name: android/android-machine + tag: 2023.05.1 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + - setup_repo + - run_ui_tests + deploy-to-playstore: + docker: + - image: cimg/android:2023.07-browsers + auth: + username: ${DOCKER_USERNAME} + password: ${DOCKER_PASSWORD} + steps: + - setup_repo + - deploy_to_play_store +# Invoke jobs via workflows +# See: https://circleci.com/docs/2.0/configuration-reference/#workflows workflows: - say-hello-workflow: + version: 2 + build-release: jobs: - - say-hello + - build-and-test: + context: appttude + - run_instrumentation_test: + context: appttude + filters: + branches: + only: + - master + - release + - deploy-to-playstore: + context: appttude + filters: + branches: + only: + - release + requires: + - build-and-test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 87e70e7..ae7f176 100644 --- a/.gitignore +++ b/.gitignore @@ -88,7 +88,7 @@ gen-external-apklibs .idea/uiDesigner.xml .idea/assetWizardSettings.xml .idea/gradle.xml -.idea/jarRepositorie +.idea/jarRepositories.xml # Gem/fastlane Gemfile.lock diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml deleted file mode 100644 index 263c04c..0000000 --- a/.idea/androidTestResultsUserPreferences.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index 0068d43..0000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index 3f6cd0f69fb12a71725208e92a594b5506cee1cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537 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≈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 - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 1431050..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index eb2873e..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 5ac489c..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8c565d6..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 86f0e87..3bba2f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ android { targetSdkVersion 31 versionCode 1 versionName "1.0" - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' vectorDrawables.useSupportLibrary = true } buildTypes { @@ -19,6 +19,7 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + useLibrary 'android.test.mock' } dependencies { diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt new file mode 100644 index 0000000..616c325 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt @@ -0,0 +1,38 @@ +package com.appttude.h_mal.farmr.application + +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.idling.CountingIdlingResource +import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.base.BaseApplication +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.model.Shift + +class TestAppClass : BaseApplication() { + private val idlingResources = CountingIdlingResource("Data_loader") + + lateinit var database: LegacyDatabase + lateinit var preferenceProvider: PreferenceProvider + + override fun onCreate() { + super.onCreate() + IdlingRegistry.getInstance().register(idlingResources) + } + + override fun createDatabase(): LegacyDatabase { + database = + LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver) + return database + } + + override fun createPrefs(): PreferenceProvider { + preferenceProvider = PreferenceProvider(this) + return preferenceProvider + } + + fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift) + fun addShiftsToDatabase(shifts: List) = shifts.forEach { addToDatabase(it) } + fun clearDatabase() = database.deleteAllShiftsInDatabase() + fun cleanPrefs() = preferenceProvider.clearPrefs() + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt new file mode 100644 index 0000000..ee712d4 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt @@ -0,0 +1,21 @@ +package com.appttude.h_mal.farmr.application + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner + +class TestRunner : AndroidJUnitRunner() { + @Throws( + InstantiationException::class, + IllegalAccessException::class, + ClassNotFoundException::class + ) + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + return super.newApplication(cl, TestAppClass::class.java.name, context) + } + +} diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt index 67a4c51..7a11af7 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt @@ -19,6 +19,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull +import org.junit.After import org.junit.Rule import org.junit.Test @@ -31,6 +32,11 @@ class ShiftProviderTest { private val contentResolver: ContentResolver get() = providerRule.resolver + @After + fun tearDown() { + contentResolver.delete(CONTENT_URI, null, null) + } + @Test fun insertEntry_queryEntry_assertEntry() { // Arrange diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt deleted file mode 100644 index 4f5c5e6..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/HomeScreenRobot.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.appttude.h_mal.farmr.data.ui.robots - -import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot - -fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } -class HomeScreenRobot : BaseTestRobot() { - - fun clickOnItem(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position) - fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text) - fun clickOnEdit(position: Int) = clickViewInRecycler(R.id.list_item_view, R.id.imageView) - fun clickFab() = clickButton(R.id.fab1) - fun clickOnInfo() = clickButton(R.id.action_favorite) -// fun clearFilter() = -// fun applySort() = - - -// fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location) -// fun refresh() = pullToRefresh(R.id.swipe_refresh) -// fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4) -// fun verifyUnableToRetrieve() { -// matchText(R.id.header_text, R.string.retrieve_warning) -// matchText(R.id.body_text, R.string.empty_retrieve_warning) -// } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt deleted file mode 100644 index 53997bd..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/tests/ShiftTests.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.appttude.h_mal.farmr.data.ui.tests - -import com.appttude.h_mal.farmr.data.ui.BaseTest -import com.appttude.h_mal.farmr.data.ui.robots.addScreen -import com.appttude.h_mal.farmr.data.ui.robots.homeScreen -import com.appttude.h_mal.farmr.data.ui.robots.viewScreen -import com.appttude.h_mal.farmr.model.ShiftType -import com.appttude.h_mal.farmr.ui.MainActivity -import org.junit.Test - -class ShiftTests: BaseTest(MainActivity::class.java) { - - // Add a shift successfully - @Test - fun test1() { - homeScreen { - clickFab() - } - addScreen { - setDescription("This is a description") - setDate(2023, 2, 11) - clickShiftType(ShiftType.HOURLY) - setTimeIn(12,0) - setTimeOut(14, 30) - setBreakTime(30) - setRateOfPay(10f) - assertDuration("2.0 hours") - assertTotalPay("£20.00") - submit() - } - homeScreen { - sc("This is a description") - } - } - - // Edit a shift successfully - @Test - fun test2() { - homeScreen { - clickOnItemWithText("Edit this shift") - } - addScreen { - setRateOfPay(20f) - assertDuration("2.0 hours") - assertTotalPay("£40.00") - submit() - } - homeScreen { - clickOnItemWithText("Edit this shift") - } - viewScreen { - matchDescription("Edit this shift") - matchDuration("2 Hours 0 minutes") - matchTotalPay("2.0 hours @ £20.00 per Hour\nEquals:£40.00") - } - } - - // filter the list with date from - @Test - fun test3() {} - - // filter the list with date to - @Test - fun test4() {} - - // Add a shift as piece rate - @Test - fun test5() {} - - // Validate the details screen - @Test - fun test6() {} - - // filter, sort, order and then reset - @Test - fun test7() {} -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt deleted file mode 100644 index e18ec2c..0000000 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/Constants.kt +++ /dev/null @@ -1 +0,0 @@ -package com.appttude.h_mal.farmr.data.ui.utils diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt similarity index 77% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt index 87e8ed5..0536254 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data.ui +package com.appttude.h_mal.farmr.ui import android.Manifest import android.app.Activity @@ -6,7 +6,9 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.view.View +import androidx.lifecycle.Lifecycle import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction @@ -15,13 +17,16 @@ import androidx.test.espresso.matcher.RootMatchers.withDecorView import androidx.test.espresso.matcher.ViewMatchers import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import com.appttude.h_mal.farmr.application.TestAppClass import com.appttude.h_mal.farmr.di.ShiftApplication +import com.appttude.h_mal.farmr.ui.utils.getShifts import kotlinx.coroutines.runBlocking import org.hamcrest.Matcher import org.hamcrest.Matchers import org.junit.After import org.junit.Before import org.junit.Rule +import org.kodein.di.android.kodein @Suppress("EmptyMethod") open class BaseTest( @@ -30,7 +35,7 @@ open class BaseTest( ) { lateinit var scenario: ActivityScenario - private lateinit var testApp: ShiftApplication + private lateinit var testApp: TestAppClass private lateinit var testActivity: Activity private lateinit var decorView: View @@ -38,7 +43,7 @@ open class BaseTest( var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) @Before - fun setUp() { + open fun setUp() { val startIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity) if (intentBundle != null) { @@ -46,13 +51,17 @@ open class BaseTest( } testApp = - InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ShiftApplication + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass + kodein(testApp) runBlocking { beforeLaunch() } scenario = ActivityScenario.launch(startIntent) scenario.onActivity { + testApp = + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass + onLaunch() decorView = it.window.decorView testActivity = it } @@ -67,6 +76,7 @@ open class BaseTest( } open fun beforeLaunch() {} + open fun onLaunch() {} open fun afterLaunch() {} open fun testFinished() {} @@ -88,4 +98,13 @@ open class BaseTest( waitFor(3500) } } + + fun navigateBack() = Espresso.pressBack() + + fun addRandomShifts() { + testApp.addShiftsToDatabase(getShifts()) + } + + fun clearDataBase() = testApp.clearDatabase() + fun clearPrefs() = testApp.cleanPrefs() } \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt similarity index 65% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt index d864acb..a85098f 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/BaseTestRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt @@ -1,12 +1,18 @@ -package com.appttude.h_mal.farmr.data.ui +package com.appttude.h_mal.farmr.ui import android.content.res.Resources +import android.view.View import android.widget.DatePicker import android.widget.TimePicker import androidx.annotation.StringRes +import androidx.appcompat.widget.AppCompatButton +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.espresso.Espresso.onData import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewInteraction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click @@ -14,11 +20,15 @@ import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.* -import com.appttude.h_mal.farmr.data.ui.utils.EspressoHelper.waitForView +import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anything import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.Matcher +import org.hamcrest.Matchers @SuppressWarnings("unused") open class BaseTestRobot { @@ -54,6 +64,17 @@ open class BaseTestRobot { .atPosition(position).perform(click()) } + fun clickOnMenuItem(menuId: Int) { + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context) + onView(withText(menuId)).perform(click()) + } + + fun clickDialogButton(text: String) { + onView(withText(text)).inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(click()); + } + fun scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { return matchView(recyclerId) .perform( @@ -88,14 +109,6 @@ open class BaseTestRobot { ) } - fun clickViewInRecycler(recyclerId: Int, text: String) { - matchView(recyclerId) - .perform( - // scrollTo will fail the test if no item matches. - RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click()) - ) - } - fun clickViewInRecycler(recyclerId: Int, resIdForString: Int) { matchView(recyclerId) .perform( @@ -107,23 +120,52 @@ open class BaseTestRobot { ) } - fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int) { + fun clickRecyclerAtPosition(recyclerId: Int, position: Int) { matchView(recyclerId) .perform( // scrollTo will fail the test if no item matches. RecyclerViewActions.scrollToPosition(position), - RecyclerViewActions.actionOnItemAtPosition(position, click()) + RecyclerViewActions.actionOnItemAtPosition(position, click()), + ) + } + + fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int, subViewId: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position), + RecyclerViewActions.actionOnItemAtPosition(position, object : ViewAction { + override fun getDescription(): String { + return "click on subview in RecyclerView at position: $position" + } + + override fun getConstraints(): Matcher { + return Matchers.allOf( + isAssignableFrom( + RecyclerView::class.java + ), isDisplayed() + ) + } + + override fun perform(uiController: UiController?, view: View?) { + view?.findViewById(subViewId)?.performClick() + } + + }), ) } fun clickOnRecyclerItemWithText(recyclerId: Int, text: String) { - scrollToRecyclerItem(recyclerId, text) - ?.perform( - // scrollTo will fail the test if no item matches. - RecyclerViewActions.actionOnItem( - withChild(withText(text)), click() - ) + matchView(recyclerId).perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(text)) + ), + RecyclerViewActions.actionOnItem( + hasDescendant(withText(text)), + click() ) + ) } fun swipeDown(resId: Int): ViewInteraction = @@ -144,9 +186,17 @@ open class BaseTestRobot { day ) ) + onView( + allOf( + withClassName(equalTo(AppCompatButton::class.java.name)), + withText("OK") + ) + ).perform( + click() + ) } - fun selectTextInSpinner(id: Int, text:String) { + fun selectTextInSpinner(id: Int, text: String) { clickButton(id) onView(withSpinnerText(text)).perform(click()) } @@ -157,5 +207,13 @@ open class BaseTestRobot { hours, minutes ) ) + onView( + allOf( + withClassName(equalTo(AppCompatButton::class.java.name)), + withText("OK") + ) + ).perform( + click() + ) } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt similarity index 92% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt index f2149b2..c28cfb1 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/AddItemScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt @@ -1,8 +1,8 @@ -package com.appttude.h_mal.farmr.data.ui.robots +package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() } class AddItemScreenRobot : BaseTestRobot() { diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt similarity index 90% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt index 22e36ed..5907ac6 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/FilterScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt @@ -1,7 +1,7 @@ -package com.appttude.h_mal.farmr.data.ui.robots +package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() } diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt new file mode 100644 index 0000000..4584b4a --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt @@ -0,0 +1,29 @@ +package com.appttude.h_mal.farmr.ui.robots + +import androidx.test.espresso.matcher.RootMatchers.isDialog +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.ui.BaseTestRobot + +fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } +class HomeScreenRobot : BaseTestRobot() { + + fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text) + fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition(R.id.list_item_view, position) + fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position, R.id.imageView) + fun clickFab() = clickButton(R.id.fab1) + fun clickOnInfoIcon() = clickButton(R.id.action_favorite) + fun clickFilterInMenu() = clickOnMenuItem(R.string.filter) + fun clickClearFilterInMenu() = clickOnMenuItem(R.string.clear) + fun clickSortInMenu() = clickOnMenuItem(R.string.sort) + + fun applySort(sortable: Sortable, order: Order = Order.ASCENDING) { + clickSortInMenu() + val label = sortable.label + clickDialogButton(label) + val orderLabel = order.label + clickDialogButton(orderLabel) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt similarity index 92% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt index 9585ec9..0dbcdad 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/robots/ViewItemScreenRobot.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt @@ -1,7 +1,7 @@ -package com.appttude.h_mal.farmr.data.ui.robots +package com.appttude.h_mal.farmr.ui.robots import com.appttude.h_mal.farmr.R -import com.appttude.h_mal.farmr.data.ui.BaseTestRobot +import com.appttude.h_mal.farmr.ui.BaseTestRobot import com.appttude.h_mal.farmr.model.ShiftType fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() } diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt new file mode 100644 index 0000000..e5f4a2f --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt @@ -0,0 +1,142 @@ +package com.appttude.h_mal.farmr.ui.tests + +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.ui.BaseTest +import com.appttude.h_mal.farmr.ui.MainActivity +import com.appttude.h_mal.farmr.ui.robots.addScreen +import com.appttude.h_mal.farmr.ui.robots.filterScreen +import com.appttude.h_mal.farmr.ui.robots.homeScreen +import com.appttude.h_mal.farmr.ui.robots.viewScreen +import com.appttude.h_mal.farmr.utils.ID +import org.junit.Test + +class ShiftTests : BaseTest(MainActivity::class.java) { + + override fun afterLaunch() { + super.afterLaunch() + addRandomShifts() + + // Content resolver hard to mock + // Dirty technique to have a populated list + homeScreen { + clickFab() + navigateBack() + } + } + + override fun testFinished() { + super.testFinished() + clearDataBase() + clearPrefs() + } + + // Add a shift successfully + @Test + fun openAddScreen_addNewShift_newShiftCreated() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.HOURLY) + setTimeIn(12, 0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(10f) + assertDuration("2.0 hours") + assertTotalPay("£20.00") + submit() + } + homeScreen { + clickOnItemWithText("This is a description") + } + } + + // Edit a shift successfully + @Test + fun test2() { + homeScreen { + clickOnEdit(0) + } + addScreen { + setDescription("Edited this shift") + setTimeIn(12, 0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(20f) + assertDuration("2.0 hours") + assertTotalPay("£40.00") + submit() + } + homeScreen { + clickOnItemWithText("Edited this shift") + } + viewScreen { + matchDescription("Edited this shift") + matchDuration("2 Hours 0 Minutes (+ 30 minutes break)") + matchTotalPay("2.0 Hours @ £20.00 per Hour\nEquals: £40.00") + } + } + + // filter the list with date from + @Test + fun test3() { + homeScreen { + applySort(Sortable.TYPE, Order.DESCENDING) + clickOnItemAtPosition(0) + viewScreen { + matchDescription("Day five") + matchShiftType(ShiftType.PIECE) + } + } + } + + // filter the list with date to + @Test + fun test4() { + homeScreen { + clickFilterInMenu() + } + filterScreen { + setDateIn(2023,8,3) + setDateOut(2023,8,6) + submit() + } + homeScreen { + clickOnItemAtPosition(0) + } + } + + // Add a shift as piece rate + @Test + fun test5() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.PIECE) + setRateOfPay(10f) + setUnits(1f) + assertTotalPay("£10.00") + submit() + } + homeScreen { + clickOnItemWithText("This is a description") + } + } + + // Validate the details screen + @Test + fun test6() { + } + + // filter, sort, order and then reset + @Test + fun test7() { + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt new file mode 100644 index 0000000..ad69acd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt @@ -0,0 +1 @@ +package com.appttude.h_mal.farmr.ui.utils diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt new file mode 100644 index 0000000..0e05bb6 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt @@ -0,0 +1,103 @@ +package com.appttude.h_mal.farmr.ui.utils + +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType + +fun getShifts() = listOf( + Shift( + ShiftType.HOURLY, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.HOURLY, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.PIECE, + "Day five", + "2023-08-05", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day six", + "2023-08-06", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day seven", + "2023-08-07", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day eight", + "2023-08-08", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ) +) \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt similarity index 98% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt index e008cfe..a2ebc7a 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/EspressoHelper.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data.ui.utils +package com.appttude.h_mal.farmr.ui.utils import android.os.SystemClock.sleep import android.view.View diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt similarity index 94% rename from app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt rename to app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt index 2a8c143..d398697 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ui/utils/TestUtils.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data.ui.utils +package com.appttude.h_mal.farmr.ui.utils import androidx.lifecycle.LiveData import androidx.lifecycle.Observer diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt new file mode 100644 index 0000000..055e5f3 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt @@ -0,0 +1,31 @@ +package com.appttude.h_mal.farmr.base + +import android.app.Application +import com.appttude.h_mal.farmr.data.RepositoryImpl +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.androidXModule +import org.kodein.di.generic.bind +import org.kodein.di.generic.instance +import org.kodein.di.generic.provider +import org.kodein.di.generic.singleton + +abstract class BaseApplication() : Application(), KodeinAware { + + // Kodein creation of modules to be retrieve within the app + override val kodein = Kodein.lazy { + import(androidXModule(this@BaseApplication)) + + bind() from singleton { createDatabase() } + bind() from singleton { createPrefs() } + bind() from singleton { RepositoryImpl(instance(), instance()) } + + bind() from provider { ApplicationViewModelFactory(instance()) } + } + + abstract fun createDatabase(): LegacyDatabase + abstract fun createPrefs(): PreferenceProvider +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt index 9fa0a96..c0195fe 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt @@ -3,12 +3,13 @@ package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentResolver import android.net.Uri import android.provider.BaseColumns +import com.appttude.h_mal.farmr.BuildConfig /** * Created by h_mal on 26/12/2017. */ object ShiftsContract { - const val CONTENT_AUTHORITY = "com.appttude.h_mal.farmr" + const val CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID val BASE_CONTENT_URI = Uri.parse("content://$CONTENT_AUTHORITY") const val PATH_SHIFTS = "shifts" diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt index 403d5e3..11481c1 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt @@ -62,4 +62,8 @@ class PreferenceProvider( ) } + fun clearPrefs() { + preference.edit().clear().apply() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt index 0e1532f..3b0cd5f 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt @@ -1,11 +1,10 @@ package com.appttude.h_mal.farmr.di -import android.app.Application +import com.appttude.h_mal.farmr.base.BaseApplication import com.appttude.h_mal.farmr.data.RepositoryImpl import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory -import com.appttude.h_mal.farmr.viewmodel.MainViewModel import org.kodein.di.Kodein import org.kodein.di.KodeinAware import org.kodein.di.android.x.androidXModule @@ -14,15 +13,12 @@ import org.kodein.di.generic.instance import org.kodein.di.generic.provider import org.kodein.di.generic.singleton -class ShiftApplication: Application(), KodeinAware { - // Kodein creation of modules to be retrieve within the app - override val kodein = Kodein.lazy { - import(androidXModule(this@ShiftApplication)) +class ShiftApplication: BaseApplication() { - bind() from singleton { LegacyDatabase(contentResolver) } - bind() from singleton { PreferenceProvider(this@ShiftApplication) } - bind() from singleton { RepositoryImpl(instance(), instance()) } - - bind() from provider { ApplicationViewModelFactory(instance()) } + override fun createDatabase(): LegacyDatabase { + return LegacyDatabase(contentResolver) } -} \ No newline at end of file + + override fun createPrefs() = PreferenceProvider(this) +} + diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt index f027a92..609fd20 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt @@ -11,5 +11,9 @@ enum class Sortable(val label: String) { companion object { val entries = Sortable.values() + + fun getEnumByType(label: String): Sortable { + return Sortable.values().first { it.label == label } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt index c0a09b9..889198e 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -18,6 +18,7 @@ import com.appttude.h_mal.farmr.model.Success import com.appttude.h_mal.farmr.utils.ID import com.appttude.h_mal.farmr.utils.createDialog import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString import com.appttude.h_mal.farmr.utils.formatToTwoDpString import com.appttude.h_mal.farmr.utils.hide import com.appttude.h_mal.farmr.utils.popBackStack @@ -158,8 +159,8 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ mUnits = units } } - mPayRateEditText.setText(rateOfPay.formatToTwoDpString()) - mTotalPayTextView.text = totalPay.formatToTwoDpString() + mPayRateEditText.setText(rateOfPay.formatAsCurrencyString()) + mTotalPayTextView.text = totalPay.formatAsCurrencyString() calculateTotalPay() } @@ -267,7 +268,7 @@ class FragmentAddItem : BaseFragment(R.layout.fragment_add_ (mUnits ?: 0f) * mPayRate } } - mTotalPayTextView.text = total.formatToTwoDpString() + mTotalPayTextView.text = total.formatAsCurrencyString() } } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt index e0f1cbd..09592b9 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -155,7 +155,7 @@ class FragmentMain : BaseFragment(R.layout.fragment_main), BackPr .setSingleChoiceItems( groupName, checkedItem - ) { p0, p1 -> sort = Sortable.valueOf(groupName[p1]) } + ) { p0, p1 -> sort = Sortable.getEnumByType(groupName[p1]) } .setPositiveButton("Ascending") { dialog, id -> viewModel.setSortAndOrder(sort) dialog.dismiss() diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt index 5310bdb..087320e 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Handler +import android.os.Looper import android.view.View import android.widget.RelativeLayout import androidx.core.app.ActivityOptionsCompat @@ -16,8 +17,7 @@ class SplashScreen : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_splash) - val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.hyperspace_jump, android.R.anim.fade_out).toBundle() - val relativeLayout = findViewById(R.id.splash_layout) as RelativeLayout + val i = Intent(this@SplashScreen, MainActivity::class.java) i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK) Handler().postDelayed({ @@ -27,11 +27,11 @@ class SplashScreen : Activity() { startActivity(i) overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) // finish(); - }, SPLASH_TIME_OUT.toLong()) + }, SPLASH_TIME_OUT) } companion object { // Splash screen timer - private const val SPLASH_TIME_OUT = 2000 + const val SPLASH_TIME_OUT: Long = 2000 } } \ No newline at end of file