mirror of
https://github.com/hmalik144/Farmr.git
synced 2026-01-31 02:41:49 +00:00
Merge pull request #18 from hmalik144/testsuite_expansion
Testsuite expansion
This commit is contained in:
@@ -1,26 +1,130 @@
|
|||||||
# Use the latest 2.1 version of CircleCI pipeline process engine.
|
# Use the latest 2.1 version of CircleCI pipeline process engine.
|
||||||
# See: https://circleci.com/docs/configuration-reference
|
# See: https://circleci.com/docs/2.0/configuration-reference
|
||||||
|
# For a detailed guide to building and testing on Android, read the docs:
|
||||||
|
# https://circleci.com/docs/2.0/language-android/ for more details.
|
||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
# Define a job to be invoked later in a workflow.
|
# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
|
||||||
# See: https://circleci.com/docs/configuration-reference/#jobs
|
# See: https://circleci.com/docs/2.0/orb-intro/
|
||||||
jobs:
|
orbs:
|
||||||
say-hello:
|
android: circleci/android@2.3.0
|
||||||
# Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub.
|
|
||||||
# See: https://circleci.com/docs/configuration-reference/#executor-job
|
commands:
|
||||||
docker:
|
setup_repo:
|
||||||
- image: cimg/base:stable
|
description: checkout repo and android dependencies
|
||||||
# Add steps to the job
|
|
||||||
# See: https://circleci.com/docs/configuration-reference/#steps
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run:
|
- run:
|
||||||
name: "Say hello"
|
name: Give gradle permissions
|
||||||
command: "echo Hello, World!"
|
command: |
|
||||||
|
sudo chmod +x ./gradlew
|
||||||
# Orchestrate jobs using workflows
|
- android/restore-gradle-cache
|
||||||
# See: https://circleci.com/docs/configuration-reference/#workflows
|
run_tests:
|
||||||
|
description: run tests for flavour specified
|
||||||
|
steps:
|
||||||
|
# The next step will run the unit tests
|
||||||
|
- run:
|
||||||
|
name: Run local unit tests
|
||||||
|
command: |
|
||||||
|
./gradlew testDebugUnitTest
|
||||||
|
- android/save-gradle-cache
|
||||||
|
- store_artifacts:
|
||||||
|
path: app/build/reports
|
||||||
|
destination: reports
|
||||||
|
- store_test_results:
|
||||||
|
path: app/build/test-results
|
||||||
|
run_ui_tests:
|
||||||
|
description: run instrumentation and espresso tests
|
||||||
|
steps:
|
||||||
|
- android/start-emulator-and-run-tests:
|
||||||
|
post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest
|
||||||
|
test-command: ./gradlew connectedDebugAndroidTest --continue
|
||||||
|
system-image: system-images;android-26;google_apis;x86
|
||||||
|
# store screenshots for failed ui tests
|
||||||
|
- when:
|
||||||
|
condition: on_fail
|
||||||
|
steps:
|
||||||
|
- store_artifacts:
|
||||||
|
path: app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected
|
||||||
|
destination: connected_android_test
|
||||||
|
# store test reports
|
||||||
|
- store_artifacts:
|
||||||
|
path: app/build/reports/androidTests/connected
|
||||||
|
destination: reports
|
||||||
|
- store_test_results:
|
||||||
|
path: app/build/outputs/androidTest-results/connected
|
||||||
|
deploy_to_play_store:
|
||||||
|
description: deploy to playstore
|
||||||
|
steps:
|
||||||
|
# The next step will run the unit tests
|
||||||
|
- android/decode-keystore:
|
||||||
|
keystore-location: "./app/keystore.jks"
|
||||||
|
- run:
|
||||||
|
name: Setup playstore key
|
||||||
|
command: |
|
||||||
|
echo "$GOOGLE_PLAY_KEY" > "google-play-key.json"
|
||||||
|
- run:
|
||||||
|
name: Run fastlane command to deploy to playstore
|
||||||
|
command: |
|
||||||
|
pwd
|
||||||
|
bundle exec fastlane deploy
|
||||||
|
- store_test_results:
|
||||||
|
path: fastlane/report.xml
|
||||||
|
# Define a job to be invoked later in a workflow.
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
|
||||||
|
jobs:
|
||||||
|
# Below is the definition of your job to build and test your app, you can rename and customize it as you want.
|
||||||
|
build-and-test:
|
||||||
|
# These next lines define the Android machine image executor.
|
||||||
|
# See: https://circleci.com/docs/2.0/executor-types/
|
||||||
|
executor:
|
||||||
|
name: android/android-machine
|
||||||
|
tag: 2023.05.1
|
||||||
|
# Add steps to the job
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
|
||||||
|
steps:
|
||||||
|
- setup_repo
|
||||||
|
- run_tests
|
||||||
|
run_instrumentation_test:
|
||||||
|
# These next lines define the Android machine image executor.
|
||||||
|
# See: https://circleci.com/docs/2.0/executor-types/
|
||||||
|
executor:
|
||||||
|
name: android/android-machine
|
||||||
|
tag: 2023.05.1
|
||||||
|
# Add steps to the job
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
|
||||||
|
steps:
|
||||||
|
- setup_repo
|
||||||
|
- run_ui_tests
|
||||||
|
deploy-to-playstore:
|
||||||
|
docker:
|
||||||
|
- image: cimg/android:2023.07-browsers
|
||||||
|
auth:
|
||||||
|
username: ${DOCKER_USERNAME}
|
||||||
|
password: ${DOCKER_PASSWORD}
|
||||||
|
steps:
|
||||||
|
- setup_repo
|
||||||
|
- deploy_to_play_store
|
||||||
|
# Invoke jobs via workflows
|
||||||
|
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
|
||||||
workflows:
|
workflows:
|
||||||
say-hello-workflow:
|
version: 2
|
||||||
|
build-release:
|
||||||
jobs:
|
jobs:
|
||||||
- say-hello
|
- build-and-test:
|
||||||
|
context: appttude
|
||||||
|
- run_instrumentation_test:
|
||||||
|
context: appttude
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- release
|
||||||
|
- deploy-to-playstore:
|
||||||
|
context: appttude
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- release
|
||||||
|
requires:
|
||||||
|
- build-and-test
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -88,7 +88,7 @@ gen-external-apklibs
|
|||||||
.idea/uiDesigner.xml
|
.idea/uiDesigner.xml
|
||||||
.idea/assetWizardSettings.xml
|
.idea/assetWizardSettings.xml
|
||||||
.idea/gradle.xml
|
.idea/gradle.xml
|
||||||
.idea/jarRepositorie
|
.idea/jarRepositories.xml
|
||||||
|
|
||||||
# Gem/fastlane
|
# Gem/fastlane
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
|
|||||||
22
.idea/androidTestResultsUserPreferences.xml
generated
22
.idea/androidTestResultsUserPreferences.xml
generated
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AndroidTestResultsUserPreferences">
|
|
||||||
<option name="androidTestResultsTableState">
|
|
||||||
<map>
|
|
||||||
<entry key="1283002349">
|
|
||||||
<value>
|
|
||||||
<AndroidTestResultsTableState>
|
|
||||||
<option name="preferredColumnWidths">
|
|
||||||
<map>
|
|
||||||
<entry key="Duration" value="90" />
|
|
||||||
<entry key="Pixel_6_Pro_API_31" value="120" />
|
|
||||||
<entry key="Tests" value="360" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</AndroidTestResultsTableState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
127
.idea/assetWizardSettings.xml
generated
127
.idea/assetWizardSettings.xml
generated
@@ -1,127 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="WizardSettings">
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="imageWizard">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="imageAssetPanel">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="actionbar">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="clipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
<entry key="opacityPercent" value="80" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="theme" value="HOLO_DARK" />
|
|
||||||
<entry key="themeColor" value="ffffff" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="launcher">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="foregroundImage">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
<entry key="scalingPercent" value="69" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="foregroundImage" value="C:\Users\h_mal\Desktop\Farmr\farmicon.png" />
|
|
||||||
<entry key="outputName" value="ic_launcher_release" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="launcherLegacy">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="clipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
<entry key="notification">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="children">
|
|
||||||
<map>
|
|
||||||
<entry key="clipArt">
|
|
||||||
<value>
|
|
||||||
<PersistentState>
|
|
||||||
<option name="values">
|
|
||||||
<map>
|
|
||||||
<entry key="color" value="000000" />
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</PersistentState>
|
|
||||||
</value>
|
|
||||||
</entry>
|
|
||||||
</map>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
BIN
.idea/caches/build_file_checksums.ser
generated
BIN
.idea/caches/build_file_checksums.ser
generated
Binary file not shown.
BIN
.idea/caches/gradle_models.ser
generated
BIN
.idea/caches/gradle_models.ser
generated
Binary file not shown.
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="17" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
21
.idea/gradle.xml
generated
21
.idea/gradle.xml
generated
@@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
|
||||||
<component name="GradleSettings">
|
|
||||||
<option name="linkedExternalProjectsSettings">
|
|
||||||
<GradleProjectSettings>
|
|
||||||
<option name="delegatedBuild" value="false" />
|
|
||||||
<option name="testRunner" value="GRADLE" />
|
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
|
||||||
<option name="gradleJvm" value="jbr-17" />
|
|
||||||
<option name="modules">
|
|
||||||
<set>
|
|
||||||
<option value="$PROJECT_DIR$" />
|
|
||||||
<option value="$PROJECT_DIR$/app" />
|
|
||||||
</set>
|
|
||||||
</option>
|
|
||||||
</GradleProjectSettings>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
30
.idea/jarRepositories.xml
generated
30
.idea/jarRepositories.xml
generated
@@ -1,30 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RemoteRepositoriesConfiguration">
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="central" />
|
|
||||||
<option name="name" value="Maven Central repository" />
|
|
||||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="jboss.community" />
|
|
||||||
<option name="name" value="JBoss Community repository" />
|
|
||||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="BintrayJCenter" />
|
|
||||||
<option name="name" value="BintrayJCenter" />
|
|
||||||
<option name="url" value="https://jcenter.bintray.com/" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="maven" />
|
|
||||||
<option name="name" value="maven" />
|
|
||||||
<option name="url" value="https://jitpack.io" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="Google" />
|
|
||||||
<option name="name" value="Google" />
|
|
||||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
|
||||||
</remote-repository>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
47
.idea/misc.xml
generated
47
.idea/misc.xml
generated
@@ -1,47 +0,0 @@
|
|||||||
<project version="4">
|
|
||||||
<component name="NullableNotNullManager">
|
|
||||||
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
|
|
||||||
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
|
|
||||||
<option name="myNullables">
|
|
||||||
<value>
|
|
||||||
<list size="12">
|
|
||||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
|
||||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
|
||||||
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
|
||||||
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
|
||||||
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
|
||||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
|
||||||
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
|
|
||||||
<item index="7" class="java.lang.String" itemvalue="android.annotation.Nullable" />
|
|
||||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
|
||||||
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
|
||||||
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
|
||||||
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
<option name="myNotNulls">
|
|
||||||
<value>
|
|
||||||
<list size="11">
|
|
||||||
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
|
||||||
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
|
||||||
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
|
||||||
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
|
||||||
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
|
||||||
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
|
||||||
<item index="6" class="java.lang.String" itemvalue="android.annotation.NonNull" />
|
|
||||||
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
|
||||||
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
|
||||||
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
|
||||||
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
|
|
||||||
</list>
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectType">
|
|
||||||
<option name="id" value="Android" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
12
.idea/modules.xml
generated
12
.idea/modules.xml
generated
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Farmr.iml" filepath="$PROJECT_DIR$/.idea/modules/Farmr.iml" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.iml" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.androidTest.iml" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.main.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.main.iml" />
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.unitTest.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -10,7 +10,7 @@ android {
|
|||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner'
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -19,6 +19,7 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
useLibrary 'android.test.mock'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -34,9 +35,33 @@ dependencies {
|
|||||||
implementation 'androidx.activity:activity-ktx:1.4.0'
|
implementation 'androidx.activity:activity-ktx:1.4.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||||
implementation 'androidx.preference:preference:1.2.1'
|
implementation 'androidx.preference:preference:1.2.1'
|
||||||
testImplementation 'junit:junit:4.12'
|
implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2'
|
||||||
|
/ * Unit testing * /
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||||
|
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
androidTestImplementation 'androidx.test:core-ktx:1.4.0'
|
||||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||||
|
/ * mockito and livedata testing * /
|
||||||
|
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||||
|
testImplementation 'androidx.arch.core:core-testing:2.1.0'
|
||||||
|
/ * MockK * /
|
||||||
|
def mockk_ver = "1.10.5"
|
||||||
|
testImplementation "io.mockk:mockk:$mockk_ver"
|
||||||
|
androidTestImplementation "io.mockk:mockk-android:$mockk_ver"
|
||||||
|
/ * Android Espresso * /
|
||||||
|
def testJunitVersion = "1.1.5"
|
||||||
|
def testRunnerVersion = "1.5.2"
|
||||||
|
def espressoVersion = "3.5.1"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$testJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion"
|
||||||
|
implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
|
||||||
|
androidTestImplementation "androidx.test:runner:$testRunnerVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
|
||||||
|
androidTestImplementation "org.hamcrest:hamcrest:2.2"
|
||||||
/ * Room database * /
|
/ * Room database * /
|
||||||
def room_version = "2.4.3"
|
def room_version = "2.4.3"
|
||||||
implementation "androidx.room:room-runtime:$room_version"
|
implementation "androidx.room:room-runtime:$room_version"
|
||||||
@@ -48,5 +73,4 @@ dependencies {
|
|||||||
implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
|
implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version"
|
||||||
/ * jxl * /
|
/ * jxl * /
|
||||||
implementation 'net.sourceforge.jexcelapi:jxl:2.6.12'
|
implementation 'net.sourceforge.jexcelapi:jxl:2.6.12'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.appttude.h_mal.farmr.application
|
||||||
|
|
||||||
|
import androidx.test.espresso.IdlingRegistry
|
||||||
|
import androidx.test.espresso.idling.CountingIdlingResource
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.appttude.h_mal.farmr.base.BaseApplication
|
||||||
|
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||||
|
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||||
|
import com.appttude.h_mal.farmr.model.Shift
|
||||||
|
|
||||||
|
class TestAppClass : BaseApplication() {
|
||||||
|
private val idlingResources = CountingIdlingResource("Data_loader")
|
||||||
|
|
||||||
|
lateinit var database: LegacyDatabase
|
||||||
|
lateinit var preferenceProvider: PreferenceProvider
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
IdlingRegistry.getInstance().register(idlingResources)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDatabase(): LegacyDatabase {
|
||||||
|
database =
|
||||||
|
LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver)
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPrefs(): PreferenceProvider {
|
||||||
|
preferenceProvider = PreferenceProvider(this)
|
||||||
|
return preferenceProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift)
|
||||||
|
fun addShiftsToDatabase(shifts: List<Shift>) = shifts.forEach { addToDatabase(it) }
|
||||||
|
fun clearDatabase() = database.deleteAllShiftsInDatabase()
|
||||||
|
fun cleanPrefs() = preferenceProvider.clearPrefs()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.appttude.h_mal.farmr.application
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
|
|
||||||
|
class TestRunner : AndroidJUnitRunner() {
|
||||||
|
@Throws(
|
||||||
|
InstantiationException::class,
|
||||||
|
IllegalAccessException::class,
|
||||||
|
ClassNotFoundException::class
|
||||||
|
)
|
||||||
|
override fun newApplication(
|
||||||
|
cl: ClassLoader?,
|
||||||
|
className: String?,
|
||||||
|
context: Context?
|
||||||
|
): Application {
|
||||||
|
return super.newApplication(cl, TestAppClass::class.java.name, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
|
|||||||
import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider
|
import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
import junit.framework.TestCase.assertNull
|
import junit.framework.TestCase.assertNull
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -31,6 +32,11 @@ class ShiftProviderTest {
|
|||||||
private val contentResolver: ContentResolver
|
private val contentResolver: ContentResolver
|
||||||
get() = providerRule.resolver
|
get() = providerRule.resolver
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
contentResolver.delete(CONTENT_URI, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun insertEntry_queryEntry_assertEntry() {
|
fun insertEntry_queryEntry_assertEntry() {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.appttude.h_mal.farmr.data.legacydb
|
||||||
|
|
||||||
|
import androidx.test.rule.provider.ProviderTestRule
|
||||||
|
import com.appttude.h_mal.farmr.model.Shift
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LegacyDatabaseTest {
|
||||||
|
@get:Rule
|
||||||
|
val providerRule: ProviderTestRule = ProviderTestRule
|
||||||
|
.Builder(ShiftProvider::class.java, ShiftsContract.CONTENT_AUTHORITY)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private lateinit var database: LegacyDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
database = LegacyDatabase(providerRule.resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
database.deleteAllShiftsInDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertShift_readShift_successfulRead() {
|
||||||
|
// Arrange
|
||||||
|
val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
database.insertShiftDataIntoDatabase(shift)
|
||||||
|
val retrievedShift = database.readShiftsFromDatabase()?.first()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(retrievedShift?.description, shift.description)
|
||||||
|
assertEquals(retrievedShift?.date, shift.date)
|
||||||
|
assertEquals(retrievedShift?.units, shift.units)
|
||||||
|
assertEquals(retrievedShift?.rateOfPay, shift.rateOfPay)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertShift_updateShift_successfulRead() {
|
||||||
|
// Arrange
|
||||||
|
val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f)
|
||||||
|
val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
database.insertShiftDataIntoDatabase(shift)
|
||||||
|
val id = database.readShiftsFromDatabase()?.first()!!.id
|
||||||
|
database.updateShiftDataIntoDatabase(
|
||||||
|
id = id,
|
||||||
|
typeString = updateShift.type.type,
|
||||||
|
descriptionString = updateShift.description,
|
||||||
|
dateString = updateShift.date,
|
||||||
|
timeInString = updateShift.timeIn ?: "",
|
||||||
|
timeOutString = updateShift.timeOut ?: "",
|
||||||
|
duration = updateShift.duration ?: 0f,
|
||||||
|
breaks = updateShift.breakMins ?: 0,
|
||||||
|
units = updateShift.units!!,
|
||||||
|
payRate = updateShift.rateOfPay,
|
||||||
|
totalPay = updateShift.totalPay
|
||||||
|
)
|
||||||
|
val retrievedShift = database.readSingleShiftWithId(id)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(retrievedShift?.description, updateShift.description)
|
||||||
|
assertEquals(retrievedShift?.date, updateShift.date)
|
||||||
|
assertEquals(retrievedShift?.units, updateShift.units)
|
||||||
|
assertEquals(retrievedShift?.rateOfPay, updateShift.rateOfPay)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun insertShift_deleteShift_databaseEmpty() {
|
||||||
|
// Arrange
|
||||||
|
val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f)
|
||||||
|
val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
database.insertShiftDataIntoDatabase(shift)
|
||||||
|
database.insertShiftDataIntoDatabase(updateShift)
|
||||||
|
val id = database.readShiftsFromDatabase()?.first()!!.id
|
||||||
|
database.deleteSingleShift(id)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertEquals(database.readShiftsFromDatabase()?.size, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
110
app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt
Normal file
110
app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package com.appttude.h_mal.farmr.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.lifecycle.Lifecycle
|
||||||
|
import androidx.test.core.app.ActivityScenario
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.espresso.Espresso
|
||||||
|
import androidx.test.espresso.UiController
|
||||||
|
import androidx.test.espresso.ViewAction
|
||||||
|
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.application.TestAppClass
|
||||||
|
import com.appttude.h_mal.farmr.di.ShiftApplication
|
||||||
|
import com.appttude.h_mal.farmr.ui.utils.getShifts
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.kodein.di.android.kodein
|
||||||
|
|
||||||
|
@Suppress("EmptyMethod")
|
||||||
|
open class BaseTest<A : Activity>(
|
||||||
|
private val activity: Class<A>,
|
||||||
|
private val intentBundle: Bundle? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
lateinit var scenario: ActivityScenario<A>
|
||||||
|
private lateinit var testApp: TestAppClass
|
||||||
|
private lateinit var testActivity: Activity
|
||||||
|
private lateinit var decorView: View
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
|
||||||
|
@Before
|
||||||
|
open fun setUp() {
|
||||||
|
val startIntent =
|
||||||
|
Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity)
|
||||||
|
if (intentBundle != null) {
|
||||||
|
startIntent.replaceExtras(intentBundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
testApp =
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
|
||||||
|
kodein(testApp)
|
||||||
|
runBlocking {
|
||||||
|
beforeLaunch()
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario = ActivityScenario.launch(startIntent)
|
||||||
|
scenario.onActivity {
|
||||||
|
testApp =
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
|
||||||
|
onLaunch()
|
||||||
|
decorView = it.window.decorView
|
||||||
|
testActivity = it
|
||||||
|
}
|
||||||
|
afterLaunch()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getActivity() = testActivity
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
testFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun beforeLaunch() {}
|
||||||
|
open fun onLaunch() {}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateBack() = Espresso.pressBack()
|
||||||
|
|
||||||
|
fun addRandomShifts() {
|
||||||
|
testApp.addShiftsToDatabase(getShifts())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearDataBase() = testApp.clearDatabase()
|
||||||
|
fun clearPrefs() = testApp.cleanPrefs()
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package com.appttude.h_mal.farmr.ui
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.DatePicker
|
||||||
|
import android.widget.TimePicker
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.widget.AppCompatButton
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||||
|
import androidx.test.espresso.Espresso.onData
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
|
||||||
|
import androidx.test.espresso.UiController
|
||||||
|
import androidx.test.espresso.ViewAction
|
||||||
|
import androidx.test.espresso.ViewInteraction
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
|
import androidx.test.espresso.action.ViewActions.swipeDown
|
||||||
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
|
import androidx.test.espresso.contrib.PickerActions
|
||||||
|
import androidx.test.espresso.contrib.RecyclerViewActions
|
||||||
|
import androidx.test.espresso.matcher.RootMatchers.isDialog
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView
|
||||||
|
import org.hamcrest.CoreMatchers.allOf
|
||||||
|
import org.hamcrest.CoreMatchers.anything
|
||||||
|
import org.hamcrest.CoreMatchers.equalTo
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
open class BaseTestRobot {
|
||||||
|
|
||||||
|
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 clickOnMenuItem(menuId: Int) {
|
||||||
|
openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context)
|
||||||
|
onView(withText(menuId)).perform(click())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clickDialogButton(text: String) {
|
||||||
|
onView(withText(text)).inRoot(isDialog())
|
||||||
|
.check(matches(isDisplayed()))
|
||||||
|
.perform(click());
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <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, resIdForString: Int) {
|
||||||
|
matchView(recyclerId)
|
||||||
|
.perform(
|
||||||
|
// scrollTo will fail the test if no item matches.
|
||||||
|
RecyclerViewActions.actionOnItem<VH>(
|
||||||
|
hasDescendant(withText(resIdForString)),
|
||||||
|
click()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <VH : ViewHolder> clickRecyclerAtPosition(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> clickViewInRecyclerAtPosition(recyclerId: Int, position: Int, subViewId: Int) {
|
||||||
|
matchView(recyclerId)
|
||||||
|
.perform(
|
||||||
|
// scrollTo will fail the test if no item matches.
|
||||||
|
RecyclerViewActions.scrollToPosition<VH>(position),
|
||||||
|
RecyclerViewActions.actionOnItemAtPosition<VH>(position, object : ViewAction {
|
||||||
|
override fun getDescription(): String {
|
||||||
|
return "click on subview in RecyclerView at position: $position"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConstraints(): Matcher<View> {
|
||||||
|
return Matchers.allOf(
|
||||||
|
isAssignableFrom(
|
||||||
|
RecyclerView::class.java
|
||||||
|
), isDisplayed()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun perform(uiController: UiController?, view: View?) {
|
||||||
|
view?.findViewById<View>(subViewId)?.performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <VH : ViewHolder> clickOnRecyclerItemWithText(recyclerId: Int, text: String) {
|
||||||
|
matchView(recyclerId).perform(
|
||||||
|
// scrollTo will fail the test if no item matches.
|
||||||
|
RecyclerViewActions.scrollTo<VH>(
|
||||||
|
hasDescendant(withText(text))
|
||||||
|
),
|
||||||
|
RecyclerViewActions.actionOnItem<VH>(
|
||||||
|
hasDescendant(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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(
|
||||||
|
allOf(
|
||||||
|
withClassName(equalTo(AppCompatButton::class.java.name)),
|
||||||
|
withText("OK")
|
||||||
|
)
|
||||||
|
).perform(
|
||||||
|
click()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
onView(
|
||||||
|
allOf(
|
||||||
|
withClassName(equalTo(AppCompatButton::class.java.name)),
|
||||||
|
withText("OK")
|
||||||
|
)
|
||||||
|
).perform(
|
||||||
|
click()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.appttude.h_mal.farmr.ui.robots
|
||||||
|
|
||||||
|
import com.appttude.h_mal.farmr.R
|
||||||
|
import com.appttude.h_mal.farmr.model.ShiftType
|
||||||
|
import com.appttude.h_mal.farmr.ui.BaseTestRobot
|
||||||
|
|
||||||
|
fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() }
|
||||||
|
class AddItemScreenRobot : BaseTestRobot() {
|
||||||
|
|
||||||
|
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.ui.robots
|
||||||
|
|
||||||
|
import com.appttude.h_mal.farmr.R
|
||||||
|
import com.appttude.h_mal.farmr.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,29 @@
|
|||||||
|
package com.appttude.h_mal.farmr.ui.robots
|
||||||
|
|
||||||
|
import androidx.test.espresso.matcher.RootMatchers.isDialog
|
||||||
|
import com.appttude.h_mal.farmr.R
|
||||||
|
import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder
|
||||||
|
import com.appttude.h_mal.farmr.model.Order
|
||||||
|
import com.appttude.h_mal.farmr.model.Sortable
|
||||||
|
import com.appttude.h_mal.farmr.ui.BaseTestRobot
|
||||||
|
|
||||||
|
fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
|
||||||
|
class HomeScreenRobot : BaseTestRobot() {
|
||||||
|
|
||||||
|
fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText<CurrentViewHolder>(R.id.list_item_view, text)
|
||||||
|
fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position)
|
||||||
|
fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position, R.id.imageView)
|
||||||
|
fun clickFab() = clickButton(R.id.fab1)
|
||||||
|
fun clickOnInfoIcon() = clickButton(R.id.action_favorite)
|
||||||
|
fun clickFilterInMenu() = clickOnMenuItem(R.string.filter)
|
||||||
|
fun clickClearFilterInMenu() = clickOnMenuItem(R.string.clear)
|
||||||
|
fun clickSortInMenu() = clickOnMenuItem(R.string.sort)
|
||||||
|
|
||||||
|
fun applySort(sortable: Sortable, order: Order = Order.ASCENDING) {
|
||||||
|
clickSortInMenu()
|
||||||
|
val label = sortable.label
|
||||||
|
clickDialogButton(label)
|
||||||
|
val orderLabel = order.label
|
||||||
|
clickDialogButton(orderLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.appttude.h_mal.farmr.ui.robots
|
||||||
|
|
||||||
|
import com.appttude.h_mal.farmr.R
|
||||||
|
import com.appttude.h_mal.farmr.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,142 @@
|
|||||||
|
package com.appttude.h_mal.farmr.ui.tests
|
||||||
|
|
||||||
|
import com.appttude.h_mal.farmr.model.Order
|
||||||
|
import com.appttude.h_mal.farmr.model.ShiftType
|
||||||
|
import com.appttude.h_mal.farmr.model.Sortable
|
||||||
|
import com.appttude.h_mal.farmr.ui.BaseTest
|
||||||
|
import com.appttude.h_mal.farmr.ui.MainActivity
|
||||||
|
import com.appttude.h_mal.farmr.ui.robots.addScreen
|
||||||
|
import com.appttude.h_mal.farmr.ui.robots.filterScreen
|
||||||
|
import com.appttude.h_mal.farmr.ui.robots.homeScreen
|
||||||
|
import com.appttude.h_mal.farmr.ui.robots.viewScreen
|
||||||
|
import com.appttude.h_mal.farmr.utils.ID
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ShiftTests : BaseTest<MainActivity>(MainActivity::class.java) {
|
||||||
|
|
||||||
|
override fun afterLaunch() {
|
||||||
|
super.afterLaunch()
|
||||||
|
addRandomShifts()
|
||||||
|
|
||||||
|
// Content resolver hard to mock
|
||||||
|
// Dirty technique to have a populated list
|
||||||
|
homeScreen {
|
||||||
|
clickFab()
|
||||||
|
navigateBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun testFinished() {
|
||||||
|
super.testFinished()
|
||||||
|
clearDataBase()
|
||||||
|
clearPrefs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a shift successfully
|
||||||
|
@Test
|
||||||
|
fun openAddScreen_addNewShift_newShiftCreated() {
|
||||||
|
homeScreen {
|
||||||
|
clickFab()
|
||||||
|
}
|
||||||
|
addScreen {
|
||||||
|
setDescription("This is a description")
|
||||||
|
setDate(2023, 2, 11)
|
||||||
|
clickShiftType(ShiftType.HOURLY)
|
||||||
|
setTimeIn(12, 0)
|
||||||
|
setTimeOut(14, 30)
|
||||||
|
setBreakTime(30)
|
||||||
|
setRateOfPay(10f)
|
||||||
|
assertDuration("2.0 hours")
|
||||||
|
assertTotalPay("£20.00")
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
homeScreen {
|
||||||
|
clickOnItemWithText("This is a description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit a shift successfully
|
||||||
|
@Test
|
||||||
|
fun test2() {
|
||||||
|
homeScreen {
|
||||||
|
clickOnEdit(0)
|
||||||
|
}
|
||||||
|
addScreen {
|
||||||
|
setDescription("Edited this shift")
|
||||||
|
setTimeIn(12, 0)
|
||||||
|
setTimeOut(14, 30)
|
||||||
|
setBreakTime(30)
|
||||||
|
setRateOfPay(20f)
|
||||||
|
assertDuration("2.0 hours")
|
||||||
|
assertTotalPay("£40.00")
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
homeScreen {
|
||||||
|
clickOnItemWithText("Edited this shift")
|
||||||
|
}
|
||||||
|
viewScreen {
|
||||||
|
matchDescription("Edited this shift")
|
||||||
|
matchDuration("2 Hours 0 Minutes (+ 30 minutes break)")
|
||||||
|
matchTotalPay("2.0 Hours @ £20.00 per Hour\nEquals: £40.00")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter the list with date from
|
||||||
|
@Test
|
||||||
|
fun test3() {
|
||||||
|
homeScreen {
|
||||||
|
applySort(Sortable.TYPE, Order.DESCENDING)
|
||||||
|
clickOnItemAtPosition(0)
|
||||||
|
viewScreen {
|
||||||
|
matchDescription("Day five")
|
||||||
|
matchShiftType(ShiftType.PIECE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter the list with date to
|
||||||
|
@Test
|
||||||
|
fun test4() {
|
||||||
|
homeScreen {
|
||||||
|
clickFilterInMenu()
|
||||||
|
}
|
||||||
|
filterScreen {
|
||||||
|
setDateIn(2023,8,3)
|
||||||
|
setDateOut(2023,8,6)
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
homeScreen {
|
||||||
|
clickOnItemAtPosition(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a shift as piece rate
|
||||||
|
@Test
|
||||||
|
fun test5() {
|
||||||
|
homeScreen {
|
||||||
|
clickFab()
|
||||||
|
}
|
||||||
|
addScreen {
|
||||||
|
setDescription("This is a description")
|
||||||
|
setDate(2023, 2, 11)
|
||||||
|
clickShiftType(ShiftType.PIECE)
|
||||||
|
setRateOfPay(10f)
|
||||||
|
setUnits(1f)
|
||||||
|
assertTotalPay("£10.00")
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
homeScreen {
|
||||||
|
clickOnItemWithText("This is a description")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the details screen
|
||||||
|
@Test
|
||||||
|
fun test6() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter, sort, order and then reset
|
||||||
|
@Test
|
||||||
|
fun test7() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package com.appttude.h_mal.farmr.ui.utils
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.appttude.h_mal.farmr.ui.utils
|
||||||
|
|
||||||
|
import com.appttude.h_mal.farmr.model.Shift
|
||||||
|
import com.appttude.h_mal.farmr.model.ShiftType
|
||||||
|
|
||||||
|
fun getShifts() = listOf(
|
||||||
|
Shift(
|
||||||
|
ShiftType.HOURLY,
|
||||||
|
"Day one",
|
||||||
|
"2023-08-01",
|
||||||
|
"12:00",
|
||||||
|
"13:00",
|
||||||
|
1f,
|
||||||
|
0,
|
||||||
|
0f,
|
||||||
|
10f,
|
||||||
|
10f
|
||||||
|
),
|
||||||
|
Shift(
|
||||||
|
ShiftType.HOURLY,
|
||||||
|
"Day two",
|
||||||
|
"2023-08-02",
|
||||||
|
"12:00",
|
||||||
|
"13:00",
|
||||||
|
1f,
|
||||||
|
0,
|
||||||
|
0f,
|
||||||
|
10f,
|
||||||
|
10f
|
||||||
|
),
|
||||||
|
Shift(
|
||||||
|
ShiftType.HOURLY,
|
||||||
|
"Day three",
|
||||||
|
"2023-08-03",
|
||||||
|
"12:00",
|
||||||
|
"13:00",
|
||||||
|
1f,
|
||||||
|
30,
|
||||||
|
0f,
|
||||||
|
10f,
|
||||||
|
5f
|
||||||
|
),
|
||||||
|
Shift(
|
||||||
|
ShiftType.HOURLY,
|
||||||
|
"Day four",
|
||||||
|
"2023-08-04",
|
||||||
|
"12:00",
|
||||||
|
"13:00",
|
||||||
|
1f,
|
||||||
|
30,
|
||||||
|
0f,
|
||||||
|
10f,
|
||||||
|
5f
|
||||||
|
),
|
||||||
|
Shift(
|
||||||
|
ShiftType.PIECE,
|
||||||
|
"Day five",
|
||||||
|
"2023-08-05",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0f,
|
||||||
|
0,
|
||||||
|
1f,
|
||||||
|
10f,
|
||||||
|
10f
|
||||||
|
),
|
||||||
|
Shift(
|
||||||
|
ShiftType.PIECE,
|
||||||
|
"Day six",
|
||||||
|
"2023-08-06",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0f,
|
||||||
|
0,
|
||||||
|
1f,
|
||||||
|
10f,
|
||||||
|
10f
|
||||||
|
),
|
||||||
|
Shift(
|
||||||
|
ShiftType.PIECE,
|
||||||
|
"Day seven",
|
||||||
|
"2023-08-07",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0f,
|
||||||
|
0,
|
||||||
|
1f,
|
||||||
|
10f,
|
||||||
|
10f
|
||||||
|
),
|
||||||
|
Shift(
|
||||||
|
ShiftType.PIECE,
|
||||||
|
"Day eight",
|
||||||
|
"2023-08-08",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0f,
|
||||||
|
0,
|
||||||
|
1f,
|
||||||
|
10f,
|
||||||
|
10f
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.appttude.h_mal.farmr.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.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 } )
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.appttude.h_mal.farmr.base
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.appttude.h_mal.farmr.data.RepositoryImpl
|
||||||
|
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||||
|
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||||
|
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
|
||||||
|
import org.kodein.di.Kodein
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.x.androidXModule
|
||||||
|
import org.kodein.di.generic.bind
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
import org.kodein.di.generic.provider
|
||||||
|
import org.kodein.di.generic.singleton
|
||||||
|
|
||||||
|
abstract class BaseApplication() : Application(), KodeinAware {
|
||||||
|
|
||||||
|
// Kodein creation of modules to be retrieve within the app
|
||||||
|
override val kodein = Kodein.lazy {
|
||||||
|
import(androidXModule(this@BaseApplication))
|
||||||
|
|
||||||
|
bind() from singleton { createDatabase() }
|
||||||
|
bind() from singleton { createPrefs() }
|
||||||
|
bind() from singleton { RepositoryImpl(instance(), instance()) }
|
||||||
|
|
||||||
|
bind() from provider { ApplicationViewModelFactory(instance()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun createDatabase(): LegacyDatabase
|
||||||
|
abstract fun createPrefs(): PreferenceProvider
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package com.appttude.h_mal.farmr.data.legacydb
|
|||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.BaseColumns
|
import android.provider.BaseColumns
|
||||||
|
import com.appttude.h_mal.farmr.BuildConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by h_mal on 26/12/2017.
|
* Created by h_mal on 26/12/2017.
|
||||||
*/
|
*/
|
||||||
object ShiftsContract {
|
object ShiftsContract {
|
||||||
const val CONTENT_AUTHORITY = "com.appttude.h_mal.farmr"
|
const val CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID
|
||||||
val BASE_CONTENT_URI = Uri.parse("content://$CONTENT_AUTHORITY")
|
val BASE_CONTENT_URI = Uri.parse("content://$CONTENT_AUTHORITY")
|
||||||
const val PATH_SHIFTS = "shifts"
|
const val PATH_SHIFTS = "shifts"
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +56,14 @@ 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))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearPrefs() {
|
||||||
|
preference.edit().clear().apply()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package com.appttude.h_mal.farmr.di
|
package com.appttude.h_mal.farmr.di
|
||||||
|
|
||||||
import android.app.Application
|
import com.appttude.h_mal.farmr.base.BaseApplication
|
||||||
import com.appttude.h_mal.farmr.data.RepositoryImpl
|
import com.appttude.h_mal.farmr.data.RepositoryImpl
|
||||||
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||||
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||||
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
|
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
|
||||||
import com.appttude.h_mal.farmr.viewmodel.MainViewModel
|
|
||||||
import org.kodein.di.Kodein
|
import org.kodein.di.Kodein
|
||||||
import org.kodein.di.KodeinAware
|
import org.kodein.di.KodeinAware
|
||||||
import org.kodein.di.android.x.androidXModule
|
import org.kodein.di.android.x.androidXModule
|
||||||
@@ -14,15 +13,12 @@ import org.kodein.di.generic.instance
|
|||||||
import org.kodein.di.generic.provider
|
import org.kodein.di.generic.provider
|
||||||
import org.kodein.di.generic.singleton
|
import org.kodein.di.generic.singleton
|
||||||
|
|
||||||
class ShiftApplication: Application(), KodeinAware {
|
class ShiftApplication: BaseApplication() {
|
||||||
// Kodein creation of modules to be retrieve within the app
|
|
||||||
override val kodein = Kodein.lazy {
|
|
||||||
import(androidXModule(this@ShiftApplication))
|
|
||||||
|
|
||||||
bind() from singleton { LegacyDatabase(contentResolver) }
|
override fun createDatabase(): LegacyDatabase {
|
||||||
bind() from singleton { PreferenceProvider(this@ShiftApplication) }
|
return LegacyDatabase(contentResolver)
|
||||||
bind() from singleton { RepositoryImpl(instance(), instance()) }
|
|
||||||
|
|
||||||
bind() from provider { ApplicationViewModelFactory(instance()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createPrefs() = PreferenceProvider(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,5 +11,9 @@ enum class Sortable(val label: String) {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val entries = Sortable.values()
|
val entries = Sortable.values()
|
||||||
|
|
||||||
|
fun getEnumByType(label: String): Sortable {
|
||||||
|
return Sortable.values().first { it.label == label }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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?) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.appttude.h_mal.farmr.model.Success
|
|||||||
import com.appttude.h_mal.farmr.utils.ID
|
import com.appttude.h_mal.farmr.utils.ID
|
||||||
import com.appttude.h_mal.farmr.utils.createDialog
|
import com.appttude.h_mal.farmr.utils.createDialog
|
||||||
import com.appttude.h_mal.farmr.utils.displayToast
|
import com.appttude.h_mal.farmr.utils.displayToast
|
||||||
|
import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
|
||||||
import com.appttude.h_mal.farmr.utils.formatToTwoDpString
|
import com.appttude.h_mal.farmr.utils.formatToTwoDpString
|
||||||
import com.appttude.h_mal.farmr.utils.hide
|
import com.appttude.h_mal.farmr.utils.hide
|
||||||
import com.appttude.h_mal.farmr.utils.popBackStack
|
import com.appttude.h_mal.farmr.utils.popBackStack
|
||||||
@@ -26,8 +27,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
|
||||||
@@ -157,8 +159,8 @@ class FragmentAddItem : BaseFragment<MainViewModel>(R.layout.fragment_add_item),
|
|||||||
mUnits = units
|
mUnits = units
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mPayRateEditText.setText(rateOfPay.formatToTwoDpString())
|
mPayRateEditText.setText(rateOfPay.formatAsCurrencyString())
|
||||||
mTotalPayTextView.text = totalPay.formatToTwoDpString()
|
mTotalPayTextView.text = totalPay.formatAsCurrencyString()
|
||||||
|
|
||||||
calculateTotalPay()
|
calculateTotalPay()
|
||||||
}
|
}
|
||||||
@@ -262,12 +264,11 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mTotalPayTextView.text = total.formatToTwoDpString()
|
mTotalPayTextView.text = total.formatAsCurrencyString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
|
|||||||
.setSingleChoiceItems(
|
.setSingleChoiceItems(
|
||||||
groupName,
|
groupName,
|
||||||
checkedItem
|
checkedItem
|
||||||
) { p0, p1 -> sort = Sortable.valueOf(groupName[p1]) }
|
) { p0, p1 -> sort = Sortable.getEnumByType(groupName[p1]) }
|
||||||
.setPositiveButton("Ascending") { dialog, id ->
|
.setPositiveButton("Ascending") { dialog, id ->
|
||||||
viewModel.setSortAndOrder(sort)
|
viewModel.setSortAndOrder(sort)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
|||||||
@@ -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?) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
@@ -16,8 +17,7 @@ class SplashScreen : Activity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_splash)
|
setContentView(R.layout.activity_splash)
|
||||||
val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.hyperspace_jump, android.R.anim.fade_out).toBundle()
|
|
||||||
val relativeLayout = findViewById<View>(R.id.splash_layout) as RelativeLayout
|
|
||||||
val i = Intent(this@SplashScreen, MainActivity::class.java)
|
val i = Intent(this@SplashScreen, MainActivity::class.java)
|
||||||
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
Handler().postDelayed({
|
Handler().postDelayed({
|
||||||
@@ -27,11 +27,11 @@ class SplashScreen : Activity() {
|
|||||||
startActivity(i)
|
startActivity(i)
|
||||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||||
// finish();
|
// finish();
|
||||||
}, SPLASH_TIME_OUT.toLong())
|
}, SPLASH_TIME_OUT)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Splash screen timer
|
// Splash screen timer
|
||||||
private const val SPLASH_TIME_OUT = 2000
|
const val SPLASH_TIME_OUT: Long = 2000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.appttude.h_mal.farmr;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
|
||||||
*
|
|
||||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
|
||||||
*/
|
|
||||||
public class ExampleUnitTest {
|
|
||||||
@Test
|
|
||||||
public void addition_isCorrect() throws Exception {
|
|
||||||
assertEquals(4, 2 + 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.appttude.h_mal.farmr.data
|
||||||
|
|
||||||
|
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
|
||||||
|
import com.appttude.h_mal.farmr.data.legacydb.ShiftObject
|
||||||
|
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.ArgumentMatchers.anyLong
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertIs
|
||||||
|
|
||||||
|
class RepositoryImplTest {
|
||||||
|
|
||||||
|
private lateinit var repository: RepositoryImpl
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
lateinit var db: LegacyDatabase
|
||||||
|
|
||||||
|
@MockK
|
||||||
|
lateinit var prefs: PreferenceProvider
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
repository = RepositoryImpl(db, prefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun readDatabase_validResponse() {
|
||||||
|
// Arrange
|
||||||
|
val elements = listOf<ShiftObject>(
|
||||||
|
mockk { every { id } returns anyLong() },
|
||||||
|
mockk { every { id } returns anyLong() },
|
||||||
|
mockk { every { id } returns anyLong() },
|
||||||
|
mockk { every { id } returns anyLong() }
|
||||||
|
)
|
||||||
|
|
||||||
|
//Act
|
||||||
|
every { db.readShiftsFromDatabase() } returns elements
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val result = repository.readShiftsFromDatabase()
|
||||||
|
assertIs<List<ShiftObject>>(result)
|
||||||
|
assertEquals(result.first().id, anyLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.appttude.h_mal.farmr.utils
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
fun <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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sleep(millis: Long = 1000) {
|
||||||
|
runBlocking(Dispatchers.Default) { delay(millis) }
|
||||||
|
}
|
||||||
@@ -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