24
.github/workflows/check.yml
vendored
24
.github/workflows/check.yml
vendored
@@ -20,12 +20,32 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
run: ./gradlew check coverageReport sonar --no-configuration-cache
|
||||
run: ./gradlew check coverageReport sonar --no-configuration-cache --no-daemon
|
||||
|
||||
- name: Publish test reports
|
||||
uses: mikepenz/action-junit-report@v3
|
||||
if: always() # always run even if the previous step fails
|
||||
with:
|
||||
report_paths: './**/build/test-results/**/TEST-*.xml'
|
||||
|
||||
uiTests:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Run tests
|
||||
run: ./gradlew allDevicesCheck --no-daemon -Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||
6
.github/workflows/sign.yml
vendored
6
.github/workflows/sign.yml
vendored
@@ -21,13 +21,15 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Sign APK
|
||||
env:
|
||||
MEALIENT_KEY_STORE: ${{ secrets.MEALIENT_KEY_STORE }}
|
||||
MEALIENT_KEY_STORE_PASSWORD: ${{ secrets.MEALIENT_KEY_STORE_PASSWORD }}
|
||||
MEALIENT_KEY_ALIAS: ${{ secrets.MEALIENT_KEY_ALIAS }}
|
||||
MEALIENT_KEY_PASSWORD: ${{ secrets.MEALIENT_KEY_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
APPSWEEP_API_KEY: ${{ secrets.APPSWEEP_API_KEY }}
|
||||
run: |
|
||||
@@ -36,7 +38,7 @@ jobs:
|
||||
echo "storePassword=$MEALIENT_KEY_STORE_PASSWORD" >> keystore.properties
|
||||
echo "keyAlias=$MEALIENT_KEY_ALIAS" >> keystore.properties
|
||||
echo "keyPassword=$MEALIENT_KEY_PASSWORD" >> keystore.properties
|
||||
./gradlew build bundle coverageReport sonar uploadToAppSweepRelease --no-configuration-cache
|
||||
./gradlew build bundle coverageReport sonar uploadToAppSweepRelease --no-configuration-cache --no-daemon
|
||||
cp app/build/outputs/apk/release/*.apk mealient-release.apk
|
||||
cp app/build/outputs/bundle/release/*.aab mealient-release.aab
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
import java.io.FileInputStream
|
||||
import java.util.*
|
||||
|
||||
@@ -17,6 +18,8 @@ android {
|
||||
applicationId = "gq.kirmanak.mealient"
|
||||
versionCode = 25
|
||||
versionName = "0.3.10"
|
||||
testInstrumentationRunner = "gq.kirmanak.mealient.MealientTestRunner"
|
||||
testInstrumentationRunnerArguments += mapOf("clearPackageData" to "true")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -54,6 +57,22 @@ android {
|
||||
packagingOptions {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
}
|
||||
|
||||
testOptions {
|
||||
managedDevices {
|
||||
devices {
|
||||
maybeCreate<ManagedVirtualDevice>("pixel2api30").apply {
|
||||
device = "Pixel 2"
|
||||
apiLevel = 30
|
||||
systemImageSource = "aosp-atd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -89,6 +108,8 @@ dependencies {
|
||||
kapt(libs.google.dagger.hiltCompiler)
|
||||
kaptTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
testImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
kaptAndroidTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
androidTestImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
|
||||
implementation(libs.androidx.paging.runtimeKtx)
|
||||
testImplementation(libs.androidx.paging.commonKtx)
|
||||
@@ -122,4 +143,13 @@ dependencies {
|
||||
testImplementation(libs.io.mockk)
|
||||
|
||||
debugImplementation(libs.squareup.leakcanary)
|
||||
|
||||
androidTestImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.test.junit)
|
||||
androidTestImplementation(libs.kaspersky.kaspresso)
|
||||
androidTestImplementation(libs.okhttp3.mockwebserver)
|
||||
androidTestImplementation(libs.androidx.test.core)
|
||||
androidTestImplementation(libs.androidx.test.rules)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
androidTestUtil(libs.androidx.test.orchestrator)
|
||||
}
|
||||
4
app/src/androidTest/AndroidManifest.xml
Normal file
4
app/src/androidTest/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Needed for screenshots in Kaspresso -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,33 @@
|
||||
package gq.kirmanak.mealient
|
||||
|
||||
import androidx.test.ext.junit.rules.activityScenarioRule
|
||||
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import gq.kirmanak.mealient.ui.activity.MainActivity
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
|
||||
abstract class BaseTestCase : TestCase() {
|
||||
|
||||
@get:Rule(order = 0)
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val mainActivityRule = activityScenarioRule<MainActivity>()
|
||||
|
||||
lateinit var mockWebServer: MockWebServer
|
||||
|
||||
@Before
|
||||
open fun setUp() {
|
||||
mockWebServer = MockWebServer()
|
||||
mockWebServer.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockWebServer.shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package gq.kirmanak.mealient
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import gq.kirmanak.mealient.screen.BaseUrlScreen
|
||||
import gq.kirmanak.mealient.screen.DisclaimerScreen
|
||||
import gq.kirmanak.mealient.screen.RecipesListScreen
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class FirstSetUpTest : BaseTestCase() {
|
||||
|
||||
@Before
|
||||
fun dispatchUrls() {
|
||||
mockWebServer.dispatch { url, _ ->
|
||||
if (url == "/api/app/about") versionV1Response else notFoundResponse
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test() = run {
|
||||
step("Ensure button is disabled") {
|
||||
DisclaimerScreen {
|
||||
okayButton {
|
||||
isVisible()
|
||||
isDisabled()
|
||||
hasAnyText()
|
||||
}
|
||||
|
||||
disclaimerText {
|
||||
isVisible()
|
||||
hasText(R.string.fragment_disclaimer_main_text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
step("Close disclaimer screen") {
|
||||
DisclaimerScreen {
|
||||
okayButton {
|
||||
isVisible()
|
||||
isEnabled()
|
||||
hasText(R.string.fragment_disclaimer_button_okay)
|
||||
click()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
step("Enter mock server address and click proceed") {
|
||||
BaseUrlScreen {
|
||||
progressBar {
|
||||
isGone()
|
||||
}
|
||||
urlInput {
|
||||
isVisible()
|
||||
edit.replaceText(mockWebServer.url("/").toString())
|
||||
hasHint(R.string.fragment_authentication_input_hint_url)
|
||||
}
|
||||
proceedButton {
|
||||
isVisible()
|
||||
isEnabled()
|
||||
hasText(R.string.fragment_base_url_save)
|
||||
click()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
step("Check that empty list of recipes is shown") {
|
||||
RecipesListScreen {
|
||||
emptyListText {
|
||||
isVisible()
|
||||
hasText(R.string.fragment_recipes_list_no_recipes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package gq.kirmanak.mealient
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
class MealientTestRunner : AndroidJUnitRunner() {
|
||||
|
||||
override fun newApplication(
|
||||
cl: ClassLoader?,
|
||||
className: String?,
|
||||
context: Context?,
|
||||
): Application = super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package gq.kirmanak.mealient
|
||||
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
|
||||
val versionV1Response = MockResponse().setResponseCode(200).setBody(
|
||||
"""{"production":true,"version":"v1.0.0beta-5","demoStatus":false,"allowSignup":true}"""
|
||||
)
|
||||
|
||||
val notFoundResponse = MockResponse().setResponseCode(404).setBody("""{"detail":"Not found"}"""")
|
||||
|
||||
fun MockWebServer.dispatch(block: (String, RecordedRequest) -> MockResponse) {
|
||||
dispatcher = object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
return block(request.path.orEmpty(), request)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package gq.kirmanak.mealient.screen
|
||||
|
||||
import com.kaspersky.kaspresso.screens.KScreen
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.ui.baseurl.BaseURLFragment
|
||||
import io.github.kakaocup.kakao.edit.KTextInputLayout
|
||||
import io.github.kakaocup.kakao.progress.KProgressBar
|
||||
import io.github.kakaocup.kakao.text.KButton
|
||||
|
||||
object BaseUrlScreen : KScreen<BaseUrlScreen>() {
|
||||
override val layoutId = R.layout.fragment_base_url
|
||||
override val viewClass = BaseURLFragment::class.java
|
||||
|
||||
val urlInput = KTextInputLayout { withId(R.id.url_input_layout) }
|
||||
|
||||
val proceedButton = KButton { withId(R.id.button) }
|
||||
|
||||
val progressBar = KProgressBar { withId(R.id.progress)}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package gq.kirmanak.mealient.screen
|
||||
|
||||
import com.kaspersky.kaspresso.screens.KScreen
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.ui.disclaimer.DisclaimerFragment
|
||||
import io.github.kakaocup.kakao.text.KButton
|
||||
import io.github.kakaocup.kakao.text.KTextView
|
||||
|
||||
object DisclaimerScreen : KScreen<DisclaimerScreen>() {
|
||||
override val layoutId = R.layout.fragment_disclaimer
|
||||
override val viewClass = DisclaimerFragment::class.java
|
||||
|
||||
val okayButton = KButton { withId(R.id.okay) }
|
||||
|
||||
val disclaimerText = KTextView { withId(R.id.main_text) }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package gq.kirmanak.mealient.screen
|
||||
|
||||
import com.kaspersky.kaspresso.screens.KScreen
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.ui.recipes.RecipesListFragment
|
||||
import io.github.kakaocup.kakao.text.KTextView
|
||||
|
||||
object RecipesListScreen : KScreen<RecipesListScreen>() {
|
||||
override val layoutId: Int = R.layout.fragment_recipes_list
|
||||
override val viewClass: Class<*> = RecipesListFragment::class.java
|
||||
|
||||
val emptyListText = KTextView { withId(R.id.empty_list_text) }
|
||||
}
|
||||
@@ -28,7 +28,8 @@ class BaseURLViewModel @Inject constructor(
|
||||
logger.v { "saveBaseUrl() called with: baseURL = $baseURL" }
|
||||
_uiState.value = OperationUiState.Progress()
|
||||
val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) }
|
||||
val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
|
||||
var url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
|
||||
url = url.trimStart().trimEnd { it == '/' || it.isWhitespace() }
|
||||
viewModelScope.launch { checkBaseURL(url) }
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
|
||||
object AuthImplTestData {
|
||||
const val TEST_USERNAME = "TEST_USERNAME"
|
||||
const val TEST_PASSWORD = "TEST_PASSWORD"
|
||||
const val TEST_BASE_URL = "https://example.com/"
|
||||
const val TEST_BASE_URL = "https://example.com"
|
||||
const val TEST_TOKEN = "TEST_TOKEN"
|
||||
const val TEST_AUTH_HEADER = "Bearer TEST_TOKEN"
|
||||
const val TEST_API_TOKEN = "TEST_API_TOKEN"
|
||||
|
||||
@@ -77,6 +77,12 @@ desugar = "1.2.2"
|
||||
kspPlugin = "1.7.20-1.0.7"
|
||||
# https://developer.android.com/jetpack/androidx/releases/sharetarget
|
||||
shareTarget = "1.2.0"
|
||||
# https://github.com/KasperskyLab/Kaspresso/releases
|
||||
kaspresso = "1.4.2"
|
||||
# https://developer.android.com/jetpack/androidx/releases/test
|
||||
androidXTest = "1.5.0"
|
||||
# https://developer.android.com/jetpack/androidx/releases/test
|
||||
androidXTestOrchestrator = "1.4.2"
|
||||
|
||||
[libraries]
|
||||
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
|
||||
@@ -133,6 +139,11 @@ androidx-room-testing = { group = "androidx.room", name = "room-testing", versio
|
||||
androidx-test-junit = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" }
|
||||
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
|
||||
|
||||
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidXTest" }
|
||||
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidXTest" }
|
||||
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidXTest" }
|
||||
androidx-test-orchestrator = { group = "androidx.test", name = "orchestrator", version.ref = "androidXTestOrchestrator" }
|
||||
|
||||
jakewharton-retrofitSerialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerialization" }
|
||||
|
||||
squareup-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||
@@ -142,6 +153,7 @@ squareup-leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-an
|
||||
okhttp3-bom = { group = "com.squareup.okhttp3", name = "okhttp-bom", version.ref = "okhttp" }
|
||||
okhttp3-okhttp = { group = "com.squareup.okhttp3", name = "okhttp" }
|
||||
okhttp3-loggingInterceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor" }
|
||||
okhttp3-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" }
|
||||
|
||||
bumptech-glide-glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" }
|
||||
bumptech-glide-okhttp3 = { group = "com.github.bumptech.glide", name = "okhttp3-integration", version.ref = "glide" }
|
||||
@@ -158,6 +170,8 @@ io-mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
|
||||
|
||||
chuckerteam-chucker = { group = "com.github.chuckerteam.chucker", name = "library", version.ref = "chucker" }
|
||||
|
||||
kaspersky-kaspresso = { group = "com.kaspersky.android-components", name = "kaspresso", version.ref = "kaspresso" }
|
||||
|
||||
[plugins]
|
||||
sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" }
|
||||
rootcoverage = { id = "nl.neotech.plugin.rootcoverage", version.ref = "rootCoverage" }
|
||||
|
||||
@@ -22,7 +22,7 @@ open class BaseUnitTest {
|
||||
val instantExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val timeoutRule: Timeout = Timeout.seconds(10)
|
||||
val timeoutRule: Timeout = Timeout.seconds(20)
|
||||
|
||||
protected val logger: Logger = FakeLogger()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user