- Test suite expanded

- config.yml updated
 -
This commit is contained in:
2023-08-29 14:02:42 +01:00
parent 07d7e6cbe7
commit ce5be16232
38 changed files with 631 additions and 447 deletions

View File

@@ -1,26 +1,130 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/configuration-reference
# See: https://circleci.com/docs/2.0/configuration-reference
# For a detailed guide to building and testing on Android, read the docs:
# https://circleci.com/docs/2.0/language-android/ for more details.
version: 2.1
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/configuration-reference/#jobs
jobs:
say-hello:
# Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub.
# See: https://circleci.com/docs/configuration-reference/#executor-job
docker:
- image: cimg/base:stable
# Add steps to the job
# See: https://circleci.com/docs/configuration-reference/#steps
# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
# See: https://circleci.com/docs/2.0/orb-intro/
orbs:
android: circleci/android@2.3.0
commands:
setup_repo:
description: checkout repo and android dependencies
steps:
- checkout
- run:
name: "Say hello"
command: "echo Hello, World!"
# Orchestrate jobs using workflows
# See: https://circleci.com/docs/configuration-reference/#workflows
name: Give gradle permissions
command: |
sudo chmod +x ./gradlew
- android/restore-gradle-cache
run_tests:
description: run tests for flavour specified
steps:
# The next step will run the unit tests
- run:
name: Run local unit tests
command: |
./gradlew testDebugUnitTest
- android/save-gradle-cache
- store_artifacts:
path: app/build/reports
destination: reports
- store_test_results:
path: app/build/test-results
run_ui_tests:
description: run instrumentation and espresso tests
steps:
- android/start-emulator-and-run-tests:
post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest
test-command: ./gradlew connectedDebugAndroidTest --continue
system-image: system-images;android-26;google_apis;x86
# store screenshots for failed ui tests
- when:
condition: on_fail
steps:
- store_artifacts:
path: app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected
destination: connected_android_test
# store test reports
- store_artifacts:
path: app/build/reports/androidTests/connected
destination: reports
- store_test_results:
path: app/build/outputs/androidTest-results/connected
deploy_to_play_store:
description: deploy to playstore
steps:
# The next step will run the unit tests
- android/decode-keystore:
keystore-location: "./app/keystore.jks"
- run:
name: Setup playstore key
command: |
echo "$GOOGLE_PLAY_KEY" > "google-play-key.json"
- run:
name: Run fastlane command to deploy to playstore
command: |
pwd
bundle exec fastlane deploy
- store_test_results:
path: fastlane/report.xml
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/2.0/configuration-reference/#jobs
jobs:
# Below is the definition of your job to build and test your app, you can rename and customize it as you want.
build-and-test:
# These next lines define the Android machine image executor.
# See: https://circleci.com/docs/2.0/executor-types/
executor:
name: android/android-machine
tag: 2023.05.1
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
- setup_repo
- run_tests
run_instrumentation_test:
# These next lines define the Android machine image executor.
# See: https://circleci.com/docs/2.0/executor-types/
executor:
name: android/android-machine
tag: 2023.05.1
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
- setup_repo
- run_ui_tests
deploy-to-playstore:
docker:
- image: cimg/android:2023.07-browsers
auth:
username: ${DOCKER_USERNAME}
password: ${DOCKER_PASSWORD}
steps:
- setup_repo
- deploy_to_play_store
# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
say-hello-workflow:
version: 2
build-release:
jobs:
- say-hello
- build-and-test:
context: appttude
- run_instrumentation_test:
context: appttude
filters:
branches:
only:
- master
- release
- deploy-to-playstore:
context: appttude
filters:
branches:
only:
- release
requires:
- build-and-test

2
.gitignore vendored
View File

@@ -88,7 +88,7 @@ gen-external-apklibs
.idea/uiDesigner.xml
.idea/assetWizardSettings.xml
.idea/gradle.xml
.idea/jarRepositorie
.idea/jarRepositories.xml
# Gem/fastlane
Gemfile.lock

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Binary file not shown.

6
.idea/compiler.xml generated
View File

@@ -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
View File

@@ -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>

View File

@@ -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
View File

@@ -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
View File

@@ -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
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -10,7 +10,7 @@ android {
targetSdkVersion 31
versionCode 1
versionName "1.0"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner'
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@@ -19,6 +19,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
useLibrary 'android.test.mock'
}
dependencies {

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -19,6 +19,7 @@ import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID
import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNull
import org.junit.After
import org.junit.Rule
import org.junit.Test
@@ -31,6 +32,11 @@ class ShiftProviderTest {
private val contentResolver: ContentResolver
get() = providerRule.resolver
@After
fun tearDown() {
contentResolver.delete(CONTENT_URI, null, null)
}
@Test
fun insertEntry_queryEntry_assertEntry() {
// Arrange

View File

@@ -1,26 +0,0 @@
package com.appttude.h_mal.farmr.data.ui.robots
import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder
import com.appttude.h_mal.farmr.data.ui.BaseTestRobot
fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
class HomeScreenRobot : BaseTestRobot() {
fun clickOnItem(position: Int) = clickViewInRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position)
fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText<CurrentViewHolder>(R.id.list_item_view, text)
fun clickOnEdit(position: Int) = clickViewInRecycler<CurrentViewHolder>(R.id.list_item_view, R.id.imageView)
fun clickFab() = clickButton(R.id.fab1)
fun clickOnInfo() = clickButton(R.id.action_favorite)
// fun clearFilter() =
// fun applySort() =
// fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location)
// fun refresh() = pullToRefresh(R.id.swipe_refresh)
// fun isDisplayed() = matchViewWaitFor(R.id.temp_main_4)
// fun verifyUnableToRetrieve() {
// matchText(R.id.header_text, R.string.retrieve_warning)
// matchText(R.id.body_text, R.string.empty_retrieve_warning)
// }
}

View File

@@ -1,77 +0,0 @@
package com.appttude.h_mal.farmr.data.ui.tests
import com.appttude.h_mal.farmr.data.ui.BaseTest
import com.appttude.h_mal.farmr.data.ui.robots.addScreen
import com.appttude.h_mal.farmr.data.ui.robots.homeScreen
import com.appttude.h_mal.farmr.data.ui.robots.viewScreen
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.ui.MainActivity
import org.junit.Test
class ShiftTests: BaseTest<MainActivity>(MainActivity::class.java) {
// Add a shift successfully
@Test
fun test1() {
homeScreen {
clickFab()
}
addScreen {
setDescription("This is a description")
setDate(2023, 2, 11)
clickShiftType(ShiftType.HOURLY)
setTimeIn(12,0)
setTimeOut(14, 30)
setBreakTime(30)
setRateOfPay(10f)
assertDuration("2.0 hours")
assertTotalPay("£20.00")
submit()
}
homeScreen {
sc("This is a description")
}
}
// Edit a shift successfully
@Test
fun test2() {
homeScreen {
clickOnItemWithText("Edit this shift")
}
addScreen {
setRateOfPay(20f)
assertDuration("2.0 hours")
assertTotalPay("£40.00")
submit()
}
homeScreen {
clickOnItemWithText("Edit this shift")
}
viewScreen {
matchDescription("Edit this shift")
matchDuration("2 Hours 0 minutes")
matchTotalPay("2.0 hours @ £20.00 per Hour\nEquals:£40.00")
}
}
// filter the list with date from
@Test
fun test3() {}
// filter the list with date to
@Test
fun test4() {}
// Add a shift as piece rate
@Test
fun test5() {}
// Validate the details screen
@Test
fun test6() {}
// filter, sort, order and then reset
@Test
fun test7() {}
}

View File

@@ -1 +0,0 @@
package com.appttude.h_mal.farmr.data.ui.utils

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.farmr.data.ui
package com.appttude.h_mal.farmr.ui
import android.Manifest
import android.app.Activity
@@ -6,7 +6,9 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
@@ -15,13 +17,16 @@ import androidx.test.espresso.matcher.RootMatchers.withDecorView
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.farmr.application.TestAppClass
import com.appttude.h_mal.farmr.di.ShiftApplication
import com.appttude.h_mal.farmr.ui.utils.getShifts
import kotlinx.coroutines.runBlocking
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.kodein.di.android.kodein
@Suppress("EmptyMethod")
open class BaseTest<A : Activity>(
@@ -30,7 +35,7 @@ open class BaseTest<A : Activity>(
) {
lateinit var scenario: ActivityScenario<A>
private lateinit var testApp: ShiftApplication
private lateinit var testApp: TestAppClass
private lateinit var testActivity: Activity
private lateinit var decorView: View
@@ -38,7 +43,7 @@ open class BaseTest<A : Activity>(
var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE)
@Before
fun setUp() {
open fun setUp() {
val startIntent =
Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity)
if (intentBundle != null) {
@@ -46,13 +51,17 @@ open class BaseTest<A : Activity>(
}
testApp =
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ShiftApplication
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
kodein(testApp)
runBlocking {
beforeLaunch()
}
scenario = ActivityScenario.launch(startIntent)
scenario.onActivity {
testApp =
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
onLaunch()
decorView = it.window.decorView
testActivity = it
}
@@ -67,6 +76,7 @@ open class BaseTest<A : Activity>(
}
open fun beforeLaunch() {}
open fun onLaunch() {}
open fun afterLaunch() {}
open fun testFinished() {}
@@ -88,4 +98,13 @@ open class BaseTest<A : Activity>(
waitFor(3500)
}
}
fun navigateBack() = Espresso.pressBack()
fun addRandomShifts() {
testApp.addShiftsToDatabase(getShifts())
}
fun clearDataBase() = testApp.clearDatabase()
fun clearPrefs() = testApp.cleanPrefs()
}

View File

@@ -1,12 +1,18 @@
package com.appttude.h_mal.farmr.data.ui
package com.appttude.h_mal.farmr.ui
import android.content.res.Resources
import android.view.View
import android.widget.DatePicker
import android.widget.TimePicker
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatButton
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
@@ -14,11 +20,15 @@ import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.*
import com.appttude.h_mal.farmr.data.ui.utils.EspressoHelper.waitForView
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anything
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.Matcher
import org.hamcrest.Matchers
@SuppressWarnings("unused")
open class BaseTestRobot {
@@ -54,6 +64,17 @@ open class BaseTestRobot {
.atPosition(position).perform(click())
}
fun clickOnMenuItem(menuId: Int) {
openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context)
onView(withText(menuId)).perform(click())
}
fun clickDialogButton(text: String) {
onView(withText(text)).inRoot(isDialog())
.check(matches(isDisplayed()))
.perform(click());
}
fun <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
return matchView(recyclerId)
.perform(
@@ -88,14 +109,6 @@ open class BaseTestRobot {
)
}
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, text: String) {
matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.actionOnItem<VH>(hasDescendant(withText(text)), click())
)
}
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, resIdForString: Int) {
matchView(recyclerId)
.perform(
@@ -107,21 +120,50 @@ open class BaseTestRobot {
)
}
fun <VH : ViewHolder> clickViewInRecyclerAtPosition(recyclerId: Int, position: Int) {
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())
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) {
scrollToRecyclerItem<VH>(recyclerId, text)
?.perform(
matchView(recyclerId).perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollTo<VH>(
hasDescendant(withText(text))
),
RecyclerViewActions.actionOnItem<VH>(
withChild(withText(text)), click()
hasDescendant(withText(text)),
click()
)
)
}
@@ -144,9 +186,17 @@ open class BaseTestRobot {
day
)
)
onView(
allOf(
withClassName(equalTo(AppCompatButton::class.java.name)),
withText("OK")
)
).perform(
click()
)
}
fun selectTextInSpinner(id: Int, text:String) {
fun selectTextInSpinner(id: Int, text: String) {
clickButton(id)
onView(withSpinnerText(text)).perform(click())
}
@@ -157,5 +207,13 @@ open class BaseTestRobot {
hours, minutes
)
)
onView(
allOf(
withClassName(equalTo(AppCompatButton::class.java.name)),
withText("OK")
)
).perform(
click()
)
}
}

View File

@@ -1,8 +1,8 @@
package com.appttude.h_mal.farmr.data.ui.robots
package com.appttude.h_mal.farmr.ui.robots
import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.data.ui.BaseTestRobot
import com.appttude.h_mal.farmr.model.ShiftType
import com.appttude.h_mal.farmr.ui.BaseTestRobot
fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() }
class AddItemScreenRobot : BaseTestRobot() {

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.farmr.data.ui.robots
package com.appttude.h_mal.farmr.ui.robots
import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.data.ui.BaseTestRobot
import com.appttude.h_mal.farmr.ui.BaseTestRobot
import com.appttude.h_mal.farmr.model.ShiftType
fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() }

View File

@@ -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)
}
}

View File

@@ -1,7 +1,7 @@
package com.appttude.h_mal.farmr.data.ui.robots
package com.appttude.h_mal.farmr.ui.robots
import com.appttude.h_mal.farmr.R
import com.appttude.h_mal.farmr.data.ui.BaseTestRobot
import com.appttude.h_mal.farmr.ui.BaseTestRobot
import com.appttude.h_mal.farmr.model.ShiftType
fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() }

View File

@@ -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() {
}
}

View File

@@ -0,0 +1 @@
package com.appttude.h_mal.farmr.ui.utils

View File

@@ -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
)
)

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.farmr.data.ui.utils
package com.appttude.h_mal.farmr.ui.utils
import android.os.SystemClock.sleep
import android.view.View

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.farmr.data.ui.utils
package com.appttude.h_mal.farmr.ui.utils
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer

View File

@@ -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
}

View File

@@ -3,12 +3,13 @@ package com.appttude.h_mal.farmr.data.legacydb
import android.content.ContentResolver
import android.net.Uri
import android.provider.BaseColumns
import com.appttude.h_mal.farmr.BuildConfig
/**
* Created by h_mal on 26/12/2017.
*/
object ShiftsContract {
const val CONTENT_AUTHORITY = "com.appttude.h_mal.farmr"
const val CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID
val BASE_CONTENT_URI = Uri.parse("content://$CONTENT_AUTHORITY")
const val PATH_SHIFTS = "shifts"

View File

@@ -62,4 +62,8 @@ class PreferenceProvider(
)
}
fun clearPrefs() {
preference.edit().clear().apply()
}
}

View File

@@ -1,11 +1,10 @@
package com.appttude.h_mal.farmr.di
import android.app.Application
import com.appttude.h_mal.farmr.base.BaseApplication
import com.appttude.h_mal.farmr.data.RepositoryImpl
import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase
import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider
import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.farmr.viewmodel.MainViewModel
import org.kodein.di.Kodein
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.androidXModule
@@ -14,15 +13,12 @@ import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
class ShiftApplication: Application(), KodeinAware {
// Kodein creation of modules to be retrieve within the app
override val kodein = Kodein.lazy {
import(androidXModule(this@ShiftApplication))
class ShiftApplication: BaseApplication() {
bind() from singleton { LegacyDatabase(contentResolver) }
bind() from singleton { PreferenceProvider(this@ShiftApplication) }
bind() from singleton { RepositoryImpl(instance(), instance()) }
bind() from provider { ApplicationViewModelFactory(instance()) }
override fun createDatabase(): LegacyDatabase {
return LegacyDatabase(contentResolver)
}
override fun createPrefs() = PreferenceProvider(this)
}

View File

@@ -11,5 +11,9 @@ enum class Sortable(val label: String) {
companion object {
val entries = Sortable.values()
fun getEnumByType(label: String): Sortable {
return Sortable.values().first { it.label == label }
}
}
}

View File

@@ -18,6 +18,7 @@ import com.appttude.h_mal.farmr.model.Success
import com.appttude.h_mal.farmr.utils.ID
import com.appttude.h_mal.farmr.utils.createDialog
import com.appttude.h_mal.farmr.utils.displayToast
import com.appttude.h_mal.farmr.utils.formatAsCurrencyString
import com.appttude.h_mal.farmr.utils.formatToTwoDpString
import com.appttude.h_mal.farmr.utils.hide
import com.appttude.h_mal.farmr.utils.popBackStack
@@ -158,8 +159,8 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
mUnits = units
}
}
mPayRateEditText.setText(rateOfPay.formatToTwoDpString())
mTotalPayTextView.text = totalPay.formatToTwoDpString()
mPayRateEditText.setText(rateOfPay.formatAsCurrencyString())
mTotalPayTextView.text = totalPay.formatAsCurrencyString()
calculateTotalPay()
}
@@ -267,7 +268,7 @@ class FragmentAddItem : BaseFragment<SubmissionViewModel>(R.layout.fragment_add_
(mUnits ?: 0f) * mPayRate
}
}
mTotalPayTextView.text = total.formatToTwoDpString()
mTotalPayTextView.text = total.formatAsCurrencyString()
}
}

View File

@@ -155,7 +155,7 @@ class FragmentMain : BaseFragment<MainViewModel>(R.layout.fragment_main), BackPr
.setSingleChoiceItems(
groupName,
checkedItem
) { p0, p1 -> sort = Sortable.valueOf(groupName[p1]) }
) { p0, p1 -> sort = Sortable.getEnumByType(groupName[p1]) }
.setPositiveButton("Ascending") { dialog, id ->
viewModel.setSortAndOrder(sort)
dialog.dismiss()

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.RelativeLayout
import androidx.core.app.ActivityOptionsCompat
@@ -16,8 +17,7 @@ class SplashScreen : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_splash)
val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.hyperspace_jump, android.R.anim.fade_out).toBundle()
val relativeLayout = findViewById<View>(R.id.splash_layout) as RelativeLayout
val i = Intent(this@SplashScreen, MainActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Handler().postDelayed({
@@ -27,11 +27,11 @@ class SplashScreen : Activity() {
startActivity(i)
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
// finish();
}, SPLASH_TIME_OUT.toLong())
}, SPLASH_TIME_OUT)
}
companion object {
// Splash screen timer
private const val SPLASH_TIME_OUT = 2000
const val SPLASH_TIME_OUT: Long = 2000
}
}