mirror of
https://github.com/hmalik144/Farmr.git
synced 2025-12-10 02:25:19 +00:00
- mid commit
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<A : Activity>(
|
||||
private val activity: Class<A>,
|
||||
private val intentBundle: Bundle? = null,
|
||||
) {
|
||||
|
||||
lateinit var scenario: ActivityScenario<A>
|
||||
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<View> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
|
||||
return matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollTo<VH>(
|
||||
hasDescendant(withText(text))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> scrollToRecyclerItem(
|
||||
recyclerId: Int,
|
||||
resIdForString: Int
|
||||
): ViewInteraction? {
|
||||
return matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollTo<VH>(
|
||||
hasDescendant(withText(resIdForString))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> scrollToRecyclerItemByPosition(
|
||||
recyclerId: Int,
|
||||
position: Int
|
||||
): ViewInteraction? {
|
||||
return matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollToPosition<VH>(position)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, text: String) {
|
||||
matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.actionOnItem<VH>(hasDescendant(withText(text)), click())
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, resIdForString: Int) {
|
||||
matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.actionOnItem<VH>(
|
||||
hasDescendant(withText(resIdForString)),
|
||||
click()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickViewInRecyclerAtPosition(recyclerId: Int, position: Int) {
|
||||
matchView(recyclerId)
|
||||
.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.scrollToPosition<VH>(position),
|
||||
RecyclerViewActions.actionOnItemAtPosition<VH>(position, click())
|
||||
)
|
||||
}
|
||||
|
||||
fun <VH : ViewHolder> clickOnRecyclerItemWithText(recyclerId: Int, text: String) {
|
||||
scrollToRecyclerItem<VH>(recyclerId, text)
|
||||
?.perform(
|
||||
// scrollTo will fail the test if no item matches.
|
||||
RecyclerViewActions.actionOnItem<VH>(
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
}
|
||||
@@ -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<CurrentViewHolder>(R.id.list_item_view, position)
|
||||
fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText<CurrentViewHolder>(R.id.list_item_view, text)
|
||||
fun clickOnEdit(position: Int) = clickViewInRecycler<CurrentViewHolder>(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)
|
||||
// }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>(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() {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.appttude.h_mal.farmr.data.ui.utils
|
||||
@@ -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<View>): ViewAction {
|
||||
|
||||
return object : ViewAction {
|
||||
|
||||
override fun getConstraints(): Matcher<View> = 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<View> = 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<View> {
|
||||
return object : BaseMatcher<View>() {
|
||||
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<View>,
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 <T> LiveData<T>.getOrAwaitValue(
|
||||
time: Long = 2,
|
||||
timeUnit: TimeUnit = TimeUnit.SECONDS
|
||||
): T {
|
||||
var data: T? = null
|
||||
val latch = CountDownLatch(1)
|
||||
val observer = object : Observer<T> {
|
||||
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
|
||||
}
|
||||
@@ -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<V : BaseViewModel> : AppCompatActivity(), KodeinAware {
|
||||
|
||||
override val kodein by kodein()
|
||||
private val factory by instance<ApplicationViewModelFactory>()
|
||||
|
||||
val viewModel: V by getViewModel()
|
||||
|
||||
private fun getViewModel(): Lazy<V> =
|
||||
ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore },
|
||||
factoryProducer = { factory } )
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
|
||||
override val kodein by kodein()
|
||||
private val factory by instance<ApplicationViewModelFactory>()
|
||||
|
||||
val viewModel: V by getActivityViewModel()
|
||||
val viewModel: V by getViewModel()
|
||||
|
||||
private fun getActivityViewModel() = createViewModelLazy<V>(
|
||||
getGenericClassAt(0),
|
||||
{ requireActivity().viewModelStore },
|
||||
{ factory })
|
||||
private fun getViewModel(): Lazy<V> =
|
||||
ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore },
|
||||
factoryProducer = { factory } )
|
||||
|
||||
var mActivity: BaseActivity<*>? = null
|
||||
var mActivity: BaseActivity? = null
|
||||
|
||||
private var shortAnimationDuration by Delegates.notNull<Int>()
|
||||
|
||||
@@ -39,7 +40,7 @@ abstract class BaseFragment<V : BaseViewModel>(@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<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
|
||||
}
|
||||
|
||||
fun setTitle(title: String) {
|
||||
(requireActivity() as BaseActivity<*>).setTitleInActionBar(title)
|
||||
(requireActivity() as BaseActivity).setTitleInActionBar(title)
|
||||
}
|
||||
|
||||
fun popBackStack() = mActivity?.popBackStack()
|
||||
|
||||
@@ -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<String, String?> {
|
||||
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))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<MainViewModel>(R.layout.fragment_filter_data),
|
||||
class FilterDataFragment : BaseFragment<FilterViewModel>(R.layout.fragment_filter_data),
|
||||
AdapterView.OnItemSelectedListener, OnClickListener {
|
||||
private val spinnerList: Array<String> =
|
||||
arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type)
|
||||
@@ -26,10 +26,10 @@ class FilterDataFragment : BaseFragment<MainViewModel>(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<MainViewModel>(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<MainViewModel>(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<MainViewModel>(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?) {
|
||||
|
||||
@@ -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<MainViewModel>(R.layout.fragment_add_item),
|
||||
class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_item),
|
||||
RadioGroup.OnCheckedChangeListener, BackPressedListener {
|
||||
|
||||
private lateinit var mHourlyRadioButton: RadioButton
|
||||
@@ -262,7 +263,6 @@ class FragmentAddItem : BaseFragment<MainViewModel>(R.layout.fragment_add_item),
|
||||
StringBuilder().append(mDuration).append(" hours").toString()
|
||||
mDuration!! * mPayRate
|
||||
}
|
||||
|
||||
ShiftType.PIECE -> {
|
||||
(mUnits ?: 0f) * mPayRate
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
viewModel.refreshLiveData()
|
||||
}
|
||||
|
||||
@@ -112,7 +111,7 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
|
||||
}
|
||||
|
||||
R.id.clear_filter -> {
|
||||
viewModel.setFiltrationDetails(null, null, null, null)
|
||||
viewModel.clearFilters()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MainViewModel>(R.layout.fragment_futher_info) {
|
||||
class FurtherInfoFragment : BaseFragment<InfoViewModel>(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<MainViewModel>(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()
|
||||
}
|
||||
}
|
||||
@@ -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<MainViewModel>() {
|
||||
class MainActivity : BaseActivity() {
|
||||
private lateinit var toolbar: Toolbar
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<List<ShiftObject>>()
|
||||
val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
|
||||
private val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
|
||||
|
||||
private var mSort: Sortable = Sortable.ID
|
||||
private var mOrder: Order = Order.ASCENDING
|
||||
|
||||
private var mFilterStore: FilterStore? = null
|
||||
|
||||
private val observer = Observer<List<ShiftObject>> {
|
||||
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
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ShiftObject>()
|
||||
|
||||
// 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<String, String?> =
|
||||
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
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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<V : ShiftViewModel> {
|
||||
@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
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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<SubmissionViewModel>() {
|
||||
|
||||
@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<Success>(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<Success>(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"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user