diff --git a/.circleci/config.yml b/.circleci/config.yml
index b9a076d..2e85eac 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -7,20 +7,11 @@ version: 2.1
# 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@1.0.3
+ android: circleci/android@2.3.0
-# 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
-
- # Add steps to the job
- # See: https://circleci.com/docs/2.0/configuration-reference/#steps
+commands:
+ setup_repo:
+ description: checkout repo and android dependencies
steps:
- checkout
- restore_cache:
@@ -35,19 +26,118 @@ jobs:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
+ run_tests:
+ description: run tests for flavour specified
+ parameters:
+ flavour:
+ type: string
+ default: "AtlasWeather"
+ steps:
+ # The next step will run the unit tests
+ - android/run-tests:
+ test-command: ./gradlew test<< parameters.flavour >>DebugUnitTest --continue
+ - store_artifacts:
+ path: app/build/reports
+ destination: reports
+ - store_test_results:
+ path: app/build/test-results
+ run_ui_tests:
+ description: run tests for flavour specified
+ parameters:
+ flavour:
+ type: string
+ default: "AtlasWeather"
+ steps:
+ - android/start-emulator-and-run-tests:
+ post-emulator-launch-assemble-command: ./gradlew assemble<< parameters.flavour >>DebugAndroidTest
+ test-command: ./gradlew connected<< parameters.flavour >>DebugAndroidTest
+ system-image: system-images;android-25;google_apis;x86
+ max-tries: 1
+ kill-emulators: false
+ - run:
+ name: Pull screenshots from device
+ command: |
+ mkdir ~/screenshots
+ adb pull /storage/emulated/0/Android/data/com.appttude.h_mal.atlas_weather/files/screengrab/en-US/images/screenshots ~/screenshots
+ when: on_fail
+ # store test reports
+ - store_artifacts:
+ path: app/build/reports/androidTests/connected
+ destination: reports
+ # store screenshots for failed ui tests
+ - store_artifacts:
+ path: ~/screenshots
+ destination: screenshots
+
+# 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:
+ # Parameters used for determining
+ parameters:
+ flavour:
+ type: string
+ default: "AtlasWeather"
+ # 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:
+ flavour: << parameters.flavour >>
+ ui-test-and-release:
+ # Parameters used for determining
+ parameters:
+ flavour:
+ type: string
+ default: "AtlasWeather"
+ executor:
+ name: android/android-machine
+ tag: 2023.05.1
+ steps:
+ - setup_repo
+ - run_ui_tests
- run:
- name: Run Tests
- command: ./gradlew lint test
- - store_artifacts:
- path: app/build/reports
- destination: reports
- - store_test_results:
- path: app/build/test-results
+ name: Setup variables for release
+ command: |
+ echo "$RELEASE_KEYSTORE_BASE64" | base64 --decode > "android/app/release_keystore.jks"
+ echo "$GOOGLE_PLAY_KEY" > "android/playstore.json"
+ # And finally run the release build
+ - run:
+ name: Assemble and Upload to PlayStore
+ command: |
+ pwd
+ bundle exec fastlane deploy<< parameters.flavour >>
# Invoke jobs via workflows
# See: https://circleci.com/docs/2.0/configuration-reference/#workflows
workflows:
- sample: # This is the name of the workflow, feel free to change it to better match your workflow.
- # Inside the workflow, you define the jobs you want to run.
+ version: 2
+ build-release-atlas:
jobs:
- - build-and-test
+ - build-and-test:
+ flavour: "AtlasWeather"
+ - ui-test-and-release:
+ flavour: "AtlasWeather"
+ filters:
+ branches:
+ only:
+ - main_atlas
+ requires:
+ - build-and-test
+ build-release-mono:
+ jobs:
+ - build-and-test:
+ flavour: "MonoWeather"
+ - ui-test-and-release:
+ flavour: "MonoWeather"
+ filters:
+ branches:
+ only: main_admin
+ requires:
+ - build-and-test
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 649c4be..0b212e0 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -41,5 +41,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index f7907eb..33eb36e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,20 +1,19 @@
-apply plugin: 'com.android.application'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
-// kotlin kapt
-apply plugin: 'kotlin-kapt'
-// Android navigation
-apply plugin: 'androidx.navigation.safeargs'
-
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'kotlin-android-extensions'
+ id 'kotlin-kapt'
+ id 'androidx.navigation.safeargs'
+}
android {
lintOptions {
abortOnError false
}
- compileSdkVersion 32
+ compileSdkVersion 33
defaultConfig {
applicationId "com.appttude.h_mal.atlas_weather"
minSdkVersion 26
- targetSdkVersion 32
+ targetSdkVersion 33
versionCode 5
versionName "3.0"
testInstrumentationRunner "com.appttude.h_mal.atlas_weather.application.TestRunner"
@@ -65,12 +64,16 @@ android {
}
flavorDimensions "default"
- productFlavors{
- atlasWeather{
- applicationIdSuffix ".atlasWeather"
+ productFlavors {
+ atlasWeather {
+ applicationId "com.appttude.h_mal.atlas_weather"
+ versionCode 5
+ versionName "3.0.0"
}
- monoWeather{
- applicationIdSuffix ".monoWeather"
+ monoWeather {
+ applicationId "com.appttude.h_mal.atlas_weather.monoWeather"
+ versionCode 5
+ versionName "3.0.0"
}
}
sourceSets {
@@ -86,8 +89,6 @@ android {
}
}
-
-
}
dependencies {
@@ -115,16 +116,28 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
// android unit testing and espresso
- androidTestImplementation 'androidx.test.ext:junit:1.1.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test:rules:1.4.1-alpha06'
+ androidTestImplementation "androidx.test:core:1.4.0"
+
+ / * 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"
//mock websever for testing retrofit responses
testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
//mockito and livedata testing
testImplementation 'org.mockito:mockito-inline:2.13.0'
- implementation 'android.arch.core:core-testing'
+ implementation 'androidx.arch.core:core-testing:2.2.0'
// Mockk
def mockk_ver = "1.10.5"
@@ -171,4 +184,6 @@ dependencies {
/ * Glide */
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
+ / * screenshot library */
+ androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
}
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseUiTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseUiTest.kt
new file mode 100644
index 0000000..91d7665
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/BaseUiTest.kt
@@ -0,0 +1,141 @@
+//package com.appttude.h_mal.atlas_weather
+//
+//import android.Manifest
+//import android.R
+//import android.app.Activity
+//import android.content.Context
+//import android.os.Build
+//import android.view.View
+//import android.view.WindowManager
+//import androidx.annotation.StringRes
+//import androidx.test.core.app.ActivityScenario
+//import androidx.test.espresso.*
+//import androidx.test.espresso.Espresso.onView
+//import androidx.test.espresso.assertion.ViewAssertions.matches
+//import androidx.test.espresso.matcher.ViewMatchers.*
+//import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+//import androidx.test.rule.GrantPermissionRule
+//import com.appttude.h_mal.atlas_weather.atlasWeather.ui.BaseActivity
+////import h_mal.appttude.com.driver.base.BaseActivity
+//import com.appttude.h_mal.atlas_weather.helpers.BaseViewAction
+//import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
+//import org.hamcrest.CoreMatchers
+//import org.hamcrest.Description
+//import org.hamcrest.Matcher
+//import org.hamcrest.TypeSafeMatcher
+//import org.hamcrest.core.AllOf
+//import org.junit.After
+//import org.junit.Before
+//import org.junit.Rule
+//import tools.fastlane.screengrab.Screengrab
+//import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
+//import tools.fastlane.screengrab.locale.LocaleTestRule
+//
+//
+//open class BaseUiTest(
+// private val activity: Class
+//) {
+//
+// private lateinit var mActivityScenarioRule: ActivityScenario
+// private var mIdlingResource: IdlingResource? = null
+//
+// private lateinit var currentActivity: Activity
+//
+// @get:Rule
+// var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+//
+// @get:Rule
+// var snapshotRule: SnapshotRule = SnapshotRule()
+//
+// @Rule
+// @JvmField
+// var localeTestRule = LocaleTestRule()
+//
+// @Before
+// fun setup() {
+// Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())
+// beforeLaunch()
+// mActivityScenarioRule = ActivityScenario.launch(activity)
+// mActivityScenarioRule.onActivity {
+//// mIdlingResource = it.getIdlingResource()!!
+//// IdlingRegistry.getInstance().register(mIdlingResource)
+// afterLaunch(it)
+// }
+// }
+//
+// @After
+// fun tearDown() {
+// mIdlingResource?.let {
+// IdlingRegistry.getInstance().unregister(it)
+// }
+// }
+//
+// fun getResourceString(@StringRes stringRes: Int): String {
+// return getInstrumentation().targetContext.resources.getString(
+// stringRes
+// )
+// }
+//
+// fun waitFor(delay: Long) {
+// onView(isRoot()).perform(object : ViewAction {
+// override fun getConstraints(): Matcher = isRoot()
+// override fun getDescription(): String = "wait for $delay milliseconds"
+// override fun perform(uiController: UiController, v: View?) {
+// uiController.loopMainThreadForAtLeast(delay)
+// }
+// })
+// }
+//
+// open fun beforeLaunch() {}
+// open fun afterLaunch(context: Context) {}
+//
+//
+// @Suppress("DEPRECATION")
+// fun checkToastMessage(message: String) {
+// onView(withText(message)).inRoot(object : TypeSafeMatcher() {
+// override fun describeTo(description: Description?) {
+// description?.appendText("is toast")
+// }
+//
+// override fun matchesSafely(root: Root): Boolean {
+// root.run {
+// if (windowLayoutParams.get().type == WindowManager.LayoutParams.TYPE_TOAST) {
+// decorView.run {
+// if (windowToken === applicationWindowToken) {
+// // windowToken == appToken means this window isn't contained by any other windows.
+// // if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
+// return true
+// }
+// }
+// }
+// }
+// return false
+// }
+// }
+// ).check(matches(isDisplayed()))
+// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+// waitFor(3500)
+// }
+// }
+//
+// fun checkSnackBarDisplayedByMessage(message: String) {
+// onView(
+// CoreMatchers.allOf(
+// withId(com.google.android.material.R.id.snackbar_text),
+// withText(message)
+// )
+// ).check(matches(isDisplayed()))
+// }
+//
+// private fun getCurrentActivity(): Activity {
+// onView(AllOf.allOf(withId(R.id.content), isDisplayed()))
+// .perform(object : BaseViewAction() {
+// override fun setPerform(uiController: UiController?, view: View?) {
+// if (view?.context is Activity) {
+// currentActivity = view.context as Activity
+// }
+// }
+// })
+// return currentActivity
+// }
+//}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/CustomViewMatchers.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/CustomViewMatchers.kt
new file mode 100644
index 0000000..a0abde6
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/CustomViewMatchers.kt
@@ -0,0 +1,54 @@
+package com.appttude.h_mal.atlas_weather
+
+import android.graphics.drawable.BitmapDrawable
+import android.view.View
+import android.widget.EditText
+import android.widget.ImageView
+import com.google.android.material.textfield.TextInputLayout
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+import org.hamcrest.TypeSafeMatcher
+
+
+/**
+ * Matcher for testing error of TextInputLayout
+ */
+fun checkErrorMessage(expectedErrorText: String): Matcher {
+ return object : TypeSafeMatcher() {
+ override fun matchesSafely(view: View?): Boolean {
+ if (view is EditText) {
+ return view.error.toString() == expectedErrorText
+ }
+
+ if (view !is TextInputLayout) return false
+
+ val error = view.error ?: return false
+ return expectedErrorText == error.toString()
+ }
+
+ override fun describeTo(d: Description?) {}
+ }
+}
+
+fun checkImage(): Matcher {
+ return object : TypeSafeMatcher() {
+ override fun matchesSafely(view: View?): Boolean {
+ if (view is ImageView) {
+ return hasImage(view)
+ }
+ return false
+ }
+
+ override fun describeTo(d: Description?) {}
+
+ private fun hasImage(view: ImageView): Boolean {
+ val drawable = view.drawable
+ var hasImage = drawable != null
+ if (hasImage && drawable is BitmapDrawable) {
+ hasImage = drawable.bitmap != null
+ }
+ return hasImage
+ }
+ }
+}
+
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImplTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImplTest.kt
index f9a16c2..1f257e6 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImplTest.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/data/location/LocationProviderImplTest.kt
@@ -6,8 +6,7 @@ import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.atlas_weather.model.types.LocationType
import kotlinx.coroutines.runBlocking
-import org.hamcrest.MatcherAssert.assertThat
-import org.hamcrest.Matchers.*
+import org.hamcrest.Matcher.*
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@@ -27,7 +26,8 @@ class LocationProviderImplTest {
@Before
fun setUp() {
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
+ val appContext =
+ InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
locationProvider = LocationProviderImpl(appContext)
}
@@ -51,7 +51,7 @@ class LocationProviderImplTest {
try {
// Act
locationProvider.getLatLongFromLocationName(randomString)
- }catch (e: IOException){
+ } catch (e: IOException) {
// Assert
assertEquals(e.message, "No location found")
}
@@ -70,16 +70,14 @@ class LocationProviderImplTest {
@Test
fun getLocationNameFromLatLong_locationTypeIsCity_correctLocationReturned() = runBlocking {
// Act
- val retrievedLocation = locationProvider.getLocationNameFromLatLong(lat, long, LocationType.City)
+ val retrievedLocation =
+ locationProvider.getLocationNameFromLatLong(lat, long, LocationType.City)
// Assert
assertEquals(retrievedLocation, city)
}
private fun assertRangeOfDouble(input: Double, expected: Double, range: Double) {
- assertThat(expected, allOf(
- greaterThanOrEqualTo(input - range),
- lessThanOrEqualTo(input + range))
- )
+ assertEquals(expected, input, range)
}
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseMatcher.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseMatcher.kt
new file mode 100644
index 0000000..ce86753
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseMatcher.kt
@@ -0,0 +1,17 @@
+package com.appttude.h_mal.atlas_weather.helpers
+
+import android.view.View
+import org.hamcrest.BaseMatcher
+import org.hamcrest.Description
+
+class BaseMatcher: BaseMatcher() {
+ override fun describeTo(description: Description?) {
+ TODO("Not yet implemented")
+ }
+
+ override fun matches(actual: Any?): Boolean {
+ TODO("Not yet implemented")
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseViewAction.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseViewAction.kt
new file mode 100644
index 0000000..9d52177
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/BaseViewAction.kt
@@ -0,0 +1,27 @@
+package com.appttude.h_mal.atlas_weather.helpers
+
+import android.view.View
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
+import org.hamcrest.Matcher
+
+open class BaseViewAction: ViewAction {
+ override fun getDescription(): String? = setDescription()
+
+ override fun getConstraints(): Matcher = setConstraints()
+
+ override fun perform(uiController: UiController?, view: View?) {
+ setPerform(uiController, view)
+ }
+
+ open fun setDescription(): String? {
+ return null
+ }
+
+ open fun setConstraints(): Matcher {
+ return isAssignableFrom(View::class.java)
+ }
+
+ open fun setPerform(uiController: UiController?, view: View?) { }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/Constants.kt
new file mode 100644
index 0000000..1469ff0
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/Constants.kt
@@ -0,0 +1,14 @@
+package com.appttude.h_mal.atlas_weather.helpers
+
+import android.os.Environment
+import java.io.File
+
+/**
+ * File paths for images on device
+ */
+fun getImagePath(imageConst: String): String {
+ return File(
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
+ "/Camera/images/$imageConst"
+ ).absolutePath
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/DataHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/DataHelper.kt
new file mode 100644
index 0000000..6b0bb0b
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/DataHelper.kt
@@ -0,0 +1,33 @@
+package com.appttude.h_mal.atlas_weather.helpers
+
+import android.content.ClipData
+import android.content.ClipData.Item
+import android.net.Uri
+import java.io.File
+
+object DataHelper {
+
+ fun createClipItem(filePath: String) = Item(
+ Uri.fromFile(
+ File(filePath)
+ )
+ )
+
+ fun createClipData(item: Item, mimeType: String = "text/uri-list") =
+ ClipData(null, arrayOf(mimeType), item)
+
+ fun createClipData(filePath: String) = createClipData(createClipItem(filePath))
+
+ fun createClipData(filePaths: Array): ClipData {
+ val clipData = createClipData(filePaths[0])
+ val remainingFiles = filePaths.copyOfRange(1, filePaths.size - 1)
+ clipData.addFilePaths(remainingFiles)
+ return clipData
+ }
+
+ fun createClipData(uri: Uri) = createClipData(Item(uri))
+
+ fun ClipData.addFilePaths(filePaths: Array) {
+ filePaths.forEach { addItem(createClipItem(it)) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/EspressoHelper.kt
new file mode 100644
index 0000000..dbc973f
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/EspressoHelper.kt
@@ -0,0 +1,123 @@
+package com.appttude.h_mal.atlas_weather.helpers
+
+import android.os.SystemClock.sleep
+import android.view.View
+import android.widget.CheckBox
+import android.widget.Checkable
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.NoMatchingViewException
+import androidx.test.espresso.UiController
+import androidx.test.espresso.ViewAction
+import androidx.test.espresso.ViewInteraction
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.util.TreeIterables
+import org.hamcrest.BaseMatcher
+import org.hamcrest.CoreMatchers.isA
+import org.hamcrest.Description
+import org.hamcrest.Matcher
+
+
+object EspressoHelper {
+
+ /**
+ * Perform action of waiting for a certain view within a single root view
+ * @param viewMatcher Generic Matcher used to find our view
+ */
+ fun searchFor(viewMatcher: Matcher): ViewAction {
+
+ return object : ViewAction {
+
+ override fun getConstraints(): Matcher = isRoot()
+ override fun getDescription(): String {
+ return "searching for view $this in the root view"
+ }
+
+ override fun perform(uiController: UiController, view: View) {
+ var tries = 0
+ val childViews: Iterable = TreeIterables.breadthFirstViewTraversal(view)
+
+ // Look for the match in the tree of childviews
+ childViews.forEach {
+ tries++
+ if (viewMatcher.matches(it)) {
+ // found the view
+ return
+ }
+ }
+
+ throw NoMatchingViewException.Builder()
+ .withRootView(view)
+ .withViewMatcher(viewMatcher)
+ .build()
+ }
+ }
+ }
+
+ /**
+ * Performs an action to check/uncheck a checkbox
+ *
+ */
+ fun setChecked(checked: Boolean): ViewAction {
+ return object : ViewAction {
+ override fun getConstraints(): BaseMatcher {
+ return object : BaseMatcher() {
+ override fun describeTo(description: Description?) {}
+
+ override fun matches(actual: Any?): Boolean {
+ return isA(CheckBox::class.java).matches(actual)
+ }
+ }
+ }
+
+ override fun getDescription(): String {
+ return ""
+ }
+
+ override fun perform(uiController: UiController, view: View) {
+ val checkableView = view as Checkable
+ checkableView.isChecked = checked
+ }
+ }
+ }
+
+ /**
+ * Perform action of implicitly waiting for a certain view.
+ * This differs from EspressoExtensions.searchFor in that,
+ * upon failure to locate an element, it will fetch a new root view
+ * in which to traverse searching for our @param match
+ *
+ * @param viewMatcher ViewMatcher used to find our view
+ */
+ fun waitForView(
+ viewMatcher: Matcher,
+ waitMillis: Int = 5000,
+ waitMillisPerTry: Long = 100
+ ): ViewInteraction {
+
+ // Derive the max tries
+ val maxTries = waitMillis / waitMillisPerTry.toInt()
+
+ var tries = 0
+
+ for (i in 0..maxTries)
+ try {
+ // Track the amount of times we've tried
+ tries++
+
+ // Search the root for the view
+ onView(isRoot()).perform(searchFor(viewMatcher))
+
+ // If we're here, we found our view. Now return it
+ return onView(viewMatcher)
+
+ } catch (e: Exception) {
+
+ if (tries == maxTries) {
+ throw e
+ }
+ sleep(waitMillisPerTry)
+ }
+
+ throw Exception("Error finding a view matching $viewMatcher")
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/SnapshotRule.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/SnapshotRule.kt
new file mode 100644
index 0000000..a346f34
--- /dev/null
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/helpers/SnapshotRule.kt
@@ -0,0 +1,19 @@
+package com.appttude.h_mal.atlas_weather.helpers
+
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import tools.fastlane.screengrab.Screengrab
+
+/**
+ * Junit rule that takes a screenshot when a test fails.
+ */
+class SnapshotRule : TestWatcher() {
+ override fun failed(e: Throwable, description: Description) {
+ // Catch a screenshot on failure
+ Screengrab.screenshot("FAILURE-" + getScreenshotName(description))
+ }
+
+ fun getScreenshotName(description: Description): String {
+ return description.className.replace(".", "-") + "_" + description.methodName.replace(".", "-")
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/BaseTest.kt
index 21509cd..cd68ceb 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/BaseTest.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/BaseTest.kt
@@ -1,41 +1,56 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
-import android.content.Intent
+import android.Manifest
+import android.app.Activity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
-import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
-import androidx.test.espresso.matcher.ViewMatchers.*
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
+import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
-import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
+import com.appttude.h_mal.atlas_weather.helper.GenericsHelper.getGenericClassAt
+import com.appttude.h_mal.atlas_weather.helpers.SnapshotRule
import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.After
import org.junit.Rule
+import tools.fastlane.screengrab.Screengrab
+import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
-open class BaseTest {
+open class BaseTest {
lateinit var testApp: TestAppClass
+ @get:Rule
+ var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
+
+ @get:Rule
+ var snapshotRule: SnapshotRule = SnapshotRule()
+
@Rule
@JvmField
- var mActivityTestRule: ActivityTestRule = object : ActivityTestRule(MainActivity::class.java) {
- override fun beforeActivityLaunched() {
- super.beforeActivityLaunched()
+ var mActivityTestRule: ActivityTestRule =
+ object : ActivityTestRule(getGenericClassAt(0).java) {
+ override fun beforeActivityLaunched() {
+ super.beforeActivityLaunched()
+ Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())
- testApp = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
- setupFeed()
+ testApp =
+ InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass
+ setupFeed()
+ }
+
+ override fun afterActivityLaunched() {
+
+ // Dismiss dialog
+ onView(withText("AGREE")).inRoot(isDialog()).check(matches(isDisplayed()))
+ .perform(ViewActions.click())
+ }
}
- override fun afterActivityLaunched() {
-
- // Dismiss dialog
- onView(withText("AGREE")).inRoot(isDialog()).check(matches(isDisplayed())).perform(ViewActions.click())
- }
- }
-
fun stubEndpoint(url: String, stub: Stubs) {
testApp.stubUrl(url, stub.id)
}
@@ -45,7 +60,8 @@ open class BaseTest {
}
@After
- fun tearDown() {}
+ fun tearDown() {
+ }
open fun setupFeed() {}
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITestScenario.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITestScenario.kt
index 9840120..ba4f3cc 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITestScenario.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITestScenario.kt
@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
+import android.Manifest
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.launch
@@ -14,7 +15,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
-import com.appttude.h_mal.atlas_weather.monoWeather.robot.homeScreen
+import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import kotlinx.coroutines.runBlocking
@@ -27,12 +28,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4ClassRunner::class)
class HomePageUITestScenario : BaseMainScenario() {
- @Rule
- @JvmField
- var mGrantPermissionRule: GrantPermissionRule =
- GrantPermissionRule.grant(
- "android.permission.ACCESS_COARSE_LOCATION")
-
override fun setupFeed() {
stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
}
@@ -49,12 +44,14 @@ class HomePageUITestScenario : BaseMainScenario() {
open class BaseMainScenario {
lateinit var scenario: ActivityScenario
- private lateinit var testApp : TestAppClass
+ private lateinit var testApp: TestAppClass
+
+ @get:Rule
+ var permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION)
@Before
fun setUp() {
scenario = launch(MainActivity::class.java)
- scenario.moveToState(Lifecycle.State.INITIALIZED)
scenario.onActivity {
runBlocking {
testApp = it.application as TestAppClass
@@ -62,12 +59,11 @@ open class BaseMainScenario {
}
}
- scenario.moveToState(Lifecycle.State.CREATED).onActivity {
- Espresso.onView(ViewMatchers.withText("AGREE"))
- .inRoot(RootMatchers.isDialog())
- .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
- .perform(ViewActions.click())
- }
+ // Dismiss dialog on start up
+ Espresso.onView(ViewMatchers.withText("AGREE"))
+ .inRoot(RootMatchers.isDialog())
+ .check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
+ .perform(ViewActions.click())
}
fun stubEndpoint(url: String, stub: Stubs) {
@@ -79,7 +75,10 @@ open class BaseMainScenario {
}
@After
- fun tearDown() {}
+ fun tearDown() {
+ testFinished()
+ }
open fun setupFeed() {}
+ open fun testFinished() {}
}
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/ui/widget/WidgetLocationPermissionActivityTest.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/ui/widget/WidgetLocationPermissionActivityTest.kt
index a0351ec..26b7d24 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/ui/widget/WidgetLocationPermissionActivityTest.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/ui/widget/WidgetLocationPermissionActivityTest.kt
@@ -16,13 +16,14 @@ class WidgetLocationPermissionActivityTest {
@Rule
@JvmField
var mActivityTestRule : ActivityTestRule =
- ActivityTestRule(WidgetLocationPermissionActivity::class.java, false, false)
+ ActivityTestRule(WidgetLocationPermissionActivity::class.java, false, false)
@Test
fun demo_test() {
- val i = Intent()
- i.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 112)
- mActivityTestRule.launchActivity(i)
+ val startIntent = Intent().apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 112)
+ }
+ mActivityTestRule.launchActivity(startIntent)
Espresso.onView((ViewMatchers.withId(R.id.declaration_text))).check(matches(isDisplayed()));
}
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/BaseTestRobot.kt
index 599989e..9a0893f 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/BaseTestRobot.kt
+++ b/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/utils/BaseTestRobot.kt
@@ -1,51 +1,157 @@
package com.appttude.h_mal.atlas_weather.utils
+import android.content.res.Resources
import android.view.View
+import android.widget.DatePicker
+import androidx.annotation.StringRes
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.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
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.contrib.PickerActions
+import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.espresso.matcher.ViewMatchers.*
+import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.matcher.ViewMatchers.withClassName
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import com.appttude.h_mal.atlas_weather.checkErrorMessage
+import com.appttude.h_mal.atlas_weather.checkImage
+import com.appttude.h_mal.atlas_weather.helpers.EspressoHelper.waitForView
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anything
+import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.Matcher
+@SuppressWarnings("unused")
open class BaseTestRobot {
- fun fillEditText(resId: Int, text: String): ViewInteraction =
- onView(withId(resId)).perform(ViewActions.replaceText(text), ViewActions.closeSoftKeyboard())
+ 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(ViewActions.click())
+ fun clickButton(resId: Int): ViewInteraction =
+ onView((withId(resId))).perform(click())
- fun textView(resId: Int): ViewInteraction = onView(withId(resId))
+ fun matchView(resId: Int): ViewInteraction = onView(withId(resId))
+
+ fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId))
fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction
- .check(ViewAssertions.matches(ViewMatchers.withText(text)))
+ .check(matches(withText(text)))
- fun matchText(resId: Int, text: String): ViewInteraction = matchText(textView(resId), 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(ViewActions.click())
+ .inAdapterView(allOf(withId(listRes)))
+ .atPosition(position).perform(click())
}
+ fun scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
+ return matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(text))
+ )
+ )
+ }
+
+ fun scrollToRecyclerItem(
+ recyclerId: Int,
+ resIdForString: Int
+ ): ViewInteraction? {
+ return matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollTo(
+ hasDescendant(withText(resIdForString))
+ )
+ )
+ }
+
+ fun scrollToRecyclerItemByPosition(
+ recyclerId: Int,
+ position: Int
+ ): ViewInteraction? {
+ return matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.scrollToPosition(position)
+ )
+ }
+
+ fun clickViewInRecycler(recyclerId: Int, text: String) {
+ matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click())
+ )
+ }
+
+ fun clickViewInRecycler(recyclerId: Int, resIdForString: Int) {
+ matchView(recyclerId)
+ .perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(withText(resIdForString)),
+ click()
+ )
+ )
+ }
+
+ fun clickSubViewInRecycler(recyclerId: Int, text: String, subView: Int) {
+ scrollToRecyclerItem(recyclerId, text)
+ ?.perform(
+ // scrollTo will fail the test if no item matches.
+ RecyclerViewActions.actionOnItem(
+ hasDescendant(withText(text)), object : ViewAction {
+ override fun getDescription(): String = "Matching recycler descendant"
+ override fun getConstraints(): Matcher? = isRoot()
+ override fun perform(uiController: UiController?, view: View?) {
+ view?.findViewById(subView)?.performClick()
+ }
+ }
+ )
+ )
+ }
+
+ fun checkErrorOnTextEntry(resId: Int, errorMessage: String): ViewInteraction =
+ onView(withId(resId)).check(matches(checkErrorMessage(errorMessage)))
+
+ fun checkImageViewHasImage(resId: Int): ViewInteraction =
+ onView(withId(resId)).check(matches(checkImage()))
+
+ 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())
+ onView(allOf(withId(resId), ViewMatchers.isDisplayed())).perform(swipeDown())
}
- fun waitFor(delay: Long): ViewAction? {
- return object : ViewAction {
- override fun getConstraints(): Matcher = isRoot()
- override fun getDescription(): String = "wait for $delay milliseconds"
- override fun perform(uiController: UiController, v: View?) {
- uiController.loopMainThreadForAtLeast(delay)
- }
- }
+ fun selectDateInPicker(year: Int, month: Int, day: Int) {
+ onView(withClassName(equalTo(DatePicker::class.java.name))).perform(
+ PickerActions.setDate(
+ year,
+ month,
+ day
+ )
+ )
}
}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/robot/HomeScreenRobot.kt b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
similarity index 88%
rename from app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/robot/HomeScreenRobot.kt
rename to app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
index 61e5364..d956a52 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/robot/HomeScreenRobot.kt
+++ b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
@@ -1,4 +1,4 @@
-package com.appttude.h_mal.atlas_weather.monoWeather.robot
+package com.appttude.h_mal.atlas_weather.robot
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.utils.BaseTestRobot
diff --git a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITest.kt b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
similarity index 61%
rename from app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITest.kt
rename to app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
index 5ec092a..3b79264 100644
--- a/app/src/androidTest/java/com/appttude/h_mal/atlas_weather/monoWeather/testsuite/HomePageUITest.kt
+++ b/app/src/androidTestAtlasWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
@@ -1,18 +1,15 @@
-package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
+package com.appttude.h_mal.atlas_weather.tests
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.action.ViewActions
-import androidx.test.espresso.assertion.ViewAssertions
-import androidx.test.espresso.matcher.RootMatchers
-import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.rule.GrantPermissionRule
-import com.appttude.h_mal.atlas_weather.monoWeather.robot.homeScreen
+import com.appttude.h_mal.atlas_weather.atlasWeather.ui.MainActivity
+import com.appttude.h_mal.atlas_weather.robot.homeScreen
+import com.appttude.h_mal.atlas_weather.monoWeather.testsuite.BaseTest
import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.Rule
import org.junit.Test
-class HomePageUITest : BaseTest() {
+class HomePageUITest : BaseTest() {
@Rule
@JvmField
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
new file mode 100644
index 0000000..d956a52
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/atlas_weather/robot/HomeScreenRobot.kt
@@ -0,0 +1,11 @@
+package com.appttude.h_mal.atlas_weather.robot
+
+import com.appttude.h_mal.atlas_weather.R
+import com.appttude.h_mal.atlas_weather.utils.BaseTestRobot
+
+fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() }
+class HomeScreenRobot : BaseTestRobot() {
+ fun verifyCurrentTemperature(temperature: Int) = matchText(R.id.temp_main_4, temperature.toString())
+ fun verifyCurrentLocation(location: String) = matchText(R.id.location_main_4, location)
+ fun refresh() = pullToRefresh(R.id.swipe_refresh)
+}
\ No newline at end of file
diff --git a/app/src/androidTestMonoWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
new file mode 100644
index 0000000..4155d15
--- /dev/null
+++ b/app/src/androidTestMonoWeather/java/com/appttude/h_mal/atlas_weather/tests/HomePageUITest.kt
@@ -0,0 +1,25 @@
+package com.appttude.h_mal.atlas_weather.tests
+
+
+import androidx.test.rule.GrantPermissionRule
+import com.appttude.h_mal.atlas_weather.robot.homeScreen
+import com.appttude.h_mal.atlas_weather.monoWeather.testsuite.BaseTest
+import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
+import com.appttude.h_mal.atlas_weather.utils.Stubs
+import org.junit.Rule
+import org.junit.Test
+
+class HomePageUITest : BaseTest() {
+
+ override fun setupFeed() {
+ stubEndpoint("https://api.openweathermap.org/data/2.5/onecall", Stubs.Valid)
+ }
+
+ @Test
+ fun loadApp_validWeatherResponse_returnsValidPage() {
+ homeScreen {
+ verifyCurrentTemperature(2)
+ verifyCurrentLocation("Mock Location")
+ }
+ }
+}
diff --git a/app/src/atlasWeather/AndroidManifest.xml b/app/src/atlasWeather/AndroidManifest.xml
index e1e047e..e6d55b7 100644
--- a/app/src/atlasWeather/AndroidManifest.xml
+++ b/app/src/atlasWeather/AndroidManifest.xml
@@ -1,6 +1,8 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.appttude.h_mal.atlas_weather"
+ tools:node="merge">
-
+ android:theme="@style/AppTheme.NoActionBar"
+ android:exported="true">
@@ -25,14 +28,17 @@
+ android:name=".atlasWeather.ui.settings.UnitSettingsActivity"
+ android:label="Settings"
+ android:exported="true"/>
+ android:name=".atlasWeather.notification.NotificationReceiver"
+ android:parentActivityName=".MainActivity"
+ android:exported="true"/>
-
+
@@ -46,7 +52,7 @@
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/MainActivity.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/MainActivity.kt
index f6854d2..1c67c86 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/MainActivity.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/MainActivity.kt
@@ -17,6 +17,8 @@ import kotlinx.android.synthetic.atlasWeather.activity_main.*
class MainActivity : BaseActivity(){
+ lateinit var navHost: NavHostFragment
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@@ -24,7 +26,7 @@ class MainActivity : BaseActivity(){
val navView: BottomNavigationView = findViewById(R.id.nav_view)
setSupportActionBar(toolbar)
- val navHost = supportFragmentManager
+ navHost = supportFragmentManager
.findFragmentById(R.id.container) as NavHostFragment
val navController = navHost.navController
navController.setGraph(R.navigation.main_navigation)
diff --git a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/home/adapter/WeatherRecyclerAdapter.kt b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/home/adapter/WeatherRecyclerAdapter.kt
index dfb54c9..4d777e9 100644
--- a/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/home/adapter/WeatherRecyclerAdapter.kt
+++ b/app/src/atlasWeather/java/com/appttude/h_mal/atlas_weather/atlasWeather/ui/home/adapter/WeatherRecyclerAdapter.kt
@@ -1,5 +1,6 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter
+import android.annotation.SuppressLint
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
@@ -13,6 +14,7 @@ class WeatherRecyclerAdapter(
) : RecyclerView.Adapter() {
var weather: WeatherDisplay? = null
+ @SuppressLint("NotifyDataSetChanged")
fun addCurrent(current: WeatherDisplay){
weather = current
notifyDataSetChanged()
@@ -71,7 +73,6 @@ class WeatherRecyclerAdapter(
when (getDataType(getItemViewType(position))){
is ViewType.Empty -> {
holder as EmptyViewHolder
-
}
is ViewType.Current -> {
val viewHolderCurrent = holder as ViewHolderCurrent
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/WeatherDao.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/WeatherDao.kt
index b701ea6..703cbc2 100644
--- a/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/WeatherDao.kt
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/data/room/WeatherDao.kt
@@ -12,24 +12,24 @@ import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
interface WeatherDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun upsertFullWeather(item: EntityItem)
+ fun upsertFullWeather(item: EntityItem)
@Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun upsertListOfFullWeather(items: List)
+ fun upsertListOfFullWeather(items: List)
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
fun getCurrentFullWeather(userId: String) : LiveData
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
- suspend fun getCurrentFullWeatherSingle(userId: String) : EntityItem
+ fun getCurrentFullWeatherSingle(userId: String) : EntityItem
@Query("SELECT * FROM EntityItem WHERE id != :id")
fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION) : LiveData>
@Query("SELECT * FROM EntityItem WHERE id != :id")
- suspend fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION) : List
+ fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION) : List
@Query("DELETE FROM EntityItem WHERE id = :userId")
- suspend fun deleteEntry(userId: String): Int
+ fun deleteEntry(userId: String): Int
}
\ No newline at end of file
diff --git a/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/GenericsHelper.kt b/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/GenericsHelper.kt
new file mode 100644
index 0000000..9b112c2
--- /dev/null
+++ b/app/src/main/java/com/appttude/h_mal/atlas_weather/helper/GenericsHelper.kt
@@ -0,0 +1,51 @@
+package com.appttude.h_mal.atlas_weather.helper
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import java.lang.reflect.ParameterizedType
+import kotlin.reflect.KClass
+
+object GenericsHelper {
+ @Suppress("UNCHECKED_CAST")
+ fun Any.getGenericClassAt(position: Int): KClass =
+ ((javaClass.genericSuperclass as? ParameterizedType)
+ ?.actualTypeArguments?.getOrNull(position) as? Class)
+ ?.kotlin
+ ?: throw IllegalStateException("Can not find class from generic argument")
+
+// /**
+// * Create a view binding out of the the generic [VB]
+// *
+// * @sample inflateBindingByType(getGenericClassAt(0), layoutInflater)
+// */
+// fun inflateBindingByType(
+// genericClassAt: KClass,
+// layoutInflater: LayoutInflater
+// ): VB = try {
+// @Suppress("UNCHECKED_CAST")
+//
+// genericClassAt.java.methods.first { viewBinding ->
+// viewBinding.parameterTypes.size == 1
+// && viewBinding.parameterTypes.getOrNull(0) == LayoutInflater::class.java
+// }.invoke(null, layoutInflater) as VB
+// } catch (exception: Exception) {
+// println ("generic class failed at = $genericClassAt")
+// exception.printStackTrace()
+// throw IllegalStateException("Can not inflate binding from generic")
+// }
+//
+// fun LayoutInflater.inflateBindingByType(
+// container: ViewGroup?,
+// genericClassAt: KClass
+// ): VB = try {
+// @Suppress("UNCHECKED_CAST")
+// genericClassAt.java.methods.first { inflateFun ->
+// inflateFun.parameterTypes.size == 3
+// && inflateFun.parameterTypes.getOrNull(0) == LayoutInflater::class.java
+// && inflateFun.parameterTypes.getOrNull(1) == ViewGroup::class.java
+// && inflateFun.parameterTypes.getOrNull(2) == Boolean::class.java
+// }.invoke(null, this, container, false) as VB
+// } catch (exception: Exception) {
+// throw IllegalStateException("Can not inflate binding from generic")
+// }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 788c924..f1273d9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,36 +1,26 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
buildscript {
-
ext {
- kotlin_version = '1.3.72'
+ kotlin_version = '1.5.20'
}
repositories {
- google()
- jcenter()
+ maven { url "https://www.jitpack.io" }
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.1'
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
-
- def nav_version = "2.3.0"
- classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
+ classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.4.1")
+ classpath ('com.android.tools.build:gradle:7.2.2')
+ classpath ("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20")
}
}
-allprojects {
- repositories {
- jcenter()
- google()
- maven {
- url 'https://maven.tomtom.com:8443/nexus/content/repositories/releases/'
- }
- }
+plugins {
+ id 'com.android.application' version '7.2.2' apply false
+ id 'com.android.library' version '7.2.2' apply false
+ id 'com.google.gms.google-services' version '4.3.15' apply false
+ id 'androidx.navigation.safeargs.kotlin' version '2.4.0' apply false
+ id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
-}
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 97041a8..fc047aa 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Dec 07 02:26:23 AEST 2018
+#Wed Jul 26 10:14:37 BST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/settings.gradle b/settings.gradle
index e7b4def..d17fa93 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,22 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ jcenter()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://www.jitpack.io" }
+ jcenter()
+ maven {
+ url "https://repositories.tomtom.com/artifactory/maps-sdk-legacy-android"
+ }
+ }
+}
+rootProject.name = "Driver"
include ':app'