mirror of
https://github.com/hmalik144/Farmr.git
synced 2026-03-17 23:15:55 +00:00
- mid commit
This commit is contained in:
@@ -44,7 +44,7 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||||
/ * mockito and livedata testing * /
|
/ * mockito and livedata testing * /
|
||||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
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 * /
|
/ * MockK * /
|
||||||
def mockk_ver = "1.10.5"
|
def mockk_ver = "1.10.5"
|
||||||
testImplementation "io.mockk:mockk:$mockk_ver"
|
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
|
package com.appttude.h_mal.farmr.base
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.ViewModelLazy
|
|
||||||
import com.appttude.h_mal.farmr.utils.displayToast
|
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 {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
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 } )
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import android.view.View
|
|||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.createViewModelLazy
|
import androidx.fragment.app.createViewModelLazy
|
||||||
|
import androidx.lifecycle.ViewModelLazy
|
||||||
import com.appttude.h_mal.farmr.model.ViewState
|
import com.appttude.h_mal.farmr.model.ViewState
|
||||||
import com.appttude.h_mal.farmr.utils.getGenericClassAt
|
import com.appttude.h_mal.farmr.utils.getGenericClassAt
|
||||||
import com.appttude.h_mal.farmr.utils.popBackStack
|
import com.appttude.h_mal.farmr.utils.popBackStack
|
||||||
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
|
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
|
||||||
import org.kodein.di.KodeinAware
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.kodein
|
||||||
import org.kodein.di.android.x.kodein
|
import org.kodein.di.android.x.kodein
|
||||||
import org.kodein.di.generic.instance
|
import org.kodein.di.generic.instance
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
@@ -21,14 +23,13 @@ abstract class BaseFragment<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
|
|||||||
override val kodein by kodein()
|
override val kodein by kodein()
|
||||||
private val factory by instance<ApplicationViewModelFactory>()
|
private val factory by instance<ApplicationViewModelFactory>()
|
||||||
|
|
||||||
val viewModel: V by getActivityViewModel()
|
val viewModel: V by getViewModel()
|
||||||
|
|
||||||
private fun getActivityViewModel() = createViewModelLazy<V>(
|
private fun getViewModel(): Lazy<V> =
|
||||||
getGenericClassAt(0),
|
ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore },
|
||||||
{ requireActivity().viewModelStore },
|
factoryProducer = { factory } )
|
||||||
{ factory })
|
|
||||||
|
|
||||||
var mActivity: BaseActivity<*>? = null
|
var mActivity: BaseActivity? = null
|
||||||
|
|
||||||
private var shortAnimationDuration by Delegates.notNull<Int>()
|
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?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
mActivity = requireActivity() as BaseActivity<*>
|
mActivity = requireActivity() as BaseActivity
|
||||||
configureObserver()
|
configureObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ abstract class BaseFragment<V : BaseViewModel>(@LayoutRes contentLayoutId: Int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setTitle(title: String) {
|
fun setTitle(title: String) {
|
||||||
(requireActivity() as BaseActivity<*>).setTitleInActionBar(title)
|
(requireActivity() as BaseActivity).setTitleInActionBar(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun popBackStack() = mActivity?.popBackStack()
|
fun popBackStack() = mActivity?.popBackStack()
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ const val SORT = "SORT"
|
|||||||
const val ORDER = "ORDER"
|
const val ORDER = "ORDER"
|
||||||
|
|
||||||
const val DESCRIPTION = "DESCRIPTION"
|
const val DESCRIPTION = "DESCRIPTION"
|
||||||
const val TIME_IN = "TIME_IN"
|
const val DATE_IN = "TIME_IN"
|
||||||
const val TIME_OUT = "TIME_OUT"
|
const val DATE_OUT = "TIME_OUT"
|
||||||
const val TYPE = "TYPE"
|
const val TYPE = "TYPE"
|
||||||
|
|
||||||
class PreferenceProvider(
|
class PreferenceProvider(
|
||||||
@@ -47,8 +47,8 @@ class PreferenceProvider(
|
|||||||
) {
|
) {
|
||||||
preference.edit()
|
preference.edit()
|
||||||
.putString(DESCRIPTION, description)
|
.putString(DESCRIPTION, description)
|
||||||
.putString(TIME_IN, timeIn)
|
.putString(DATE_IN, timeIn)
|
||||||
.putString(TIME_OUT, timeOut)
|
.putString(DATE_OUT, timeOut)
|
||||||
.putString(TYPE, type)
|
.putString(TYPE, type)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
@@ -56,8 +56,8 @@ class PreferenceProvider(
|
|||||||
fun getFilteringDetails(): Map<String, String?> {
|
fun getFilteringDetails(): Map<String, String?> {
|
||||||
return mapOf(
|
return mapOf(
|
||||||
Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)),
|
Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)),
|
||||||
Pair(TIME_IN, preference.getString(TIME_IN, null)),
|
Pair(DATE_IN, preference.getString(DATE_IN, null)),
|
||||||
Pair(TIME_OUT, preference.getString(TIME_OUT, null)),
|
Pair(DATE_OUT, preference.getString(DATE_OUT, null)),
|
||||||
Pair(TYPE, preference.getString(TYPE, 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.ShiftType
|
||||||
import com.appttude.h_mal.farmr.model.Success
|
import com.appttude.h_mal.farmr.model.Success
|
||||||
import com.appttude.h_mal.farmr.utils.setDatePicker
|
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 {
|
AdapterView.OnItemSelectedListener, OnClickListener {
|
||||||
private val spinnerList: Array<String> =
|
private val spinnerList: Array<String> =
|
||||||
arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type)
|
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 dateToET: EditText
|
||||||
private lateinit var typeSpinner: Spinner
|
private lateinit var typeSpinner: Spinner
|
||||||
|
|
||||||
private var description: String? = null
|
private var descriptionString: String? = null
|
||||||
private var dateFrom: String? = null
|
private var dateFromString: String? = null
|
||||||
private var dateTo: String? = null
|
private var dateToString: String? = null
|
||||||
private var type: String? = null
|
private var typeString: String? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -47,21 +47,29 @@ class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_
|
|||||||
|
|
||||||
val filterDetails = viewModel.getFiltrationDetails()
|
val filterDetails = viewModel.getFiltrationDetails()
|
||||||
|
|
||||||
filterDetails.let {
|
filterDetails.run {
|
||||||
LocationET.setText(it.description)
|
description?.let {
|
||||||
dateFromET.setText(it.dateFrom)
|
LocationET.setText(it)
|
||||||
dateToET.setText(it.dateTo)
|
descriptionString = it
|
||||||
|
}
|
||||||
it.type?.let { t ->
|
dateFrom?.let {
|
||||||
val spinnerPosition: Int = adapter.getPosition(t)
|
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)
|
typeSpinner.setSelection(spinnerPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationET.doAfterTextChanged { description = it.toString() }
|
LocationET.doAfterTextChanged { descriptionString = it.toString() }
|
||||||
dateFromET.setDatePicker { dateFrom = it }
|
dateFromET.setDatePicker { dateFromString = it }
|
||||||
dateToET.setDatePicker { dateTo = it }
|
dateToET.setDatePicker { dateToString = it }
|
||||||
typeSpinner.onItemSelectedListener = this
|
typeSpinner.onItemSelectedListener = this
|
||||||
|
|
||||||
submit.setOnClickListener(this)
|
submit.setOnClickListener(this)
|
||||||
@@ -73,7 +81,7 @@ class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_
|
|||||||
position: Int,
|
position: Int,
|
||||||
id: Long
|
id: Long
|
||||||
) {
|
) {
|
||||||
type = when (position) {
|
typeString = when (position) {
|
||||||
1 -> ShiftType.HOURLY.type
|
1 -> ShiftType.HOURLY.type
|
||||||
2 -> ShiftType.PIECE.type
|
2 -> ShiftType.PIECE.type
|
||||||
else -> return
|
else -> return
|
||||||
@@ -83,7 +91,7 @@ class FilterDataFragment : BaseFragment<MainViewModel>(R.layout.fragment_filter_
|
|||||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||||
|
|
||||||
private fun submitFiltrationDetails() {
|
private fun submitFiltrationDetails() {
|
||||||
viewModel.setFiltrationDetails(description, dateFrom, dateTo, type)
|
viewModel.applyFilters(descriptionString, dateFromString, dateToString, typeString)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(p0: View?) {
|
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.show
|
||||||
import com.appttude.h_mal.farmr.utils.validateField
|
import com.appttude.h_mal.farmr.utils.validateField
|
||||||
import com.appttude.h_mal.farmr.viewmodel.MainViewModel
|
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 {
|
RadioGroup.OnCheckedChangeListener, BackPressedListener {
|
||||||
|
|
||||||
private lateinit var mHourlyRadioButton: RadioButton
|
private lateinit var mHourlyRadioButton: RadioButton
|
||||||
@@ -262,7 +263,6 @@ class FragmentAddItem : BaseFragment<MainViewModel>(R.layout.fragment_add_item),
|
|||||||
StringBuilder().append(mDuration).append(" hours").toString()
|
StringBuilder().append(mDuration).append(" hours").toString()
|
||||||
mDuration!! * mPayRate
|
mDuration!! * mPayRate
|
||||||
}
|
}
|
||||||
|
|
||||||
ShiftType.PIECE -> {
|
ShiftType.PIECE -> {
|
||||||
(mUnits ?: 0f) * mPayRate
|
(mUnits ?: 0f) * mPayRate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
|
||||||
viewModel.refreshLiveData()
|
viewModel.refreshLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +111,7 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
R.id.clear_filter -> {
|
R.id.clear_filter -> {
|
||||||
viewModel.setFiltrationDetails(null, null, null, null)
|
viewModel.clearFilters()
|
||||||
return true
|
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.data.legacydb.ShiftObject
|
||||||
import com.appttude.h_mal.farmr.model.ShiftType
|
import com.appttude.h_mal.farmr.model.ShiftType
|
||||||
import com.appttude.h_mal.farmr.utils.CURRENCY
|
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.hide
|
||||||
import com.appttude.h_mal.farmr.utils.navigateToFragment
|
import com.appttude.h_mal.farmr.utils.navigateToFragment
|
||||||
import com.appttude.h_mal.farmr.utils.show
|
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 typeTV: TextView
|
||||||
private lateinit var descriptionTV: TextView
|
private lateinit var descriptionTV: TextView
|
||||||
private lateinit var dateTV: TextView
|
private lateinit var dateTV: TextView
|
||||||
@@ -52,17 +53,19 @@ class FurtherInfoFragment : BaseFragment<MainViewModel>(R.layout.fragment_futher
|
|||||||
hourlyDetailHolder = view.findViewById(R.id.details_hourly_details)
|
hourlyDetailHolder = view.findViewById(R.id.details_hourly_details)
|
||||||
unitsHolder = view.findViewById(R.id.details_units_holder)
|
unitsHolder = view.findViewById(R.id.details_units_holder)
|
||||||
|
|
||||||
val id = arguments!!.getLong(ID)
|
|
||||||
|
|
||||||
editButton.setOnClickListener {
|
editButton.setOnClickListener {
|
||||||
navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!)
|
navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
setupView(id)
|
viewModel.retrieveData(arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupView(id: Long) {
|
override fun onSuccess(data: Any?) {
|
||||||
viewModel.getCurrentShift(id)?.run {
|
super.onSuccess(data)
|
||||||
|
if (data is ShiftObject) data.setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ShiftObject.setupView() {
|
||||||
typeTV.text = type
|
typeTV.text = type
|
||||||
descriptionTV.text = description
|
descriptionTV.text = description
|
||||||
dateTV.text = date
|
dateTV.text = date
|
||||||
@@ -74,12 +77,12 @@ class FurtherInfoFragment : BaseFragment<MainViewModel>(R.layout.fragment_futher
|
|||||||
hourlyDetailHolder.show()
|
hourlyDetailHolder.show()
|
||||||
unitsHolder.hide()
|
unitsHolder.hide()
|
||||||
times.text = StringBuilder(timeIn).append("-").append(timeOut).toString()
|
times.text = StringBuilder(timeIn).append("-").append(timeOut).toString()
|
||||||
breakTV.text = StringBuilder(breakMins).append("mins").toString()
|
breakTV.text = StringBuilder().append(breakMins).append(" mins").toString()
|
||||||
durationTV.text = buildDurationSummary(this)
|
durationTV.text = viewModel.buildDurationSummary(this)
|
||||||
val paymentSummary =
|
val paymentSummary =
|
||||||
StringBuilder().append(duration).append(" Hours @ ").append(CURRENCY)
|
StringBuilder().append(duration).append(" Hours @ ")
|
||||||
.append(rateOfPay).append(" per Hour").append("\n")
|
.append(rateOfPay.formatAsCurrencyString()).append(" per Hour").append("\n")
|
||||||
.append("Equals: ").append(CURRENCY).append(totalPay)
|
.append("Equals: ").append(totalPay.formatAsCurrencyString())
|
||||||
totalPayTV.text = paymentSummary
|
totalPayTV.text = paymentSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,23 +92,11 @@ class FurtherInfoFragment : BaseFragment<MainViewModel>(R.layout.fragment_futher
|
|||||||
unitsTV.text = units.toString()
|
unitsTV.text = units.toString()
|
||||||
|
|
||||||
val paymentSummary =
|
val paymentSummary =
|
||||||
StringBuilder().append(units).append(" Units @ ").append(CURRENCY)
|
StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ")
|
||||||
.append(rateOfPay).append(" per Unit").append("\n")
|
.append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n")
|
||||||
.append("Equals: ").append(CURRENCY).append(totalPay)
|
.append("Equals: ").append(totalPay.formatAsCurrencyString())
|
||||||
totalPayTV.text = paymentSummary
|
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 com.appttude.h_mal.farmr.viewmodel.MainViewModel
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
class MainActivity : BaseActivity<MainViewModel>() {
|
class MainActivity : BaseActivity() {
|
||||||
private lateinit var toolbar: Toolbar
|
private lateinit var toolbar: Toolbar
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.appttude.h_mal.farmr.utils
|
package com.appttude.h_mal.farmr.utils
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.text.NumberFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
import java.util.Currency
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@@ -16,6 +18,14 @@ fun Float.formatToTwoDp(): Float {
|
|||||||
return formattedString.toFloat()
|
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 {
|
fun Float.formatToTwoDpString(): String {
|
||||||
return formatToTwoDp().toString()
|
return formatToTwoDp().toString()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ class ApplicationViewModelFactory(
|
|||||||
with(modelClass) {
|
with(modelClass) {
|
||||||
return when {
|
return when {
|
||||||
isAssignableFrom(MainViewModel::class.java) -> MainViewModel(repository)
|
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")
|
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
} as T
|
} 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
|
package com.appttude.h_mal.farmr.viewmodel
|
||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
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.Repository
|
||||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK
|
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_TYPE
|
||||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT
|
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.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.Order
|
||||||
import com.appttude.h_mal.farmr.model.Shift
|
|
||||||
import com.appttude.h_mal.farmr.model.ShiftType
|
import com.appttude.h_mal.farmr.model.ShiftType
|
||||||
import com.appttude.h_mal.farmr.model.Sortable
|
import com.appttude.h_mal.farmr.model.Sortable
|
||||||
import com.appttude.h_mal.farmr.model.Success
|
import com.appttude.h_mal.farmr.model.Success
|
||||||
import com.appttude.h_mal.farmr.utils.CURRENCY
|
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.convertDateString
|
||||||
import com.appttude.h_mal.farmr.utils.dateStringIsValid
|
import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
|
||||||
import com.appttude.h_mal.farmr.utils.formatToTwoDp
|
|
||||||
import com.appttude.h_mal.farmr.utils.getTimeString
|
|
||||||
import com.appttude.h_mal.farmr.utils.sortedByOrder
|
import com.appttude.h_mal.farmr.utils.sortedByOrder
|
||||||
import com.appttude.h_mal.farmr.utils.timeStringIsValid
|
|
||||||
import jxl.Workbook
|
import jxl.Workbook
|
||||||
import jxl.WorkbookSettings
|
import jxl.WorkbookSettings
|
||||||
import jxl.write.Label
|
import jxl.write.Label
|
||||||
@@ -46,26 +33,25 @@ import jxl.write.WritableWorkbook
|
|||||||
import jxl.write.WriteException
|
import jxl.write.WriteException
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Calendar
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val repository: Repository
|
private val repository: Repository
|
||||||
) : BaseViewModel() {
|
) : ShiftViewModel(repository) {
|
||||||
|
|
||||||
private val _shiftLiveData = MutableLiveData<List<ShiftObject>>()
|
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 mSort: Sortable = Sortable.ID
|
||||||
private var mOrder: Order = Order.ASCENDING
|
private var mOrder: Order = Order.ASCENDING
|
||||||
|
|
||||||
private var mFilterStore: FilterStore? = null
|
|
||||||
|
|
||||||
private val observer = Observer<List<ShiftObject>> {
|
private val observer = Observer<List<ShiftObject>> {
|
||||||
|
it?.let {
|
||||||
val result = it.applyFilters().sortList(mSort, mOrder)
|
val result = it.applyFilters().sortList(mSort, mOrder)
|
||||||
onSuccess(result)
|
onSuccess(result)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Load shifts into live data when view model has been instantiated
|
// Load shifts into live data when view model has been instantiated
|
||||||
@@ -148,8 +134,9 @@ class MainViewModel(
|
|||||||
var countOfTypeP = 0
|
var countOfTypeP = 0
|
||||||
var totalUnits = 0f
|
var totalUnits = 0f
|
||||||
var totalPay = 0f
|
var totalPay = 0f
|
||||||
val lines = _shiftLiveData.value?.size ?: 0
|
var lines = 0
|
||||||
_shiftLiveData.value?.forEach {
|
_shiftLiveData.value?.applyFilters()?.forEach {
|
||||||
|
lines += 1
|
||||||
totalDuration += it.duration
|
totalDuration += it.duration
|
||||||
when (ShiftType.getEnumByType(it.type)) {
|
when (ShiftType.getEnumByType(it.type)) {
|
||||||
ShiftType.HOURLY -> countOfTypeH += 1
|
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) {
|
fun deleteShift(id: Long) {
|
||||||
if (!repository.deleteSingleShiftFromDatabase(id)) {
|
if (!repository.deleteSingleShiftFromDatabase(id)) {
|
||||||
onError("Failed to delete shift")
|
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(
|
private fun buildInfoString(
|
||||||
totalDuration: Float,
|
totalDuration: Float,
|
||||||
countOfHourly: Int,
|
countOfHourly: Int,
|
||||||
@@ -488,61 +192,19 @@ class MainViewModel(
|
|||||||
stringBuilder.append("Total Units: ").append(totalUnits).append("\n")
|
stringBuilder.append("Total Units: ").append(totalUnits).append("\n")
|
||||||
}
|
}
|
||||||
if (totalPay != 0f) {
|
if (totalPay != 0f) {
|
||||||
stringBuilder.append("Total Pay: ").append(CURRENCY).append(totalPay).append("\n")
|
stringBuilder.append("Total Pay: ").append(totalPay.formatAsCurrencyString())
|
||||||
}
|
}
|
||||||
return stringBuilder.toString()
|
return stringBuilder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshLiveData() {
|
fun refreshLiveData() {
|
||||||
_shiftLiveData.postValue(repository.readShiftsFromDatabase())
|
repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun Boolean.validateField(failureCallback: () -> Unit) {
|
fun clearFilters() {
|
||||||
if (!this) failureCallback.invoke()
|
super.setFiltrationDetails(null, null, null, null)
|
||||||
}
|
onSuccess(Success("Filters have been cleared"))
|
||||||
|
|
||||||
/**
|
|
||||||
* Lambda function that will invoke onError(...) on failure
|
|
||||||
* but update live data when successful
|
|
||||||
*/
|
|
||||||
private inline fun doTry(operation: () -> Unit) {
|
|
||||||
try {
|
|
||||||
operation.invoke()
|
|
||||||
refreshLiveData()
|
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"))
|
|
||||||
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)
|
@RequiresPermission(WRITE_EXTERNAL_STORAGE)
|
||||||
@@ -574,7 +236,8 @@ class MainViewModel(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val sortAndOrder = getSortAndOrder()
|
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
|
var currentRow = 0
|
||||||
val cells = data.mapIndexed { index, shift ->
|
val cells = data.mapIndexed { index, shift ->
|
||||||
currentRow += 1
|
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