From addff93e6f102808c7ad5bdff0fe6958ffabab12 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 20 Feb 2023 13:06:57 +0100 Subject: [PATCH 001/526] SIL-52: add github actions --- .github/workflows/run-unit-test.yml | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/run-unit-test.yml diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml new file mode 100644 index 000000000..a245d2956 --- /dev/null +++ b/.github/workflows/run-unit-test.yml @@ -0,0 +1,47 @@ +name: "Run unit tests" + +on: + pull_request: + push: + branches: + - 'develop' + - 'main' + +jobs: + unit-tests: + runs-on: ubuntu-20.04 + + steps: + - name: Cancel previous runs for the same branch + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'adopt' + cache: gradle + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + + - name: Gradle cache + uses: gradle/gradle-build-action@v2 + + - name: Run test + run: ./gradlew testDebugUnitTest + env: + GITHUB_USER: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Clean-up Gradle cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties From 3d8fd38e3bbb0d7e3733f291f8bca5a774b23508 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 21 Feb 2023 10:24:49 +0100 Subject: [PATCH 002/526] SIL-64: add network module and web exception --- app/build.gradle | 4 +++ .../com/appunite/loudius/common/Constants.kt | 6 ++++ .../com/appunite/loudius/di/NetworkModule.kt | 32 +++++++++++++++++++ .../appunite/loudius/network/WebException.kt | 19 +++++++++++ 4 files changed, 61 insertions(+) create mode 100644 app/src/main/java/com/appunite/loudius/common/Constants.kt create mode 100644 app/src/main/java/com/appunite/loudius/di/NetworkModule.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/WebException.kt diff --git a/app/build.gradle b/app/build.gradle index e55a81e29..2abb2d5f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,8 +83,12 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" //retrofit + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0' + //gson + implementation 'com.google.code.gson:gson:2.10.1' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt new file mode 100644 index 000000000..d60b782fe --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -0,0 +1,6 @@ +package com.appunite.loudius.common + +object Constants { + + const val BASE_URL = "https://api.github.com" +} diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt new file mode 100644 index 000000000..0ecb44481 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -0,0 +1,32 @@ +package com.appunite.loudius.di + +import com.appunite.loudius.common.Constants +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object NetworkModule { + + @Provides + @Singleton + fun provideRetrofit(gson: Gson, baseUrl: String): Retrofit = + Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + + @Provides + @Singleton + fun provideGson(): Gson = GsonBuilder().create() + + @Provides + fun provideBaseUrl() = Constants.BASE_URL +} diff --git a/app/src/main/java/com/appunite/loudius/network/WebException.kt b/app/src/main/java/com/appunite/loudius/network/WebException.kt new file mode 100644 index 000000000..95da4f2d4 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/WebException.kt @@ -0,0 +1,19 @@ +package com.appunite.loudius.network + +import java.io.IOException + +sealed class WebException : Exception() { + + /** + * Represents exception which comes from backend. In this project successful + * response (status code = 200) often comes with error response (status + * in response body is not equal to 200). + */ + data class UnknownError(val code: Int, override val message: String?) : WebException() + + /** + * Represents web exception which can be thrown during network communication. + * For example [IOException]. + */ + data class NetworkError(override val cause: Throwable? = null) : WebException() +} From 7593c353d722336d00944be01122f3d8f74cb1af Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 21 Feb 2023 10:27:59 +0100 Subject: [PATCH 003/526] SIL-64: code cleanup --- .../main/java/com/appunite/loudius/network/WebException.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/WebException.kt b/app/src/main/java/com/appunite/loudius/network/WebException.kt index 95da4f2d4..85162485f 100644 --- a/app/src/main/java/com/appunite/loudius/network/WebException.kt +++ b/app/src/main/java/com/appunite/loudius/network/WebException.kt @@ -5,9 +5,7 @@ import java.io.IOException sealed class WebException : Exception() { /** - * Represents exception which comes from backend. In this project successful - * response (status code = 200) often comes with error response (status - * in response body is not equal to 200). + * Represents exception which comes from backend. */ data class UnknownError(val code: Int, override val message: String?) : WebException() From 863b9e3e78b81f403175bf6b9d2b58e76511342a Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 21 Feb 2023 11:11:47 +0100 Subject: [PATCH 004/526] SIL-64: add handle api call --- .../appunite/loudius/network/ApiCallUtil.kt | 42 +++++++++++++++++++ .../loudius/network/RequestErrorParser.kt | 6 +++ .../appunite/loudius/network/StatusTracker.kt | 6 +++ 3 files changed, 54 insertions(+) create mode 100644 app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/RequestErrorParser.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/StatusTracker.kt diff --git a/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt new file mode 100644 index 000000000..14ea8c253 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt @@ -0,0 +1,42 @@ +package com.appunite.loudius.network + +import retrofit2.HttpException +import retrofit2.Response +import java.io.IOException + +suspend fun safeApiCall( + errorParser: RequestErrorParser = DefaultErrorParser, + apiCall: suspend () -> Response +): Result { + return try { + handleSuccessfulCall(apiCall, errorParser) + } catch (throwable: HttpException) { + Result.failure(WebException.UnknownError(throwable.code(), throwable.message())) + } catch (throwable: IOException) { + Result.failure(WebException.NetworkError(throwable)) + } +} + +private suspend fun handleSuccessfulCall( + apiCall: suspend () -> Response, + errorParser: RequestErrorParser +): Result { + val response = apiCall() + val body = response.body() + return if (response.isSuccessful && body != null) { + val status = body.status + if (status == 200) { + Result.success(body) + } else { + Result.failure(errorParser(status, body.message)) + } + } else { + Result.failure(errorParser(response.code(), response.message())) + } +} + +object DefaultErrorParser : RequestErrorParser { + + override fun invoke(responseCode: Int, responseMessage: String): Exception = + WebException.UnknownError(responseCode, responseMessage) +} diff --git a/app/src/main/java/com/appunite/loudius/network/RequestErrorParser.kt b/app/src/main/java/com/appunite/loudius/network/RequestErrorParser.kt new file mode 100644 index 000000000..0984f8cb9 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/RequestErrorParser.kt @@ -0,0 +1,6 @@ +package com.appunite.loudius.network + +interface RequestErrorParser { + + operator fun invoke(responseCode: Int, responseMessage: String): Exception +} diff --git a/app/src/main/java/com/appunite/loudius/network/StatusTracker.kt b/app/src/main/java/com/appunite/loudius/network/StatusTracker.kt new file mode 100644 index 000000000..1dbedc61e --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/StatusTracker.kt @@ -0,0 +1,6 @@ +package com.appunite.loudius.network + +interface StatusTracker { + val status: Int + val message: String +} From 528f8ec95a2c643962eec50f4bcb2562fc3a386e Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 21 Feb 2023 11:29:35 +0100 Subject: [PATCH 005/526] SIL-64: remove double check status 200 --- .../java/com/appunite/loudius/network/ApiCallUtil.kt | 11 +++-------- .../com/appunite/loudius/network/StatusTracker.kt | 6 ------ 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 app/src/main/java/com/appunite/loudius/network/StatusTracker.kt diff --git a/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt index 14ea8c253..7c9337469 100644 --- a/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt @@ -4,7 +4,7 @@ import retrofit2.HttpException import retrofit2.Response import java.io.IOException -suspend fun safeApiCall( +suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, apiCall: suspend () -> Response ): Result { @@ -17,19 +17,14 @@ suspend fun safeApiCall( } } -private suspend fun handleSuccessfulCall( +private suspend fun handleSuccessfulCall( apiCall: suspend () -> Response, errorParser: RequestErrorParser ): Result { val response = apiCall() val body = response.body() return if (response.isSuccessful && body != null) { - val status = body.status - if (status == 200) { - Result.success(body) - } else { - Result.failure(errorParser(status, body.message)) - } + Result.success(body) } else { Result.failure(errorParser(response.code(), response.message())) } diff --git a/app/src/main/java/com/appunite/loudius/network/StatusTracker.kt b/app/src/main/java/com/appunite/loudius/network/StatusTracker.kt deleted file mode 100644 index 1dbedc61e..000000000 --- a/app/src/main/java/com/appunite/loudius/network/StatusTracker.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.appunite.loudius.network - -interface StatusTracker { - val status: Int - val message: String -} From abf110153e7e3aa6290cb497d1716664114883b1 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 21 Feb 2023 12:20:46 +0100 Subject: [PATCH 006/526] SIL-64: safe api call --- .../appunite/loudius/network/ApiCallUtil.kt | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt index 7c9337469..8a87d4888 100644 --- a/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt @@ -1,35 +1,22 @@ package com.appunite.loudius.network import retrofit2.HttpException -import retrofit2.Response import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, - apiCall: suspend () -> Response + apiCall: suspend () -> T ): Result { return try { - handleSuccessfulCall(apiCall, errorParser) + val response = apiCall() + Result.success(response) } catch (throwable: HttpException) { - Result.failure(WebException.UnknownError(throwable.code(), throwable.message())) + Result.failure(errorParser(throwable.code(), throwable.message())) } catch (throwable: IOException) { Result.failure(WebException.NetworkError(throwable)) } } -private suspend fun handleSuccessfulCall( - apiCall: suspend () -> Response, - errorParser: RequestErrorParser -): Result { - val response = apiCall() - val body = response.body() - return if (response.isSuccessful && body != null) { - Result.success(body) - } else { - Result.failure(errorParser(response.code(), response.message())) - } -} - object DefaultErrorParser : RequestErrorParser { override fun invoke(responseCode: Int, responseMessage: String): Exception = From d3af10226c69d2082a59b6c1ac52e3ec1d4cb918 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 21 Feb 2023 13:23:03 +0100 Subject: [PATCH 007/526] Add Loudius top app bar --- .../loudius/ui/components/LoudiusTopAppBar.kt | 54 +++++++++++++++++++ .../com/appunite/loudius/ui/theme/Color.kt | 4 ++ .../com/appunite/loudius/ui/theme/Theme.kt | 7 ++- .../com/appunite/loudius/ui/theme/Type.kt | 4 +- app/src/main/res/drawable/arrow_back.xml | 5 ++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt create mode 100644 app/src/main/res/drawable/arrow_back.xml diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt new file mode 100644 index 000000000..f4f802d7e --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -0,0 +1,54 @@ +package com.appunite.loudius.ui.components + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.appunite.loudius.R +import com.appunite.loudius.ui.theme.LoudiusTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoudiusTopAppBar( + title: String, + onClickBackArrow: () -> Unit +) { + TopAppBar( + title = { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = { onClickBackArrow() }) { + Icon( + painter = painterResource(id = R.drawable.arrow_back), + contentDescription = stringResource(R.string.back_button) + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ), + ) +} + +@Preview +@Composable +fun LoudiusTopAppBar() { + LoudiusTheme { + LoudiusTopAppBar( + onClickBackArrow = {}, + title = "Loudius" + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt index a19761d95..3c3cbc803 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt @@ -9,3 +9,7 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) + +val White99 = Color(0xFFFFFBFE) +val Black90 = Color(0xFF1C1B1F) +val PurpleBlack30 = Color(0xFF49454F) diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index 1fc95a266..ccf522574 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -24,11 +25,13 @@ private val DarkColorScheme = darkColorScheme( private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, - tertiary = Pink40 + tertiary = Pink40, + surface = White99, + onSurface = Black90, + onSurfaceVariant = PurpleBlack30 /* Other default colors to override background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), onPrimary = Color.White, onSecondary = Color.White, onTertiary = Color.White, diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt index fde8f14db..27f1bb96b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt @@ -14,8 +14,7 @@ val Typography = Typography( fontSize = 16.sp, lineHeight = 24.sp, letterSpacing = 0.5.sp - ) - /* Other default text styles to override + ), titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, @@ -23,6 +22,7 @@ val Typography = Typography( lineHeight = 28.sp, letterSpacing = 0.sp ), + /* Other default text styles to override labelSmall = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Medium, diff --git a/app/src/main/res/drawable/arrow_back.xml b/app/src/main/res/drawable/arrow_back.xml new file mode 100644 index 000000000..8452791cf --- /dev/null +++ b/app/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb6270aee..5a928c477 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Loudius + Back button From f76959c22738100252b4f96c814a82619beb65aa Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 22 Feb 2023 11:35:57 +0100 Subject: [PATCH 008/526] Use function reference --- .../java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index f4f802d7e..83cc1d8b8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -29,7 +29,7 @@ fun LoudiusTopAppBar( ) }, navigationIcon = { - IconButton(onClick = { onClickBackArrow() }) { + IconButton(onClick = onClickBackArrow) { Icon( painter = painterResource(id = R.drawable.arrow_back), contentDescription = stringResource(R.string.back_button) From c284393d83df844374e21e6aa71bbd93734cdf92 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 22 Feb 2023 15:18:51 +0100 Subject: [PATCH 009/526] SIL-57: login api call --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 ++ .../java/com/appunite/loudius/MainActivity.kt | 21 +++--------- .../com/appunite/loudius/common/Constants.kt | 2 ++ .../com/appunite/loudius/di/GithubModule.kt | 34 +++++++++++++++++++ .../loudius/domain/GithubRepository.kt | 10 ++++++ .../loudius/domain/GithubRepositoryImpl.kt | 16 +++++++++ .../com/appunite/loudius/network/GithubApi.kt | 27 +++++++++++++++ .../loudius/network/GithubDataSource.kt | 25 ++++++++++++++ .../loudius/network/model/AccessToken.kt | 9 +++++ .../network/{ => utils}/ApiCallUtil.kt | 2 +- .../network/{ => utils}/RequestErrorParser.kt | 2 +- .../network/{ => utils}/WebException.kt | 2 +- .../loudius/presentation/login/LoginScreen.kt | 28 +++++++++++++++ .../presentation/login/LoginViewModel.kt | 26 ++++++++++++++ 15 files changed, 188 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/di/GithubModule.kt create mode 100644 app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt create mode 100644 app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/GithubApi.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt rename app/src/main/java/com/appunite/loudius/network/{ => utils}/ApiCallUtil.kt (94%) rename app/src/main/java/com/appunite/loudius/network/{ => utils}/RequestErrorParser.kt (72%) rename app/src/main/java/com/appunite/loudius/network/{ => utils}/WebException.kt (91%) create mode 100644 app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt create mode 100644 app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index 2abb2d5f5..d7c21eb7f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -73,6 +73,7 @@ dependencies { //DI - Hilt implementation "com.google.dagger:hilt-android:2.45" kapt "com.google.dagger:hilt-compiler:2.45" + implementation "androidx.hilt:hilt-navigation-compose:1.0.0" //DI - for local unit tests testImplementation 'com.google.dagger:hilt-android-testing:2.45' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b0719e33d..2bf8cfad9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + + + suspend fun authorize(): Result +} diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt new file mode 100644 index 000000000..abebb5de0 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt @@ -0,0 +1,16 @@ +package com.appunite.loudius.domain + +import com.appunite.loudius.network.GithubDataSource +import com.appunite.loudius.network.model.AccessToken +import javax.inject.Inject + +class GithubRepositoryImpl @Inject constructor( + private val githubDataSource: GithubDataSource +): GithubRepository { + + override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = + githubDataSource.getAccessToken(clientId, clientSecret, code) + + override suspend fun authorize(): Result = + githubDataSource.authorize() +} diff --git a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt new file mode 100644 index 000000000..d3e244c20 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt @@ -0,0 +1,27 @@ +package com.appunite.loudius.network + +import com.appunite.loudius.common.Constants.CLIENT_ID +import com.appunite.loudius.network.model.AccessToken +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST +import retrofit2.http.Query + +interface GithubApi { + + @GET("login/oauth/authorize") + suspend fun authorize( + @Query("client_id") clientId: String = CLIENT_ID + ): String + + @Headers("Accept: application/json") + @POST("login/oauth/access_token") + @FormUrlEncoded + suspend fun getAccessToken( + @Field("client_id") clientId: String, + @Field("client_secret") clientSecret: String, + @Field("code") code: String + ): AccessToken +} diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt new file mode 100644 index 000000000..6062c7536 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -0,0 +1,25 @@ +package com.appunite.loudius.network + +import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.utils.safeApiCall +import javax.inject.Inject +import javax.inject.Singleton + +interface GithubDataSource { + + suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result + + suspend fun authorize(): Result +} + +@Singleton +class GithubNetworkDataSource @Inject constructor( + private val api: GithubApi +): GithubDataSource { + + override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = + safeApiCall { api.getAccessToken(clientId, clientSecret, code) } + + override suspend fun authorize(): Result = + safeApiCall { api.authorize() } +} diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt new file mode 100644 index 000000000..deb7c2e13 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt @@ -0,0 +1,9 @@ +package com.appunite.loudius.network.model + +import com.google.gson.annotations.SerializedName + +data class AccessToken( + + @SerializedName("access_token") + val accessToken: String +) diff --git a/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt similarity index 94% rename from app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt rename to app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index 8a87d4888..dddbfe466 100644 --- a/app/src/main/java/com/appunite/loudius/network/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.network +package com.appunite.loudius.network.utils import retrofit2.HttpException import java.io.IOException diff --git a/app/src/main/java/com/appunite/loudius/network/RequestErrorParser.kt b/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt similarity index 72% rename from app/src/main/java/com/appunite/loudius/network/RequestErrorParser.kt rename to app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt index 0984f8cb9..388cc4b5b 100644 --- a/app/src/main/java/com/appunite/loudius/network/RequestErrorParser.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.network +package com.appunite.loudius.network.utils interface RequestErrorParser { diff --git a/app/src/main/java/com/appunite/loudius/network/WebException.kt b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt similarity index 91% rename from app/src/main/java/com/appunite/loudius/network/WebException.kt rename to app/src/main/java/com/appunite/loudius/network/utils/WebException.kt index 85162485f..100771b25 100644 --- a/app/src/main/java/com/appunite/loudius/network/WebException.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.network +package com.appunite.loudius.network.utils import java.io.IOException diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt new file mode 100644 index 000000000..47a9be478 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt @@ -0,0 +1,28 @@ +package com.appunite.loudius.presentation.login + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel + +@Composable +fun LoginScreen( + viewModel: LoginViewModel = hiltViewModel() +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = viewModel::onLoginClick + ) { + Text(text = "Login") + } + } +} diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt new file mode 100644 index 000000000..5748c1553 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt @@ -0,0 +1,26 @@ +package com.appunite.loudius.presentation.login + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.appunite.loudius.common.Constants.CLIENT_ID +import com.appunite.loudius.domain.GithubRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val githubRepository: GithubRepository +): ViewModel() { + + fun onLoginClick() { + viewModelScope.launch { + githubRepository.authorize().onSuccess { code -> + githubRepository.getAccessToken(CLIENT_ID,"", code).onSuccess { token -> //TODO add client secret + Log.i("access_token", token.accessToken) + } + } + } + } +} From a4dfdbacff37f929b1d1b1c4426ecacd00e6ba97 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 23 Feb 2023 09:18:43 +0100 Subject: [PATCH 010/526] Implement DetailsScreen.kt UI. --- .../appunite/loudius/domain/model/Reviewer.kt | 8 + .../com/appunite/loudius/ui/DetailsScreen.kt | 141 ++++++++++++++++++ .../com/appunite/loudius/ui/theme/Color.kt | 3 + .../com/appunite/loudius/ui/theme/Theme.kt | 5 +- .../loudius/ui/utils/BottomBorderModifier.kt | 28 ++++ .../main/res/drawable/person_outline_24px.xml | 10 ++ app/src/main/res/values/strings.xml | 4 + 7 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt create mode 100644 app/src/main/res/drawable/person_outline_24px.xml diff --git a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt new file mode 100644 index 000000000..6189cdf5c --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt @@ -0,0 +1,8 @@ +package com.appunite.loudius.domain.model + +data class Reviewer( + val name: String, + val isReviewDone: Boolean, + val hoursFromPRStart: Int, + val hoursFromReviewDone: Int?, +) diff --git a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt new file mode 100644 index 000000000..f46ff13db --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt @@ -0,0 +1,141 @@ +package com.appunite.loudius.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.appunite.loudius.R +import com.appunite.loudius.domain.model.Reviewer +import com.appunite.loudius.ui.components.LoudiusTopAppBar +import com.appunite.loudius.ui.utils.bottomBorder + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DetailsScreenStateless(reviewers: List) { + Scaffold( + topBar = { LoudiusTopAppBar(onClickBackArrow = {}, title = "TODO") }, + content = { padding -> + DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) + }, + modifier = Modifier.background(MaterialTheme.colorScheme.surface) + ) +} + +@Composable +private fun DetailsScreenContent(reviewers: List, modifier: Modifier) { + LazyColumn( + modifier = modifier.fillMaxWidth() + ) { + itemsIndexed(reviewers) { index, reviewer -> + val backgroundColor = + if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface + ReviewerView(reviewer = reviewer, backgroundColor = backgroundColor) {} + } + } +} + + +@Composable +private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(backgroundColor) + .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) + .padding(16.dp) + ) { + ReviewerAvatar(Modifier.align(CenterVertically)) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + .align(CenterVertically) + ) { + IsReviewedHeadlineText(reviewer) + ReviewerName(reviewer) + } + NotifyButton(onNotifyClick, Modifier.align(CenterVertically)) + } +} + +@Composable +private fun ReviewerAvatar(modifier: Modifier = Modifier) { + Image( + painter = painterResource(id = R.drawable.person_outline_24px), + contentDescription = stringResource( + R.string.details_screen_user_image_description + ), + modifier = modifier + ) +} + +@Composable +private fun IsReviewedHeadlineText(reviewer: Reviewer) { + Text( + text = if (reviewer.isReviewDone) stringResource( + id = R.string.details_reviewed, + reviewer.hoursFromReviewDone ?: 0 + ) else stringResource( + id = R.string.details_not_reviewed, + reviewer.hoursFromPRStart + ), + style = MaterialTheme.typography.labelMedium, + color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error + ) +} + +@Composable +private fun ReviewerName(reviewer: Reviewer) { + Text( + text = reviewer.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) +} + +@Composable +private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifier) { + OutlinedButton(onClick = onNotifyClick, modifier = modifier) { + Text( + text = stringResource(R.string.details_notify), + color = MaterialTheme.colorScheme.onSurface + ) + } +} + +@Preview +@Composable +private fun ReviewerViewPreview() { + ReviewerView( + reviewer = Reviewer("Kezc", true, 12, 12), + backgroundColor = MaterialTheme.colorScheme.onSurface + ) {} +} + +@Preview +@Composable +fun DetailsScreenPreview() { + val reviewer1 = Reviewer("Kezc", true, 24, 12) + val reviewer2 = Reviewer("Krzysiudan", false, 24, 0) + val reviewer3 = Reviewer("Weronika", false, 24, 0) + val reviewer4 = Reviewer("Jacek", false, 24, 0) + val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) + DetailsScreenStateless(reviewers = reviewers) +} diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt index 3c3cbc803..a62f4449b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt @@ -13,3 +13,6 @@ val Pink40 = Color(0xFF7D5260) val White99 = Color(0xFFFFFBFE) val Black90 = Color(0xFF1C1B1F) val PurpleBlack30 = Color(0xFF49454F) + +val NeutralVariant30 = Color(0xFF49454F) +val Error40 = Color(0xFFB3261E) diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index ccf522574..7e8abc6e8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -28,7 +27,9 @@ private val LightColorScheme = lightColorScheme( tertiary = Pink40, surface = White99, onSurface = Black90, - onSurfaceVariant = PurpleBlack30 + onSurfaceVariant = PurpleBlack30, + outlineVariant = NeutralVariant30, + error = Error40, /* Other default colors to override background = Color(0xFFFFFBFE), diff --git a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt new file mode 100644 index 000000000..c7c8dbe1d --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt @@ -0,0 +1,28 @@ +package com.appunite.loudius.ui.utils + +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed( + factory = { + val density = LocalDensity.current + val strokeWidthPx = density.run { strokeWidth.toPx() } + + Modifier.drawBehind { + val width = size.width + val height = size.height - strokeWidthPx / 2 + + drawLine( + color = color, + start = Offset(x = 0f, y = height), + end = Offset(x = width, y = height), + strokeWidth = strokeWidthPx + ) + } + } +) diff --git a/app/src/main/res/drawable/person_outline_24px.xml b/app/src/main/res/drawable/person_outline_24px.xml new file mode 100644 index 000000000..beb6560c2 --- /dev/null +++ b/app/src/main/res/drawable/person_outline_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a928c477..57279d578 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,8 @@ Loudius Back button + User image + Notify + Reviewed %d h ago. + Not reviewed for %d h. From ae15341734542748e25cf9a19eaed2f327733f6f Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk <33498031+Krzysiudan@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:20:31 +0100 Subject: [PATCH 011/526] Create github workflow file run-code-quality-check. --- .github/workflows/run-code-quality-check.yml | 83 ++++++++++++++++++++ .gitignore | 3 + .mega-linter.yml | 5 ++ app/build.gradle | 1 + 4 files changed, 92 insertions(+) create mode 100644 .github/workflows/run-code-quality-check.yml create mode 100644 .mega-linter.yml diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml new file mode 100644 index 000000000..b606b2192 --- /dev/null +++ b/.github/workflows/run-code-quality-check.yml @@ -0,0 +1,83 @@ +--- +# MegaLinter GitHub Action configuration file +# More info at https://megalinter.github.io +name: MegaLinter + +on: + push: + pull_request: + branches: [ master, main, develop ] + +env: + APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) + APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all) + APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request) + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +permissions: write-all + +jobs: + build: + name: MegaLinter + runs-on: ubuntu-latest + steps: + # Git Checkout + - name: Checkout Code + uses: actions/checkout@v3 + with: + token: ${{secrets.PAT || secrets.GITHUB_TOKEN }} + fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances + + # MegaLinter + - name: MegaLinter + id: ml + # You can override MegaLinter flavor used to have faster performances + # More info at https://megalinter.github.io/flavors/ + uses: oxsecurity/megalinter@v6 + env: + # All available variables are described in documentation + # https://megalinter.github.io/configuration/ + VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY + # DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks + + # Upload MegaLinter artifacts + - name: Archive production artifacts + if: ${{ success() }} || ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: MegaLinter reports + path: | + megalinter-reports + mega-linter.log + + # Create pull request if applicable (for now works only on PR from same repository, not from forks) + - name: Create Pull Request with applied fixes + id: cpr + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + commit-message: "[MegaLinter] Apply linters automatic fixes" + title: "[MegaLinter] Apply linters automatic fixes" + labels: bot + - name: Create PR output + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + run: | + echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" + echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" + + # Push new commit if applicable (for now works only on PR from same repository, not from forks) + - name: Prepare commit + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + run: sudo chown -Rc $UID .git/ + - name: Commit and push applied linter fixes + if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') + uses: stefanzweifel/git-auto-commit-action@v4 + with: + branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} + commit_message: "[MegaLinter] Apply linters fixes" diff --git a/.gitignore b/.gitignore index 101a73e30..bca17a291 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ google-services.json # Android Profiling *.hprof + +# Mega-linter reports +megalinter-reports/ diff --git a/.mega-linter.yml b/.mega-linter.yml new file mode 100644 index 000000000..3bb1edd12 --- /dev/null +++ b/.mega-linter.yml @@ -0,0 +1,5 @@ +# Configuration file for MegaLinter +# See all available variables at https://megalinter.io/configuration/ and in linters documentation + +DISABLE_LINTERS: + - SPELL_CSPELL diff --git a/app/build.gradle b/app/build.gradle index e55a81e29..dde3681f4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,3 +1,4 @@ + plugins { id 'kotlin-kapt' id 'com.android.application' From a20c599b3c418f736af6a28ff08466a2b088100d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 21 Feb 2023 14:05:02 +0100 Subject: [PATCH 012/526] Implement github action workflow for code quality check with mega-linter. --- .github/workflows/run-code-quality-check.yml | 3 ++- .mega-linter.yml | 1 + app/build.gradle | 3 --- github_conf/branch_protection_rules.json | 4 ++++ 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 github_conf/branch_protection_rules.json diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml index b606b2192..562e1b081 100644 --- a/.github/workflows/run-code-quality-check.yml +++ b/.github/workflows/run-code-quality-check.yml @@ -6,7 +6,7 @@ name: MegaLinter on: push: pull_request: - branches: [ master, main, develop ] + branches: [ main,develop ] env: APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) @@ -65,6 +65,7 @@ jobs: commit-message: "[MegaLinter] Apply linters automatic fixes" title: "[MegaLinter] Apply linters automatic fixes" labels: bot + base: ${{ github.head_ref }} - name: Create PR output if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix') run: | diff --git a/.mega-linter.yml b/.mega-linter.yml index 3bb1edd12..071d16777 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -3,3 +3,4 @@ DISABLE_LINTERS: - SPELL_CSPELL + - GROOVY_NPM_GROOVY_LINT diff --git a/app/build.gradle b/app/build.gradle index dde3681f4..aeff5194c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,3 @@ - plugins { id 'kotlin-kapt' id 'com.android.application' @@ -51,8 +50,6 @@ android { } dependencies { - - //Base android deps implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' diff --git a/github_conf/branch_protection_rules.json b/github_conf/branch_protection_rules.json new file mode 100644 index 000000000..3a24768fb --- /dev/null +++ b/github_conf/branch_protection_rules.json @@ -0,0 +1,4 @@ +{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest" +} From 82c2a10b2265848491439738864bde7be4787020 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 23 Feb 2023 12:13:42 +0000 Subject: [PATCH 013/526] [MegaLinter] Apply linters fixes --- .github/workflows/run-code-quality-check.yml | 2 +- github_conf/branch_protection_rules.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml index 562e1b081..404526d41 100644 --- a/.github/workflows/run-code-quality-check.yml +++ b/.github/workflows/run-code-quality-check.yml @@ -6,7 +6,7 @@ name: MegaLinter on: push: pull_request: - branches: [ main,develop ] + branches: [main, develop] env: APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool) diff --git a/github_conf/branch_protection_rules.json b/github_conf/branch_protection_rules.json index 3a24768fb..8e614b55c 100644 --- a/github_conf/branch_protection_rules.json +++ b/github_conf/branch_protection_rules.json @@ -1,4 +1,4 @@ { - "message": "Not Found", - "documentation_url": "https://docs.github.com/rest" -} + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest" +} \ No newline at end of file From 673b2c9853e4e5adacaa010345150d2c77566258 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 23 Feb 2023 13:29:15 +0100 Subject: [PATCH 014/526] Extract resolveIsReviewedText function. --- .../com/appunite/loudius/ui/DetailsScreen.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt index f46ff13db..c2b11b69a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt @@ -28,9 +28,9 @@ import com.appunite.loudius.ui.utils.bottomBorder @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun DetailsScreenStateless(reviewers: List) { +private fun DetailsScreenStateless(topBarTitle: String, reviewers: List) { Scaffold( - topBar = { LoudiusTopAppBar(onClickBackArrow = {}, title = "TODO") }, + topBar = { LoudiusTopAppBar(onClickBackArrow = {}, title = topBarTitle) }, content = { padding -> DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) }, @@ -89,18 +89,19 @@ private fun ReviewerAvatar(modifier: Modifier = Modifier) { @Composable private fun IsReviewedHeadlineText(reviewer: Reviewer) { Text( - text = if (reviewer.isReviewDone) stringResource( - id = R.string.details_reviewed, - reviewer.hoursFromReviewDone ?: 0 - ) else stringResource( - id = R.string.details_not_reviewed, - reviewer.hoursFromPRStart - ), + text = resolveIsReviewedText(reviewer), style = MaterialTheme.typography.labelMedium, color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error ) } +@Composable +private fun resolveIsReviewedText(reviewer: Reviewer) = if (reviewer.isReviewDone) { + stringResource(id = R.string.details_reviewed, reviewer.hoursFromReviewDone ?: 0) +} else { + stringResource(id = R.string.details_not_reviewed, reviewer.hoursFromPRStart) +} + @Composable private fun ReviewerName(reviewer: Reviewer) { Text( @@ -137,5 +138,5 @@ fun DetailsScreenPreview() { val reviewer3 = Reviewer("Weronika", false, 24, 0) val reviewer4 = Reviewer("Jacek", false, 24, 0) val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) - DetailsScreenStateless(reviewers = reviewers) + DetailsScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers) } From 90cb6f038b130a3e337acbc3ed876d4ee660b193 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 23 Feb 2023 13:47:28 +0100 Subject: [PATCH 015/526] Extract resolveReviewerBackgroundColor function. --- .../com/appunite/loudius/ui/DetailsScreen.kt | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt index c2b11b69a..2d79ba10d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.ui.components.LoudiusTopAppBar +import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.ui.utils.bottomBorder @OptIn(ExperimentalMaterial3Api::class) @@ -44,13 +45,19 @@ private fun DetailsScreenContent(reviewers: List, modifier: Modifier) modifier = modifier.fillMaxWidth() ) { itemsIndexed(reviewers) { index, reviewer -> - val backgroundColor = - if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface - ReviewerView(reviewer = reviewer, backgroundColor = backgroundColor) {} + ReviewerView( + reviewer = reviewer, + backgroundColor = resolveReviewerBackgroundColor(index), + onNotifyClick = {} + ) } } } +@Composable +private fun resolveReviewerBackgroundColor(index: Int) = + if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface + @Composable private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { @@ -61,7 +68,7 @@ private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyCli .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) .padding(16.dp) ) { - ReviewerAvatar(Modifier.align(CenterVertically)) + ReviewerAvatarView(Modifier.align(CenterVertically)) Column( modifier = Modifier .weight(1f) @@ -76,7 +83,7 @@ private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyCli } @Composable -private fun ReviewerAvatar(modifier: Modifier = Modifier) { +private fun ReviewerAvatarView(modifier: Modifier = Modifier) { Image( painter = painterResource(id = R.drawable.person_outline_24px), contentDescription = stringResource( @@ -124,10 +131,12 @@ private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifie @Preview @Composable private fun ReviewerViewPreview() { - ReviewerView( - reviewer = Reviewer("Kezc", true, 12, 12), - backgroundColor = MaterialTheme.colorScheme.onSurface - ) {} + LoudiusTheme { + ReviewerView( + reviewer = Reviewer("Kezc", true, 12, 12), + backgroundColor = MaterialTheme.colorScheme.surface + ) {} + } } @Preview @@ -138,5 +147,7 @@ fun DetailsScreenPreview() { val reviewer3 = Reviewer("Weronika", false, 24, 0) val reviewer4 = Reviewer("Jacek", false, 24, 0) val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) - DetailsScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers) + LoudiusTheme { + DetailsScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers) + } } From 8c55bd645823ff326015d6fa6adb7c1e3f587a03 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 23 Feb 2023 12:51:51 +0000 Subject: [PATCH 016/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/DetailsScreen.kt | 23 ++- .../com/appunite/loudius/ui/theme/Theme.kt | 6 +- .../loudius/ui/utils/BottomBorderModifier.kt | 4 +- megalinter-reports/IDE-config.txt | 27 ++++ megalinter-reports/IDE-config/.checkov.yml | 6 + megalinter-reports/IDE-config/.gitleaks.toml | 21 +++ megalinter-reports/IDE-config/.jscpd.json | 22 +++ .../IDE-config/.secretlintrc.json | 7 + .../IDE-config/.vscode/extensions.json | 7 + .../com/appunite/loudius/ui/DetailsScreen.kt | 152 ++++++++++++++++++ .../com/appunite/loudius/ui/theme/Theme.kt | 72 +++++++++ .../loudius/ui/utils/BottomBorderModifier.kt | 28 ++++ 12 files changed, 358 insertions(+), 17 deletions(-) create mode 100644 megalinter-reports/IDE-config.txt create mode 100644 megalinter-reports/IDE-config/.checkov.yml create mode 100644 megalinter-reports/IDE-config/.gitleaks.toml create mode 100644 megalinter-reports/IDE-config/.jscpd.json create mode 100644 megalinter-reports/IDE-config/.secretlintrc.json create mode 100644 megalinter-reports/IDE-config/.vscode/extensions.json create mode 100644 megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt create mode 100644 megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt create mode 100644 megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt diff --git a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt index 2d79ba10d..00f52c49c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt @@ -35,20 +35,20 @@ private fun DetailsScreenStateless(topBarTitle: String, reviewers: List DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) }, - modifier = Modifier.background(MaterialTheme.colorScheme.surface) + modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) } @Composable private fun DetailsScreenContent(reviewers: List, modifier: Modifier) { LazyColumn( - modifier = modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth(), ) { itemsIndexed(reviewers) { index, reviewer -> ReviewerView( reviewer = reviewer, backgroundColor = resolveReviewerBackgroundColor(index), - onNotifyClick = {} + onNotifyClick = {}, ) } } @@ -58,7 +58,6 @@ private fun DetailsScreenContent(reviewers: List, modifier: Modifier) private fun resolveReviewerBackgroundColor(index: Int) = if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface - @Composable private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { Row( @@ -66,14 +65,14 @@ private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyCli .fillMaxWidth() .background(backgroundColor) .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) - .padding(16.dp) + .padding(16.dp), ) { ReviewerAvatarView(Modifier.align(CenterVertically)) Column( modifier = Modifier .weight(1f) .padding(start = 16.dp) - .align(CenterVertically) + .align(CenterVertically), ) { IsReviewedHeadlineText(reviewer) ReviewerName(reviewer) @@ -87,9 +86,9 @@ private fun ReviewerAvatarView(modifier: Modifier = Modifier) { Image( painter = painterResource(id = R.drawable.person_outline_24px), contentDescription = stringResource( - R.string.details_screen_user_image_description + R.string.details_screen_user_image_description, ), - modifier = modifier + modifier = modifier, ) } @@ -98,7 +97,7 @@ private fun IsReviewedHeadlineText(reviewer: Reviewer) { Text( text = resolveIsReviewedText(reviewer), style = MaterialTheme.typography.labelMedium, - color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error + color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error, ) } @@ -114,7 +113,7 @@ private fun ReviewerName(reviewer: Reviewer) { Text( text = reviewer.name, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } @@ -123,7 +122,7 @@ private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifie OutlinedButton(onClick = onNotifyClick, modifier = modifier) { Text( text = stringResource(R.string.details_notify), - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } } @@ -134,7 +133,7 @@ private fun ReviewerViewPreview() { LoudiusTheme { ReviewerView( reviewer = Reviewer("Kezc", true, 12, 12), - backgroundColor = MaterialTheme.colorScheme.surface + backgroundColor = MaterialTheme.colorScheme.surface, ) {} } } diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index 7e8abc6e8..b2eac4cbe 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -18,7 +18,7 @@ import androidx.core.view.ViewCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80 + tertiary = Pink80, ) private val LightColorScheme = lightColorScheme( @@ -46,7 +46,7 @@ fun LoudiusTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { @@ -67,6 +67,6 @@ fun LoudiusTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt index c7c8dbe1d..b0f5ebd5b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt +++ b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt @@ -21,8 +21,8 @@ fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed( color = color, start = Offset(x = 0f, y = height), end = Offset(x = width, y = height), - strokeWidth = strokeWidthPx + strokeWidth = strokeWidthPx, ) } - } + }, ) diff --git a/megalinter-reports/IDE-config.txt b/megalinter-reports/IDE-config.txt new file mode 100644 index 000000000..0d18f3fc6 --- /dev/null +++ b/megalinter-reports/IDE-config.txt @@ -0,0 +1,27 @@ +MegaLinter can help you to define the same linter configuration locally + +INSTRUCTIONS + +- Copy the content of IDE-config folder at the root of your repository +- if you are using Visual Studio Code, just reopen your project after the copy, and you will be prompted to install recommended extensions +- If not, you can install extensions manually using the following links. + +IDE EXTENSIONS APPLICABLE TO YOUR PROJECT + +ktlint (KOTLIN) + - emacs: + - flycheck-kotlin: https://github.com/whirm/flycheck-kotlin + - vim: + - ale: https://github.com/w0rp/ale + +checkov (REPOSITORY) + - vscode: + - Checkov: https://marketplace.visualstudio.com/items?itemName=Bridgecrew.checkov + +devskim (REPOSITORY) + - vscode: + - VSCode DevSkim: https://marketplace.visualstudio.com/items?itemName=MS-CST-E.vscode-devskim + +trivy (REPOSITORY) + - vscode: + - VSCode Trivy: https://marketplace.visualstudio.com/items?itemName=AquaSecurityOfficial.trivy-vulnerability-scanner diff --git a/megalinter-reports/IDE-config/.checkov.yml b/megalinter-reports/IDE-config/.checkov.yml new file mode 100644 index 000000000..5f8d74a68 --- /dev/null +++ b/megalinter-reports/IDE-config/.checkov.yml @@ -0,0 +1,6 @@ +# You can see all available properties here: https://github.com/bridgecrewio/checkov#configuration-using-a-config-file +quiet: true +skip-check: + - CKV_DOCKER_2 + + diff --git a/megalinter-reports/IDE-config/.gitleaks.toml b/megalinter-reports/IDE-config/.gitleaks.toml new file mode 100644 index 000000000..bf3bff15f --- /dev/null +++ b/megalinter-reports/IDE-config/.gitleaks.toml @@ -0,0 +1,21 @@ + +title = "gitleaks config" + +[extend] +# useDefault will extend the base configuration with the default gitleaks config: +# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml +useDefault = true + +[allowlist] + description = "Allowlisted files" + paths = [ + '''.automation/test''', + '''megalinter-reports''', + '''.github/linters''', + '''node_modules''', + '''.mypy_cache''', + '''(.*?)gitleaks\.toml$''', + '''(.*?)(png|jpg|gif|doc|docx|pdf|bin|xls|pyc|zip)$''', + '''(go.mod|go.sum)$'''] + + diff --git a/megalinter-reports/IDE-config/.jscpd.json b/megalinter-reports/IDE-config/.jscpd.json new file mode 100644 index 000000000..1e8ed15a8 --- /dev/null +++ b/megalinter-reports/IDE-config/.jscpd.json @@ -0,0 +1,22 @@ +{ + "threshold": 0, + "reporters": [ + "html", + "markdown" + ], + "ignore": [ + "**/node_modules/**", + "**/.git/**", + "**/.rbenv/**", + "**/.venv/**", + "**/report/**", + "**/megalinter-reports/**", + "**/*cache*/**", + "**/*.json", + "**/*.yaml", + "**/*.yml", + "**/*.md", + "**/*.html", + "**/*.xml" + ] +} diff --git a/megalinter-reports/IDE-config/.secretlintrc.json b/megalinter-reports/IDE-config/.secretlintrc.json new file mode 100644 index 000000000..c9bad1c81 --- /dev/null +++ b/megalinter-reports/IDE-config/.secretlintrc.json @@ -0,0 +1,7 @@ +{ + "rules": [ + { + "id": "@secretlint/secretlint-rule-preset-recommend" + } + ] + } \ No newline at end of file diff --git a/megalinter-reports/IDE-config/.vscode/extensions.json b/megalinter-reports/IDE-config/.vscode/extensions.json new file mode 100644 index 000000000..530cec0d7 --- /dev/null +++ b/megalinter-reports/IDE-config/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "AquaSecurityOfficial.trivy-vulnerability-scanner", + "MS-CST-E.vscode-devskim", + "Bridgecrew.checkov" + ] +} \ No newline at end of file diff --git a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt new file mode 100644 index 000000000..00f52c49c --- /dev/null +++ b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt @@ -0,0 +1,152 @@ +package com.appunite.loudius.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.appunite.loudius.R +import com.appunite.loudius.domain.model.Reviewer +import com.appunite.loudius.ui.components.LoudiusTopAppBar +import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.ui.utils.bottomBorder + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun DetailsScreenStateless(topBarTitle: String, reviewers: List) { + Scaffold( + topBar = { LoudiusTopAppBar(onClickBackArrow = {}, title = topBarTitle) }, + content = { padding -> + DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) + }, + modifier = Modifier.background(MaterialTheme.colorScheme.surface), + ) +} + +@Composable +private fun DetailsScreenContent(reviewers: List, modifier: Modifier) { + LazyColumn( + modifier = modifier.fillMaxWidth(), + ) { + itemsIndexed(reviewers) { index, reviewer -> + ReviewerView( + reviewer = reviewer, + backgroundColor = resolveReviewerBackgroundColor(index), + onNotifyClick = {}, + ) + } + } +} + +@Composable +private fun resolveReviewerBackgroundColor(index: Int) = + if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface + +@Composable +private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(backgroundColor) + .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) + .padding(16.dp), + ) { + ReviewerAvatarView(Modifier.align(CenterVertically)) + Column( + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + .align(CenterVertically), + ) { + IsReviewedHeadlineText(reviewer) + ReviewerName(reviewer) + } + NotifyButton(onNotifyClick, Modifier.align(CenterVertically)) + } +} + +@Composable +private fun ReviewerAvatarView(modifier: Modifier = Modifier) { + Image( + painter = painterResource(id = R.drawable.person_outline_24px), + contentDescription = stringResource( + R.string.details_screen_user_image_description, + ), + modifier = modifier, + ) +} + +@Composable +private fun IsReviewedHeadlineText(reviewer: Reviewer) { + Text( + text = resolveIsReviewedText(reviewer), + style = MaterialTheme.typography.labelMedium, + color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error, + ) +} + +@Composable +private fun resolveIsReviewedText(reviewer: Reviewer) = if (reviewer.isReviewDone) { + stringResource(id = R.string.details_reviewed, reviewer.hoursFromReviewDone ?: 0) +} else { + stringResource(id = R.string.details_not_reviewed, reviewer.hoursFromPRStart) +} + +@Composable +private fun ReviewerName(reviewer: Reviewer) { + Text( + text = reviewer.name, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) +} + +@Composable +private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifier) { + OutlinedButton(onClick = onNotifyClick, modifier = modifier) { + Text( + text = stringResource(R.string.details_notify), + color = MaterialTheme.colorScheme.onSurface, + ) + } +} + +@Preview +@Composable +private fun ReviewerViewPreview() { + LoudiusTheme { + ReviewerView( + reviewer = Reviewer("Kezc", true, 12, 12), + backgroundColor = MaterialTheme.colorScheme.surface, + ) {} + } +} + +@Preview +@Composable +fun DetailsScreenPreview() { + val reviewer1 = Reviewer("Kezc", true, 24, 12) + val reviewer2 = Reviewer("Krzysiudan", false, 24, 0) + val reviewer3 = Reviewer("Weronika", false, 24, 0) + val reviewer4 = Reviewer("Jacek", false, 24, 0) + val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) + LoudiusTheme { + DetailsScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers) + } +} diff --git a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt new file mode 100644 index 000000000..b2eac4cbe --- /dev/null +++ b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -0,0 +1,72 @@ +package com.appunite.loudius.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80, +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + surface = White99, + onSurface = Black90, + onSurfaceVariant = PurpleBlack30, + outlineVariant = NeutralVariant30, + error = Error40, + + /* Other default colors to override + background = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun LoudiusTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() + ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content, + ) +} diff --git a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt new file mode 100644 index 000000000..b0f5ebd5b --- /dev/null +++ b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt @@ -0,0 +1,28 @@ +package com.appunite.loudius.ui.utils + +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed( + factory = { + val density = LocalDensity.current + val strokeWidthPx = density.run { strokeWidth.toPx() } + + Modifier.drawBehind { + val width = size.width + val height = size.height - strokeWidthPx / 2 + + drawLine( + color = color, + start = Offset(x = 0f, y = height), + end = Offset(x = width, y = height), + strokeWidth = strokeWidthPx, + ) + } + }, +) From e74449e688c36a84c2630b27bfb2dd7989f050db Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 23 Feb 2023 14:05:05 +0100 Subject: [PATCH 017/526] Remove mega-linter reports files accidentally pushed with auto commit. --- megalinter-reports/IDE-config.txt | 27 ---- megalinter-reports/IDE-config/.checkov.yml | 6 - megalinter-reports/IDE-config/.gitleaks.toml | 21 --- megalinter-reports/IDE-config/.jscpd.json | 22 --- .../IDE-config/.secretlintrc.json | 7 - .../IDE-config/.vscode/extensions.json | 7 - .../com/appunite/loudius/ui/DetailsScreen.kt | 152 ------------------ .../com/appunite/loudius/ui/theme/Theme.kt | 72 --------- .../loudius/ui/utils/BottomBorderModifier.kt | 28 ---- 9 files changed, 342 deletions(-) delete mode 100644 megalinter-reports/IDE-config.txt delete mode 100644 megalinter-reports/IDE-config/.checkov.yml delete mode 100644 megalinter-reports/IDE-config/.gitleaks.toml delete mode 100644 megalinter-reports/IDE-config/.jscpd.json delete mode 100644 megalinter-reports/IDE-config/.secretlintrc.json delete mode 100644 megalinter-reports/IDE-config/.vscode/extensions.json delete mode 100644 megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt delete mode 100644 megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt delete mode 100644 megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt diff --git a/megalinter-reports/IDE-config.txt b/megalinter-reports/IDE-config.txt deleted file mode 100644 index 0d18f3fc6..000000000 --- a/megalinter-reports/IDE-config.txt +++ /dev/null @@ -1,27 +0,0 @@ -MegaLinter can help you to define the same linter configuration locally - -INSTRUCTIONS - -- Copy the content of IDE-config folder at the root of your repository -- if you are using Visual Studio Code, just reopen your project after the copy, and you will be prompted to install recommended extensions -- If not, you can install extensions manually using the following links. - -IDE EXTENSIONS APPLICABLE TO YOUR PROJECT - -ktlint (KOTLIN) - - emacs: - - flycheck-kotlin: https://github.com/whirm/flycheck-kotlin - - vim: - - ale: https://github.com/w0rp/ale - -checkov (REPOSITORY) - - vscode: - - Checkov: https://marketplace.visualstudio.com/items?itemName=Bridgecrew.checkov - -devskim (REPOSITORY) - - vscode: - - VSCode DevSkim: https://marketplace.visualstudio.com/items?itemName=MS-CST-E.vscode-devskim - -trivy (REPOSITORY) - - vscode: - - VSCode Trivy: https://marketplace.visualstudio.com/items?itemName=AquaSecurityOfficial.trivy-vulnerability-scanner diff --git a/megalinter-reports/IDE-config/.checkov.yml b/megalinter-reports/IDE-config/.checkov.yml deleted file mode 100644 index 5f8d74a68..000000000 --- a/megalinter-reports/IDE-config/.checkov.yml +++ /dev/null @@ -1,6 +0,0 @@ -# You can see all available properties here: https://github.com/bridgecrewio/checkov#configuration-using-a-config-file -quiet: true -skip-check: - - CKV_DOCKER_2 - - diff --git a/megalinter-reports/IDE-config/.gitleaks.toml b/megalinter-reports/IDE-config/.gitleaks.toml deleted file mode 100644 index bf3bff15f..000000000 --- a/megalinter-reports/IDE-config/.gitleaks.toml +++ /dev/null @@ -1,21 +0,0 @@ - -title = "gitleaks config" - -[extend] -# useDefault will extend the base configuration with the default gitleaks config: -# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml -useDefault = true - -[allowlist] - description = "Allowlisted files" - paths = [ - '''.automation/test''', - '''megalinter-reports''', - '''.github/linters''', - '''node_modules''', - '''.mypy_cache''', - '''(.*?)gitleaks\.toml$''', - '''(.*?)(png|jpg|gif|doc|docx|pdf|bin|xls|pyc|zip)$''', - '''(go.mod|go.sum)$'''] - - diff --git a/megalinter-reports/IDE-config/.jscpd.json b/megalinter-reports/IDE-config/.jscpd.json deleted file mode 100644 index 1e8ed15a8..000000000 --- a/megalinter-reports/IDE-config/.jscpd.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "threshold": 0, - "reporters": [ - "html", - "markdown" - ], - "ignore": [ - "**/node_modules/**", - "**/.git/**", - "**/.rbenv/**", - "**/.venv/**", - "**/report/**", - "**/megalinter-reports/**", - "**/*cache*/**", - "**/*.json", - "**/*.yaml", - "**/*.yml", - "**/*.md", - "**/*.html", - "**/*.xml" - ] -} diff --git a/megalinter-reports/IDE-config/.secretlintrc.json b/megalinter-reports/IDE-config/.secretlintrc.json deleted file mode 100644 index c9bad1c81..000000000 --- a/megalinter-reports/IDE-config/.secretlintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "rules": [ - { - "id": "@secretlint/secretlint-rule-preset-recommend" - } - ] - } \ No newline at end of file diff --git a/megalinter-reports/IDE-config/.vscode/extensions.json b/megalinter-reports/IDE-config/.vscode/extensions.json deleted file mode 100644 index 530cec0d7..000000000 --- a/megalinter-reports/IDE-config/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "AquaSecurityOfficial.trivy-vulnerability-scanner", - "MS-CST-E.vscode-devskim", - "Bridgecrew.checkov" - ] -} \ No newline at end of file diff --git a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt deleted file mode 100644 index 00f52c49c..000000000 --- a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.appunite.loudius.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.appunite.loudius.R -import com.appunite.loudius.domain.model.Reviewer -import com.appunite.loudius.ui.components.LoudiusTopAppBar -import com.appunite.loudius.ui.theme.LoudiusTheme -import com.appunite.loudius.ui.utils.bottomBorder - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun DetailsScreenStateless(topBarTitle: String, reviewers: List) { - Scaffold( - topBar = { LoudiusTopAppBar(onClickBackArrow = {}, title = topBarTitle) }, - content = { padding -> - DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) - }, - modifier = Modifier.background(MaterialTheme.colorScheme.surface), - ) -} - -@Composable -private fun DetailsScreenContent(reviewers: List, modifier: Modifier) { - LazyColumn( - modifier = modifier.fillMaxWidth(), - ) { - itemsIndexed(reviewers) { index, reviewer -> - ReviewerView( - reviewer = reviewer, - backgroundColor = resolveReviewerBackgroundColor(index), - onNotifyClick = {}, - ) - } - } -} - -@Composable -private fun resolveReviewerBackgroundColor(index: Int) = - if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface - -@Composable -private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor) - .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) - .padding(16.dp), - ) { - ReviewerAvatarView(Modifier.align(CenterVertically)) - Column( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp) - .align(CenterVertically), - ) { - IsReviewedHeadlineText(reviewer) - ReviewerName(reviewer) - } - NotifyButton(onNotifyClick, Modifier.align(CenterVertically)) - } -} - -@Composable -private fun ReviewerAvatarView(modifier: Modifier = Modifier) { - Image( - painter = painterResource(id = R.drawable.person_outline_24px), - contentDescription = stringResource( - R.string.details_screen_user_image_description, - ), - modifier = modifier, - ) -} - -@Composable -private fun IsReviewedHeadlineText(reviewer: Reviewer) { - Text( - text = resolveIsReviewedText(reviewer), - style = MaterialTheme.typography.labelMedium, - color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error, - ) -} - -@Composable -private fun resolveIsReviewedText(reviewer: Reviewer) = if (reviewer.isReviewDone) { - stringResource(id = R.string.details_reviewed, reviewer.hoursFromReviewDone ?: 0) -} else { - stringResource(id = R.string.details_not_reviewed, reviewer.hoursFromPRStart) -} - -@Composable -private fun ReviewerName(reviewer: Reviewer) { - Text( - text = reviewer.name, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, - ) -} - -@Composable -private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifier) { - OutlinedButton(onClick = onNotifyClick, modifier = modifier) { - Text( - text = stringResource(R.string.details_notify), - color = MaterialTheme.colorScheme.onSurface, - ) - } -} - -@Preview -@Composable -private fun ReviewerViewPreview() { - LoudiusTheme { - ReviewerView( - reviewer = Reviewer("Kezc", true, 12, 12), - backgroundColor = MaterialTheme.colorScheme.surface, - ) {} - } -} - -@Preview -@Composable -fun DetailsScreenPreview() { - val reviewer1 = Reviewer("Kezc", true, 24, 12) - val reviewer2 = Reviewer("Krzysiudan", false, 24, 0) - val reviewer3 = Reviewer("Weronika", false, 24, 0) - val reviewer4 = Reviewer("Jacek", false, 24, 0) - val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) - LoudiusTheme { - DetailsScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers) - } -} diff --git a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt deleted file mode 100644 index b2eac4cbe..000000000 --- a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.appunite.loudius.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.ViewCompat - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80, -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, - surface = White99, - onSurface = Black90, - onSurfaceVariant = PurpleBlack30, - outlineVariant = NeutralVariant30, - error = Error40, - - /* Other default colors to override - background = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun LoudiusTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit, -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() - ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content, - ) -} diff --git a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt deleted file mode 100644 index b0f5ebd5b..000000000 --- a/megalinter-reports/updated_sources/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.appunite.loudius.ui.utils - -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp - -fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed( - factory = { - val density = LocalDensity.current - val strokeWidthPx = density.run { strokeWidth.toPx() } - - Modifier.drawBehind { - val width = size.width - val height = size.height - strokeWidthPx / 2 - - drawLine( - color = color, - start = Offset(x = 0f, y = height), - end = Offset(x = width, y = height), - strokeWidth = strokeWidthPx, - ) - } - }, -) From 3b2cc794fdf0053721e4eb71db0d13c1f36108f8 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 23 Feb 2023 14:52:19 +0100 Subject: [PATCH 018/526] SIL-57: authorize user and get code --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 10 ++++++- .../java/com/appunite/loudius/MainActivity.kt | 27 ++++++++++++++++++- .../com/appunite/loudius/common/Constants.kt | 1 + .../loudius/domain/GithubRepository.kt | 2 -- .../loudius/domain/GithubRepositoryImpl.kt | 3 --- .../com/appunite/loudius/network/GithubApi.kt | 8 ------ .../loudius/network/GithubDataSource.kt | 5 ---- .../loudius/presentation/ScreenRoutes.kt | 8 ++++++ .../loudius/presentation/login/LoginScreen.kt | 19 ++++++++++--- .../presentation/login/LoginViewModel.kt | 26 ------------------ 11 files changed, 61 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/presentation/ScreenRoutes.kt delete mode 100644 app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index d7c21eb7f..817e0d793 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,6 +64,7 @@ dependencies { implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling-preview' + implementation "androidx.navigation:navigation-compose:2.5.3" debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2bf8cfad9..8f0b12651 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,9 +21,17 @@ android:theme="@style/Theme.Loudius"> - + + + + + + + diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 61d8a95a3..4bc136bc6 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -7,6 +7,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navDeepLink +import com.appunite.loudius.common.Constants.REDIRECT_URL +import com.appunite.loudius.presentation.ScreenRoute +import com.appunite.loudius.presentation.login.ExampleScreen import com.appunite.loudius.presentation.login.LoginScreen import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.AndroidEntryPoint @@ -22,7 +29,25 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - LoginScreen() + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = ScreenRoute.Login.route + ) { + composable(route = ScreenRoute.Login.route) { + LoginScreen( + context = this@MainActivity + ) + } + composable( + route = ScreenRoute.ReposScreen.route, + deepLinks = listOf(navDeepLink { + uriPattern = REDIRECT_URL + }) + ) { + ExampleScreen(intent = intent) + } + } } } } diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index f75540c26..e3341afa2 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -3,6 +3,7 @@ package com.appunite.loudius.common object Constants { const val BASE_URL = "https://api.github.com" + const val AUTH_URL = "https://github.com" const val CLIENT_ID = "91131449e417c7e29912" const val REDIRECT_URL = "loudius://callback" } diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt index 01c5d1a43..2c7c89438 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt @@ -5,6 +5,4 @@ import com.appunite.loudius.network.model.AccessToken interface GithubRepository { suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result - - suspend fun authorize(): Result } diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt index abebb5de0..c01348cfc 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt @@ -10,7 +10,4 @@ class GithubRepositoryImpl @Inject constructor( override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = githubDataSource.getAccessToken(clientId, clientSecret, code) - - override suspend fun authorize(): Result = - githubDataSource.authorize() } diff --git a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt index d3e244c20..e3f15ee37 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt @@ -1,21 +1,13 @@ package com.appunite.loudius.network -import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.network.model.AccessToken import retrofit2.http.Field import retrofit2.http.FormUrlEncoded -import retrofit2.http.GET import retrofit2.http.Headers import retrofit2.http.POST -import retrofit2.http.Query interface GithubApi { - @GET("login/oauth/authorize") - suspend fun authorize( - @Query("client_id") clientId: String = CLIENT_ID - ): String - @Headers("Accept: application/json") @POST("login/oauth/access_token") @FormUrlEncoded diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt index 6062c7536..f04f9128a 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -8,8 +8,6 @@ import javax.inject.Singleton interface GithubDataSource { suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result - - suspend fun authorize(): Result } @Singleton @@ -19,7 +17,4 @@ class GithubNetworkDataSource @Inject constructor( override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = safeApiCall { api.getAccessToken(clientId, clientSecret, code) } - - override suspend fun authorize(): Result = - safeApiCall { api.authorize() } } diff --git a/app/src/main/java/com/appunite/loudius/presentation/ScreenRoutes.kt b/app/src/main/java/com/appunite/loudius/presentation/ScreenRoutes.kt new file mode 100644 index 000000000..7bb68cfa8 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/presentation/ScreenRoutes.kt @@ -0,0 +1,8 @@ +package com.appunite.loudius.presentation + +sealed class ScreenRoute(val route: String) { + + object Login: ScreenRoute("login_screen") + + object ReposScreen: ScreenRoute("repos_screen") +} diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt index 47a9be478..a046817df 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt @@ -1,5 +1,8 @@ package com.appunite.loudius.presentation.login +import android.content.Context +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -8,11 +11,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel +import com.appunite.loudius.common.Constants.AUTH_URL +import com.appunite.loudius.common.Constants.CLIENT_ID @Composable fun LoginScreen( - viewModel: LoginViewModel = hiltViewModel() + context: Context ) { Column( modifier = Modifier.fillMaxSize(), @@ -20,9 +24,18 @@ fun LoginScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Button( - onClick = viewModel::onLoginClick + onClick = { + val url = "$AUTH_URL/login/oauth/authorize?client_id=$CLIENT_ID" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + } ) { Text(text = "Login") } } } + +@Composable +fun ExampleScreen(intent: Intent) { + Text(text = intent.data?.getQueryParameter("code") ?: "empty code") +} diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt deleted file mode 100644 index 5748c1553..000000000 --- a/app/src/main/java/com/appunite/loudius/presentation/login/LoginViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.appunite.loudius.presentation.login - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.appunite.loudius.common.Constants.CLIENT_ID -import com.appunite.loudius.domain.GithubRepository -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class LoginViewModel @Inject constructor( - private val githubRepository: GithubRepository -): ViewModel() { - - fun onLoginClick() { - viewModelScope.launch { - githubRepository.authorize().onSuccess { code -> - githubRepository.getAccessToken(CLIENT_ID,"", code).onSuccess { token -> //TODO add client secret - Log.i("access_token", token.accessToken) - } - } - } - } -} From d7a8d8bea13717f8b4a96199258b716f99fa0168 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 24 Feb 2023 09:21:52 +0100 Subject: [PATCH 019/526] SIL-57: get access token --- .../java/com/appunite/loudius/MainActivity.kt | 6 ++-- .../com/appunite/loudius/common/Constants.kt | 5 +-- .../{presentation => common}/ScreenRoutes.kt | 2 +- .../loudius/presentation/login/LoginScreen.kt | 21 +++++++------ .../loudius/presentation/repos/ReposScreen.kt | 18 +++++++++++ .../presentation/repos/ReposViewModel.kt | 31 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 68 insertions(+), 16 deletions(-) rename app/src/main/java/com/appunite/loudius/{presentation => common}/ScreenRoutes.kt (78%) create mode 100644 app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt create mode 100644 app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 4bc136bc6..8cb8d4b73 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -12,9 +12,9 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import com.appunite.loudius.common.Constants.REDIRECT_URL -import com.appunite.loudius.presentation.ScreenRoute -import com.appunite.loudius.presentation.login.ExampleScreen +import com.appunite.loudius.common.ScreenRoute import com.appunite.loudius.presentation.login.LoginScreen +import com.appunite.loudius.presentation.repos.ReposScreen import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.AndroidEntryPoint @@ -45,7 +45,7 @@ class MainActivity : ComponentActivity() { uriPattern = REDIRECT_URL }) ) { - ExampleScreen(intent = intent) + ReposScreen(intent = intent) } } } diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index e3341afa2..af5f11855 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -2,8 +2,9 @@ package com.appunite.loudius.common object Constants { - const val BASE_URL = "https://api.github.com" - const val AUTH_URL = "https://github.com" + const val BASE_URL = "https://github.com" + const val AUTH_PATH = "/login/oauth/authorize" + const val NAME_PARAM_CLIENT_ID = "?client_id=" const val CLIENT_ID = "91131449e417c7e29912" const val REDIRECT_URL = "loudius://callback" } diff --git a/app/src/main/java/com/appunite/loudius/presentation/ScreenRoutes.kt b/app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt similarity index 78% rename from app/src/main/java/com/appunite/loudius/presentation/ScreenRoutes.kt rename to app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt index 7bb68cfa8..79086c33b 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/ScreenRoutes.kt +++ b/app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.presentation +package com.appunite.loudius.common sealed class ScreenRoute(val route: String) { diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt index a046817df..23679dc50 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt @@ -11,8 +11,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import com.appunite.loudius.common.Constants.AUTH_URL +import androidx.compose.ui.res.stringResource +import com.appunite.loudius.R +import com.appunite.loudius.common.Constants.AUTH_PATH +import com.appunite.loudius.common.Constants.BASE_URL import com.appunite.loudius.common.Constants.CLIENT_ID +import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID @Composable fun LoginScreen( @@ -24,18 +28,15 @@ fun LoginScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Button( - onClick = { - val url = "$AUTH_URL/login/oauth/authorize?client_id=$CLIENT_ID" - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) - } + onClick = { startAuthorizing(context) } ) { - Text(text = "Login") + Text(text = stringResource(id = R.string.login)) } } } -@Composable -fun ExampleScreen(intent: Intent) { - Text(text = intent.data?.getQueryParameter("code") ?: "empty code") +private fun startAuthorizing(context: Context) { + val url = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) } diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt new file mode 100644 index 000000000..6146c0da2 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt @@ -0,0 +1,18 @@ +package com.appunite.loudius.presentation.repos + +import android.content.Intent +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel + +@Composable +fun ReposScreen( + intent: Intent, + viewModel: ReposViewModel = hiltViewModel() +) { + val code = intent.data?.getQueryParameter("code") + Text(text = code ?: "empty code") + code?.let { + viewModel.getAccessToken(code) + } +} diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt new file mode 100644 index 000000000..1bbed2382 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt @@ -0,0 +1,31 @@ +package com.appunite.loudius.presentation.repos + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.appunite.loudius.common.Constants.CLIENT_ID +import com.appunite.loudius.domain.GithubRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ReposViewModel @Inject constructor( + private val githubRepository: GithubRepository +) : ViewModel() { + + fun getAccessToken(code: String) { + viewModelScope.launch { + //TODO add client secret [SIL-66] + githubRepository.getAccessToken( + clientId = CLIENT_ID, + clientSecret = "", + code = code + ).onSuccess { token -> + Log.i("access_token", token.accessToken) + }.onFailure { + Log.i("access_token", it.message.toString()) + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a928c477..b67b249f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ Loudius Back button + Log in From 05a69576e76d1fee7978989c7f2000386c6df6df Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 24 Feb 2023 09:25:25 +0100 Subject: [PATCH 020/526] SIL-57: code cleanup --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 8 ++++---- .../main/java/com/appunite/loudius/common/ScreenRoutes.kt | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 8cb8d4b73..c1279ad4a 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -12,7 +12,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import com.appunite.loudius.common.Constants.REDIRECT_URL -import com.appunite.loudius.common.ScreenRoute +import com.appunite.loudius.common.Screen import com.appunite.loudius.presentation.login.LoginScreen import com.appunite.loudius.presentation.repos.ReposScreen import com.appunite.loudius.ui.theme.LoudiusTheme @@ -32,15 +32,15 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() NavHost( navController = navController, - startDestination = ScreenRoute.Login.route + startDestination = Screen.Login.route ) { - composable(route = ScreenRoute.Login.route) { + composable(route = Screen.Login.route) { LoginScreen( context = this@MainActivity ) } composable( - route = ScreenRoute.ReposScreen.route, + route = Screen.Repos.route, deepLinks = listOf(navDeepLink { uriPattern = REDIRECT_URL }) diff --git a/app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt b/app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt index 79086c33b..e50693476 100644 --- a/app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt +++ b/app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt @@ -1,8 +1,8 @@ package com.appunite.loudius.common -sealed class ScreenRoute(val route: String) { +sealed class Screen(val route: String) { - object Login: ScreenRoute("login_screen") + object Login: Screen("login_screen") - object ReposScreen: ScreenRoute("repos_screen") + object Repos: Screen("repos_screen") } From 6e8886d1445e3c6f9b3cde29f5c5710469caae66 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 24 Feb 2023 09:36:42 +0100 Subject: [PATCH 021/526] SIL-57: code cleanup --- .../com/appunite/loudius/common/{ScreenRoutes.kt => Screen.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/appunite/loudius/common/{ScreenRoutes.kt => Screen.kt} (100%) diff --git a/app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt similarity index 100% rename from app/src/main/java/com/appunite/loudius/common/ScreenRoutes.kt rename to app/src/main/java/com/appunite/loudius/common/Screen.kt From 6055fc8418d617e413b3fc28490cc46b3c9e3a12 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 24 Feb 2023 08:39:48 +0000 Subject: [PATCH 022/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/MainActivity.kt | 14 ++++++++------ .../java/com/appunite/loudius/common/Screen.kt | 4 ++-- .../java/com/appunite/loudius/di/GithubModule.kt | 4 ++-- .../loudius/domain/GithubRepositoryImpl.kt | 4 ++-- .../java/com/appunite/loudius/network/GithubApi.kt | 2 +- .../appunite/loudius/network/GithubDataSource.kt | 4 ++-- .../appunite/loudius/network/model/AccessToken.kt | 2 +- .../appunite/loudius/network/utils/ApiCallUtil.kt | 2 +- .../loudius/presentation/login/LoginScreen.kt | 6 +++--- .../loudius/presentation/repos/ReposScreen.kt | 2 +- .../loudius/presentation/repos/ReposViewModel.kt | 6 +++--- 11 files changed, 26 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index c1279ad4a..03d7ae111 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -27,23 +27,25 @@ class MainActivity : ComponentActivity() { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.background, ) { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Screen.Login.route + startDestination = Screen.Login.route, ) { composable(route = Screen.Login.route) { LoginScreen( - context = this@MainActivity + context = this@MainActivity, ) } composable( route = Screen.Repos.route, - deepLinks = listOf(navDeepLink { - uriPattern = REDIRECT_URL - }) + deepLinks = listOf( + navDeepLink { + uriPattern = REDIRECT_URL + }, + ), ) { ReposScreen(intent = intent) } diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index e50693476..4322d4564 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.common sealed class Screen(val route: String) { - object Login: Screen("login_screen") + object Login : Screen("login_screen") - object Repos: Screen("repos_screen") + object Repos : Screen("repos_screen") } diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 2b6acfb4a..5a161c93c 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -23,12 +23,12 @@ object GithubModule { @Singleton @Provides fun provideGithubRepository( - githubDataSource: GithubDataSource + githubDataSource: GithubDataSource, ): GithubRepository = GithubRepositoryImpl(githubDataSource) @Singleton @Provides fun provideGithubDataSource( - api: GithubApi + api: GithubApi, ): GithubDataSource = GithubNetworkDataSource(api) } diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt index c01348cfc..3e2e73001 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt @@ -5,8 +5,8 @@ import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject class GithubRepositoryImpl @Inject constructor( - private val githubDataSource: GithubDataSource -): GithubRepository { + private val githubDataSource: GithubDataSource, +) : GithubRepository { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = githubDataSource.getAccessToken(clientId, clientSecret, code) diff --git a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt index e3f15ee37..7ef8e4fa4 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt @@ -14,6 +14,6 @@ interface GithubApi { suspend fun getAccessToken( @Field("client_id") clientId: String, @Field("client_secret") clientSecret: String, - @Field("code") code: String + @Field("code") code: String, ): AccessToken } diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt index f04f9128a..9150a99b4 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -12,8 +12,8 @@ interface GithubDataSource { @Singleton class GithubNetworkDataSource @Inject constructor( - private val api: GithubApi -): GithubDataSource { + private val api: GithubApi, +) : GithubDataSource { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = safeApiCall { api.getAccessToken(clientId, clientSecret, code) } diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt index deb7c2e13..4111ed9d3 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt @@ -5,5 +5,5 @@ import com.google.gson.annotations.SerializedName data class AccessToken( @SerializedName("access_token") - val accessToken: String + val accessToken: String, ) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index dddbfe466..ee6d6df01 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -5,7 +5,7 @@ import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, - apiCall: suspend () -> T + apiCall: suspend () -> T, ): Result { return try { val response = apiCall() diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt index 23679dc50..e60669a88 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt @@ -20,15 +20,15 @@ import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID @Composable fun LoginScreen( - context: Context + context: Context, ) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Button( - onClick = { startAuthorizing(context) } + onClick = { startAuthorizing(context) }, ) { Text(text = stringResource(id = R.string.login)) } diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt index 6146c0da2..50e248d52 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt @@ -8,7 +8,7 @@ import androidx.hilt.navigation.compose.hiltViewModel @Composable fun ReposScreen( intent: Intent, - viewModel: ReposViewModel = hiltViewModel() + viewModel: ReposViewModel = hiltViewModel(), ) { val code = intent.data?.getQueryParameter("code") Text(text = code ?: "empty code") diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt index 1bbed2382..4ce0d73af 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt @@ -11,16 +11,16 @@ import javax.inject.Inject @HiltViewModel class ReposViewModel @Inject constructor( - private val githubRepository: GithubRepository + private val githubRepository: GithubRepository, ) : ViewModel() { fun getAccessToken(code: String) { viewModelScope.launch { - //TODO add client secret [SIL-66] + // TODO add client secret [SIL-66] githubRepository.getAccessToken( clientId = CLIENT_ID, clientSecret = "", - code = code + code = code, ).onSuccess { token -> Log.i("access_token", token.accessToken) }.onFailure { From 21e115af7bdac433f9c3e836f84a7210f4422a95 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 24 Feb 2023 10:02:32 +0100 Subject: [PATCH 023/526] SIL-57: code cleanup --- .../java/com/appunite/loudius/ExampleInstrumentedTest.kt | 6 ++---- app/src/main/java/com/appunite/loudius/MainActivity.kt | 8 +++++--- app/src/main/java/com/appunite/loudius/common/Screen.kt | 4 ++-- .../com/appunite/loudius/domain/GithubRepositoryImpl.kt | 2 +- .../java/com/appunite/loudius/network/GithubDataSource.kt | 2 +- .../appunite/loudius/presentation/repos/ReposViewModel.kt | 2 +- .../appunite/loudius/ui/components/LoudiusTopAppBar.kt | 2 +- app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt | 1 - app/src/main/java/com/appunite/loudius/ui/theme/Type.kt | 2 +- app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt | 3 +-- 10 files changed, 15 insertions(+), 17 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt index ac59ed093..f57da3ce0 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.appunite.loudius -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index c1279ad4a..5018d7b42 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -41,9 +41,11 @@ class MainActivity : ComponentActivity() { } composable( route = Screen.Repos.route, - deepLinks = listOf(navDeepLink { - uriPattern = REDIRECT_URL - }) + deepLinks = listOf( + navDeepLink { + uriPattern = REDIRECT_URL + } + ) ) { ReposScreen(intent = intent) } diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index e50693476..4322d4564 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.common sealed class Screen(val route: String) { - object Login: Screen("login_screen") + object Login : Screen("login_screen") - object Repos: Screen("repos_screen") + object Repos : Screen("repos_screen") } diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt index c01348cfc..ba6eb025a 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt @@ -6,7 +6,7 @@ import javax.inject.Inject class GithubRepositoryImpl @Inject constructor( private val githubDataSource: GithubDataSource -): GithubRepository { +) : GithubRepository { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = githubDataSource.getAccessToken(clientId, clientSecret, code) diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt index f04f9128a..beb3fa49b 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -13,7 +13,7 @@ interface GithubDataSource { @Singleton class GithubNetworkDataSource @Inject constructor( private val api: GithubApi -): GithubDataSource { +) : GithubDataSource { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = safeApiCall { api.getAccessToken(clientId, clientSecret, code) } diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt index 1bbed2382..8640fc86a 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt @@ -16,7 +16,7 @@ class ReposViewModel @Inject constructor( fun getAccessToken(code: String) { viewModelScope.launch { - //TODO add client secret [SIL-66] + // TODO add client secret [SIL-66] githubRepository.getAccessToken( clientId = CLIENT_ID, clientSecret = "", diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 83cc1d8b8..4e6725c1e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -38,7 +38,7 @@ fun LoudiusTopAppBar( }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface - ), + ) ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index ccf522574..fca873821 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt index 27f1bb96b..be36ad9be 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt @@ -21,7 +21,7 @@ val Typography = Typography( fontSize = 22.sp, lineHeight = 28.sp, letterSpacing = 0.sp - ), + ) /* Other default text styles to override labelSmall = TextStyle( fontFamily = FontFamily.Default, diff --git a/app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt b/app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt index c80cbdc2b..d8c0d4425 100644 --- a/app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt +++ b/app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package com.appunite.loudius +import junit.framework.TestCase.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * From 91ec19f09df690493b83f62eac42814447a0d4aa Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 24 Feb 2023 09:08:55 +0000 Subject: [PATCH 024/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/MainActivity.kt | 6 +++--- .../appunite/loudius/domain/GithubRepositoryImpl.kt | 2 +- .../com/appunite/loudius/network/GithubDataSource.kt | 2 +- .../loudius/ui/components/LoudiusTopAppBar.kt | 12 ++++++------ .../main/java/com/appunite/loudius/ui/theme/Theme.kt | 8 ++++---- .../main/java/com/appunite/loudius/ui/theme/Type.kt | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index ccd032df3..03d7ae111 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -27,16 +27,16 @@ class MainActivity : ComponentActivity() { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.background, ) { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Screen.Login.route + startDestination = Screen.Login.route, ) { composable(route = Screen.Login.route) { LoginScreen( - context = this@MainActivity + context = this@MainActivity, ) } composable( diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt index ba6eb025a..3e2e73001 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt @@ -5,7 +5,7 @@ import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject class GithubRepositoryImpl @Inject constructor( - private val githubDataSource: GithubDataSource + private val githubDataSource: GithubDataSource, ) : GithubRepository { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt index beb3fa49b..9150a99b4 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -12,7 +12,7 @@ interface GithubDataSource { @Singleton class GithubNetworkDataSource @Inject constructor( - private val api: GithubApi + private val api: GithubApi, ) : GithubDataSource { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 4e6725c1e..4eeccec92 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -18,27 +18,27 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusTopAppBar( title: String, - onClickBackArrow: () -> Unit + onClickBackArrow: () -> Unit, ) { TopAppBar( title = { Text( text = title, color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) }, navigationIcon = { IconButton(onClick = onClickBackArrow) { Icon( painter = painterResource(id = R.drawable.arrow_back), - contentDescription = stringResource(R.string.back_button) + contentDescription = stringResource(R.string.back_button), ) } }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface - ) + containerColor = MaterialTheme.colorScheme.surface, + ), ) } @@ -48,7 +48,7 @@ fun LoudiusTopAppBar() { LoudiusTheme { LoudiusTopAppBar( onClickBackArrow = {}, - title = "Loudius" + title = "Loudius", ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index fca873821..076cdb93b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -18,7 +18,7 @@ import androidx.core.view.ViewCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80 + tertiary = Pink80, ) private val LightColorScheme = lightColorScheme( @@ -27,7 +27,7 @@ private val LightColorScheme = lightColorScheme( tertiary = Pink40, surface = White99, onSurface = Black90, - onSurfaceVariant = PurpleBlack30 + onSurfaceVariant = PurpleBlack30, /* Other default colors to override background = Color(0xFFFFFBFE), @@ -44,7 +44,7 @@ fun LoudiusTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { @@ -65,6 +65,6 @@ fun LoudiusTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt index be36ad9be..f08a44f91 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt @@ -13,15 +13,15 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp + letterSpacing = 0.5.sp, ), titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 28.sp, - letterSpacing = 0.sp - ) + letterSpacing = 0.sp, + ), /* Other default text styles to override labelSmall = TextStyle( fontFamily = FontFamily.Default, From c50d92d9b24531f8d9f2ac2435dc11fcbbe5fa01 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 24 Feb 2023 12:35:10 +0100 Subject: [PATCH 025/526] SIL-57: code cleanup --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 4 +--- .../main/java/com/appunite/loudius/di/GithubModule.kt | 8 ++++---- .../main/java/com/appunite/loudius/di/NetworkModule.kt | 4 +++- .../domain/{GithubRepository.kt => UserRepository.kt} | 2 +- .../{GithubRepositoryImpl.kt => UserRepositoryImpl.kt} | 4 ++-- .../com/appunite/loudius/network/model/AccessToken.kt | 3 --- .../appunite/loudius/presentation/login/LoginScreen.kt | 10 ++++++---- .../loudius/presentation/repos/ReposViewModel.kt | 6 +++--- 8 files changed, 20 insertions(+), 21 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{GithubRepository.kt => UserRepository.kt} (87%) rename app/src/main/java/com/appunite/loudius/domain/{GithubRepositoryImpl.kt => UserRepositoryImpl.kt} (85%) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 03d7ae111..4e93ec868 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -35,9 +35,7 @@ class MainActivity : ComponentActivity() { startDestination = Screen.Login.route, ) { composable(route = Screen.Login.route) { - LoginScreen( - context = this@MainActivity, - ) + LoginScreen() } composable( route = Screen.Repos.route, diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 5a161c93c..e4b049bba 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -1,7 +1,7 @@ package com.appunite.loudius.di -import com.appunite.loudius.domain.GithubRepository -import com.appunite.loudius.domain.GithubRepositoryImpl +import com.appunite.loudius.domain.UserRepository +import com.appunite.loudius.domain.UserRepositoryImpl import com.appunite.loudius.network.GithubApi import com.appunite.loudius.network.GithubDataSource import com.appunite.loudius.network.GithubNetworkDataSource @@ -22,9 +22,9 @@ object GithubModule { @Singleton @Provides - fun provideGithubRepository( + fun provideUserRepository( githubDataSource: GithubDataSource, - ): GithubRepository = GithubRepositoryImpl(githubDataSource) + ): UserRepository = UserRepositoryImpl(githubDataSource) @Singleton @Provides diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 0ecb44481..aaf23b9ab 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.di import com.appunite.loudius.common.Constants +import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder import dagger.Module @@ -25,7 +26,8 @@ object NetworkModule { @Provides @Singleton - fun provideGson(): Gson = GsonBuilder().create() + fun provideGson(): Gson = + GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() @Provides fun provideBaseUrl() = Constants.BASE_URL diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt similarity index 87% rename from app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt rename to app/src/main/java/com/appunite/loudius/domain/UserRepository.kt index 2c7c89438..b872c20e0 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GithubRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.model.AccessToken -interface GithubRepository { +interface UserRepository { suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result } diff --git a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt similarity index 85% rename from app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt rename to app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 3e2e73001..78f0d7a58 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GithubRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -4,9 +4,9 @@ import com.appunite.loudius.network.GithubDataSource import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject -class GithubRepositoryImpl @Inject constructor( +class UserRepositoryImpl @Inject constructor( private val githubDataSource: GithubDataSource, -) : GithubRepository { +) : UserRepository { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = githubDataSource.getAccessToken(clientId, clientSecret, code) diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt index 4111ed9d3..395a29af6 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt @@ -1,9 +1,6 @@ package com.appunite.loudius.network.model -import com.google.gson.annotations.SerializedName - data class AccessToken( - @SerializedName("access_token") val accessToken: String, ) diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt index e60669a88..17393f18f 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTH_PATH @@ -19,9 +20,8 @@ import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID @Composable -fun LoginScreen( - context: Context, -) { +fun LoginScreen() { + val context = LocalContext.current Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, @@ -36,7 +36,9 @@ fun LoginScreen( } private fun startAuthorizing(context: Context) { - val url = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + val url = buildAuthorizationUrl() val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) context.startActivity(intent) } + +private fun buildAuthorizationUrl() = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt index 4ce0d73af..75d24242a 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt @@ -4,20 +4,20 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Constants.CLIENT_ID -import com.appunite.loudius.domain.GithubRepository +import com.appunite.loudius.domain.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ReposViewModel @Inject constructor( - private val githubRepository: GithubRepository, + private val userRepository: UserRepository, ) : ViewModel() { fun getAccessToken(code: String) { viewModelScope.launch { // TODO add client secret [SIL-66] - githubRepository.getAccessToken( + userRepository.getAccessToken( clientId = CLIENT_ID, clientSecret = "", code = code, From ea9b495903d27580d64b4820fd792206253a9e25 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 24 Feb 2023 15:10:37 +0100 Subject: [PATCH 026/526] Implement network layer for details screen data populating. --- .../domain/DefaultPullRequestRepository.kt | 40 ++++++++++++++++ .../loudius/domain/UserRepositoryImpl.kt | 2 + .../com/appunite/loudius/network/GithubApi.kt | 21 +++++++++ .../loudius/network/GithubDataSource.kt | 12 ++++- .../network/PullRequestNetworkDataSource.kt | 47 +++++++++++++++++++ .../appunite/loudius/network/model/Review.kt | 10 ++++ .../loudius/network/model/ReviewState.kt | 7 +++ .../loudius/network/model/Reviewer.kt | 7 +++ 8 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/model/Review.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/model/Reviewer.kt diff --git a/app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt b/app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt new file mode 100644 index 000000000..b567a6920 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt @@ -0,0 +1,40 @@ +package com.appunite.loudius.domain + +import com.appunite.loudius.network.PullRequestDataSource +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.Reviewer +import javax.inject.Inject +import javax.inject.Singleton + +interface PullRequestRepository { + suspend fun getReviews( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> + + suspend fun getReviewers( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> +} + +@Singleton +class DefaultPullRequestRepository @Inject constructor( + private val pullRequestDataSource: PullRequestDataSource +) : PullRequestRepository { + override suspend fun getReviews( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> = + pullRequestDataSource.getReviews(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") + + override suspend fun getReviewers( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> = + pullRequestDataSource.getReviewers(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") +} diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 78f0d7a58..554523076 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -3,7 +3,9 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.GithubDataSource import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject +import javax.inject.Singleton +@Singleton class UserRepositoryImpl @Inject constructor( private val githubDataSource: GithubDataSource, ) : UserRepository { diff --git a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt index 7ef8e4fa4..3e5fa8cc8 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt @@ -1,10 +1,15 @@ package com.appunite.loudius.network import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.Reviewer import retrofit2.http.Field import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Headers import retrofit2.http.POST +import retrofit2.http.Path interface GithubApi { @@ -16,4 +21,20 @@ interface GithubApi { @Field("client_secret") clientSecret: String, @Field("code") code: String, ): AccessToken + + @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers") + suspend fun getReviewers( + @Path("owner") owner: String, + @Path("repo") repo: String, + @Path("pull_number") pullRequestNumber: String, + @Header("Authorization") token: String, + ): List + + @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/reviews") + suspend fun getReviews( + @Path("owner") owner: String, + @Path("repo") repo: String, + @Path("pull_number") pullRequestNumber: String, + @Header("Authorization") token: String, + ): List } diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt index 9150a99b4..b17e1cf7d 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -7,7 +7,11 @@ import javax.inject.Singleton interface GithubDataSource { - suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result + suspend fun getAccessToken( + clientId: String, + clientSecret: String, + code: String + ): Result } @Singleton @@ -15,6 +19,10 @@ class GithubNetworkDataSource @Inject constructor( private val api: GithubApi, ) : GithubDataSource { - override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = + override suspend fun getAccessToken( + clientId: String, + clientSecret: String, + code: String + ): Result = safeApiCall { api.getAccessToken(clientId, clientSecret, code) } } diff --git a/app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt new file mode 100644 index 000000000..e1116b12a --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt @@ -0,0 +1,47 @@ +package com.appunite.loudius.network + +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.Reviewer +import com.appunite.loudius.network.utils.safeApiCall +import javax.inject.Inject +import javax.inject.Singleton + +interface PullRequestDataSource { + suspend fun getReviewers( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> + + suspend fun getReviews( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> +} + +@Singleton +class PullRequestNetworkDataSource @Inject constructor( + private val api: GithubApi, +) : PullRequestDataSource { + + override suspend fun getReviewers( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> = safeApiCall { + api.getReviewers(owner, repository, pullRequestNumber, accessToken) + } + + override suspend fun getReviews( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> = safeApiCall { + api.getReviews(owner, repository, pullRequestNumber, accessToken) + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/model/Review.kt b/app/src/main/java/com/appunite/loudius/network/model/Review.kt new file mode 100644 index 000000000..7cd3f8d24 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/Review.kt @@ -0,0 +1,10 @@ +package com.appunite.loudius.network.model + +import java.time.LocalDateTime + +data class Review( + val id: String, + val userId: String, + val state: ReviewState, + val submittedAt: LocalDateTime +) diff --git a/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt b/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt new file mode 100644 index 000000000..4a7e2e4ba --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt @@ -0,0 +1,7 @@ +package com.appunite.loudius.network.model + +enum class ReviewState { + APPROVED, + CHANGES_REQUESTED, + COMMENTED +} diff --git a/app/src/main/java/com/appunite/loudius/network/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/network/model/Reviewer.kt new file mode 100644 index 000000000..bb7795bd7 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/Reviewer.kt @@ -0,0 +1,7 @@ +package com.appunite.loudius.network.model + +data class Reviewer( + val id: String, + val login: String, + val avatarUrl: String, +) From 8b6febe9a531dfb779b0f2871052332195f6143a Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 24 Feb 2023 10:49:51 +0100 Subject: [PATCH 027/526] Basic pull requests list --- app/src/main/AndroidManifest.xml | 1 + .../java/com/appunite/loudius/MainActivity.kt | 6 +- .../com/appunite/loudius/common/Constants.kt | 1 + .../com/appunite/loudius/common/Screen.kt | 2 + .../com/appunite/loudius/di/GithubModule.kt | 7 +- .../com/appunite/loudius/di/NetworkModule.kt | 16 ++- .../network/GitHubPullRequestsDataSource.kt | 16 +++ .../network/GitHubPullRequestsRepository.kt | 9 ++ .../network/GithubPullRequestsService.kt | 16 +++ .../loudius/network/model/PullRequest.kt | 15 +++ .../network/model/PullRequestsResponse.kt | 7 ++ .../ui/pullrequests/PullRequestsScreen.kt | 109 ++++++++++++++++++ .../ui/pullrequests/PullRequestsViewModel.kt | 33 ++++++ 13 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsRepository.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/GithubPullRequestsService.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8f0b12651..5b8f929e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 4e93ec868..01a8f98bc 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -15,6 +15,7 @@ import com.appunite.loudius.common.Constants.REDIRECT_URL import com.appunite.loudius.common.Screen import com.appunite.loudius.presentation.login.LoginScreen import com.appunite.loudius.presentation.repos.ReposScreen +import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.AndroidEntryPoint @@ -32,7 +33,7 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Screen.Login.route, + startDestination = Screen.PullRequests.route, ) { composable(route = Screen.Login.route) { LoginScreen() @@ -47,6 +48,9 @@ class MainActivity : ComponentActivity() { ) { ReposScreen(intent = intent) } + composable(route = Screen.PullRequests.route) { + PullRequestsScreen() + } } } } diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index af5f11855..30aa5f782 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -3,6 +3,7 @@ package com.appunite.loudius.common object Constants { const val BASE_URL = "https://github.com" + const val BASE_API_URL = "https://api.github.com" const val AUTH_PATH = "/login/oauth/authorize" const val NAME_PARAM_CLIENT_ID = "?client_id=" const val CLIENT_ID = "91131449e417c7e29912" diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index 4322d4564..bb5bd3502 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -5,4 +5,6 @@ sealed class Screen(val route: String) { object Login : Screen("login_screen") object Repos : Screen("repos_screen") + + object PullRequests : Screen("pull_requests_screen") } diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index e4b049bba..a10b95f4c 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -5,6 +5,7 @@ import com.appunite.loudius.domain.UserRepositoryImpl import com.appunite.loudius.network.GithubApi import com.appunite.loudius.network.GithubDataSource import com.appunite.loudius.network.GithubNetworkDataSource +import com.appunite.loudius.network.GithubPullRequestsService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,7 +19,7 @@ object GithubModule { @Singleton @Provides - fun provideGithubApi(retrofit: Retrofit): GithubApi = retrofit.create(GithubApi::class.java) + fun provideGithubApi(@NetworkModule.GitHubNonApi retrofit: Retrofit): GithubApi = retrofit.create(GithubApi::class.java) @Singleton @Provides @@ -31,4 +32,8 @@ object GithubModule { fun provideGithubDataSource( api: GithubApi, ): GithubDataSource = GithubNetworkDataSource(api) + + @Provides + fun provideGithubReposService(retrofit: Retrofit): GithubPullRequestsService = + retrofit.create(GithubPullRequestsService::class.java) } diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index aaf23b9ab..adad5c442 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -8,9 +8,10 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -18,6 +19,7 @@ object NetworkModule { @Provides @Singleton + @GitHubNonApi fun provideRetrofit(gson: Gson, baseUrl: String): Retrofit = Retrofit.Builder() .baseUrl(baseUrl) @@ -29,6 +31,18 @@ object NetworkModule { fun provideGson(): Gson = GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() + @Provides + @Singleton + fun provideApiRetrofit(gson: Gson): Retrofit = + Retrofit.Builder() + .baseUrl(Constants.BASE_API_URL) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + @Provides fun provideBaseUrl() = Constants.BASE_URL + + @Qualifier + @Retention(AnnotationRetention.RUNTIME) + annotation class GitHubNonApi } diff --git a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt new file mode 100644 index 000000000..aa8348c7a --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt @@ -0,0 +1,16 @@ +package com.appunite.loudius.network + +import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.utils.safeApiCall +import javax.inject.Inject + +const val auth_token = "BEARER xxxxxxx" // temporary solution + +class GitHubPullRequestsDataSource @Inject constructor(private val service: GithubPullRequestsService) { + suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { + service.getPullRequestsForUser( + auth_token, + "author%3A${author}+type%3Apr+state%3Aopen" + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsRepository.kt new file mode 100644 index 000000000..68e2bf954 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsRepository.kt @@ -0,0 +1,9 @@ +package com.appunite.loudius.network + +import com.appunite.loudius.network.model.PullRequestsResponse +import javax.inject.Inject + +class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSource: GitHubPullRequestsDataSource) { + suspend fun getPullRequestsForUser(author: String): Result = + remoteDataSource.getPullRequestsForUser(author) +} diff --git a/app/src/main/java/com/appunite/loudius/network/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/GithubPullRequestsService.kt new file mode 100644 index 000000000..027d8af2a --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/GithubPullRequestsService.kt @@ -0,0 +1,16 @@ +package com.appunite.loudius.network + +import com.appunite.loudius.network.model.PullRequestsResponse +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface GithubPullRequestsService { + @GET("/search/issues") + suspend fun getPullRequestsForUser( + @Header("Authorization") authorization: String, + @Query("q", encoded = true) query: String, + @Query("page") page: Int = 0, + @Query("per_page") perPage: Int = 100, + ): PullRequestsResponse +} diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt new file mode 100644 index 000000000..c5617f05b --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt @@ -0,0 +1,15 @@ +package com.appunite.loudius.network.model + +import com.appunite.loudius.common.Constants + +data class PullRequest( + val id: Int, + val draft: Boolean, + val number: Int, + val repositoryUrl: String, + val title: String, + val updatedAt: String, +) { + val fullRepositoryName: String + get() = repositoryUrl.removePrefix(Constants.BASE_API_URL + "/") +} diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt new file mode 100644 index 000000000..712760d11 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt @@ -0,0 +1,7 @@ +package com.appunite.loudius.network.model + +data class PullRequestsResponse( + val incompleteResults: Boolean, + val items: List, + val totalCount: Int +) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt new file mode 100644 index 000000000..f5aef88fb --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -0,0 +1,109 @@ +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) + +package com.appunite.loudius.ui.pullrequests + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.appunite.loudius.R +import com.appunite.loudius.common.Constants +import com.appunite.loudius.network.model.PullRequest +import com.appunite.loudius.ui.components.LoudiusTopAppBar + +@Composable +fun PullRequestsScreen(viewModel: PullRequestsViewModel = hiltViewModel()) { + val state = viewModel.state + PullRequestsScreenStateless(pullRequests = state.pullRequests) +} + +@Composable +private fun PullRequestsScreenStateless( + pullRequests: List, +) { + Scaffold(topBar = { + LoudiusTopAppBar(title = stringResource(R.string.app_name)) { + // TODO: navigation + } + }, content = { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + pullRequests.forEach { + PullRequestItem( + repositoryName = it.fullRepositoryName, + pullRequestTitle = it.title + ) + } + } + }) +} + +@Composable +private fun PullRequestItem(repositoryName: String, pullRequestTitle: String) { + Text(text = repositoryName) + Text(text = pullRequestTitle) + Spacer(modifier = Modifier.height(8.dp)) +} + +@Preview("Pull requests - empty list") +@Composable +fun PullRequestsScreenEmptyListPreview() { + PullRequestsScreenStateless(emptyList()) +} + +@Preview("Pull requests - filled list") +@Composable +fun PullRequestsScreenPreview() { + PullRequestsScreenStateless( + listOf( + PullRequest( + 0, + false, + 0, + "${Constants.BASE_API_URL}/appunite/Stefan", + "PR 1", + "2021-11-29T16:31:41Z" + ), + PullRequest( + 1, + true, + 1, + "${Constants.BASE_API_URL}/appunite/Silentus", + "PR 2", + "2022-11-29T16:31:41Z" + ), + PullRequest( + 2, + false, + 2, + "${Constants.BASE_API_URL}/appunite/Loudius", + "PR 3", + "2023-01-29T16:31:41Z" + ), + PullRequest( + 3, + false, + 3, + "${Constants.BASE_API_URL}/appunite/Blocktrade", + "PR 4", + "2022-01-29T16:31:41Z" + ), + ) + ) +} diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt new file mode 100644 index 000000000..4b56948b7 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -0,0 +1,33 @@ +package com.appunite.loudius.ui.pullrequests + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.appunite.loudius.network.GitHubPullRequestsRepository +import com.appunite.loudius.network.model.PullRequest +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +data class PullRequestState( + val pullRequests: List = emptyList() +) + +@HiltViewModel +class PullRequestsViewModel @Inject constructor( + private val gitHubReposRepository: GitHubPullRequestsRepository +) : ViewModel() { + var state by mutableStateOf(PullRequestState()) + private set + + init { + viewModelScope.launch { + gitHubReposRepository.getPullRequestsForUser("kezc") // TODO get logged user + .onSuccess { + state = state.copy(pullRequests = it.items) + } + } + } +} From b330240f5135c8d63e944926624fd1f0a4ecd5ee Mon Sep 17 00:00:00 2001 From: kezc Date: Sun, 26 Feb 2023 14:32:59 +0000 Subject: [PATCH 028/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/di/NetworkModule.kt | 4 ++-- .../network/GitHubPullRequestsDataSource.kt | 2 +- .../loudius/network/model/PullRequestsResponse.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 14 +++++++------- .../ui/pullrequests/PullRequestsViewModel.kt | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index adad5c442..c0d9d4c34 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -8,10 +8,10 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Qualifier -import javax.inject.Singleton import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Qualifier +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt index aa8348c7a..caba6551d 100644 --- a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt @@ -10,7 +10,7 @@ class GitHubPullRequestsDataSource @Inject constructor(private val service: Gith suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { service.getPullRequestsForUser( auth_token, - "author%3A${author}+type%3Apr+state%3Aopen" + "author%3A$author+type%3Apr+state%3Aopen", ) } } diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt index 712760d11..03fa49905 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt @@ -3,5 +3,5 @@ package com.appunite.loudius.network.model data class PullRequestsResponse( val incompleteResults: Boolean, val items: List, - val totalCount: Int + val totalCount: Int, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index f5aef88fb..67fe5decd 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -42,12 +42,12 @@ private fun PullRequestsScreenStateless( modifier = Modifier .padding(padding) .fillMaxSize() - .verticalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), ) { pullRequests.forEach { PullRequestItem( repositoryName = it.fullRepositoryName, - pullRequestTitle = it.title + pullRequestTitle = it.title, ) } } @@ -78,7 +78,7 @@ fun PullRequestsScreenPreview() { 0, "${Constants.BASE_API_URL}/appunite/Stefan", "PR 1", - "2021-11-29T16:31:41Z" + "2021-11-29T16:31:41Z", ), PullRequest( 1, @@ -86,7 +86,7 @@ fun PullRequestsScreenPreview() { 1, "${Constants.BASE_API_URL}/appunite/Silentus", "PR 2", - "2022-11-29T16:31:41Z" + "2022-11-29T16:31:41Z", ), PullRequest( 2, @@ -94,7 +94,7 @@ fun PullRequestsScreenPreview() { 2, "${Constants.BASE_API_URL}/appunite/Loudius", "PR 3", - "2023-01-29T16:31:41Z" + "2023-01-29T16:31:41Z", ), PullRequest( 3, @@ -102,8 +102,8 @@ fun PullRequestsScreenPreview() { 3, "${Constants.BASE_API_URL}/appunite/Blocktrade", "PR 4", - "2022-01-29T16:31:41Z" + "2022-01-29T16:31:41Z", ), - ) + ), ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 4b56948b7..b1a681be0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -8,16 +8,16 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.network.GitHubPullRequestsRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject data class PullRequestState( - val pullRequests: List = emptyList() + val pullRequests: List = emptyList(), ) @HiltViewModel class PullRequestsViewModel @Inject constructor( - private val gitHubReposRepository: GitHubPullRequestsRepository + private val gitHubReposRepository: GitHubPullRequestsRepository, ) : ViewModel() { var state by mutableStateOf(PullRequestState()) private set From 25bb3294f83afd4c8867f007cb9456b5e5830147 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 27 Feb 2023 11:58:05 +0100 Subject: [PATCH 029/526] SIL-55: add logo and login button --- .../java/com/appunite/loudius/MainActivity.kt | 12 +-- .../com/appunite/loudius/di/GithubModule.kt | 4 +- .../loudius/domain/UserRepositoryImpl.kt | 2 +- .../appunite/loudius/domain/model/Reviewer.kt | 2 +- .../com/appunite/loudius/network/GithubApi.kt | 2 +- .../loudius/network/GithubDataSource.kt | 2 +- .../loudius/network/model/AccessToken.kt | 2 +- .../loudius/network/utils/ApiCallUtil.kt | 2 +- .../loudius/presentation/login/LoginScreen.kt | 44 ----------- .../com/appunite/loudius/ui/DetailsScreen.kt | 22 +++--- .../loudius/ui/components/LoudiusTopAppBar.kt | 12 +-- .../appunite/loudius/ui/login/LoginScreen.kt | 72 ++++++++++++++++++ .../{presentation => ui}/repos/ReposScreen.kt | 4 +- .../repos/ReposViewModel.kt | 6 +- .../com/appunite/loudius/ui/theme/Theme.kt | 8 +- .../com/appunite/loudius/ui/theme/Type.kt | 6 +- .../loudius/ui/utils/BottomBorderModifier.kt | 4 +- .../main/res/drawable-hdpi/loudius_logo.png | Bin 0 -> 46701 bytes .../main/res/drawable-mdpi/loudius_logo.png | Bin 0 -> 27437 bytes .../main/res/drawable-xhdpi/loudius_logo.png | Bin 0 -> 68586 bytes .../main/res/drawable-xxhdpi/loudius_logo.png | Bin 0 -> 119401 bytes .../res/drawable-xxxhdpi/loudius_logo.png | Bin 0 -> 170896 bytes app/src/main/res/drawable/ic_github.xml | 9 +++ 23 files changed, 126 insertions(+), 89 deletions(-) delete mode 100644 app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt rename app/src/main/java/com/appunite/loudius/{presentation => ui}/repos/ReposScreen.kt (79%) rename app/src/main/java/com/appunite/loudius/{presentation => ui}/repos/ReposViewModel.kt (87%) create mode 100644 app/src/main/res/drawable-hdpi/loudius_logo.png create mode 100644 app/src/main/res/drawable-mdpi/loudius_logo.png create mode 100644 app/src/main/res/drawable-xhdpi/loudius_logo.png create mode 100644 app/src/main/res/drawable-xxhdpi/loudius_logo.png create mode 100644 app/src/main/res/drawable-xxxhdpi/loudius_logo.png create mode 100644 app/src/main/res/drawable/ic_github.xml diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 4e93ec868..3f002ed6a 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -13,8 +13,8 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import com.appunite.loudius.common.Constants.REDIRECT_URL import com.appunite.loudius.common.Screen -import com.appunite.loudius.presentation.login.LoginScreen -import com.appunite.loudius.presentation.repos.ReposScreen +import com.appunite.loudius.ui.login.LoginScreen +import com.appunite.loudius.ui.repos.ReposScreen import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.AndroidEntryPoint @@ -27,12 +27,12 @@ class MainActivity : ComponentActivity() { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, + color = MaterialTheme.colorScheme.background ) { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Screen.Login.route, + startDestination = Screen.Login.route ) { composable(route = Screen.Login.route) { LoginScreen() @@ -42,8 +42,8 @@ class MainActivity : ComponentActivity() { deepLinks = listOf( navDeepLink { uriPattern = REDIRECT_URL - }, - ), + } + ) ) { ReposScreen(intent = intent) } diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index e4b049bba..bea361018 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -23,12 +23,12 @@ object GithubModule { @Singleton @Provides fun provideUserRepository( - githubDataSource: GithubDataSource, + githubDataSource: GithubDataSource ): UserRepository = UserRepositoryImpl(githubDataSource) @Singleton @Provides fun provideGithubDataSource( - api: GithubApi, + api: GithubApi ): GithubDataSource = GithubNetworkDataSource(api) } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 78f0d7a58..02e6e058a 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -5,7 +5,7 @@ import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject class UserRepositoryImpl @Inject constructor( - private val githubDataSource: GithubDataSource, + private val githubDataSource: GithubDataSource ) : UserRepository { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = diff --git a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt index 6189cdf5c..4e7e4d969 100644 --- a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt @@ -4,5 +4,5 @@ data class Reviewer( val name: String, val isReviewDone: Boolean, val hoursFromPRStart: Int, - val hoursFromReviewDone: Int?, + val hoursFromReviewDone: Int? ) diff --git a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt index 7ef8e4fa4..e3f15ee37 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt @@ -14,6 +14,6 @@ interface GithubApi { suspend fun getAccessToken( @Field("client_id") clientId: String, @Field("client_secret") clientSecret: String, - @Field("code") code: String, + @Field("code") code: String ): AccessToken } diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt index 9150a99b4..beb3fa49b 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -12,7 +12,7 @@ interface GithubDataSource { @Singleton class GithubNetworkDataSource @Inject constructor( - private val api: GithubApi, + private val api: GithubApi ) : GithubDataSource { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt index 395a29af6..094a30b11 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt @@ -2,5 +2,5 @@ package com.appunite.loudius.network.model data class AccessToken( - val accessToken: String, + val accessToken: String ) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index ee6d6df01..dddbfe466 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -5,7 +5,7 @@ import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, - apiCall: suspend () -> T, + apiCall: suspend () -> T ): Result { return try { val response = apiCall() diff --git a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt deleted file mode 100644 index 17393f18f..000000000 --- a/app/src/main/java/com/appunite/loudius/presentation/login/LoginScreen.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.appunite.loudius.presentation.login - -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.appunite.loudius.R -import com.appunite.loudius.common.Constants.AUTH_PATH -import com.appunite.loudius.common.Constants.BASE_URL -import com.appunite.loudius.common.Constants.CLIENT_ID -import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID - -@Composable -fun LoginScreen() { - val context = LocalContext.current - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Button( - onClick = { startAuthorizing(context) }, - ) { - Text(text = stringResource(id = R.string.login)) - } - } -} - -private fun startAuthorizing(context: Context) { - val url = buildAuthorizationUrl() - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) -} - -private fun buildAuthorizationUrl() = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID diff --git a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt index 00f52c49c..734947705 100644 --- a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt @@ -35,20 +35,20 @@ private fun DetailsScreenStateless(topBarTitle: String, reviewers: List DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) }, - modifier = Modifier.background(MaterialTheme.colorScheme.surface), + modifier = Modifier.background(MaterialTheme.colorScheme.surface) ) } @Composable private fun DetailsScreenContent(reviewers: List, modifier: Modifier) { LazyColumn( - modifier = modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth() ) { itemsIndexed(reviewers) { index, reviewer -> ReviewerView( reviewer = reviewer, backgroundColor = resolveReviewerBackgroundColor(index), - onNotifyClick = {}, + onNotifyClick = {} ) } } @@ -65,14 +65,14 @@ private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyCli .fillMaxWidth() .background(backgroundColor) .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) - .padding(16.dp), + .padding(16.dp) ) { ReviewerAvatarView(Modifier.align(CenterVertically)) Column( modifier = Modifier .weight(1f) .padding(start = 16.dp) - .align(CenterVertically), + .align(CenterVertically) ) { IsReviewedHeadlineText(reviewer) ReviewerName(reviewer) @@ -86,9 +86,9 @@ private fun ReviewerAvatarView(modifier: Modifier = Modifier) { Image( painter = painterResource(id = R.drawable.person_outline_24px), contentDescription = stringResource( - R.string.details_screen_user_image_description, + R.string.details_screen_user_image_description ), - modifier = modifier, + modifier = modifier ) } @@ -97,7 +97,7 @@ private fun IsReviewedHeadlineText(reviewer: Reviewer) { Text( text = resolveIsReviewedText(reviewer), style = MaterialTheme.typography.labelMedium, - color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error, + color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error ) } @@ -113,7 +113,7 @@ private fun ReviewerName(reviewer: Reviewer) { Text( text = reviewer.name, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onSurface ) } @@ -122,7 +122,7 @@ private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifie OutlinedButton(onClick = onNotifyClick, modifier = modifier) { Text( text = stringResource(R.string.details_notify), - color = MaterialTheme.colorScheme.onSurface, + color = MaterialTheme.colorScheme.onSurface ) } } @@ -133,7 +133,7 @@ private fun ReviewerViewPreview() { LoudiusTheme { ReviewerView( reviewer = Reviewer("Kezc", true, 12, 12), - backgroundColor = MaterialTheme.colorScheme.surface, + backgroundColor = MaterialTheme.colorScheme.surface ) {} } } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 4eeccec92..4e6725c1e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -18,27 +18,27 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusTopAppBar( title: String, - onClickBackArrow: () -> Unit, + onClickBackArrow: () -> Unit ) { TopAppBar( title = { Text( text = title, color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleLarge ) }, navigationIcon = { IconButton(onClick = onClickBackArrow) { Icon( painter = painterResource(id = R.drawable.arrow_back), - contentDescription = stringResource(R.string.back_button), + contentDescription = stringResource(R.string.back_button) ) } }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + containerColor = MaterialTheme.colorScheme.surface + ) ) } @@ -48,7 +48,7 @@ fun LoudiusTopAppBar() { LoudiusTheme { LoudiusTopAppBar( onClickBackArrow = {}, - title = "Loudius", + title = "Loudius" ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt new file mode 100644 index 000000000..0b2750d1c --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -0,0 +1,72 @@ +package com.appunite.loudius.ui.login + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.appunite.loudius.R +import com.appunite.loudius.common.Constants.AUTH_PATH +import com.appunite.loudius.common.Constants.BASE_URL +import com.appunite.loudius.common.Constants.CLIENT_ID +import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID +import com.appunite.loudius.ui.theme.Pink40 + +@Composable +fun LoginScreen() { + val context = LocalContext.current + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image(painter = painterResource(id = R.drawable.loudius_logo), contentDescription = "Loudius logo") + OutlinedButton( + onClick = { startAuthorizing(context) }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 46.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_github), + contentDescription = "Github icon", + tint = Color.Black + ) + Text( + modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), + text = stringResource(id = R.string.login), + color = Pink40 + ) + } + } +} + +private fun startAuthorizing(context: Context) { + val url = buildAuthorizationUrl() + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) +} + +private fun buildAuthorizationUrl() = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + +@Preview(showSystemUi = true, showBackground = true) +@Composable +fun LoginScreenPreview() { + LoginScreen() +} diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt similarity index 79% rename from app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt rename to app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt index 50e248d52..7929464e3 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.presentation.repos +package com.appunite.loudius.ui.repos import android.content.Intent import androidx.compose.material3.Text @@ -8,7 +8,7 @@ import androidx.hilt.navigation.compose.hiltViewModel @Composable fun ReposScreen( intent: Intent, - viewModel: ReposViewModel = hiltViewModel(), + viewModel: ReposViewModel = hiltViewModel() ) { val code = intent.data?.getQueryParameter("code") Text(text = code ?: "empty code") diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt similarity index 87% rename from app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt rename to app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index 75d24242a..c7c0bd4bd 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.presentation.repos +package com.appunite.loudius.ui.repos import android.util.Log import androidx.lifecycle.ViewModel @@ -11,7 +11,7 @@ import javax.inject.Inject @HiltViewModel class ReposViewModel @Inject constructor( - private val userRepository: UserRepository, + private val userRepository: UserRepository ) : ViewModel() { fun getAccessToken(code: String) { @@ -20,7 +20,7 @@ class ReposViewModel @Inject constructor( userRepository.getAccessToken( clientId = CLIENT_ID, clientSecret = "", - code = code, + code = code ).onSuccess { token -> Log.i("access_token", token.accessToken) }.onFailure { diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index b2eac4cbe..e17981485 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -18,7 +18,7 @@ import androidx.core.view.ViewCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80, + tertiary = Pink80 ) private val LightColorScheme = lightColorScheme( @@ -29,7 +29,7 @@ private val LightColorScheme = lightColorScheme( onSurface = Black90, onSurfaceVariant = PurpleBlack30, outlineVariant = NeutralVariant30, - error = Error40, + error = Error40 /* Other default colors to override background = Color(0xFFFFFBFE), @@ -46,7 +46,7 @@ fun LoudiusTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit, + content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { @@ -67,6 +67,6 @@ fun LoudiusTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content, + content = content ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt index f08a44f91..be36ad9be 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt @@ -13,15 +13,15 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp, + letterSpacing = 0.5.sp ), titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 28.sp, - letterSpacing = 0.sp, - ), + letterSpacing = 0.sp + ) /* Other default text styles to override labelSmall = TextStyle( fontFamily = FontFamily.Default, diff --git a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt index b0f5ebd5b..c7c8dbe1d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt +++ b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt @@ -21,8 +21,8 @@ fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed( color = color, start = Offset(x = 0f, y = height), end = Offset(x = width, y = height), - strokeWidth = strokeWidthPx, + strokeWidth = strokeWidthPx ) } - }, + } ) diff --git a/app/src/main/res/drawable-hdpi/loudius_logo.png b/app/src/main/res/drawable-hdpi/loudius_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c473e6915a25b5e33f28794991046eeb9fb709db GIT binary patch literal 46701 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/loudius_logo.png b/app/src/main/res/drawable-mdpi/loudius_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b62d4b67abef6b2632188b21f74df38850e1a90b GIT binary patch literal 27437 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/loudius_logo.png b/app/src/main/res/drawable-xhdpi/loudius_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bbc4793682b67aa5f4eeebfead7a43a330749ef5 GIT binary patch literal 68586 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⋙Fixv3n(QvMH=!nu~ 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%0A$$qOu*7`bdkuWfQP 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

OC+BX;34mg8hlLbt{Yc|2P_gGCy+B#t` zyTcqt1e-WCyOjvBMN3j_>9`T8)xc%?Vz32v?)i}wA*LVLWK!s2b1gcR6lNlvi{W;Z z{2`NNoV)8ulw5wZg~~415Bt)Ti4t=x%~(RYMx_=$rwzNCy*sL8$X3W37oL-plE-gt zyvvuP*q9&P+KU>%6EmG-RtnJ3r9hsP@jIdqUzFQd)*_DOXDLvrCI6a?ld#guHli0d z4~WiMI>pR~V{Jw^>a{k+u<1++7`rMno8@Zq^^`hLl9^~hUL)obC*}1iZSgwqya_%LKe`w1$G>4!7YE5vT`yJ55s!*E)B_v&HkM2we(4rZ430un99K-)DIO$MuvS@G%zG(Ls(syV~jRPOx^B5qOMqOR;u; zoyRIWHBQ8GX!SbK=XZ&VW`lj_Xoa_t=g`UWoPY=JfEx~vO#Yk!6y2R>L!2Yn1gF0V z?zVOj$<3xipF7-Y$|oAS6rutsoJ@H`6_i{aS;p`gF-t*acD8|a$~V*~JH(M@PBm+n zIYgvUuz^|2c@b}Z@!90_f{ArYnQOl-n>M1|d`?kUiBM*}w;ffrSKo5*BE}Ad6vK4! z9WqbosdG6bC$S*Z6WX zTROA;%}NEfV$Q5>($8x<|6SH-7qI?%%;8ARXz8P^9(0){<(;RB*;vHW96+$Q4b6jX z@HLCu8{Uh2^8;`+b;@(RgUU-}a%H-pRRO!F6}>^zwBwew$px$18pm7fOiq^(#)Yn3 zD7N>b*xZRiXTK43c5#_)rp#hQoa{N+c^`|F8Dl$25U;{oad|z8{-Df9BG$evLKi+! zT`TWfz&8ym2tEdDliUSZB^5mJ->jC56$UK5{Vc4KiOs-PwI~*K&&_mf;mil}AnFBY zICO~Lcpge4Qj+)B3Wi>PLC&d-6VzS}S#iA=M^)@cz5Ues>!&4 zF(g8pF8o?x?kFSN;+eF#>gyN9ToIu#{(s+C!e2e!j4r~M zgSGW^E{|Ap{q^Qq`Ms4y6}hQ(BJB5>l9HjxIa4ZeBdmY!2nR9H(PrH4MEcH;o9&~y zsc@}rqb;Y{{dOpS1+1p5JUgDi;WiIWb$AhSZ~fi*c9#S0kRRTb5JEixINdq8Tm?%A z2cvL`_;<+p%2wiJ!a8r2>`oMX`%&~X&Wgl_Q7W492|G=wYW|E{mgv0!*y%_Xt51`# zXA&qa7hzAtU|*URQRfx;B>#rfW4Y477L>vrBA8ED@nhUr#(Lz)yqq{1tE1j#Vd{$I zb>(wca^0Xi`C9Y&VVO&vx+C5I%WFR+V~^;^KZX^r#?5prz2y?yrq9N!eFpddACmD1 ztOrydVk^MxI50O=lQ)=BTS{_4Nr)(pE>0ttX*|(VDFstLQN$MQPF)ccag09eY7XOx z?(NMon|uY$T$pLh8r?=C9PnXxdn>+q<`UwmjOpZPE0-X!zBuS} z;NJbaaDHSG!?W3UH0^jd+%(7X5KclIYwT{vrC`=HS2JOuTwvSh%PScaZ6yRmOde}> z%l&7DF}4o!95_5KxO{H7ydrpACHPwW@U@4~yekB^CnMK=Sw=2!a&og3T$>23R$2P* zM$z4dorV!kG@i0mZjyHSM0^D;aav8Z+bYIEmcJu^&#_W=v0ZL)J)2Ny>orcYEf5lM z9JWd@ynfi|B)dIUjWiz*Ym)gl_&FoscABFskJrY@g+D0A_zj{sUw*w=*DVtn*y=hN z<}w;JBR9`cEPMi1>_vUU8P-9yqL^a%FT*OPt`B^%m^Lvt@pcW2(lD?IL0(Rxi&LW} zabi1>6IHyB+F4FAA)75URSfbcjlil=*TnTS3 zWHG#w$Isr?Vls>auC3!3vo<(9*oE-!cC_yH!|hm>=Y<;(`J9)Z_uI^?N_G$O!IR>o zi6FM^yfhB8!Ok>Ws!}X^MHI`hapkaHxx=iHjV!=D6ZcFV{1qr@WU4^i(hFtigHq!9(q&eD-n}Cv0dPDa#q~T z%AhMuF~;oO=0N@S%d!>p!M<*c&MmF$_nQ1Z9BZIWcB_O$sB@zmGnHB1!I-&$_IDZ| zJ*{DU-(&aU_+T%dIeQ6LCT2xIr%gN^TQv`MwHrtNGmo4yna}Hqoud?HsfEp6n~NFC zh7!3V{>POlPIY#@p5ee_KGra2tZbINW|V(1^lmPsnqVUwgMxn*$|> zABCn<;-u}&_SY1}wp?{CF_R6<8Iw*dMdwDIKIwGFmtaptVV|EcwaF!x1c+nI*f)55 zD0J^be*fJ@tg|e`7LlJ{m=FQKVs+cgZS4-Mw}j7)yuKx-PrSQ(BCp-7D{krDh!Tp1 zX5Ejz++~F0NmvEaO{fQaGtEzmLp*7OxN%69>Dho|cSoz~ zM#tLNrgb#os00ecBO-2DZotXMTC#QNqIQaldiVZaXf#+N!O_*+Wb%mFrSILMm?HPc zF1BtCic@Yk!gnQ+G@NB5>DJA8c9*%Q03fe}{9xq8zQGu^ik^ zn<*dSE7T!k)72om_FVxl+#xT#VK@9OPWW1V2zI;R_lvt*q7b7yxjq-Ln*M_ zb%w`}Vu!5Ev~*cnzGA>}v*G{G-hY5ec4pUs;0f=2dT-17Dxlz9HyUl#hGdJLvMW-a z+SPuev6|6pw7a8`M$$?ni5g0jD4Hg_o9%&ac#lG%ysz}$g!ksXczf^vvVba}3Y8fq zGcxi&-9IubO=OrC@16hLbI)ZZ+Q@dmY+aHWHXpY<#a24Mn?6gkLl}@5H~0kdW~|aq;8vNGH+In%E&2F%PL8pxRo2r$|n1B zIp4QL7YQcBTw2c+6hWRihqg?lXe*vMCd8c~KYsO@hlrqc5Fz8e?dA}kK2oJm?o#m| zi2N}rTn{N`CA6m{fJgV%ub;PC@VM&&J)Ze+497?MKk@xr699SfvA{_YJkr`z@n7r5P-YU24iVQnaWFKk<5CAr+IAz^^@W49^_eg9H6tR7$`9g;l7>v`*ca z!Vgbf5KF0-|Bm3K`^2dROoVrLwUct$xg~j>>Qh*e2niHS}~ptj0J@297(OL#?3Mx``wm!A^#elN+kE z4+xurqUagL4GeAqH1S%Vg5~Oa`tFhx<+7hhGG`x4NG z5q67hb?xJgio&n0fmCvnuuvFoSswW{k1XsgkM%vUp))dFz*$Zjru8LS^{KJ)XkdOb z#UuI)CDD+`CZePg$5G5T=ca{~;&kcWveT_eV%&_@)8fZSXB+nPb%`9`^-g%x`KUoC zpd7=+>-t=6n-%$fa!xLn^LttY;(qQWWjN&ZZ0QtQGnF*^+d^U!5%RckWo7~Mk+>K@ z!O0YMBEl^`H@bp;?4wP_XjdD8-um_P)&l%Go?aVDqSEHZWZI;3A%kw=Fw|MYjpkL{ zj2FdUIoatH<8saQ9)HL0Gm8nqk#;x(CfMl;WHBaTL=Bq3ZiBE+n4F{%JFt@>2ywN< zh$qr%<*F6uO$Q%NZ2QQvQamq6FMre)C!iVDT%$yq)%F3XZGABC@DbuFY&qcuHZe@3 z(kXkJiCesio9%9qVpfV-5pTk>e5qIz%WAWUbMpOqFE%Pubgr$#s0S(LT^D!1F3#u; zb`lK;ee7`}vP65A2))0WvjLI`ak6lC)s~4STO!;JJ03qUii7>#czEv+eSM?qnL!X! zCR1!-;IjLT`Q@s-*@m@@VUFmsTS$qt=y71jNBgn6yA!*6+R+FT%h)5#q}W5xA?F}J zf5wiBle0)<^TLAQcQ`TB)k62T1J51XkG6o92v}pMAPlsI#A_E$rjRWZ#qqHjN<6>f zR$-q&T@lNdktL$Dlu^*_wxY#h6hl62b;@irn2oSFEU-FlXx-Nicc2KHHBG0W+Iu`T ziyvi+R}kYJ4NiMfsw2h1vnojOHf+2Wq&O+ohi4li!kWvcTP<3;pawgH^@6?AhGZDV zR8%bRdyoSGgqNC#vC7eZ<%Y9f7}i1yc=(CoR-%_6#i_<8^cw*;45w0v$D&Bm8r;2C z5aRnu6&oeRJh|>kTJ_mZ^BZ!R%3L{#jCrdxW2}TI?rc%?(z6^5zG^7Fj7VHi~#I3!Xl>hsybbV$vFWT^kiak4vn> zYY}AHTH~Fm!3pr)(CYV)y6r(Moy8?mzK;#}3F}9_p%3pcS2et2y(sXM$DE3PcQJ~G zNiE(RwBb-|ePZ$gLtO|A`_Vo^$8|ZjMlMhnsP+LWC$DdUU^7Fgo=SBUS}|#k$yK#k z$BhDRP)kN&;x&I$AGgLtnUvNtkE7;M)E$E;9(_g-F+(9Ik}pillfswGv2l%*Xd#B1 z(ECVpxTzw)qJNP|F~{dM3n}J?c2e?x`rYr~?H6Cf`S;$%^Ve>yOQcxW)toL=Os+X# z`7zWpf^FgoFmprNCK?#|ObXG(Dde+hXm#|zZY8+6E?LkJFMjd_!KCx?{Ui9yk$t4R zcGQFrGgx`%alt2c4G1g2Mkc=T+uTaz>*#Q=P=yZ-_6pkpH*mS_jhE~DY0zyc*tXyV zi8zzusd*6}w;80E!Gw4-?J(SgcQKsDWU`2bObN%@?FjL7xGG;fgHa#D-22!lJV6x> zHxUeu!>Dstyk_qqQe2-PrEP^`_4WzP<`C%(hGrqfcLNW?;IYXoGcZOLi6CEt<;puK zJ3V>>UuzFa!$*ibTg0(zOsv8(c^O6`%Gl!T=7+C~^|oR^zvj7AQ}hQ#R#tHH(j~m` zop0mJTW`Te*T9~^AsE*ww5<1v&Y_L05c3q0+Bes+uO*k!DOyXvWV7mpLVwWyz$WqyhZ^vP)J ze12JB=KIA%H%^T93oC}T4rv6N1w#YaJ1PZx$`>MW;SDb-n{yuH=gYXsI|=5gk-Um{ zp$xO3AT}}sUMqUs7C0@H-4`Pr0KPy$zsJsy6YdTtf;|>kttF~RZ_d;)&Duh5!p8Qj zZw3st@WirLl`RNOlw1D^h$u}}@T9E<0YKIakCYD-mum_FxYV6XNKy?kSdNNR$eBI@d)JUtOScLUV3hgeNqwAbXEEwnz7kh*JMx zM+<5Vjvf2V;k|hI+%+sklk2V&b4D!_^^?1YMDm5i)`7E%+ez_%<-#>6n>do5D;Qg` z3F7z2c%mqp@b1^dL*tRtqzs#8ufUkqJ-f>2ag^Kogyq7z#iUq>kaT}F^KA)9 zOy0RasKO2CEdhr=8n?juRxA_{U0KGr|KsoC?7MGcc6{Q)`{WOWFfcm0?%TRADW)6d zt1Me4o&f5QXwJK-IGq45zAXuHgJWsxDyhV?O{oyy0tZfv$4jIZ-%0wp49<;fzw*>c zF>Ik8;N~-t#Y@CnrHcxe;O_?}Lp*)%8;>4Ci)Uv};c6uCG{B`;65(V9i;;v-?0I5f zU131YrrA}B1ifF*tmH+m@s|eOq!^QG>@lxP^Jj(F>UCh`nO-;pq&#aIn{-$e3}%r_ ztQpwIFogC9>qgX zpd4t2+PzEID7H*VFXnaM_wA#ec@x97)myK>aec`-`CJZ5v$Oc!fA&wY`_Li$(qH>6 zY$lkCup<1%Pkw^;UVRnwQvu0}rhGHrGRPIW7Nb)GLCGQzkf%IGy|` z9z>#{z|D2pR2-Q^6a~?(+PtKgl`r;4W|cc1BP)PBbRpz(3-Veo@NFL&?8ZtwiK{b< zQ14U9>qac9cbGhf{O{Ug_KHd^AvVLh%J8JRbXdBCvUh_@~;ML7oh(c zcEWTtdaNoDVzqk&rlmC80c->Z2D&j9iNka5DiYb;J(YR(k@l{QXLi|k@D#2F0+UnrnvZn0VtP4DRR#N6 z3|{>p5>y*N_oYx2e$WqBy1CRZ!pyKhCHhs%IGmz zuDqqEb8JD?^_Hjj4*SF{6mz=5Z@s$)swj#e#7lE?IQ_=!_~6YqMZ&|$r=AvsxSr=B zZB2DgBoah|FJt1`HT>y+_#KR2zb5PxcZ0*}B2pYgTj%<1h4+(|4$wMD>nQ4hWDJ-c zr*#t&jR{tXi@6MDubpXXLd;--n<=T7OW!$qncwCkU2XW>vHjxTWAiIGJ3fOSo;r`! zL`t03%w{86ydLzn1Vjn_K!3MLAMm*x8?MT-9b$i9C%%005Wf5NS!D8ef5W?MHqlu3 zm6L}c(Kz6VeGl###Os%Cz~!*w$pgDkZ?F>ENR5?I(S)aBJW;}P11{_d*wF31^K!%9 z!`_E`;q9`)s7A0QFxgPH)V@DpyTMRP4=|VB*9c6rLm4_G8thC<6ELpMe(KK}<0Jsu z9VmK(y6>2Uz^$0ryO!>yS?}VZXAA46q<_s#PGaiD4gBN3`?ryZM3K$viukVm`$>&m ze=}PpVB#BITEu_;SN{qxegAu;r7j4;?$a=`Zy(ybyQu=T{yvBMk>a06JtNqzo(5u^ zlh^!jp6v>@OvILFbwXT)ieoUDV4_{M#Y-mxK0xV$Sc*rX5aHGuglxy?LOz`=iBjYiPmz_yPFFaM0yY@%y`QCt90xdJK3R633(@WIV#F?yHN2o4YS;^{+s zNIf>Jd%y=rs(t0;VZ3?yCW^E>FDajV^MCI6ejFL@g=?F|t~EP229IC!Z@qC^SSWIZ z5*)VrMza|kFvm{JVk(vK`dk*{@ghcv7@ugf!(}nRLeEFv(|h3#7Gc5GMQ~~c3n^xe zJ34&xpy`#{T3m|PtUSV3+{~cIsdf&*7+Z#E^78$EUf2`zFoqXIbQ@bLj9Wm84II12 zdlt)Gt{bKNCu|)|Mq}YHe)xxffD7-R!pzvXh&@wo8_v?ddl$O;`%n+CLVWJscktdT zuj16}uVMP;4RL?g)9lX;cA@r-RqD+5xD)(racJYi&|}zY3(mw7gF0wu#nV+JCvHe+ zv^7KS%`GEJ`hc6er^-rIRS7Vo+(ET{5>Y)oLL-6edJN&$!-ooyn2W{2e##)i)IE}Y}$NYv5_fJ37Lm^ zIBY)bG>8g1y)Ra>3U2YEPNxZ-K06)PZuq+Fuvl~S?%Bd#5(BB4JAI1XEeK_|1{Y&a zHb9dr#eB{++w=h#t8*}B);Y+Eu5ZRnQrIqF*&;E6Cl>Oq#dsI`$Aoz8T3=?HU?PE~ zxmk=|zJfPjcmdPn<5*evr~zC|P^H^qdxAGm+qB*k{p~1`@2Rmdyz}DEaOw1E+_-cJ z*-U!fx6wN^1Ya3u1+OCSwiILbs_IZ8KyjFLm#$r zFQTIf5^!mH9;dEPiiouzzkdOL?x7^@YWM2 z_7t0>QY=s-KezT_azXQYiZ!dANo*+QV9cjJ*;scEh{(Hi44OLtExAROAyvP|adh4% zzo(O87I}*KyhwC-{rMl`M}PE3C>4vJ=xMHS=pGytk!%~my-4v8t-k=B9M>M4IC1$DBpMsD*UlnE8<%ng-N^Q?et1Ur zqwSfm!DO|;Xtmxo^2p@z!o6!h96f_@4emjD{2F5C-rDqwWl=4QvCh-UPm=PUkbi>- z0{T=owpa|bh49sf4&$e1uHx-0W5Q?p1oTAg#nIjvlMv|A&?=784H1!H^_1|E;R zUI?W)E}HZ1s?=@<*!I9Lwdrn3v4XjYaa?}$1N@hN^)C=!T0$ZozbheTI6WQ=jqZZW z?G{N7Ro^xC4U;BLW@=k*jtb9qGj@7p@Dh>apQI^tA>*+08GpE~N8C{Qk@D;L8sl z7LAdXIz`#ps8Ui=3n&!pY!BchEk)fH`QeB5(?+q+1($;_at&~Q`TS#&$t}9cv_{Dv zY)RppQkyrDjguS74lfKGQM%D_)*=VkxVCxNd>F(I2zSH~-fA*`|JAo}Jyygu5@E)# zUPYdSm{RH9=P#Shjy@vAtQJ?>=zIO*1x#JPfotc_jKRmnk zVbo2w7c6BJGb!nR2V84gsyV}>hS$9#=sT4WHBb@gPHbc8UrSg zRV)Gh_^C;woldrzIj2~o_qgV8!4Qt?DH0p{Wr*iU+bU5*yA>m<_rj!#Dl)VdQU(0* z%xkzZvyAzrm37^e!|lT0uF;BBVkX>7ym_%z;)A!|!uzkkj#NB`YT$4>MK-aGs)5E0 zo`3K5NS;Bxrq?myxOqXU#0`!tkztNt+wd83O%ZN+@(XZu4bbNs-wE5>yI>2oVfB?C z>d6$P&FM@(7tV<&w!0zVa=}@H9AG!+*LK}{ zHB$E3kN(t?yHRhH%4L+d0V{s=h#LcKd_ULUEK^r7If2|@3jNSvsEr%TXwcQjX6~Tp z0VSOCds}1LMj(9GTQx974-^-#cabWiFO5fVW+90mUtbkA!j0%qPNxe8PMm^pCxR&dZYo2S^ZlD$#fL40Y~z;f6ls7DMNH`npxxUYVPj(=lf(JRS^Vjn zr(vfmLVtS;Z1lQ@6KN_vE}=lTb%Yds&XHzsc|E}wvC;-FW}A&*z=`3376ctRSPYGH zM-5x&?QA44Ar>Cf8l2};J?EW?U$Yv#*ncgW<~B-h4Z6zAgG~%u<+&yWe`#eE9c z@Y5TZjO7r`mgpR^v1VPqE;&7J4DZjucDO5#o?oyH5_{!_gE!V4I?bV)?4)e=s( z8@mo1+^}_m@!{Ek2r(bOY@O((RoiS01_y&j7WJ^|W;Wn&YiP(OV?>B!!c)9a7&$S) z7l6^`5V^z^L6npXq!za$w^Ui`#YR9?BP+ixSy*5^h8Iwz^;#lE0KAwO$OiJY34`QnX4{BXXWN;-mhcl@~d@!FtBCk-TsWRWL+vC|#M}-u&cYB$W%KKvzQ=LS7#P* zXrNn^^Gzmsp{^JB`F;GrD1LJKA{HZYp$1={U&fiS8Qff0#zT7s@!0+mF^!O$^4K2n z=9Qb6r=Mj4&94lTP|lD3?|<>j*xT1hU*9@Q3zKk_&a34HmWsPNTzK+iH&scv0@X-f zQ%Q0vIS51n!F8Fh_ zHkNd}9&p1J6Rmhys>BTnmENGMbK}S)BlKRaaKaoZ4MvN#CZyOvJ3T|4m4-y2jQO;J zOX2)oQzvdATPTRBaP z8LoFD-TlCu*({Owasy8g^Eiwc={As>E(vHOLgRByjn20Qt_pJ#m$`XKv7wNKF+2}- z^!SGIhBcEFnupHqb2sQVHfNG6Cri9B9m8o-gx9&ef(K*tkBr{;=T;UM zaq0B?_}O>9i??6;8FINC3c31C@@sAHKv#c1y#7EX*HBHo3rMx&1#2XJz^4(w6fC;Q!WidslKE*NviNl zx`_8?c|vSd7to0l))%-e6{>iP4ycJ7=Q z4_$8rTU*i5(^ILaq~1Ded87~9CI(RJS`weA>EJZ~$7?_SG3KYI>2kg~(`tEwU8ud{Yv+fgcn_^-Q4e!fG6ryhK%`Fo z5s9i1S(rwAWscsX)y1Ew*$k35E~1mmLN3o;C)lkQscYv@Oh!=;a6{cA9Fj^g~poM?`-n$56sRxK%sT#^sET_WAUdLM5SH+1oYyMOvuzls0( z_rEXvu6DYON2zpQ$>0!)I)T+ME(`Oa&F_~6P-odo%+?OILHjjE2V2qK8z4fwbtByG z35`<#@>JHYEG?(mAjlmb-&I6m3I&uK?JUil|EzDOEZ0w=>~BZu$kVXB_1#bZ6?#52 zR+eop7}N1}kA~g|@=94K#Q*%wDO?X{a3frwq%XeK^^cBVc;6={b#mVD5B}i4;`AG@ zZ;CSfarBRlisXq3QMh%bW|g@1F>H**Nja#afx$#Los1w+10|o8VwDtLj#6Vn%+YKc zg<2>OxlSXuFatGH`z%w|KZ?X--e1az2)6ZzM6;_=BE@$fHo#uvNHQ%(`R4LP!IjwC z$t9{1NA~cvJ|m|c1kmR9qLqm9-o9@5sa(&c_uIiIngD(MGe`H~;^Z9u?47fsiHl>v zEX`bsY*er}o||Yo`e>8qqvhtbdZG4+BZ+{&fB)FYA&zVr4prdC!-MK++_F9&vP8hB^JY5Kl(X7 zK2tG$7!mhPuu{t@JG808CQkF>yTP7!wLv|_UY89XcY~5*j@HsPHwB=f!6UDY)isV9 zlw0E;qO(4_QA_Yefe#8Q%|nqp@kYhU^bNX>C#i~9y9;Xgy;nsE&feBVYVrEB zb<0GkGgCLP{Ni`&Mk-cwikR)re&Y3FdL@e0M2gh7(kG^$xokEZA080vZeX&)je6%M zW^rX^0as@i@w-2J3xn+~I6l&k&m7r<6Qg(jE_;c2h{Uz|1$^(F^LUn&(B6BK1hxk;Y4)xK_Oz3>z>^^7C-pq`O#&;#Ji5BvpksCYJ}W#fCdCa% zKH1WtZr*Fv=+LaylVahOHSP?hm^qGV{sxibF!E1+6Ph;!BUN23SKfgkpSr`Et@=YK zyS*@m=W3=DFQoGrkL2*L-kHXHvVdG^b1YPBPjHa%#S`g%{H<@{kN@4jLn0QXBEi;K zq|9`@wRUt0Ke5N}M}-ii_$$zfac#jRMOIHXX-u!g#=ui$O2r()^W!x)z-v@Kik^NQ z+4*U7e(4v*APzHojPLdHbP`q4DJ=f@kCC05Kz?~1^+1+()mI`#ytR3fgt9sV%+ zU^fxtzy77C@DG0PyEr>Oi@w&7Y`m*C*rMX4lj+f?xP_Tzhm8$1XrYqbk;j16F5ujo zy63pgmIk&?@cTEC$~LT0>~xr6vu$l`-MwG{DSJu+G8ah)TLE}{aW&0FhFlt&%PXt` zI|B6nW3|}v;(vkCzaQmL2lAi)i$s2BVT>%p7+VtYZCaom8dac7^EXLRu6%=7R5YYY z3jXbTGq}8*#zHbL25@X}pJZ2Cu-A?~cCxd#AyLeykyu@6e4f^6!wl@*jhXF@y~R! z6cz+h+1NW=O2$!)ts+kZIz4ff$T6wbb=Yn4QOJaNDp^FVurcz$#A7j*z}d-}4U=N_ z35zk!HY4n?i2LDn+3}z{h<2Y>j2j+b4ioX6!F(i!FP=Ck8t&Le(QfjhGvG(IP{ifw zc_PKEy33r`x_}8WS2bF@Ico^8yIFeJ&`pTjaMI?lg-CH{zpfb9IiR13aGu^TT*hqF zGp$}lZqR#I)*J04*1&eRD*G1z(TH zLG2zPrMR8c<1CE1^v1(I)~2dX#B!tzXK{5kgZJkWSWXv_Dr~+wd-LKYp$w;!Nvbq& ztv!|_*t+`qFt}?sKKr$=p?hEe6@o+PXZfh*^yS*;46(OP#;`RkiuC48tk!SS!o*fd z#SwY$6*#&E;p`oRd-MQo?OiaMEusOf7+xYZc@o)~F{G|t&~u6FaDQ&y;+1GY4C^RW zU=`sJzt?7~Pf%e>%M;XC^&RSH!Na5u|FiG@3^(SM#MHE|pq~`wVUbwDLm?j7J0y}V zUb%1`UwimyHGv)yp9U+b!gQQs_YuHVMN~VJ3M!Lg&v9URuI{9mquA29BB?};D?XGI z&OvX?`)7c(Puz|&RY#0f4&5-&wO^z2JFCt+3iKW{Mi+q?sly8sDEIGy+A#pNtq*Fj zlSp$CX6`|#sCuq}XY))DA%1--h8M>oNEf%xsxWrtG8zR;iua#5frp-c22XzROBGGA zTSJ)EGXM5I)EXQ$5?uinrXbOH$Y)bX5FuTe8Asj0o$>P%VnyD430r~XtcvSIh%c;i z;zVT^=eckU^R%XxBWUrt;r^JnxDqhwbvsP>AAkAF;`T32&EbFjw9>S9ccHs*r z4&bW~9U;nU)Ac*nN;l57{myoCe?4M*3@Z?u?rPx-33HMgJt`raMr_=2GN2wHb z0Q=Qf;z=|za-?MCl?F8^lnbP6h9RLt< zPtckaF>qfwc^G|dAsiX(#cLODi0NyR6xR?wQgyqB0FS@&TvbI6@~3(tNw-;Ifm9yl zIKkx^J&B@@C=~Va(~XQ`QKhG-QSB2&7?n6AbZAsDsdhZGftyU3jq&D?^NJgI{CKqx z&lmfrJw$|CyT!kSB}Xm6a_J2*%Gr=l3t}wJ3+v1hFR2;|^s~?32;<61TBJ+xcxGjt zRn9K29o=3V1_BNoX!oGqWd$dI>hn9aZAXYLM0z`WdeA#Gj0c~15(B$;i$PKi& zV4y7sTMdV5@HD$#x;^i)eTWem=4|9Qu8d(9k#C39jD7uG`2CmP#S*E4Ej|x@O%C*^ z8aBFU2U>x?(Y0YSA8V@P<3*(CsQSf>|IOr=O|oqD5la;#U&Z3{UM8hq{E~DLycNq- zN90Q-BpaL*XLAa&c}{z&XzXfTVJOr6Qn*Rz*12Ja`;(aDnW{Xu+_DESF=k(}Rv%m3 zmVh1;k8}n-M?9I1m359;D|<*G_O}ojR)_@WV2m%rG&fFFi0sBJRP;P0Nyv%jilh+d z@xsjrZidr{WJ{kaVOdQEcx@Iqtw#9l7VK$tp`Qrx-c~mb6Dj8Jv;X+~Qi`sN*cLEb zETVai8|ONEx-qiv0QMd|f=565tVo!skve<}+r3279PXp2GBUM5xNco$HUfleh=CsjjPEk5aoEm=N>0ael9``jw~iCcS?| z?mEXUF#%4}ztz%~O}69w>sV%EX)2qINGyXyidFaqtZ--r7{my7K0ipqzQ&tFs~uH? zi7}Bu&F+SQ1u}K({I9ixPV&p4E|lAPp|%f*&ke~aOyN0FhTj&R;&qLCz8mNo#b=RI za|yA2e0>%1{QaXtO+56;VZpRUFH*f@&$43;LZqR1EZCbqV) zLY$&H(f7tuB9FLO!whCFJH1J2F;A@f%`ZM7e8Gkqcfavi=YvB%_<#P~b3z?Hbz>6O z=a%r$-XTm9DQ5C4NpTIp7cchv@&=yIUF9|Pw@Bx(Vosk}cS{j2-=-MzDzB|A&S2*u zFN@K}F8@!@#xdW|i8Jm^9cr>Jh|{17V{rwV*$vI;#!d#4SX~JB6@GhCLt+X+&nK;Kx+qR^=Re(*H9HuMgU z;MuQz4G%o=1ll^ggj($OdTV~2{TQC7wZNa$mK3vMl8NiYxIr^BcPdnRvlcRG(O{>l z%{#$~vl^GO3MLYCl8G0QC30L#OB>(R#Nw*RAHFm_kA1yeHMLPNxRId4??nr##s>#_ zFuAmfxkyY*wVPa8A-zHqQC$3UQroHm?Du8jYh2OE+AYG%=P$mv3F6z>=dn~Sk8rB7 z1&Bl{hfH39W-!AbMk+VbhFrfdhEjt5W0s_t{lr$A-fUNm1+v5nd{N}`YKidnIZp{M zv`ttdG`oZDzZYc}_Z!gj!uieFB#h~V7}mkl>`c+6HJ|Xtk7>@oXT9d?Y8p4fS==O2 z{PuJVi>ZPb+k87LCIehnGrGJs1Z-xsxUD$U?m>^wj&`>V!$HTVl13JDIJ#{nk!vW6 zw}>kx((4O_Ftle6I(mE2H#CexCr_fQuV0LkHq<|IiBIxpX+`-TwI#)@nnae@Z6=r4 zJd_G~F}W_AjG~#raceh;48J*_6-{>a027hvl?Yxqa|J`B*xKsg?`1WZ*Pnml0sPs! zXYu3rFN*1T<3y18WLl!#zRhBQ%xJ3uY4(zCedv5ONaYJyjx~1wMv)Y+Vu=U<>@YzQ zo4Q7Wu}JSL6A{6kZq;A&%$7;vHC9224LrCepGDd3q0M_^4sN+C3`X(T)y^SOh5Mkk z4~oZOiY~&mG!4_#B_jLznrxQ2nT}Igesp~m@69GK5zUHo``ut6X_yJ|Q-eVqC58A< zhlfb=`kz-8i{IMp1~pX4h~`y!7n6B@~_Z}@fT`K zit}`^%#7)Zu|z{5x;Ty1xd}8Y*fNo)cmBUVA4NQ0M!ZnpjoET6DXa~g1aWkz4-f9H z&l}n8u;Ckz9K-REKK#vp^M~T3nJ1F*_SJDQ0naWQ?shDSB?Zw$8uO9HPfsYSW+Vzu z1k9O++{YR7Q>2o50ec^II#}H;2RpP;T!> z+1nx-=u8V^FwWkD<@Ad%CTrW2&Xn|}u?T)fi*(=cp2vK)y+vAK4-?olTZZI)jx&67S!b#EH=XNs4zcRuUhGB=KWLMY)5k)f=GI)ybtb9-UPabiRm#+7*( zb99}|rRe^rX|q(jX>IYmf=ME$+(h@md;&9xJThxJq1-Izv76B2wPSy~7acAOdIJvh z`Rr(MS>dr(r?#^tgMG_e1)JRtr`wGOpL`O-d-kGdXh=+;>*(o*%e7_Wz&V2LBCYS; z{>9pnV)oy0$wcH6OZ!A4A(Mz8pNfeocFl$y?R;kiAysE;Si#uhs<2Q<>|BJC8Kkng?LmsEdNVbjN58KO za}B}`Y7L_jgFz)Cq|qkBP`3{EZ2`^dgys#R8W{OnV@X3}KdzY-PNv0Mbez)plkbDx zL}=5Ma3;f^AdzDqNtae4#1TC?A{~b@v5H!O-=H!UZzJ5IO}rifl?F09VoL@vkvu;xB9(?V|M9V%;nMU1F4J;4tVCQ#G3uy&GK7UrFr!_q_{xKaaDGxB`>aUs z{*K1fav0I{_SonyM{;=o;tC$w#T&*3l#10H3`zpVQWUDKJ{6zs0?pxq>aTJ*QI3>i zCZbM{-n_@ew7do9D^`YWE|h&OP=*eo+}b0?Pa6|qkse{0z64Wvp>Fq$tCrzR5r1%g z@e{t`pxcU8Qh|TrK!@-M_xl{MTk2D#m`xPK@Q=*|6XJoxe-eN7%e&$Cm>aU1Xi(x*MxUix?+z!$oIJq;JWOk^Q$==2O&@Wb zjF3W0q?E|9ag+D~-+xx9%Pn2HPuQv}|EBOfty4k`Hmt>{88~{e4hb{-jGbFd3L^ae zToO~UEQ(asC)iWm47tS2 zdzt)>C5pHiD`K8hV$LCMgonm=fv3>%2`G`t3Txv2{w}n5-3WTPOjMf_9uN26)sO1R zn-bd@n!Yhyjv%&8l;UiTDmg_3`I3z)JVsayjYx`tMIz zSWq6b+|k>IU`q=+dwX&Cp@%Vg;DAtxJ9>K1Kww`CN3Wfx^)js_5&2J88)`_3v$~&H zqG3>=jY@2JwrMh>nGh!mDqfgOW0_7osZ!%TuDNhbBvV|RTEH(o`5+$NGYr4m*^tde z65A7E+O%30o3QvcA;o1<8_T2|#tTlg()-q8t*>fT3D`biD$GM>CvnuH392UqqpA|2 ztneA(EcMu`uq&{QqF!Ko2HPXbfp#b($Hd=b(raEAgZbKfFlG`|0oWYDE=Jd->#G_3 z=d<$|i)Muyysy=b&kTpKo5*m7$GRE0yB~rt{oG{7acuRdrt)LqDRU>DcpQfwco2_$ z?m0n>8x-TT{*cyB>3%QV>xXJbiaGx_Ry%{bM0HS=5^c=GVuIav2Gd@CGoM<@;Zmf4 zE3}jbPd6#ig(RFz;a~mWCH&q?Z=pTl!(aRS6WG_+h3=3~?5t~ra=9!#YUai$O-XD5 zBB=}}mR1qZ=ESkKo$!N~m+T`0Hr!XO4}L|{GhB8>4BQinG$ zf1GU*Jb~`_&Msh_h_I8?->>d#!_z|{QiJPfi&)ovqs{Y~g(NB1wLMSt3=WEka}Pf8 zB%b=xm-UHr#)eF7S)}z6t=DP&F0C>V;!hE}#!t^&5fkuQy&ep7P??E{a=_z+&*gx1 z1I-6K$u3_kVKtE=Ra!4cx@=Y)9g+E}I|b=n0W&L6sOt8s0A0JejDv%%bY%*_s5RhZ zO-+Tdun5)Sr`qt&CdI72YL+UZLbS3_X*H7t2KM#x#=XK#oS2N~^g?s{i4eDmm^Zdf za8iUZx*(KbLq4l#3Kuu83b9ILSR^~7ig>8kkH`80Fi}~s%VUGfVuX2%ON1;SO~rD8 zxL1pwfkE_-j-q#Hh{*60XzS{R)8&Gll;Q>pALktr`=GAVIz@|>K_1eut|y^3q&Sz+ zGm0fCZV+flMV63F#!=ozN|MM)p*JzVku!>hi9}J1Yd*bPK%NMIx()NMnH2N->DkM0 z5P9`F?08_;0QUBFqQ5PK?iN41PKP*=8o>T6`tNPcd7AK#YNGtiX%o3ZNo3Prn_a?% z$vI)mX!Cn$p7VN&?B8*gq#-G%#EX%vZr_RkJpQ@Q={AtYA;YD$#bu2t=!E!rT0f-~r~A5YB2+yo zW+jqTNud-=GypX5f1Y5MO~$aDz+`wiqvB?~fEQ;n$dt2j2EK`ymH|h9S4o)%e^1?B0aqfoVf`+xepBoL%9G`<{}i|QBpVkbSiC#2&pYW zIY5dm=lBvK0yk?{bR$HJV@sqsTcLIi!#sZe{=Y-#G**Sxt`Vq2g4NzVx<6TrTAqUS z^iTCs%gWY?aC}u_Ti>AHiQ#~=Vb8lf!S3=(Ml=9!1h)wBffEnl*rSi)xo`X&M)&WB z#aj7Py*h#!KP$nUCh;F>U8eiLhzd~~rI_=HOWW@R-Hc!_aWJGM|3 z5o;I1c`=cWo9R-;GL$Ar%=k3iQX3W`M0VpzSg9mvG8uK%c+{%okXQDQq@xPhM=|&+?^CN2{w}&9O{A==ztn% z6@xXHR~YkY;S)BbVlWi4LJ{UDH4*Q#guU+?bx&~99H&N% z?AwpeeD$m7=XpM;TLG#pfwFWVWKh<{2FSY6qogkVriXd2yhHr zDz*%zRHXApW0qGH(LGO!S4KgU2yiT~ip=3_(IWEn?k(-0G2#43bH!WT-5cM;@b9N{ zdAxD?CVZqcOH$n2xG}eksg_8k z+PT#ijcp4=XsaT{i7-@`7h13bA5OO8N|=3ZvYegMDctmteujw`PufmK#qV)^nsJpq z!*MiHLy2Jl!VpIf;n1S80VwNUBN1NDY+YvOdjDlH!JdsuYo+lSo->T)!ck{@FA9-ckmFfdy+)Jxx7srbwWUXRg-y%TFfPx) zJU<50;>4D#!P_12dy78HB{vtmA3*Ur47!=lt1_Te@U?Tiyw7x@Y ziimK{bAW536vyc#-9!`9wktH+m=v-Z;U_LPm`coz2t1ORC(unNi&)93+b&6>QNbi5 zn#t;(<9G_~emUu`*}&d%9@KGuY913y+nW?KQdHLcFMo0dLtQ6fvOCc3YOHCfV&rpS z9;V!NlpVv+3=Qd}tUJnH&i_q{sdR>7wj#CGP%MZka;C@va-aJ(F~~!VT8^*6Ja-e; z4}Jz?cwP|aW*9QnC|FDuaB=DG;wcXi83{V^_5JNQ@fZINg8NUvFmxCV2)0N}(fUuc z-lBE!ZUZ-JiQ0II*@Q2Z;w?uZpCwY9(!Ht;aB|_QO*p5BK^shh7t$rnr%Ff{HONz6 zZNWZcP9*4U3F6sf4cNgXQ5D!Oadv!0#QSZxY~p(%mCoa*Z_FSP4d53(>!*uRo@yKo zvYF>NF(n3b`Z|iXK2kV$CMjlRSBz;6b_gX`uV8N+1`^FUKUi(;fho2OLu?gBR*(}B zVUaL!!@eZMHASkZU^Sx$Ah7q@|MIdMfJV%!*y z&rnLhl(`N=;89`~c2bLX=71cIQd3*|U|eN2w?a}(9KpCU3$=GQjMG<6Agxdv;N4OW)2%l=H^{N&fYsqw zp*PT7pljI_HWAg4VkW{xnNQp(NX5fQhL^C_m`;{(DU!$KNC9&xd2DPJ#uisav|EaZ z6RQG}7&iyF5pzBo!w*hf5a}b$ghHW&>yt73_CGm?fAQZQL`Ogsig81rOv*J~-%RNn zP@Tg>4m+?Dp*dVoMh?Sr^8{u8ZtX}v+$t~qQGsw2gGf|g$5 zhJc9X%A}A@Z{B#z+rc)8rL2kzt9g+m!I5g|l5CXNY{WA;yn5j}o;kb+ZpY4^$ki;k zI=hG$&s{~aq%F_2zOVyni7gvR_S9;do%b3r=eZX5-e|@_6jCSjAP8ccNM>a)hlY2RmUD z5v-L8FB(h>6VN;Xlv{g%1|!8qwJi9e&1~5ARW_T+fL^a1MvDce)=m_SWjcPdcUJZ3 zHAQP5VA)i^JX<4J1?D_q4y|TFe3905dd{KNKlWBbKy>rOXW>vln+aD84tF+0P!I`lsG&>^EG=BEx z0{-o`C3p7gi=h3vC`hJE0qnjY0px1Gfq_t zwK4kCtAd?&msV1X&1Mr+R`?6KPkOld`ETpvi5=KPOwsxYt$$1FzoGSChzy@?6hh3X z(mp{daiPNTj}kRSD!Kw?6Rj*4BL!Rx=W#PyM696dClW|(HCRpL33r#K=V`}lKv&2Q ziR}ljz$moA{pa8P8RjFg52qhCI|@Yw<1;b*=70MVKKsNF9zWiKhxeOFq0DY+GKC>O z3v-z&aqhh+o4xdfhRkuTD|k#ZlPlBIWief@60r5ckc_~XNs95!#-#~;LR|yEV>gkS z3cNZQ!Gk?Mcx~JNHP%cPsO^JdM7AMuTa+U!;XJ;3@IbvuF`u|judSD$+aJ&MwayKXJd!z@Q!$q*;p_@G(UlO+t90>}bhweg1d5XiJa)Sz z#oH8|H+^G%3FC_^qS0=ryuP+xB~p*GxO&4(2*1Ca!wpi13#uf;jf!`# zPGYpP11EP6!ECC|B_*-B$QMeOUWwqPb5{{bWw28*HW|jmbQEuXV1dyZ!u}yEdRofx zxeTxpd$C&hy1z-aZu%PrH4k&@Jj(VS7)&l`#vPVjY&1e6Qmk|h!!&z+tu(mt=iZo# z6Z4pd^5AnQPh6mlMy!D%T@50!VIrCnTOO~~w0-@>Mw5us(_B8PbfjU(-A0O?KA@#b z_Z8O$tjHDC78Bo7(A8m{J{P6uaN8s@RM}`(mT_zif&wYUWu-m}3KL>Zl=$I9S~S#U z6-kC09X#D`W;KG@)hGr#s`CL#Y%VU(EDA#Wowv_or-LVRX3_=xAOGPUSS%)3%qINi zZ#;w}yWJS-Bqh(vjq0N17j=iGz{oZVQj1kJ1BDdhod(StgtF@dtfyaupcDStDGjXKs4(QF1(Iq`~;&y(( zqr3a|qK4q+Ic}Q!8T4qie7z^n)eF@R?`W`b!aEfP(KA{59c_fvVV*+w{B%YP-^fdK zc-w$W@aHDyBq`n|U;;Q7j^o>Je}MO{P3#yS@p_>sWfBz2_}2F?;03n{PP-9arwI?A z=tWn%3w>QqwD~kxNR%=giahro^?)gT6$T}ZvbBp=kWRaeSz_nPp`s|HTdBld%i{Pd z7Ib2y($`D0$|7+=6+c^COOjGb^gaGuyLB6?_<8<noptO4s~S zoXXJ)RGqUzTWh-0Zdm{{JvV;sPU~M-6OY7g($9T(9u?=*@<)I0b{heG-C}preYVgt zV}G3MJ>)`&BI)Yfi;|GsL73t0<6KtMbpQx{6!4MRtoN3 zj4X(lFpY@6+BpQ%3@Pvp;w?VW?ZtAcfVZY&I7h^IAm9*d+km)$=JrALhG1QqhT--= z1fF6S>;&8uk>V(=6cRt$J8_-wyh{H(5VYPSi(w?Fy!Q$U$UpZ@6AZm*htyb0~L z?`cG!?|6yt_&xnQCbJbbhZ8NkkHJW!IaescL)Y5HnK?|)FX=_vwV&7UQM2J@Rq_%A z1#eC#;G%O1dvGg-<=O{Odqz+^{4^Y^r2nh2hTs$)s|iCv2c8-X;h()ZiLdW($Il(; zfOnguQK%h5(2QmnqsyPPP7EIfT6$3dc-qPz()tdqS^hIh5NaAeySgRO`iZo7DFuTMpB zVJU?Uj}2eg)r#!^wY^WIg_xpu@oIOE0vj8`!Z*NgA|}IZm6)ODr)kT_)tO|Zr?_EY zcX{9qw88DKqN$2r&hbPM*P;bXCFS_;W&taanY_#sZ#3knY8;5%=h!HtC#WQ)?K zv>DhA+YTF%Vt-pVbhX+5=9>Yh4Q6BG_*!Sx?x(Hq&5E0_jtq6 zF+{to_Ki-l<3tILTAL?A-1Hs5)stf5_RR2WP8jHf>PE-l9+V1s$$=crhuLrx>D;!-PH$S|NeP}_iQ?_66Zl|k3X{uW*-*F15RShFkI?b8bi(Bg zt%*)L_%kz+VUODlr^A6vrm`{1m4K}T{P#T$QW>F%+miB#E)K$)&T z#tNQWdJnLD!ZdpWn%N9>=pc+2-)M-4HpZg^0ccYiR_2rV;q?{t`yFtYO%0h}tU0|< z+j?PMnSvqnF@H86Q$vR{l~nCUU!Gf1&7mtNI0xFkI1~XhQtDu=rj2usbpoC4D$WDnGg|K z^t>-dlUR-?ab;!^FMMzblS^S_3ewWD*;ormYe)$4N#|AW4{`ZF*}P^XD(WH8!{u^r z3Mm%%$DoftHj;wd5%dWn?646Trhm87ze8R(TD>lqU8G)Hsp6rev0<c~-9>&Ef7)f#e zaAUzdji=agHZfZ$HU?(a5lB+J4)6t5D#Z;3-O|!Ed;rPF5_pflJbnG1KYcNh$C;HJ z#^W3F5IgC_Ji2ETT|J$6@%fifEEFXvZcr@6lA@uD#|OLYHb`t9N@`go2mI-qAK;y9 z6Bt`qL7^o5`dbAN1&qc76Jjfo8c(PV4v+7yKNARe;q$n$vQqyfEKcjNkaBCIWhWy1 zz^*}zbhKh$UnlnTcC3-+HBwk2F)_IegvTrHGcqlZQtbAiOr?LNcQ?$l*F_WFR%2hA z8<$p6=y2PFxA+IwR*1uw>FX{ul*B;CPnkAUO5a{M&c1R->V#&o!@#zQ{{6zDvO(Nx zwA&G=juacEeWFppCo7M?6|RE;n5|aCmS>So#y|XirdY;uPQmxaQz%wonfT&Yzl4V$ zKZ!m2cH^7B`tx}EwRdpi+D*Lo_9^7DS=mM3K;X>^5AtBec(khx67>Z)&@IPOc;oU7 z+$0jr136aWDd8P1D$*{om1yKH7#pdh?E|}s7&~#Vd)XH7Bj9zTUf}Bn53k@#AQRum z_mAN4U@wM<3_EEYMx1z+3nJAfY`y||6F2dEdSSBg!t9NU%ZyOhps-b#txG~ZX7y?@Tf&*; zylAl7NIlU?1e+}r#~(O~q2WRJ{8UnN*in8=!_deOhDV2S<Sgz;6ABCa0yAcwQH@+Wk8BfI10T*)N%w8jR*F}noW&&46}8@VzI#!>L4NG-UQ3*w8QPFee!{5Z#^kCq8w}|Nw@&>($pQ_;>Oav(E^LHV!Fon+Sybv>&?jJz}Ke zfxX5*_lsZ0pZmElBbi9zr59et+poQiYgeu#9#24GYmms~5KC>WImcO~jXqo9R~gox*Up2$>KMIgRx2a3 zr6SBX&!Ft`LG!hs@X(iGJ@YfY@otO2mJ*-cf?qk@i9f!$gjpiP>^l7azB!3sKGcOj zf3O2q^VV=ijW$!t?&I{l6~yRlV{G}u@2R91b5BXuu>e?eh5nkEqNHLY> zVcBpeqr?`0trQM-0M9-A0G!?caA{nWc-M=rZYrmZ4q|lID4k3{{m?Pf-{LOm2kCG1 z^z~qVb{^w5Ch-2d?;{qABb(W}6WmS&E5(%zhiLPA#pY)&62nqFDaJ<6tVXEJ>Jpvk zE{6?%r-KwM2doxzLk$;LAM|;-c}{5|Wjt3XVRCtu2yliJ;1ZI#ycm<6&gVs}9V_QN ziH@JoR6d7fCNFb_w-K1!m>cjAb-I0UdG9|^Az-(WT3Nji#a0P^zYevx;`m4(4i0qV z&`=L7wRm#a{^1PhpQn~kDaohDNhHp!Ds5dFrxwj&6Z%6Fl-fmd>FZ55u0V*qhCM z9d1UEO7VTc#K^mhw+nW0;6d&(T<57Q$|U#AD`QxVrvwR(kUBh1$0~nclHctQ_7hu- zjinTGzlWVju@EZm0}rXVULwUNdVi@EW?b|8((iJJiVxc)o;kdSl;RfHtu?Y`a0S`n z)vcAe7|E5ziaf!_#H#+Wtsmu3r?6ufizKELx8{0br3%AP(1||39m|;_mQ%DrE~>a5 z&ItR&$zCsgG>h}yWoADbRk zD#i31*(536Zs10Zle>pR#11P4Z(Sb47%9e|cG}`8N>4uXB#xapijBfVC;b4G-A_L9 zARc(=1QzBOXf5JfzyBv9v0`OOlHz(JySDM!D6FLT^9A(yXg}V)K7nvTR~v+KN{gGy zZox(bm^;*4ydJoTAhU|>qiO*YX9r!#nM4~msmY0tWrb9dd`S_6mSfRa<<6J%&r?Kr z`T5``IaYp|V2H_%^tJE2b%s=9MJA{;A=s{KX)wLZ?sOA}^uzADza_~{55Pn3Ei0{D zHit^U8`cB;-T2Irz4*qX$50p8i4=<#q*P2# zPT|b^XL0GmWpSb}H~JjbZHN*Pyh;-g_#)cZ8WJRPVrdmO=6M74$@5~7zF*4Z1hM3# z3MRn(df%FeWg-QcGfypaVP~>!x0*!`Dtj8)c40Pu6c@zJP)5U@c^905!PB((#LgeN zfrQmyK3=&Qkkc}9`6As9Re0$XMJKwqeop@gp<3}kja$>rZXf8tjaUJfu8Nl%U~qs$ zZNW;hwZS~aExjXDRa>7&pWjPPrvu$x?HC)I!g?X9HuOGo5n=m#-*^rO2D;G|@S(2Y z$!RS^dcz$n#-$3x4^}_+yX{o>cRtvPF+%XK*-wNC7e*)a*x#n|aGMlH0mo8#xd`_ed zMKQ7rYp@Ll(}t)YZzEa#WVM)$$y*;DT}RZW(T>kCQdHS?V9L?k8Wf6NPfGwRi6qXB z&k7IaY9b|mhRfcZHu1>g4D8wCRSCmq>9W%bZ_77ua35MfhgZ2io z#*9qaXhyJw$J|g;9Sy=uY}tTy!9tr!pLhL@u3Qb|5zTCuI7DQa{lgu8Z`}v^@O!mD z`^s=ugC$o+fCNUHxq|l+!Y4LtHaM5K)onw!-;TvZ zUI-3s$zq@JnT4c2?XJE4jmS#Z5Ru&yjL8U$`Lx(T+(L$`c=@6t1Ofp*XOeQPq++lJ zT2KM_h2vcm$^ViR-y6K!;P@4Oz6_cq#@iG@w@avFf8$Hf;9GB;#v7M!en^U$z_>jw zJoVWp;qle}jqdY##o~P9hlzOKygr8a-a3U9BE~!=PpWmb0XJMIYIU(jAvz8$LT$2P zF`f{L{zj%4@=q3+FgNf33!%i(hK-eLnJ3YFU`}Rf7^PUhAKp;A7_hWnu=h7aq;_3@ zVND@!^?QUm{M%oDR=j@=n_y$&b3>FWN0-7SIH^i?z^#f(Q6(@CnKLhp!JrgTIPn~; zM26W)VNf=opE)f?baQTTyBpzjkw~#Bs!mDT0lYaCr*mAnzNDC6JKETlc0T~~^i`PV zZjcIp=fJs0GArz1w^v=ug&Ycz6*#(wVW{k7;AW!QMV~Y%s?RCrk%11EZhn`IDBBi% zl5~?|{FgrWC_aCDA1+QU;2-_r4{`9=VLbfgqX>qAs5jc$+r)bGiAV68fBXL?>=ftE zoX4v#y^e+11>|y-dFUh{JhFvS_3uyi2lL`o3QjJ$F~5Z6cnZw|5xwMcY{L`{iS@ui ztk`R9jMtW%o*f=Ptd5QG47bC9!Or&k{+xMj1Izs*f9(rTV5F;sRO9-U^|iGs&MOzL zxG}dPdR4d|;Yfc60&Rl`8J6^zwhBf;8xmodGf82`P=*dc?b{9OyFY?46}@9v z#b#h6|{U_*O6{Us^(IN(p2{bXPpHIldnBUZ_LP=8G6bN|SLRI)X zzwiYFMt7m(-~ntlY&I(fhX#bDV%Of?m>3(!^u!FNCZ*fF$v-Be|AqLuG+$pU2Ky2rSZ^EaKtOe?d(wyC!DqImczSTDQ|LpA|Jar0_B z>>2Lx*ogc(P%LXigg;VhIho@7*H(mD%#-l!HOv6iyg@O!jw9-{ z&j{V?8X?ndOBevMLV0O@=!L5)#X*FGAZD`P&Bn_`^4>FKQP-V z+;s8d5*=3>c!)*1P(XoB%-ra)9bXal+1u!XXKKVzz!Fu#Ae!tNWq1dVb+*Ifzkj^t z$HB)ZPoCr4cut{U0l`f|2KRh8oi339U!e2%zP?U8ylW5-?&?RaqY(Msoy`{!O=Ym0 zNaE_u62=ym#h{3uPyoBS+r_4t{mblW{?M*bSZk(bTU@>Z>jS4JkTPssu?SLK8aXCN zqA_8EAsOB@4#E~3pUI4o78l}qub|YgKub9&0(& z1p_^QrlmPaz|8Qp#$C8w1& z8|;I_D7)M!o3?t8jX`7`^LqN(rw|T@aq-MWT)%b$KmOiN#4wO@u>^^GLs7U1t@b5G zo5h0Ny`AW#3*bsDrKeLYE)yxe0ji0u}u7`rRcTwpG2Ga7|j+z7DQo$$4GVx#an z>=mz7zkHV$t1-x@@ z96xyXylB`yI^2h!f9!-H!uN#DtqW;9^lRyAMK%fsPi-^gQX+AI6DdqfQ!r0mf@yvX zn~7(Jf`}6#=B(mdV1Xt_j+4c;;*%(mI#AEy6I{}53wXq^kKVQ*R^myK zVZ0FeD7I)@fX-*!3}lo_anr$XBSLI4pb=hKr`Iq1#1`wu6Cms+BifxdeBmpf7fP|+ zZWlw4{`3#OjhU-A@X+W0o;$o3UBTM(7tZ(Q5$FHuXK#rJ#(>j-zwza#G1%FP&VW~> zT-+Bt@XHX;U*~l(Q@qNLh$?4IPC?uK#bfQ)RL1>m5O*pDCI5(O7bG+D25o`%jDX~+GljexG zL?VGmB!<Co6HL zaJhkB4}NAiey$;ylSECy4R(!^Ji*iJ>{NxTV32~Cm}aH}w6mj?-pvN2G8w%4_PdDA zFQA2p|H+|V^tS~nPI~w_`24Amx;U{I#%wq$n&p&^IX-{(Jl=TuO{CLlD6#=mT+~XcKCq(RA}C)R9U;HSMcvcWA)4@3k|~TW(8V;J zMWcgBu~D+(ri0IBLW2ZPz~u|U?yNAj>lV@DUJCjwol4`6e(#U5wG4PonPM5=*x!K;uN6Mi#&Z6d z2&=t&gqN7fzj1k*9`8rvn)#uX=-j>-N$QQxpCUMDN{J2`5{YOd!oxfa8y7M;g33jI z!(=cR8aG$7N*J_iWi{)4Z3JtI)4e5FI>U7 zGv{&k)LG=H{8!d?=ES_?BmMYyf9uy^vzRv}r}R$f)8tkYX_0}=){3d+uo%0{WW7-! znz2aTHudzU8{UO4>p*QvnjhuK>B0FB(i z<9Pku5Af=n?;*V~kH7u-$1&Q;0RgppZ;vmo;?#{v{QDojjz{;8;251lK6i94+>VMO zz3vVst~~8-@v?5E(5m2DMD#>lTlXl`wmw+j`GHW3jj0Ig1#T){NagYCe{vD=JfF|* zJXf%?-%o1tUq9I=Y!vINRWnLF_y|AoXlXoIQWnM z0Is3Euy`wqS^ej{=sY=#CPi(OVlK1uo10$5+q)qpwi)QPEW14@JGcMnZYDQ8A+58k z6Gl40v~{$JW|So={VgvpVQPE|DNbIEO1Ie-3-)GuTUrq4>_F%6ASq2=j8D$sowwe_ z!rTJX1|B#fu|9aLrY*Nma092=Vy9&l=cQ^ec6|&p6LcPnMDXl^Q3PB(Y^+v~Ps5q< z8C;l{6P2FN9NB}DdxnH%!fL6pRZ>jGrL&T!lbC)K$S3ZY&bA@Y`Aygh*2FrsYeZNN zOz~xy7RK~E;~Jk^M02j$gahqvoLx+b#=AT2k_M4ZaV?xdr`Lu<9iLty;HiCTsFR5O zE}an5{WgXdKJjz$Ob&@mp3a{ei=r!~qEPcWBE_f#yc^>3fF#AA3O=aWJa3hxcsqkV z#Hz;!)!7`jAtN1^+?~w}M}Z`j@F&J5arM$w+Q(V2IKO}-DVHTGDdp${)2!LY2M-+} zHA6+G&jpn>uVsrx?}nzwYAAm+26QuEr@ze$;sWj!WSG|qsmDA5nq%FBH;yh|s#q$@ z>PI7;YYC$mIc*m4e=eUFEj``n7#YIA{(ba4Qk;lj1Oovq%`ZWPB*ohuL7PdWPi!Sj zW;*^|9-$B$E9}G3sM5t^ zzwz0JvA@5oCV%4-5FTS%{!ZOW!O1m+wD1gX81&p2ql+-6qcCKWqB+eFXoE(oydg{DaCFPjNXj!dObLD?2uT$^y_~fiwjGbpPk2hZ=b?T&%c6bG>Uu^ zPR%=T3WlbF_(pjPLAgAG7PjSz?N(V4DdP~VzC0nC<`L2iLziM zl|hu$d`_|mC(|`2#vXq_5aG_@A@m*C4;z()Z&&X)|FyTj7dNlp+z}OpW=D_54CfY~ z+DueU^|y6br4nZgCGp<)o!|dceD#r|_)A}TMvNw}1(*=Oc=ifjzHkkT(FA_^sgrno z-zb9KO%O_0Ee`Y$muLf4Z_opc>DG!$z{wPg?dmZNa z>$-$nBen0k1<}cEBo&4K&1#Ba3mOzfdD)<$&Cl^}PV~10Ny%4{%;kk5lF75p zA}1y{aZr7l)RL0AzC;U$#{+jT0AEW8okK(Lwgl;GUMh0gNWF7@qVQ~`uXpxz3U!a@ z0wlI4cn}Lmv2pr@d5fd=%%rq>f^DKgQ7L9tqxjxC=kU1`2XJht7oGkJ8}@DkS7lQ4 z-um62zJ)MV&zw~C{RiLtqCU0Kv5AH&&M@ZZhU41OtNMVC^lHVMc0Px3rm=-oV~#Q_ zdxKEfb3AfbkYi(X3C8$J)nlSL=+9*}<4A`G3d#7V=RY~Offe5QWFEhBdJg~nlf&rt z*@c})bNR%CI!>9;d_h>QyaQu+9>(M+eb78GC7#KWXppLSdZ?11>sogG9^{XK54Mem zYi}demJ~aie%MJ-9FR^B~Lo@80p#|(C<>m3Z(b3t4Zc+_<`+5)zQOTTCwzpRpIJ!ue%fusbKq$p!zIc#I&Qp&JRE{;2h$z!VlFN(+QkM;+ zPCzZez!wkRDAcK0(Xe(Oxnyg*&AaGx*+mRgj+B<5J1aV^b4-kLMda!J^X7#c_Db|P z)V13%vTxI7hlwumX8P!&*VXCu3S!*a(*rw^Zquh!MOZ0rYi|?YbcyW?o?_>-8DZJ# z(cQG+Gg)jRqqx!t7>?fJ0XbJ@7w{LJdmMvAT5aa)yvgk0=j`FL<1?6Ej)+8wp^jD@ z=CtU`&MhSM__mD_K{`(v@)><`;sTx5RHl<8;PGTQ&FVxs z&_Tp`1%_Nw*eg(h7{D(lZ@3P1cu5Ub@xem!uGfl8QN`7jG%l~w{qgwG+tCKKeE^!n zDNcUuhZba=?(c`OYtRRRaP%5C)kWz#lqpnhQ{`B;a^;i%q&9G05M`kxXIVwg5@9Z^k><_q9bhjbFAv?tHvgTQZzjjNB9Y{RuA=ZIO=ZQ|8-Ohs zgq@Zn)BVOTIdZ=#1$*obv0M5+{R?c#9-pJI1V+~ z0d@EYl#wGO2>D^Tb_(XD34l~%gHpK#bYHt084?iw@WSF<={sA%*mL~Ccns#jz38C7 zuZ$jtdG;DiGuL3Aym(hHHCMv8GPasbBb~1&nDkSsI5}$_UR3G z+-xT!jJ5?ddfik%m6ZHdOx$Tg@RTU_;D%a*c;xYikb%_dsegfY4R6DhaCUa`(| ze2de9(U23*3>Rb*fc1lps7^4(abwnCB7TPg^QNc=@iTBQ>c8U zh$Mueks++EtP0=p{M-T(@dU!F5zNlck&vMXqTKZ098FR0GeR|+MQM@=u=uDd`bVw& z;nzP*y5Q!PTf+x5eHrd@qSuUTmpz02H#9M2&Y%-wQC>CayAelLhLJoo1mT+2(=yYv;^RU(K6v-2n&^a!hFOo_eCWScQcH;10PgPmtoLs=S z_h;{%6I0NJJKJz__b^(#Zde-VT5xNc-j$gJOwsu@5?;cgRtNh17PQz+bbhH!IcDYc z&8ZlYr6`<^Fnn`mIH+1`ce}+naF0oYiz*B*R|i_UcEMCG!9-%8uwihOjQ_5p=zsq< zrpgsJr?rH``|UWjaE3_ny~i$3FRb8=)0gq|aUbk-5;T9}<efwp$Qi zj&Zwk=?doN=7qh2pVM46iwr3d3aKy!I`L>6DOPSiC0Qq;eA=e>yyM=v)`AH0#X}Hg zh2<&TkIYXAe@)Zws>0k(g27~$F4P*6T_(Zi%~5VS(vL~8$K!#O)MrVGw;znOxpG+y zXm@VC+HWCJY;{zo!g3?rjrnCUc!)=*4|P_~OL#ind?Y43#BW_0!xv5*6dA<(`x<9p z6E~5u9V$onm78ooxH*IKQ*&62#o=^~A{1~TNShs#A){v+Yx-}k2i$mRp^f%?vk6>U zP7&$W5IQ@Jkjn-C`XA(QqpojW;oC!WqB zU#wmFjgn44&1c26se&=%(o?)4Fe&aJWqom~BD1>0=0UUBpt`+KJ<M<*jTia+WsO&!4%3!S)s$9_oX!isrZX$ENVY2baZQj(_myzd$N+ zE86^xbsbPi8Gr9Ooku6<@MmwI!@w;6 za1fd1K^w=ryvUYR+z4k7$(6(sn4#M|GjYf7xNH`99X9lLgwR2+K}UNKUG%ZHryH$p zEeHeyu#wu!%CU{s?qC-C`Z@%$U5=&hdOwO(bu5xf|G&RJiNA2X8;|w}*1ldAdF{)w zq#(q#24@tj`Ls}s%^an>F>t9vjC$sqq76aQDLyqO#WtXgNU=#q7&SC_aviCAN=G*& zY6n)5JGwd%424Kh-iu5oBm8TrWD?gdU&jI|$}7t&7`r}!e7-rG_L>o`?XB>5Jy={= zgv9njuiJu7_tsk{%yeyJ`vk9zf>mP9CO%Ij_HiPG`+6%&#EPY}q_|Gwd+(kTx!`~M ztDhAEIh^*bnQ{4Xh{&=?9OH{C_`!SUNqsfJ=W^ojef?SN>S;sB;}$WEIt+SBg)68h zR4^$n(n>Dq?IMPl{R6nViI1nhUNR*_T`l{-kFJmhb`_3SOtmZ*SlCpVX;itA0-*DgcNg0 znRjH&;JG6)k0wKHNiml-x!I1r#RYTaqZ%dZ3ZAJhny6gOF}bc$z-2TSJ*T`_Ibaky z86lSwW%!oXknkdVJwC+aagk<`rSE0ZX%y)s$V#&u647{QX$ivRmhYAAh)$OoA^X;= z#8#(EOtCXmFtmgHq>)65cwQN;v^n#+9UH!l@x@gv5aE1>Hsn5+gH+5;p}z9qoUI01 zLh?lgOR)rQ&aVjna4MTePiqj}Eq)vs>ZQ;9pH#<1rm+=bB1_S>MvggBjRZNRp!fXxWM$%QCSR}~H1s2j>0O_!@UWrK+^vuQRaOH%x);DglW(4*P%SxJf; z6skV}mCAT!s~r;c1$$9>oqXg$u^8+}o}He_x0Btzv0$3GrCPUBMD{ zLJO5gU9OEQd=gs^_7XP&DRz=rWDb2Hyo{AZ8V;KUzxjnHg^ImC(oWB*)uAtEKp*&# zqw{ErG}=Pyo}{?lZNr{c7f$r}@WRb7Dg|4HS#h3E6vV0o?@Y(A7*E4p_MzQn629kp zqMXm6lun=ukVFr@Sf$Wps89DnTbI6L%U3)#MWP+ zI1~Bk@gw-+SHCDcbljM?vb2n;$r<{%BF0cpPfU`!91#f@Qbpbxbai*ZAMin9`+z5f z`K$(^6bI~EpEKsVgmGpihC_qBL>M;~EypPn-+B8C-n}+~7OxBc+h6$w z40p8Zac)}(ZY0a*3;6NrOSmvOk4r?@AKp8JZ#;4gdpHWy>x8M2`^aR{?xE!dyd9(< z=X6^|J`N>1CoBrLr*;elo%oGo-T2v9L|7=cDqfTGi)Yg%{OQdkes!-;44bJZF=myx z6px?^(36X%^pn+x6bD*?=>42D5}OTZMa@k!X?NuaRn$&*hM!uO+D5n8{{D3PvWA6&~ds5eK))VB3q6 ztwwy2lKJb~0(N?T4SKC2j?K8mqn#Nxrw2w$W!~mgHirnk-`NN3u&)0$IAP-Y+_F%K z_x5&Ru%iY2bSyI=Hf+&z!l@FGWCru$INrTFjtHIC+zuj-$N8<=y; zX2H>Nd^K}}M1nTV59~gU{r%l=+O}qh32)Zfmm4J#Jh>dkx$!yt^vq@K?eD_z zkv=?qcn^F|dsV3#cZHFjbH3hLT~z0{Bby<`IGcjUX@kpVBjw!;hsA`$9c~m96^Vkf zWoj`;!zBwUE=3B0Ah$V8FxP1`IVVsk*(9m}H_bFHDV~LHnyxG>=5FxOCW7a?b0%Z9 z@{x%WwS#7{qU`oSX>F5)c$*b-42JjF*bt^w-07>g`_xFL^@YM!0mu#osZ|c z@0=y#IZNa?i+}o8et`(5W8H+h9!L;Lp9{zEkN)rn;(W#fNdA{!`>If{U7M7CVR!qH zj4X={1}_HR7p^2c+-|{C)QIT>4@$w7;`CAu*-{xHBE_S1n#9PFY*pGP+G#aqQxcQn zcW50%ElDwxE)U&Y=KwH%2@+cm)fa#gYL$eziNTR@ZjTE#Qb0m2K@sJ|iZLf&SZ!NB z=5{;7?eU*@zvaTG*YW)_-hc zBC2X&BBY|1j+;!-h6}4(8>qlS^-@N~3)5-zdHk>#4K=p_aig8WL)Iz>kqK;lbV(h+ zr1%D{+K;l~DOW*KCC4GL)u0mr$FG$gvV*>vq5JTRFgeU0`jsVWgHUS-Bc!;q60g#Q zzR2-;#nRfxqOeu4eTAV(%h=X|xI@rxLWk3gMsQ9rAvSRoTNR{~txr}e@fUc6{HGjR zJUwk@HHtH1GlE3*w}mj;)wU%~bxei}MFlq&mW8jG?-$<>wr~v4{Tk>9t&8N>5@xfN z_9dTu9r*pq_oT&cCK9UQQn(<#wpH(9{=3PdiXy$(%NYd$n;9NkjY)B-2sKyLFd{BJ zZhZiF9l)gcEm}jUHM~I{_71%NG97lMtwHy4K#+`kz?3cp{xenh~}cjDsbsjJniF4 zt9XM{+3&u67XRB{{(0;oHI_$0Zv_^kagmMu&%XOJwE2B_aQ6Vddh!T5gIky4!mq`r zHSq27TSZdIi!&J%)vev!-kNsz+FTBgbUBF>*H-CQtB+*kQO`~5DLzH(FIA94nngxWu-$xVpHHC<(;Qj@lY0REy!P0)|OTlgB7?L zGL=Y*tZ8nFREI+?cJzA9@HRjU8^dTct>qI}j!rt} zqNyA%OwQtg(E;FNys|{c;Ph$)|JUfy=IZI%B`~y`b@Au22B&*>-gIcAD~o!@6`MIm3r>hcN}7niY0YVswta;ar(W z@fJr>5av=P(J*?r&4xCofj+MVD=tG*O2S5AaGEL^KhB~6`2*J-^% z>laaL^z9-Ynv~*|1^S0Psc#l|I0t))i~Y5?i%UpIY#dxlcQ_sB8|)YB<6z>;KIOUT zIV=%rUS3+p^{dy3M2AH#bu1Qx#NEM@>kfpNG&I&wqOwv%DPL7X6C75n7~JvqzwtRS zo$h@ipTG9ZLm265!yA{!@Y2~U*w@#E&mTV^l;X_-H`7flhVfrtc^8*w7BJM=f`9lc ze-4lC8$lz2-+%Wb#iANyHsT;v9C*sy*_Ev)#jF*llOyTPz9x>RX7TZ3Jh_OO~ zrDz1*I4NeUEqgL~ch*!kfLo-P$Io$g8jm>PtzGTTEUZ#`{Q9ac0z_g1P=hT{-Ci-) zSwcdh@xd2t&O_!*>dx*?k#uq7_~A7RMi#MXOn8;KsgLa!^Ro+x6H$)Szm?L?^(pgI zvQ8qyhg%!3??ca25@P>#rn^>Z1Ln@75f94VRU7M%M z&WB@o@5Us?7gkUpa{SH5PoSqYh|#WgG%7@%gzmGs?@%$Gh8W4IxE3uUr)<5LSD^c_ zz|-#HB@xlq?ySLeot^_Fn})S=HQcebnvxWA8zM`&Bh z^Kz?RKU9xT<`PRtY!h&2JS)k(J`T2C@M!PVr4_8Mh7l!FGBq)cFnt~gN0Cen~-`vjh7I`-pvwm22E3WxO>ItV~g2@zP2DvjP}#9 zpy8eC6JjEq&0-cR^6}9=@p*lriCDNuDuef~PYK1kP%4Sow+D9(5-ASA+Q7Xl!ZU1l z!ASyLK9eG)RJ~`yM8IK02UQ=Kd`=~|-sewZG2(d@V~HXu*#-n{CfX=fd2QFqV$`#y zR(y`&MmtuFo2rB*j$l$K%5-q>%scd`h=ntVWQGS2fCYTi~%S7y5`4 zTa1k-0Eltxw77xNY~kj&>PX|kfgWLz`230e_{RV1|HS8x?!#A39u^jhdV_n;rj{dk z{o)P$=a=6V9_A+x?!rTRwu?oNi0N zSMs7aYmkojYSm)4PpIiQJqIOJ2+kVxpviFiv&!?BzfJ2YTHUB6TwZ;UM+=o!ViDT* z$Vj4kp=@`e)YG@tTqjjx35jNgk%*w(Zo}}dAq))nQ@Oq@2w1ULLMEG`(`_6#uHL}> z>^$aX<}fumB|OSX6U4&#ZTcA8{UIy7w#`dgs08VF7;ayPl*;Or)WujrOo`jm(~d(! zJvdCOp5Ug>Y@vjI|C86nsOES&i~rwW|7EoKJez9SSPwW7?(D=2W>;h4^RGN~1Rlpn zwi=t$jZpU>T)qG%F1(9OJVNi6yI221b`u_Kci?B!Y2*|QdA0I>GTyI=208kFHHjAd zyAwpxzkMT#-#i@D3&W@ktdy4$(R*$T)&VBPTNgZCC*1xHWeuID+s)GAE)Xwj1%BnI zY(d*JAQ6|OxK7YWiQxD(Zm`p&Z9+m~2f!dsuNHdm^AcDiDy5Pip->23uLq;MM?`*d zG#nu{IZO(20_k){H1x6cBAG}DpYnE@Ji^gge!CI7gI2gK#>RKg#w~0vv>6t-^#uo4Om69-CPP4 zgKwUF?}uCMBJOP^s~}NOkuH@noh%_B(xmz~e-S@AbrA_ti2vT#KZhp{?jm)#F`60K z{<9cM;?;}S@vYZS!(p@12EmPgO&bMPv;FS-+dAxSFZ^xYDC9F(9KU?epL@8&DaJl8 zWK^tXE30xd$9;PtN58M)*Z27_;3~K{Ap6YR8kV?^Po7gVM=Q2==c@pJHRKBmASkA4+Q7oz0UB~FNxz#9U!!a=s zgp)0vKD-+Th=AW!b=^diwX#URxHL77tFw!k4=0eJ4ck{AK7w{q%-j4PbkY5FZD4wy zm8z=#)~-(_#Y|T^oM!9~+VE<&a_eV_6zygD{dX60c&yt2+sCaPYyWYR`fD?p;GyH2 zea);4muRuYg6$PNX)mLwq%4<7B{m_+jJ*=7#!7J}EgJ4B1e>)TXfoXGnVf?@@|h#r z_?=hV@W+-;IzwcE#c80BgG7~Bdnc)ZAj)<(G+lE*o&Ve3vU$R?mY21->SWukPPT2= z$&1UjxomscTK1Otp6&a4pa0Kup8JFQzHVOepzkt_khNiD*=|@pJ3M?IrWiIVrz~d; z?lYw5XHUh??u9BH=2xY{Q~)0~dw2S)ve9{G-cXH(a@K9trrH0Ph{2^mCX#E^>kj@Q zxybi1J8VSlGaVX@N+3cVXHL^}uyhOX_@wZ=m~OTXR`mhI`6nVd751{XdMpJtu{@Q` zdR#4~l%zTM_#!Tvr3BmL6@LsLx{c1G+|oJ$Vo{>y4Uoa& zbqcCg0CsX)-s*iCd_X3V%C#|sT@25{2fLAaAVZ|5Al20@l`st0$Rff{pk#RgYJrD< zU#!M=+#eeoLi$(g?V9hk0}wu-V|m-Hg*07mogV!OBrCUkVT*sL6WXcYQ7k9+ zIUgLO>|{CYkcG2Cr0$o6aM|v;u{IJ>W2FsrJy(7W9x|-9 zqTKX6*N8~G(qPu@OHUd4OAg)UxpFz4+rh*2GdAEBqby*iKich1BN;+PAji+qXENw0 zj;XRw|K2O1MfhC0AzYw;z>CYB(Mgmp!DK^2O`s!wmgXm7e1JZ=J6;_n{{V_<`5Rv4 zfnqHDMF0KpZh@G>F(jyS<#vI|5;)Y7%TP0~?Je*WdiSprifChya<8T~>|_(*8~Wymug99F7(AzqV@o)k!U zW-uLIvM@QSSafo6Dh=?vozxl^1GyXAk;S0iK|<`k)@uY~Fxc0SY&t%q4V6!W<}j5V z^rIXnTc8dv9|+cIKTk7ek3>0giP~Pk(cOw?g4fR)0gJa-+FL65*|{HycPvzT#0;b! zsJ5qV6U&0rztZVL-)YIuY6oYDD6RUNvOazvWergGccrK0m{07UfX=KLWx`YSlXRYw zmXws7!|q*_Ik_$MBB_=D8hEtpH#9c&T-+geiSZC&8kK6KXecoPlo)N=WlyNt=A`PD zSW5eQ*=pqoPR>J>1;e0{$~{Jj(Tz%tTQlcDq3TrgEt=_?D%Abl2gwcz!v`=O@=s5# z_U1M4kX%VKmF=fYX0mU?TPx3bWXD8^Lvqd)XcyRs98Whx8>!lnis~xibrEtY2eIjK+!@_sm@)=(DTu(TNVY#uAStKFs}^RV$fQD? zZBgHo8#nv?7j{i;%@r0p)5&4_m0dmgQ#8$HPI6O#RQz~6LIDP5?NTa&&GS_a@2Dsp z==-sn`|SyR0amA%4^;6`ks)XL<&8uP1(3eNArhEuPZ5or;B;Wv8~HH8%S9A~jfjoru0)uTMS5hUyjMIE=!0g?lyEn(WA%)8aTH zOv&Z=7@!}C5Yl9|d965DSfbhKwM6{w6iW(Bujn1&c!hBJ1cYnxaZE>dJ+}g{^DvP#Y0?U#h_-h690uy-$MKF9up)Gx5Z;6(VCDbbeM5b#bvwL-o!7FV@a zKSK)HN^#NDYtLici5j-@_fhim4#!REL7U;X1sv@P{0p&(H{O?r7=3{d&@&d*0%DV# zrVXuE`*ekzMn_y+oH+&lq4(a62MY_&yW-ghY#IeJq6h;=YbfsOXhRnh94Gca6g|Dd zKvo0p?JHY&xOR0In~BElgZSQ&L$(#3dQw6fVaQa{RILb*<>$aXO(fee9Hyti>i69H zDOWxBQTG`yeURq3{^83BVEiyXI{EfC86-R=Rnb8fv z1(A#JNmCD8!V`?{MEbBDCHC4#)0ZG ziq35Zbsn;4=sD$W*btZWhwC11-3XPgG;Ss??9d9MeVWI(trI7D${5nmD+ImYU<$Nx zgEjmkyQ`Lv%5K|62)|@hxGCYOb?$-1OLN}{*5ZD$LyY8=y%|GH^sSMb8zEP@|5pQ2 zJ0)^ZCly=R5S~VSuGL!~1nM<6L-$>$%BCy6s78v>QVruE-`O>^5w|-S`8}0vg58CM ziyZMnx@DDnDw-wJg(=0Qeyyl#%i=&s$B5nHrqfdn@KF@s@80|4cAfgYFX=nkyFJ1M zpBt`!WwZg@g^1&&C~L$v$U*L=HJ^RL_p!6yfIj$vR!%v)-*Y-Fle%Fp9#EG2QKmqx zTvTDUP;V|&%6N=&8>5U~@p-i-Memnw%6lQd=Q4cV8Qjb61KqBPts9129?@VP9on7d z&iexM4R-6iFn7*)+!@Qaq3n)ua*Av8ix0giBs^4(q1>}$8FE7wPRSMK?y%f`f(tf) z?|oXHa^;BAJEz<146yM6{J?q4pplx=S5RY|#P{Tp+U*=x^@vQW3<9Yym5x@^Jeufq z`!i1579G+jw8yLqQ&P->8%AG35A2y0^$KeT!CY7zVaW95{rnyB)bf7?b+XUT658rJ zPeAW;zotcL0o2uF4}Y2@;s%zk)-)z`GxjRy~1rDQ== zX0eUa@S!dhc#&x>=gA!{_Kfr+8-FkaI(F|ZrBOSuZfGh2gY?LCKG1cBypY2u*VEhf zBdMLZd#LrzOOnv=odR1wrHg~d2J#dm8DJo+fae_14}N+O82CG5nN<{i)-O0q@(NE0 z{a)nDn{8GHg7+mom0+OwN7h?RhR$~!GB*j{5CN$Oh}SUfP#bb~TfX|=ey@(WFS+Dn z&a2^wT>DMLro!D^Sef?)$%KnUJc>j#rvoo3FFjFRUx9dqk}YhRK7JCXW=#O5ehRsG zF_O&ZZ6dofYQ&1${!dUnh+cPEWm6c!m?Yd^@p3}i5g0MRBHp%o8SA%u;%+{Lag6AT zmUmQvAF_`5<0&pynLL(OEg!i>R?K&2b!LYJq|gmE79N0Sn4i$WKqG&=FXsNnvtl`v zO?Yb!juy#15Uy-biTq4e?)+zZ*Q;58OFPKV>CCP3OQe-vqu+q8Gua&qGCHD?G*s^} ztB1|0sS2eYG3E-dsvdzV=XClPAytry=Ue*)%|aTn`$DL4XocEs zQor%XZ4?;FK2mb8iF|)b?OY0>jfaaTQ~JbU>c`4`t`~`WWuJn5n#VGK)mmNZDen+s zfVQNnH9*KKy(}&aKM(MEM7MY`(>^P~llM(NygT8g+t_BcslZ5C;m!GA0+SvLdHiQk z5@v&9m|rP4l=WIr_+Osh2wDoYXh-k~h%;h!v`2D*{TtRMr1ocJ{-Mewa1^{DftZkH zl|U(_>76@JjzF*{S^NYf29$8W(RL7?F$lqdv5?#Z`RUmiu%o-on(J%7pU<9vx4o0N zd->b#Vpb1yKJ=Y;1`;vNva<;7btBg5DK+#85jFqxdG~S>|4zv9Q|>*NyB^hzN4J@9 zWWMj>D;u#NBm-I!+|hD1dIS8Z!@UCD_0g`L_KZ{akrh#2@xDH%Fx-HLbkPo#4Wvz= zC5;+XKtjNfXARbrVVnuOwdkO4g35JvGe_gC=Q&qA76iFqxn#kMY3HEp(EAV9Reih#N?#I3F(}B>(npu% zdprUyRqlUq!_1$EexJjbpy=rTi;vu(SLyH5whzz4IPnALH$_YgagIi;XyfTMf1OB( zYSVL2spD;5-{&r_Td7cU17vot=s&{7&m$z=S6+U$;`WY$x0huOs|6@*t(;<=I{R3j z-2R;cg?8%}xzwnQ4zpc5Eu~xlp(dKyj3LXAp55f!y;MX;L%R;orM{Dy8~!GpA@fnu zN>lQaEe&V$s)<-xf#F9L!m7GdIzu)k;IM6P_@*}AZq+cIdKuVw)#C{Jt~>kZan8-V z%EviLA|@Y8ovH8*`;@Aep0z{8pi0`dw9z3ZNSH>6`_h=ntQk2bMjyE0hdRPwmWzQ& zpF*g@Dqq1wwzM3U2ASbAgnC-Zr4>j1dkMtaZBd=6JpR2;$n%W?b4Tn4vFr2kEIMRJ z5#Gv+{v7hM2%^(AzVBVJ9eDzC&Zn(`Y5^atuyDZY!sk35nWP|NXT(OhE=w+D$ESAF zU-OO=k`78UEVvB4(&;GF12^v=1#BT;!ZTYB2muB401t#g4|`WkyDQQ&zy=^e8pdOI ziGMiE<_e|xVSmI$nv9lm>91Abf`XI$v9Ku$*PQ-pURGmFodXfMH=imQP5cChbMKStOZz8I4Ij4-c3RICW)&lj+MDw??WNCI*t2vd^RcESduogdh7s6r=@xR2qRxY=y-a~>$V#B*@m+r?z z0z+WsP0N7q0KMk?o9=0Oi9dxJDuM@b?V&bY#6< zSEOT66qJDas!!ew(E3BADpNH&RK&->RBpL*zo%o3D1CaF3|Z4*j;cQLtjH;ig`3p% zra_`XgUDt~yTgjiBxEAb-RJM!iIY6?6Ry-)f8=KwFmp1rJO#SSew0>hh|SwA6HZn= zV2G!1eUQ2QhGmm~(5cRt$PR8i<{!8_w_Xb^5wj;-qWheN8dLo_K|l@XdaU35UPUM%rz! z^)veDce7FyzRgy9!=Ak-4YZ{>hNa%ph(LK;CG8SVhlYM!4gANKOQF`QZ8CW^dhPP- z(4&f5lg_tcEZ`2;iPi)MU>OJOY1b~qyNM2|P>k;4Im9Y+{?tt<+N7w8uz}N3(?_{1 zh~_)6Jay@#{XG8abJ!NPazT`e@APNAmd4qSwb};bXD4QjbOnrvqL1%iqaZnl?`*=R zZGA~Bh$yl6&KsVj`)A}PH7HK>^Hn)}oHQ@pH! z35LZ>JKt*p-`DMYNIq9AZ1Yp%hI5lY4_!U~yP!%Wewd*A1s=QmE9iW8=L>{K?Vv-X z2Scv;tS5kU27P?rpJL|il{L2gd z9%&1Phu=JHdWt-KRa|eJkhtSypvxo832IVOrA1A`u+u7iKg9mE8hm);VAPsN5th}A zntM@NV2Vl-?%t^4r???mPdL20J;%xhwAYsX3ae08 z@D-4Np%i5eH*>2_FMzP6Zh!)mhN)GK`XLgkd>*{Mp2@?p6!e97&ag?kMuP{^*lEiN zWQyXfnTe}8&m8?% zut=B2ylYwEFIwr!GZ9*bV}v=PUN$P&bA+u-_sSDa6c&O1HBX^SF7m*Bq5Ad$5h@e9 zReT*-kgB$$?1jhCCMsW@06Ewt>`~5DjWfs3z1b3$r)*ewn-Z+;73p+b-|@@K0#%)1 zLv)cKC0*iHNsI54#Zaw`iyeL>(upC2v$l z_!T4ZX_sg=N$w5HG3Qy#wjH@%n!8)zF*du=s$jeb$6|}F;tKDzwJ9RxiI5AJu?{dZ z7Rp!;x;z&Qo?rA6DZ3&{oERVcnr@Fr*|=iPLcYZ5Z`pl|*k==M!qkl1c6-{p`C|9V zglVtF_!^|^ dB6I9zRjx(z9)f=Z+KD$rFCljI7vfL)=&~793YtM46#q-u2?DegS z(XV^f=N-E@o9o-V?rleK`=;Xtyvnz%CxQmlBu6dCTt~>Hi*JE^znaPNc`l!g2^xDp z*}k2nOYw7W^>*fg&Y(P~d3TfO7rLHSI zw-@9h8L+*ozejEP9gCC{U0ZV=1E+)Cq|uK5DJ~8SIu7j$mN>HQQ$n%BZK%3gqhZ-i z0IRLGMM7F@#8tsArZV^iK{XwiOHYw@=E`z~8Z0OvF4tc(uw%X|gb>!~X~#~Zwf39Q zDNyViU)7J-mNJ!A#JxsQypHkylbC!QEfK5@2e{P5k_<*F{IBV0f|C`gbS6O~@_=c&B}vsInfd-zdJq_) zwHiEVJIS~|V)gOSOWkX40XD{%9hciyh<4`U62RZJidbIA1}5XMd-RL3WyqQ|WKHfb z{O<95`OFC!6LgJ=wgnk#fyCR!x9zQPq2!1NZ~tk@k&*Vb6}P zIs?|PWSCa$<0K+A*4rD)n8;E|D@K@Kpe-42stpp4N=w;&`E{^dv%PAd%t*NmfuD`{ zI@d>p5b~CR)rm(BD2Pb>?N5b^W)`Nan7ehWL3P~k=1+qDL5GE2gQo6zKE`AS8(_ae zSHMz>TTa0V>CphpN6`Xz7gwV7e0U`~LECIi=G1Ano9dz-Ikhp0(X#<3jmvx$JWOU< zghrSvx3{(~=5Tw7yJD(lHoB(IicKpeWY$&mo}4YM!PflJ=$0qSWxA%6Sw}A0jSq~< zNwF%O3`O6>fZ^5`NGmmCb~q09gm8A(3v-|>s1{)mL=3KXlgy@+6L0b-$PF?1`UWbh zHEN{rFRx=?_$$f>qhlJ4s^?aOgMaw4@BZB-D*B-O4!k0X>>#UO@6`1l1d(K%6i>W; z==Nt3rlRw$jJxFJQE@O-m0vYKnL1XiE9))3?-KHV?Rg)McUrf)ux>u&yRDlWo+5?1 zZ{GObo=A|QH`Zql`SAEeR6_wiew!)YvX;ai30r}fqlo^_rFgd-6jYnE6+qGkr7nvR zum{(o+1aE5h%5_DF=e~dj5?%L)`{rS>|ana={;nzb7r8TTwXv{j%=}2p&Ce?-_dJ$ zG@m!GdD7dmKZ$nhLDQq#cl2M`>J#7zoB)X}@8XdD6N>_0Ud-n|cE5l%~ zqViSiM*`Xb3dcPd=?5ZNXB(F9pO#oS&O|{KhD<-1T>`;XIq0q@ z>#v>SDa&kZz~F>s{%r2A6|&)p@ioJ#QSLyV8!4vo5_oeBWivmJw|5YESTkv7M88Ql>n1xVaNB=4LB zgSD~IHSdI>EAa4$JC39+@UT?FlCAg7mKHmH59jOmFz&hc32nV6&~*ORxh$y@R{i&{ z_(UXuQx`*fd z)>b#X^@Vrz5H{k1C;gIfw-M|u$(Bhf3ff;*DbI>@sit+>GSC6JUJ4dXg+xCc5xvPS4utpw)TbRJob!Ij`9(P@X{Dd3B6(2oQu!mdJ(7YU=Nthtd_8*Zc(% z;@EscE%{xRM>dh31V`SFG8t%Fn|>vkg8c^wX1~_0Bjr(}M=oss^0p6*fYSLuzcohW z)BH`%!S!?4i!eilI5gvlY{AxnRJNrCrqY2>FSDl#islkPc|1P{k>B$?*1`w((LVP5 z+*nxeX!)#M!97d&dhc4tXrDlnf%Ac$upQd`0bVI~8qplYRVJ^kG+@C#LTJLG^M#{s zT`(s|Iv%PPro>TGv0NNe-igwq7+BCO5K%_S1Zdcwosg0Mbu{4f;%O&+>`5a3M2bi@ zU?~w3or)>=p=*a@JiXjV$zefPGOE{dCFkV|f5M522|Ie_QcqekV&5??hK|nw19PJq zv!F5mw1*O}uY9Vd)k)|++Y7#GwxznHrTZD{`q@BRVwZ?vz|uusg?d14 z2EP$!Tu|ghPx}ptm({ei!50pS8*@s&_2C5_YH{)%XtCX;P2v`dq4G=>w@w|mp7QU8 z&9Jace_l6sBfH+3t$|&Q>-0dR({7|+$BXp9U$h3e7E8#k>j-h+GpK?Ip{Q&fC|fO{ zAq9a?G&L0fiW1^GxgWFKeEC|WEj>MMMYCe7)CXI2H$U9l#e!>>kAnMFyt zerABoFACI=BA)~?w8OemmpE2w`k&EMVJxys00sDF<}}>$*{>fet3|WD7h^mF1f;}; zSbo4GF#l}w!Wk}V-k<387*$15hW@0A_08gM6)@H+DI9*K>kl}$KJ%t-W2c-%seBAA zk?(3N^kVo)!$=GRBZo-i&I0wRL?>@1+jLCenUPgQIN!-1=;28ls9*`$*3Lh0B39jS zPdENH-PDh<^ZhEUsb(7^oNAtOIBKG)XL$X|^A z0pyeDvDZEfRSM8RqZq8fl?W$#>pI%C%gZWA?Qt**;M%&R?4CgG?%xBCNzH$$*Uo7b z=v9DJwm{Zg*ZvxCLYE6lM-%i$waPM(c1)NuSb&?o@HQ<<_#>Qs3h<&3<5d@hWWUM z-sQceyut0@ZHPVZ&;j}wO5jeTJ}{!RWK?M*HEQZL|C~5x!QMepXk72PT#dT?xl?Q}Akrp2<D;_pY*$ifzZ5pbMN{@5STmOxK~S%?deZN%f6ow`~g}AM!1Os9~ss#0>YV`O(>c9 zYYO$c-Ci^&0N_x2?@_ijxzA3Q7_ORqIg4!t_p;DHd(4BWhifM7CLol zSj=de+V|#aUf}OCTmvNsp$4}QIH=Z4)$-jmh?UqKe)!C$#U5nE&Vt`esP!S*zu<4x! z5Yqkg{80T>=-S)RkpsthrCeRV&5cX5IdcNSufoJ&XKMwt;5xYt}`Xh zUf@o%M~W0jsl>7j-EE=d6~09MY*_goyBLl?yBteM$P*}sf*t?QBlFmr8D95cSnT^r2(5jDC*2sY~~w85%2( zqdEd5m$Ar)=cx%&Lw-u#(WJ|Nsr>w4=W8Aaz~f5%eZ@MwO?>kX(m2dTXRdRBe8&N3FvV04MYXeUfn$ zT41a^*2pEBDn0baaI#nuFXhutR5h-xq!qqgIGug~-@h%)L<_q28{7RFY~{{uO{|B| z_4*uYD~y4Ed4Hhw!<)SJ{7K{UUhWS;WyZAMl89W?SS))#TR8&}iWSqAvAr4Scw(wKjEk&Y=2gFMz0CCQ|Z4%VMUES z!$i$P*hK!=>bGo6goP8xt!=4xOR)d#r;4ru#%FnKR#W9Q=@W}Nl(Z$((gx_)d;DQ6 zOcVV-+*A2h*BFdv0xW6tN*<`Rt7o2EwLT2&_J4676#-_GV!Gx-3nCR$rnoI-L26ut zgc$R9Yquy{>NQI(C#xIlVKTxb^1d;Ga^UBWy@|4iat^E3jLx}nV!TcD@UE)R8X*1;)~be= z^q+Mjw2mXG=4mM<7*rUR(rQQ86|W*yqW;luWdx(>dc^y$?1~SNooEE2YAZnGE%JYz36MYy+^PYKmYA<#Bv;5@$kSkBDG&td5TToFMdPE1 zH*AvPAgn9E!s9jA&HW5Vg1OS+37 ziWP0KD~QIFCPOGJME!Tduueh-4kX4zvxP4YjqF{~*Wd8MyAw-)O|%T~eJDMw3pYwA zAzJg*s;Y7%3v;u^S`t*6ZAt7EtQEe%^h(N$oZZ>IjF&Lv(lOU~8!3(%sX4w9spkx7 z6#pkf!ZzRiJ6YE-#dBqDW}o9JN6T-@a<7+Ma6{oZkTfqrVPe_nQqtBnV$51OzsVHJ zca?U!EqP8nON3~7vSYXxiHxt~SKRHC=u-;4kfdSXS;!SGD{hCgIAka+&60&ue54v@c|WHz!yrb21G3? zC+tn|B3x7K?jsy^0;5o)QcrkCdKHsVj4+|kM@ev*$XC>bpZk46QRI_V%fG}Q7FJUC zJ1+@lkyw6Y2{Y#LNw2R^Ex+{C-HMdQ^^cvsZ?I>|un5DbD|naQU zDYHzsZj>rNwao!-#2LyX0kh04ll=I9`|Ljh3NX7c3?}hXryUH99q_J`*4_wM2&mH* zsqOX!EgNWHn_ES@viaoYO0jY;v(i-^T_Z=kChd7At+zU3fu4ZQ7I5D|Xdm+}Y!t)( zu6r;r`pY7&#NE3!Q7=Q(#-rIoTSBS<6T3Y2B?|+W)ao8-R_2#}&H!oxyz(!zzX067 zgA7)I6R52wD|+-t1mlu;d3or#K6;O=L)23rJ3!|JF5K8YU&>*ME#S6|17c9l{LfE(Q7PD2JEm(d=jDBW8 z(%59N{&e8dEyhF6c6#gsfs?44zkanSYK1B6)9vl;g4s@#QeBn%t_))lP2tbCFoWsa)(_6>Dx5!?CsnKdK$^vmF09)cD zIL&>2%TVAB87KVjs_8(pBXSfe63Tv$8lJRsy^Q;YrPr0PU07Duft3wYtS72g7V)F} zA*ZoY1hPsbm>Mbytg-G3^o(uzCB`O>t$)={g}%@~V@1`31(y&4LjWIV<%tGd zx?erOJiJM?mj>wph3yJ~>iFG;xxIYtVKd0I-O!hT@Qmxd%O#*#iOF^?=i>B3u$ehJ ze;c~+-+vX!!!)2xG8o^RKAjG)*kh+FFe;BLA?;Uf8+|^43?M?Z&mTO(wj^&8ehLyP#?=>V^nf(tCaC7RG|nOlA0%un0W0xwss4stLgnuK_$1j zEWjq%SceA1l>HO(S{r)r%m(y-`zY>Ts~H?`)Zp5U5ecQ{Ap-qRqzSIhK;?T!$kHLTgunDn{1$auMFJ>QBzOZl0jTMj0FS9*lyO- z439tmu5C0dwBD3!V0LPcD4!~w>^x~8ARkLo!GX9Z0^8+^p0;5YqB^PpXW zF+==TIKaBvJ&I=jFmLw{h!n$EmQgD$!ed7i|#PD%X#(`|CX^YWsm zW&;UQh3-ydp>8=ll3bAJ2ljChpd1)}f>qhnR6jbQATnVZ*Y1fnxk2tOe%D7}9R_K| zi){G&ihi^pVY*13rp)(e`TNW^W{^d|>Q&IKS`viR)@OF}p+Si18@W|m`mr63)f|5+ zg2nnlCqP4&_j}g3FKVpR@fW5{HK&%ER@O1yFrze$i&$X1lEb&0>AYAss;ve!`XpL!};IT3&&dfD{$g@LD>}Njq&?BK|?izO} ziqc-cXC=%dQ`c`~ws9zs4q%8v@nNAwisI{68JGLo`+JFT0Wn|6u9rq$S&Z8LrMt}A z1T|XL7~-n``#OE|BnhBx&wE-lZ*6J10LR>SA5(Ix>4rba_HC?)(L2(d5_eG4ieMT4 zQ@~z5U)V?#YT0Q|NJSbuauJovwbuHn)Ab5eYbTT^io{^sauutV+CC-i@CIhcNOD8%;63qfse5dbGPwdDM%AA#!z1)3%OY~L!kyR&x4ED;3nrAIVH<@%Mdl&F|8u&k~^gy!pG z+(<>XmW&DrX2(G0VCnx3Omw$C2wIKEO%@CK4mwE_$1Y~bs$pgO@I&CYiRrv!&Z}?Y zPzj()@HX*v8(eRbZ!JL*U~x_`*Cy8oB%oMFXIyNX0|b8NW=A{dW?Itb-O3>@jGv`1 zsQkCkLw#RhW)xTG77*7}D(Zalq`2NwN1`qmAdlNJ%c>I+psGD)*gb%oUCfO0S76r3m3w|gW={0~bLxz!%YFQlr%+mJv**DxFP)sS=W*8Vc@<{UA_NU1Fdq2AYEv{RX zOCUMHnY-s#cK?A-Wl%capK5;iSY1`1hK?qILa7E^ptx7&Iljr!4z$=zSB`C%wPugV zkdk@qF{*Dq(ghA>`*On)VRI6{r$ISezD@?#siCMhBYOaa+@T|6F8X#*yZtU7on|GVd|C@3LBQKxhp-&>uD+u^T)%^w^F}L;uu!*Jj2~-1rFYW}Y9O>G7lY=-AYBS{LU+KA8_;*$&3>0cd zHo|Wa<>&{@$)=He6=<7{7v>0al%&2)6wznS{nX5HRHh>lzMiuR?|cgtK*g(`$=T{m8*F>)h<(FO?>8?9Q$Ut_;A{)L9)k!1cp0ZBCy9AtSm* z&+?S23C_fjL#~Yf2_k2Tz`$jUNauq*^|iZnDSk)kZ%xlvg>WLbzGZdtH*|J&?ttif8m)Bq*#wal)t$?gjFwCNePfTL7gf) zku+zNGh{}4o|8-5+>L2ip>OM^EK8LtRqHY#*po+}0IFZy)GYYXx+?=QR`0l%<^^Zc z&anC0q2=1gih^2x)Irz>dcEx9v`>!-L+v(MAzJ+}z6aKed}Pxd#>0plz(j!Ea^%$V z{G2`5XS<=fEXoFg{qV4@YUeE!VQF}SiW>|LPi>NTy3Mat21ey-_^FWC8 ztn7dmI=CEJQN_V{u$sc*%7-U{jPFStQbC!5{TpI^(vZH(;ySTA-JdsihB3dkOzkcO z4Na@&Sf#{=WD<*mU7mmzI!++!{wBpXMKEf!8jE{bW=B3AD(pXFSLGoek$SKmm9?V~ zAYBHV5ba>XiS}&O#<$cI}dzieVq3WPz zpJCP*<@s~|be|yf_P#X}4QT+S@OQY62>*cA5+eze*G0^Nr~9d-v8BZUNs01aa$urP zx`@MpAQdurryv6KVPMBkem{CIf|LPSM-x7$!+D(q>wublp~dcvj=J+ISPR6#u0eCt z=`JEU`8^{ISg74|a}uekr6ty}(S0pD2&|<$Ey=KHz-~`GU|W^Gs`9ZfZx*l7dg`z0 zkR*vRmcja_K;-3~%_Sqh{>U5G679NuInkfv@PN$mTFtzb$YA=}jKC}FNk*3686`;o z-lmvX1VM^029u;v8FBNe$F0WO=!4nD=OEJ8!?0l}tUhYJQ)?ofNd$^z-qI#P=|WAg zV#tUN;Z3rssS-zdH?3$X>BQDmj-n#VvG|G zIVr*APb6P*U3C=L_EO)h67Ps=I^bK=Pw=_f9;Ro24#zL4QVk0IUHf}?EBps@f(|s+ z<8ou7FV8gQ`Y%7*4U4I^P~}U#>wTQ39V8r#NXV&hh`tL&&H8!6YgFsj2TGS-n>ChK z!Ou^AOxLIC0jTRS2U3lF5tjEs! z7rhS!N~ect(}cszT-VUg3;A5nOO_%M52QA~YdlvAiI24seB{AvgKnK~HqYyu&jOu1 zcOlW{3`t`P+VQ3pDL=F%nBuP^0L_g|ljkBg)7)FC)aQ?>Ep{)ZXFpau9dLGO>cl~X zJfh;nf2QO_V<`)^g9Ehr-VcX7|$f5R>!%!?C6I$X;TRQAdrt3 zgYrVx+uR^HWGm=SsJAax(3pF>@WZ?fq2byWlK6R0Rcute&DbZ8AW0|Yc%Yl9VpUQxng`Qv<*)-3ugn&X8U zI&5^FfZsT4HK~xqRzELfri%9fV!;WsCBz=XMCkc-Pu&7tkQOu=QtxrwR)({c$lDtR zbte{A@094n^Y4XxTaQ6Xnnb3p{1RFtup^S#$QUF*QM5a(Jc<+p99 ze;nt&vqP6s9|$1J`f}Edsfs2T{`>qT)$_NXtIn+?f__W`6UR9mzvP*w_ zAS7;Pf_Td5SoUFx+1JW3_D6)|CosVR61^1>Vl+tZ(+mwco2% zJ=Ha_tiUZwQIukGG*mFgHy9ERM#-Feq1TC%`!T#%8J$n+beTOeDCx%yIo6ZDopDGG zH6ym|^m04Wg#u0%Lrj2iX$wO~xT?}~X^@J?g)g8C+a7$yxzkAa=hsr>5Fj_J^@e#* z-@0sIeFBw3uBXiRT$F?9lfmBlCMBBlH9afC04uiFW3aoEGuj{Gu!#G%G!3S-KgYQ) zH&|up{NXD-l(e!FE9%L?7;H?IjYvgBMT(3#wVkao(Ko2FauDh;Wl;LCVINx0VC}~( zx0DyjruhaBrcot(>5qif)J^b<;fuN4-9Lpr)rVAL+fK&zHO0(aM#8t5r}f*d%ZILB zbhzKxX#tS0X!8;A=4lmu0$PV341;%=D_LBn%@p|_S=wp3B1Z%IOx1mcCo{HrPm?7O zK75|n#$k72S4a5Tww(mqej>Y3 z^}NmI{)YEiWOd_Y?HlIsz$YorA6$te@5Zf|o^nsSY`~uSKC>HkI*ZJ3#FiSkGwtp@ z6@hjuWC8aEkHDY6T1}cAoiLXB%p%Up1VaqQJ!|jlEP)J5)Ol!$nuF!8FE`UcO@$of z!BJ@=BZ%GyUYmoB42xF}W(Swu@uHHHWu5QmrQFhYg|Eh?3$pzYh24w>NTMSTI&a+y zGrdm5dFc@31Rgmo!(!m%cqstE&!+3tigQzb;Kq7CU|+?cw23>_fBs<|1BbopH#dOWX;aX^>FlP2#SjUg*eovo4uza*dyd-D{dfA%rCZmF zm#pkUoH`L@Tw;oXlC{79;@f(g6~mJC+)TSG<_T#hw3{Pb;WBY5&m+ERjL(=h87M(hlR{E))v~#sUMxo?wXpMmG;re{)@3 z7Q15;_w*Cm1?rjguFyKn5jKNiMs1v5AB( z>g~+0yC2&0_PJZ>^!_$`b<1ghnk+aDKOTW=(dBA5A3fJFc<)FkV^HXA>g|KFXmv!O zQS$Vkm5BMfw#(;hc?8}u?i%C%DvloB>uUY>lijN4`M1#Mkl1nh6F9baVh-c|BqP*W z1sr$sQ&6smvb=jPi(6mMwW||3V6MioX4TC-JnlO6ku8${kGZdWi0b*`{;I@+h{V#} z-QCg+(jX;Ew}f;^!_r81Bi$t>uuG?O%hJ-)CC~NuJb%RVh8OI;Gk4CMIdjhEeCA97 z8r3wmS?BZA3#0o#)Z%yhGulxQx^vT??n+4@*YBBZDUg*ewvr*YQ&tYh#lH=+BL;EF zx~g^NOZ6sZ{QHYNl&-p`g>ro~<|Sm}HW6C+NWF#o-2 z4%lv_5$zZ{@EQ~Tt4XfP!O(-X~H-Lhci>-s7S0RoOJ3EHi&`RNtWvcCg4uxtbl1ZLvZq)v+y zm0*@kN#k;UESdCxy9g7UZ{TO;WG!R?GB%Bj#dCqzn%v?mN6`u`n0Ge zt5PmyejJtc&U?Dd1rCFU(8%rKtIs5vCYdWCh*@6k`dPhQ8mdrG!%~fIC1i5FtId6r zyVTlBlL}grU~VY7M|fC2cMxBM`v!6mF2Fyfq>X+1fNaI34Q2gZ(NSZ z`SGbg>hooM;Ez~19WD71md%NQT2Y)RO`<~plxE4P?DH0S=0bPpowc+1oRgqB7*LHy zO;TJ}YFY3g`_E%J5h%j_=jdKC41FW=BT22b6WFOa9N6`vfA@N0N&N|pLEAj(__=#G z^$NAFzB{gYj<2fL@uQt^JB!+gwtPu*6^Yji;j|8mKIW@^6`=MhueSfmn}&K~@RHmHlubizKb3!1Tg&ELYRwuf`>sEq@WQ zv8t>lrYbFrC+pqx5XzP4AK|hAs))AcWN^wwMeB^=GpXaXD>C`f(h}KvT`YvQ=);?D{KsYi3!`ym ziNli)ZnVSJYJ*m z%oLXi8ZIt;O@T-$fXIEeshVPcb$PcjQ$mWJQ2 zUoTd!z1?^+gnlS}&z;sHEq-HJrOE13+~q}9O`_*09S-M?!vv^8FM~AE0TMrVp%;bt^N}0xZ$X#+L zneTfp7_l|7n+0{2XvrO#43&z@()ElRj&S)fSdyyy6+7V0{iVv6%){!1Pp8=I4V zUp*TDF~8uB7TWNZ*ix(jr*UCCgf@9w-OF6p)w0L}DoeWKN(7KJzza3T{X5ZViG(Va z+I);q*>pwHEIb$(PD6)S0^}&S?H$SJfz7~uc%H76Eqg?d0R^;6VI+k+F7UzD=oX>O znk^DvOj(XQv0K`#u}xI2vy7tJq!vRif@XFvgR|J64vS{(@iGb?rcPokJYb>+@aE6P zq<+6B=NANkuZDD0ZVj#$`Mt5!<qC=oSJ63mP%t=H)DA0_0_pL z#IdEMG#SBXzog~sZDr08&9d&ebxAf)up|vubK+>hvu(c3@cwDsr{sTT$1u_1e2GB@ z+?u)Cm8s#UrE$J^6@`U^Gw4!hzK3}v(MB<{`eL%v4P{G?&+rrR@KK$B9ZY;0Z-vI| zl$n2Ts7)mKk_7a-Vt=;aufFou%R0O%;a4E%OmK*?lyQQd+veVQT?RxI;P~mQvR; zvjy{L*y{4Vq=n2Jcq#sr>v##dD%#J_GL~D{A8~oV?)G2UFwr+DZ12GnwOu(8si7~V zqVE4SideZE|NdN6Q&YG^yg`mUXWMH#t;K57p_qFebuOvyf%jmW0aUogFUUtR0?;irTM{5|%~%4Vc%|ym&!+M*!}IT!RKUo}5gU zyTkzCydm%57{Bvg_)m0c={Y3AivF2s+fV$d}1o@jKZk2leZ!n=g)8 z)H404#Wgy0m?jd++IU-EhYbt0?J)m|b7!^y#z%{;RjT>;OZqKj^Ws9^zy!F<waJ?8HEJDi>w0+X?o3 z3JhES&GjS5cJ>o>605q)*mcy=7{8^tu^D85REu$JY+9VkUF1xpNnKy&@jJ30oH<(x ztwO(r!c3Q-*fK4t#n|Ij7(vtENz2$Y-(D?pZ9A2iRlByLYt)*9HD>#HRju={`?u2MaTC}l?}hbW7t2WKS~Ei;%i6k4X_K)` zzyYSv8pDiGtr{XOqoAa@kX#BC2k90U)zkad)Bolk;_t+QZ9mHo4<&c>u$=C<`qL8RMchx53uMMLAm4 z1&fuK9dnA{u|!`B@9Hd$bF1c01$NcE)S)>IcdIUrbc;@+M^R>Xs~;l#)($P>vkDi* z*R@kp4l|?6?U_fAANX2@%>t~ZL0eEqD8h5OrBFqIT+sD0^2zG}VHra_XLnT<0HYRy zlhZ5>NKHJ!q{3ynUT!9bDb+_JmMEEr`0_eRU9&xd<2-~a!OEW(RU)&0CmV>RH>bGv zvFTfKwbwg9^}ujiiZKZM~r%xaPsvQM8n$gXt@x%6i!jutum%{jcer|G;Z(a)eYh`*~i#6gy7jt{y?tLcD*BXQ58}Z2CySue! z*^8UQt}$n+hT+!kix_N}DWYq}x{9!)-Q{!Sq65VCROJxHHgtDG+bFa*{n1=WV@OskYu~gLb$oU#n4Lu4%17YYWN- zXiL!4_fByHBhsXUyvg90_qgX^zBHDRBWvVb^Vfy>Rk%gMa5ATE>?r{dim&d;Ih3b<21Spzll=IsY@QC zuH*${e6`i6j34Xo84J67$V~i8T3-fx)ek%D23CJxb0{N57VN+@Fss_ZVv#lP&-Oe) zXIPS!`Q+&OGDV4Kp~j?D!?7P9KGK;6aC7{~IHbi;G?Cq)xC_(lEqgQqPKtR74{wZ^ zKO@t8iPqu~=%JsF8u{Dr(XVZS{SZ1)=ikRP_ny13iL+}|*D*_vV9KCtzH#hh+v0!# z*@bhiwC^4Iy76Q(q)Gk!+W5=Imu?nxF&m* zs(lvj`zGN}zW`?qmcR@@Fg;;AUANEja2VtpU7G8PaJb^EMPhorv}(g({5o1o#4)VR zT*lV(IJ|m2?$wg}f`K7#@MDEReL7}neK{#^5}(J>s;w5I_}?>g-rt`wNGu09%eZoH zJ~vFa7&7S<%f++N?>;s$D1rwMndZOmDJ$Kuu=1m|x>kZT;4h(#BEh>DLh$Yuq9*kr znL#G?MHEsG9y9ZCz~};jBt`v(?T;0s<~){G8<~fyX3rQ14-Qok#KQ@t!{elm4JdB- zm4Dke!lB76vq|0W4VgPDo%xsRs46{Z*X(Iy<&n@wnW1WIhlca=hSL{W99eo*eDvli zkSqdGu82NZ)741}KF1GSNzoIMP$Jrn*v~96J8yKdm+av#xeS)HzGGt(A5Vk)^>2?v z`2~LW*QQ7L2j?P05^|iH4njzdxeu8z{SprZ4)w?dp@`KQ!|vVWQO47C=PaK^dJJz^ z4bp88=fsyA6(M9pD)!^W_>+5X;mU=AlGx9|HKvOB`&DcP%Cfc#W^Fo(GG@)`79r*3 zibqSWBqgrc0MraeT#*ps^T$=2>zW9wRcH@ymZ5fsH5?gxB<{0r z?dNe8NcBi5s=b>(dY|-74F~`H#0d|xYt8>2BiKpC5-Em`sK-{tWdSrLTr@siIXB2a zqWUk|l8Rn7nkIvc-zv1hJF{6@KIB^qsjtWr#;SgD&VM?`axFDpk#r$~1@l-@R)M#=!wWA59bevx1dGAJi6FYnF)CKn_a}aqlep>yIAj?|$Dv5uS zp}os>vgN1AcZdXs+G)#i*+C(tLB2Ng=AzbM-Sftc45 zbQKmqWne7j3j|!vc#UIgBY$y{jVIK57}-_EmKQb}$GLvGjQd>i35MZlmqTQpSRV&` zrq1IMz+3o6qK}LHw@;^k(SYgguS+N;tB}_46A`$HO5nmCmY}+4yGlXOer}4svrwv0 zwwSqn{~o8q64SQ3KneblXgTurfe}MF&eyjTjU)#Z4{tXv_S)vqX5&O1$UR}&M^@7& zoxyR~@amnzGyVlfsSszeit2n^gEHK036B-Y*GCxc{f)Vh3*TZFA3K`yu}xk2YD#>w z4E&(lt`;#1G=1t1e)h4!KD8I^zORx?VOt$CymR8l+%_W!@~M0vezi zxNbC;$vFDSL3{q6$DyyiM;(U+E2}c2+Q}pUV0F>>w$9@^UV+8X7G&#)TCv*DOzt&) z_0i3EP`t31}y$L^Y^RIURSCY+1pz_za&hEbFH0tfnYgEmEM0yMuA=lB6g;= zn6!HT3`;e~%=i^WH&nz(PxNsxPN=F1CF>Znc1x$rVNmcj%1>#q+6-YIe}KCMi)W2A zU;&BLorR-BZYpGS{@O|dzPFw;;o&AlP!~9{J`+-tI772kISY#X@`P?`ms%7#r>KuDb zJn^2f>c|KBm3#NnC#p4uF~uC0;)OG6Al=KwYHhBkeE|)z%Qv%_Vv)lUAbo2-FJsn84(9n6K7>CZi`~P)EHB8ffo2v(}%xjiaUxW62>9g<0Fw9 z3y9fmW?8)5#+VoPBjFkB4*3};#yD}(b~^C6T%H3M^7?DntXLPC-EGzl^qMl5Y5FW!$vG>gl-fn9vl| zT&hyMad6140+b$PVqhFGU)fXrdKN&NnPM>W4?RsEdrZV0u}Ud?ms@3P% zb60-qj$9JVqy>6UiUDf+o<^>Ts;ZygMtYDyF5=ROCgeK6QE$pgJyV^3EkEC!YKVBv zws_^H2g1g>PDBZ713mCZdjemxoc@)*uI%%Chw{F5t$r?Sma4W~bN)GJcR?-p`x-5y zP)!y1YYb;=e@quH?tc2Y9r}g~Mg5ss|FO$g3&@t-|69Zzpaq!SN}C5C<(&Cbbb` z2SJ{gCQQ*T+Nk-fQ5z$13R*dUz@eC?Zz}yTpySnGOVhef*-SOAoY*OVlcc~rhkkvu z7IxLl$4mT1M)q@8z8PvYt|uI*l70`njZ9sHE4jolZcl6ZaaiD*_`e!wF(rJ_={jm^ zYui^}Zzl#9o-|IhI9Shqjc*L1a?xT>5O&%e| zyem|)fH5wk0Km9>>74FvdSsuXC!Ovx7gk{d*48r_jwCcm>Z}^>woR1!$}t1R0ve^L zmQT~Y4J0fdX+^XvbO897K(Ce^;%`Z&k?04Rs|Z;oQ`|gX{OjYe@Aypi9~6-ub<=J` z_L+$iTVeGBkgr_7*URM}-gV?KR<{**j)}xlw#RS04$j$6lGj~S&Xvr`H7o8A83X+mv|uo4v-(Fb#u(!`e8ro$nbrB|xsZY)S1r3`RsKD+`3zirGy)b zl!*blxWKE*g{pp@KEcxz#INu<#bdiKk&s~n4<#KfCU$DaZ@E|fF3}!?f)}!isr#8J z*>YV$kn%_=s^xHfy3;oo7vReL8(7VWt8*Wr*+m@f5(9)GC1>K~ot{j6IRD zQJ~gTPzr7AHJaoRO0G6kxm5G0R2{60D&oCf@xE!@>3Et$u|c|HZs5-!bb5M*_chvB zXy>@f$jQN{xeCE=1eRmpl8Gh7UPlo|-L+fL064_BM-uwi#Qki_zW8jjEE z;ukBe8uz?t6-F-84AEkwQjJXawCAzff%L1WeQ3H!P|?^}g@s)Z)LEZ80T2ug`pK>w zC3ILUajujd2#h1};6Mr|XmRw!1Dld27ZQzqdz?bDU&C%7f{fjy`b(kOzi^@by&oJR z{mFjXsgp*w69z|L9SO~7szz2M4z-AsD~f{KN2h1xNCMdxM!|76G9Y2A zACoK`Pa7M)Jka^DGCuoo2?PO0%^$vv6D8V)mVSL4_Aqmu(1kV|;G`;XZ7nLKo~Z)I zjvdKNvwOb0inunnVqa#Cv*SII6LIw{c8O^2oAwmrA6vGcCB|ieN6{a>xWWd^-ecVWmG7irmQ{oV4pl{Ok|eIwOAe1*RPLu>ugH_Pt(x5GZVU0srDN!kJT z@bEA_Cug9z97oy21)LpjuvS!dn>&P*RgD;osV!Pp*Q#aGB92>TDCgJ?4BK!la@l#g z%o4rNKQD=ORC4*f3qwQb=Up%=K9s&wiYzS3)@$dD2vC-e zcHQ`LTvK^gu$(AVP?ss1cO`DkWz8=C^D(KC2xcFX?lDQ9a}5K%YYlJ5f=BIF2gGhk zyulrH#!gP_M#!G`?X+89xNCEaWMa$25oCcPmllo78&b6xt{D=4=)s1~#K2VI6dPHs z-5oB)%+H#*uYhOldq7}=!^SYTaIy<+4h36Z(-+o9aI>((hjZkx)R)mctaqSff_!Ph ziUSGu-=}Rpp5GTf`TgznZgUD%a%r{SS{!;xq@@Dl|x{Sg`3FkCjd|G7-mNwiL@`9FuDskvzXy!ft$mZ^<16u6h=7e*2J;1pgNeyF64E_ zo7|}{nDMqw1~jLrhqPm&-5@25sPj-K`)>ko4{8|Z%&@ol1(y7(o4wYx>kfB~7#uGs zsLrR4JzREkcosXH-(pTu$Z02MREapW%o5~ol=wr&ka7_UB*z1XSJ6Sm0Q5)%MTma9 zVB?t=?VDH}r#kmV=21|jO_nJ@>o$l>k!Ei3(2~J8CB^fibCAX?({EwIMOdaf`3kU+ zCHY9LAPqB85Ah2K05E?Z5OT(wpZ*n#3~w<$(vD_fd7|WRD>%~Hk9ABDd(il%nxVtN z(}E}4F--3`8B^lu?qnouZ9eX|K5|1yP_3!*ATxI6q>Lh60b6r8`H)KbE3Oy>(veQ&_4DSx7j@e3ijj-i0&T&d*DR&pC1-2H(*BBac#Q0|# zsDxNVdyd;;Yajp zhTZ%wz6ZWbkXO*j?--t=3m)DH=QnpH)1%^HbN^Hx$I8b4bs1-J2?#FErKFWaB zSqJDIE(cfR;3g4>Kkf5$K1Iyj_P*_jeuerrUpMO=xHvkeK~+Ss;0V=J_Yu<4tMgVO z^k6XF{3h?|HMq=k^~!~3;}Ix>t2YHC!`UBKo|klEKVvqQ_;dV($pGNixa@J_u@V=u zi}=~gN=b68%|k`f%sYQs*hoXZ*O!vM*Ak203S0TBRxu!_23IA-tnq1)Ogqc*f4Zj7 zbFt@5%-7J(?8k@BXhRZ2T+Gi=x<#Mq$o(HRwCw>06{&t?(Imo*`Wi3Riy2<=zD_r) zeM{oq@l?n>w~NH;GVMsmwfWxLzdTw(e?^*7Yj)HL4~GtLk8B8N>Z2jLQ5QkvQejl( z`jc5gGC#iWC<r8U%BF)M%vj!DlhLojbD<;d%bJc{Y zdF4--o?pJMPTP8Zx*E~(bNMU!cgOVaYc0Asb|{(cYVS#rf6;NVI5|tYVUEnz4{RFf zGpy>{F)#9$2W2vBxLOG`C-BeYn9>rS`}l7oPk%gbDe9#^xmwxA522c-WVAE4R{R{w z&ffj@j*I4%sRd9v8n4mbK`D+E{j0e338!*SPr9zY+(upYG!R;Qj=K0gSdZOUwsi(R zvvFUVAtKk~{LN}oT=Kr}0&xLO^E?^o4ZMPE1be+U5c+zW@vt60reuU2-SZHb$Phd} zn|Oq)k{21pra{U9hPD4-zp`fahhRC$%|uP!rlJ-2&2~K>W+_`_O5TO)uRq3@9TlIW z2HrpAQet|aCWI2&YOUHy2@p?Reg1a1P>l5ECj~zmb+}H&&zi1p;%9GwfIkcewo(pU zIr$L!a3b-e(wSO7;bkNRQ&MaSg0P@09V49e&LV%@&7z7y{fux&N`b;l4#_>{lmGKt zqZgul@P8TB6iUo>S*6fS(0W{kpK@nrRZZk4PSaK)#IE>O`ed?x{0B!7ra3tUAvsEx z4~h2!twin9lX~8ROtY23x!%(Yuy9tn5#>2Zu(+Dq4J z?jFic|0mh-+L>><9v(}`3}}#*IX%sDwk9_Gz+gKnG8eMU0I?FhMvK$U81#@DQT&(3 z5??M|Hr)7TUizW0c)ATw>xIXdSA%7AT@>3&+m2Hq${G3B4j|YS1kj`p&2B4irA|&* z_`Un$o2CILfIeK4sg!a8l|L=bJ3llbLEpgs1eeb3kjYkPj39e3fmAA_x&ItrlO^7kgU<<11K_b zWusgc*$UYfq`8RQX60|Dx1vKIDW>G6O^oI2&zUoU%r(QKiQeoBvwsxa7Bz>bh(5u? z#cecQC>n4%lo4u)CiZcS0kgH=vVIgx()fyzwC|4a2LTkvnX2jT0HX0qg3Z=HmU$rl zWI3fgg{~V$rb2yZ5kyD|!iciqv%*A0zN&Uh{|2AhA z{qFe*jQvw-c}oY>Ds59-gD_SWhEsR8{@ zI2*fcCAe|iEe4h+Zsv)tg=bA>$Yk|z#%XI78JI13Rb}f(=Su58^67KPsSfI7@JS5X z&)46tp8^OabmBKReUA$UawBs-wB-10(=$P(?A9Lw8`F-+tN0CRYe%NkiqFe+wLu%W z1Dh;R0K<#y#D$P2s-&cog}VkBdmJ{5NQ2NKw!Sm!DcN{_-~Xv4O7HIc)p!h3>EdEu zLdaZTJu0Vlt;HgFWApJkRiGn^w>vB=ZwznV{Tu z4y8y5ge1jb19Rcg2l{W$jFdQh*y*TfO@WiEDrzbp8`rF8SJD%pRo2JO{pY71x@kxC z*S#x@Dk{Bv@-99T=n}OW@(bFD9a;92Vq*_KoLhhC+JVsi7X>KQ@0+EaM3LrqtQoO? z#+E9ix{h)+IKnk9LKX~S+cFW7d>a=6DSh|xgVU|xK^O|{Dj6b!qab*Y}jUvrhIr&HDZ$VPNh=JF2b(}pJ0%`)Q| zD%iHPFY~&J()3vz0dyGZgv+Q%>@vX5qC5>xFWU#q7pa*T{GX+M@l9 za~^O&soo|)+6r$kh%$-oXVP0L+A@`iF?|gqYuc5e^Ks@pYcV(_+PexE_Gt+~Qv9J? zA~?_*#_hA1;=`qIjV21O`hz9ar&TjIHUfF!$bU*G^nH)1lwJ*`xAaQkMzb8O1(v7% z8|RTT;U8(X2&gZ_y2Rs8O{A|u%CVjdUBzbaUKSmD5_Egey67BNbSg#$gXiWsx$nrV>Br!@c;>s`@iPl%rzT5XBP}2 zz9iD^`f)c%0&~5S0Co_uCM)zS;^7}}DFVCSsU12jpBv7cBH5s7>w&5ya?M>S$Q*}T zRXr7*r;yB2`Gy?^81w%6Y`@H$^C8 zT-SJ`c4Uq#GA>X~-7w?7VScA(C}Iq=Xh-w#R2`}1b#bw;TNs_UzH)jbL`@%I1@S@Q zbJMo6D*1|fRBe;~l1=Bg`rFgH%sfNhn}gv4vQ_IeEF}>S;4}s$HI{GZ38P{XqlsMh z97OBD)n04yhU1w9OA18gxpkf+&*gHaA^P{o@)yTnsrk!eab#GIKRZH`+xv-X`;Ssf zmFQmERpjH?36}80@ae`e1Oh?F?YIuI2!8`xEAR@Xl?9Jx-t4T)1MOK@Xxs6Tdf%Fn z3j6Th1}{d?ZmmbPGjsGc2o{f2K4nN(d^)76id-qH3<-Eit6UXQD9BA6UQ6X<%0(55 zhDPk*NUHmc$jv1vHSIa5*ENSQVJjLR!J)rn4+-YO6A{$ z#JTeVB0r`|g8u#XGYGU9ACvl^$xEiG^s|L4PhgPl-!MZHnkWlz{fn~!Gk7!Rf{9`M z?`e?YtHPz4^es;XAw^ucbLJMCTX73O;0At6q9NYB{XtqwwL_~4c1{5b9{#U=g`amU zNXX-O#o01ZkdkDJm0`J#9=^d$p~cADfBn#UD?rj|Bcd3^*JeJI)4P;MeXu{^G2|X( zauc?#%|@V4!*2s2tcA=(|xJ^i#OZHtu~kR@4ZYkCTW>H&8QAZh=1F94D`4~)%+7@_=c97F_3ye=c8&)AZUaVj7PRlVw9 ztl&ClpT62H!>-pX=?4Fx+IvFWe}ag?VbUKF=QzjC;~xaRd?Qjhz$rpQj|J95FUcj5gFk(K7Dx@SX2m5FR&d0)3yjov zOoK-rThcD3n2d4YU+O4ZcRUdn0NRuL>=QK zf3n}iGCR7#EGJaviF3kt#QER<{=fZ`{|*!tyVaQP=#0PD{AVv^fHgTYt(Ij_ zh?=7LG_N&q-VUz!H~4x^$+E%s{t{jH9f(S3r(Vg@RJ@X-7nh3{l>ckYuEhFy!I#?X;bxVcu!Y``5sWYHz)$c_p)Ua;=~nb<)n)EN zE>yI#i9Za9FS=LAabOZTE{~QO4BxgF)p5SZMKb|#-=M1DonWrGKcAq!{dj^RlFS8n}Cnm9Qw}8rLu2uSvM5K>R@e%Bu8^1*( zu&K<;3_k@F`aR!KnVed=ejDGO5<)h8F5UJ}%W=v)PfieL{F1P8RxDf+PJgXAiVH_5 z?)+ldox#247c|-(PrfSlCQ~D4AyBv9fNm`$P&&XH-u5Om+1nZ|v_`tnsF}_!XYmEM z-OT+G=6?A?db5J94a9ujN=15Vs| zZ(1alaN{c+izD8~Z7S2#KNsVtwRvgw-9^z>FKgGag0h>ql4;+fh9>w@Ll!|ZZgN;X z=q!=)6E1hHJ3>QlFBIvh2O8;i_gOK~+d;4lvSfq4dL`PA7@0Z~OFq&M z@OXTXp>)HbdwQp#GGv_aI~-$Qj7v2BoTrDcPU}YkVTQoxj=W6hvQYav6R`rRs0p*! z#?@z_WD5=t)3C)1`eL%~n_{w-7xr48!(3;vORlPPtsZCX$jxqi7$0AT zuD*gol6+;bXeuc2v5&q9kWL0xZsDu@FaG9Onb-9)CiQIkTOaO29>R0TvUg>Ux3~Q; zj+ur!Ga20e%>vR~$gA5eUVkra8injTMiU@WW}M>HkZ)?y9J?CV&TkSy{P9&5k;wSf zlfC?b06+8BG6G%Q`|F2^$%zRyeu22G-HZ~l2AR-Cbiwh1~s9p>)`QD0eGCc~;q1el(bwyg= zql#>YJbFJeJ5Syx#3w%WUM%Z4i=U(h{*|n}LI+7=8QO}68s2e37OOrz*%b-jjf~ef z5;;8^o<68u#p!Kl*76DD^_VC2g2nIIbQ2z>G*n!b>sI%ADo|~X7}t5Vmb}hY8uhsB zWjDrFaV2Yq&t>4N7AUxbjgS(~o)sRW-3fu#*C=&Nga~>vqx~{ zH)$7BWW#^e?3zyapmS)3{z* zybtVI6S7?|kq~(HT67&bpRh`dda+&OK!EWo+Psl=eEVzm%pAjm|F>!^7T_-D?oGi2 zd$Q2W7qVQ#Wb@CZSQUM9BtVtW2)&ypxjnq|y+7EiHOW&|iavi-QbkAItWxqZ6Wl`I}*|S9VByR+o9+vzA z2Syz;m2R<~q-%r$b-HZcJQ|U_K|P&fL}B_iw5}zJ7KC7vA!L#7`SNM3^{@oyxkoen z&b3Bt(BJAm5qQ+#sHKUuP>~_irDKl9vePXs?eu%_*VT_{Uee3-zqZ0?*=~k+yX!_* zch4m(o6|3&jZV8%a|@UUFdx&ra&}(t9Cn*Jn{JX}C4B;9=d`aA^&n?|6-~SQv>Y42 ztUYI$ItRf)!5(;3rF14dO4RMT`{3^&`S6;Ob`$8w2&+ZO;mP^<8n6Q7s|5YpfArbt z&FT|FlKgO9npodS9sXs3=k9)%OnM8564|U0)}Ty`R;oArpcCnw;mGH(z$A4qm!#gt zED0KZ{f}?Ucu7W*oND|t%<3kEa}IK0^~?oAL(`*EU=_r?s8U1}Fw$r2l_x!j_VNB! z@JjKDp826PWOt5c>yIua^+N~ERjY^SXx8n&$|tQec&0TX2F=f8CJ)e4S#3?xlszGXWYeZ3T4~Woq^Dd9EI++2?A>!EU;o;v1a}C} zYX6Qw!u3lLmYr#c>{j#t zwc&k$Sy>u5eI9UNChU<^LZi`*xh<*h zr9}b+WDrA~`(}#)v%{8Fko9 zT44X-R&LbO11m+kf9wLD;V$MZ7ZW9V|G(vr^CTJ60W?lq`;zj|AAL=nTesUEWnjeh)wr zleTiuLL1P`qryIrgg-gc;KF`SFaf0R-H-?AF~{Z8NBFQ_u2=1GSCHfr+;g|h^^E!d zy)jnU{QEh)>guItoPiHUy?c4wmxLE*&{v2k$`6MFkNIY^7CLeJ=~sbyQGb^f_2R zQC+LEZ9fO@GTrPaR1CZT57N>z0$?a`Mx3|U&aY)o$gXQZDX*_CUyvRfSR#ODJT!$} zd;Tzf;-+-4mw#@L5&e#|XZVdbEh_+nScGx5kG-#?wD`~A!Ci`)^0;rq*;DORZsY#9HRjDJB)J1f7Cd zjsN5_rkO~Hj}DWzO90B=cq^{Ckw61BBc@}l(^Ns}a}k5S#u2d=$(hj>FI)|WU`1e07H1GM;M(G!#;v~ir*TSZ8-0`7DS2|x z`l$b?MBxC8WASbbs4IA*ls-o+brP=~FH0(4uVbUvg)VjSAFx1KOUD1FBxT;Xa{m@a zv7rBXz-(7N8}O)qQ#=Zs@c;hx?Z5g1lR5WF$3s&Wc}+K_MAs=ql!4yR=#yUGT~1oMoajH2J`fdB@S%wq= z`b1hRuFAk_{b4OH^`8l?|hIs#k)ABOzGqTFk$^-Vbe*x z*uoooTWv%xB!euwonh&IcR!pHJhbyj+HqQlae|R8Kw4~=mPPNC%386c!e9O~bvRqN z1A0fde-(={DAb+{^|#!OQvY+4KDg&nxRW*}Wu6IK$wj8G&(CC37l&{EZ9Ib- zUUaa2>zSsz!f2^)WV79_{n^pJ2%EryjX|FpB^glh9LTkB7zxiLUt*7!bK_Yuf?*dqg+fL1v|a2P%vft$cM_W{(;gl^JoQI z;C=_-iVL@7epfa^6v+x$K@Cp~yj2`5rTU1IWU^&{=~y=6V68m&?7M)LC@0C;_gYa>omcumkh3y}1t19{op)-;%ho$n~ zN^qUjq08(hrUsz+Gt=ay!ktMz__9;vG{fttjlfKdA48Grh z?RQM*3vgW{ne^-(fnDQ3tnvq+zV>72M75rMe$LWbxzgOgoioGYrV3@D!g`Q-?siG? z|L4na;#H;fi<&S!C1GnuMIU)LXnHYeBSOpF@8T|q!~4qvC~S}}FS?ZG_1|OOUqkJ~ z3RI|n26w8F8T;|9!jx+*%9T39e*P`*rl`DBAJk#P%)%=4%UQD*?P{O47z5VODkWZ3 zUG=-z1RwuwsS}Sl0v99uE7$bXMCS9l3QsdnpDdtley(L^7m%rgE;q39+rQg3j*Sx`Ko8 zSk%t1I5*6@V1TzpNL|3s#hgvr+^Roen9f|ksf`aemKZ9w=adwEJajP9KTN?bH15=n zmKRMtp$qL?y%DhK42t=A4P}rL^$Y8^Yajz`9m~<1i8na~KAkg+-9i)4^ZdCEjcqt) zUDWxUM(${hDVFJxUr+pGv@^64k$3zwhO^>BV>`Da2#U2(XGH+0+LwgeM8QkZ?~=P4%Fi zvt1Mkry+Xgm9EH81)7rMkZ2Yb>Rr!OFFJZ~MxxRb@?}BP z^!dI&J(Xt13W4HSs{Zj^Q&E6;T(1c~SIa1zqB4R1ZF>fBqKD?gEqrO>m%x$9&QZw( zB`#X;;Oil*{}ZDHT>5f8+MC6B^NSsa;~ZDgxI}vr+ZCC-3F0keg~`?4+adC<1=;BN zs=3C0;(7WPQN{`x9|{hB8paZ|9rGR=q!4gdg1rv6ja6ncYR0gW$K2wAnP~pTysus} z@9Uv>celn%J-~PwtfX~7$aZ2F9!b`CqUfGfj`J!~gckp7yED*$|74}`vbo7etyW6Q z<5MD#E0f*6J4K0bY1Q2mN;xMSt$#9zr*k{w7Axjlo-ofL)TW=?2z4H%^67fs^JdLU zI@O)PfQ;h2g?wL$h*;53D{5QITJ8Ji@AP2vbC1}+*O)aG`+4)JTu1X54nKdt)d{t^ zGAg}Qlll8SF3LF2_j9Qhe>UJmdJ8KSeNVs9k3g2JbS2kz#A_Twc-^D|E`q<0huZhG z5C1nxI}3w~WEn4;8-?SJtDcWEUYhIV{UKi`B%M$cldq9CQ7ptHjBlc}UNHmq=`B&C zwO+MfS8B#SKrpL)O59eR2~ z8sAsj5qpVPj;xJf#3V@++vig%z81Fq^*y*{0@Rz>Y)xwB5d-G6`_&dG7aa6W7^qY>N7NP3`sf)H3M#mlAwv=Xrbbd#P~JE3F< z*4t*EW^~{77{r1Ia7X<-DSlXcE=$Gvm_)-owr+&m&Yk^Z4Cha7koD^13urN_^j??= z$Qzrr$rl$^R(PTe#dhasopMq9ynQ!`xCi!Ud~K|V33=__Z+BxC#7|5VkE0t&qz32C zysz!=e7E=W*5Aj0?6+_V6vX7NaHAa|JtV5lQ|{3LQYprUJXcedeJ*Jm#LUvOY;~SP(sORjdvL={W-tU)2eAw7p(4ct zGgeX%lY2o?OvpB5z_g7B5-#WjSrV;RC!QJBCYGp(FgAiJp%G{LlsR`Nc!RtPMnK4G zC)d|F>=J2UOBfH3x-t@Ee319x&?Ykq&mtwS(Y+C2e5Td?nYokx97!SY=Kg#b z9^x&I%SKL(w~z|-DJgVN5R-nOC?@1Ch$X??SdJz})TOj~C+9I4)pvbL`*rD=10?ZX5(k~RngxrS& zh~v{~uikA{-^X(k)u%-8$<1+`)go6RIj@@t28Gug-+xiAtFx`{3$#f@L#}ai9-o(S z!kpj{UuR!k6KU=1K<{6pCU( zB4C5{9UHy&td`DB1Bc1q9Mr|9cPdX+I}s1QR?i0lG15(RLE7*-p8wYR#MB#UyD`qs z&dyev&DjFF^YUlq>3QFAX0_JAHD#_XbA8O*?fbQ_ZDS8tYdH@qo^}@M6B7#*2;)Uw z*QxsY2_Me$kZU~B1W3!|<}DGzX%T;0lR!I0N+&=c#}PGOeA`4XP3M9nABti^Vnl9$ zlg*9e%Ga}A@8+i*b!7WvjCi&|^;_bl;}lQR^Y#@o(QuVN?<;5C3c@>xK@osTsTH&Q zzv96;P4P$4MBcCQ?c?M<&p~gj+*InUwNruJ$4>ZMMKqayy(*OVf07*qoM6N<$ Eg4G~uA^-pY literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/loudius_logo.png b/app/src/main/res/drawable-xxxhdpi/loudius_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdf6b9a5a116036b0e5d9a092196136c96d64a3 GIT binary patch literal 170896 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▹DBY6@B!r-c&N?MxuJHYHGC*U6*E3;|KL`>PH^Y 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%0A_KJ3bgx$nJm+)yNv^$|?_p4-Y>@CWGYA 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 geQp` + + From db696d954c606241458251210f1b33859a985a89 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Feb 2023 13:50:24 +0100 Subject: [PATCH 030/526] Add .jscpd.json configuration file. --- .jscpd.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .jscpd.json diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 000000000..7bf9cb418 --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,15 @@ +{ + "threshold": 0, + "reporters": [ + "html", + "console", + "xml" + ], + "ignore": [ + "**/__snapshots__/**" + ], + "ignorePattern": [ + "import .*" + ], + "absolute": true +} From d7c65af3e861e5d4e99ffd55759aab0ef4aaa3f0 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 27 Feb 2023 12:55:36 +0000 Subject: [PATCH 031/526] [MegaLinter] Apply linters fixes --- .jscpd.json | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.jscpd.json b/.jscpd.json index 7bf9cb418..0e07c4553 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,15 +1,7 @@ { "threshold": 0, - "reporters": [ - "html", - "console", - "xml" - ], - "ignore": [ - "**/__snapshots__/**" - ], - "ignorePattern": [ - "import .*" - ], + "reporters": ["html", "console", "xml"], + "ignore": ["**/__snapshots__/**"], + "ignorePattern": ["import .*"], "absolute": true } From 24e4155d036ccb49ff58539fd1c07775c76b04d1 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Feb 2023 16:25:30 +0100 Subject: [PATCH 032/526] Add jetpack security library to dependencies. --- app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index d6def0969..14302dfe6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,6 +89,9 @@ dependencies { //gson implementation 'com.google.code.gson:gson:2.10.1' + //security + implementation "androidx.security:security-crypto:1.0.0" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' From 38f8c26a767832411474b564920b782695a25665 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Feb 2023 16:26:02 +0100 Subject: [PATCH 033/526] Implement storing access token in EncryptedSharedPreferences. --- .../domain/AccessTokenLocalDataSource.kt | 38 +++++++++++++++++++ .../appunite/loudius/domain/UserRepository.kt | 8 +++- .../loudius/domain/UserRepositoryImpl.kt | 22 ++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt diff --git a/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt new file mode 100644 index 000000000..be6e53215 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt @@ -0,0 +1,38 @@ +package com.appunite.loudius.domain + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AccessTokenLocalDataSource @Inject constructor(@ApplicationContext context: Context) { + + companion object { + private const val FILE_NAME = "com.appunite.loudius.sharedPreferences" + private const val KEY_ACCESS_TOKEN = "access_token" + + } + + private val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC + private val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) + + private val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create( + FILE_NAME, + mainKeyAlias, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + fun saveAccessToken(accessToken: String) { + sharedPreferences.edit().putString(KEY_ACCESS_TOKEN, accessToken).apply() + } + + fun getAccessToken(): String? = + sharedPreferences.getString(KEY_ACCESS_TOKEN, null) + +} diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt index b872c20e0..b1a78eb38 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt @@ -4,5 +4,11 @@ import com.appunite.loudius.network.model.AccessToken interface UserRepository { - suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result + suspend fun getAccessToken( + clientId: String, + clientSecret: String, + code: String + ): Result + + suspend fun saveAccessToken(accessToken: AccessToken) } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 78f0d7a58..46be770f8 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -6,8 +6,26 @@ import javax.inject.Inject class UserRepositoryImpl @Inject constructor( private val githubDataSource: GithubDataSource, + private val accessTokenLocalDataSource: AccessTokenLocalDataSource, ) : UserRepository { - override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = - githubDataSource.getAccessToken(clientId, clientSecret, code) + override suspend fun getAccessToken( + clientId: String, + clientSecret: String, + code: String + ): Result { + val tokenFromLocal = accessTokenLocalDataSource.getAccessToken() + + return if (tokenFromLocal != null) { + // TODO: Propose removal of AccessToken data class + Result.success(AccessToken(tokenFromLocal)) + } else { + githubDataSource.getAccessToken(clientId, clientSecret, code) + + } + } + + override suspend fun saveAccessToken(accessToken: AccessToken) { + accessTokenLocalDataSource.saveAccessToken(accessToken.accessToken) + } } From 7a218e79f9ff02de8adb17b0f9521b3a6fbf10a3 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 28 Feb 2023 08:24:58 +0000 Subject: [PATCH 034/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/MainActivity.kt | 8 +++---- .../com/appunite/loudius/di/GithubModule.kt | 4 ++-- .../loudius/domain/UserRepositoryImpl.kt | 2 +- .../appunite/loudius/domain/model/Reviewer.kt | 2 +- .../com/appunite/loudius/network/GithubApi.kt | 2 +- .../loudius/network/GithubDataSource.kt | 2 +- .../loudius/network/model/AccessToken.kt | 2 +- .../loudius/network/utils/ApiCallUtil.kt | 2 +- .../com/appunite/loudius/ui/DetailsScreen.kt | 22 +++++++++---------- .../loudius/ui/components/LoudiusTopAppBar.kt | 12 +++++----- .../appunite/loudius/ui/login/LoginScreen.kt | 8 +++---- .../appunite/loudius/ui/repos/ReposScreen.kt | 2 +- .../loudius/ui/repos/ReposViewModel.kt | 4 ++-- .../com/appunite/loudius/ui/theme/Theme.kt | 8 +++---- .../com/appunite/loudius/ui/theme/Type.kt | 6 ++--- .../loudius/ui/utils/BottomBorderModifier.kt | 4 ++-- 16 files changed, 45 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 3f002ed6a..cfe84c9c4 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -27,12 +27,12 @@ class MainActivity : ComponentActivity() { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background + color = MaterialTheme.colorScheme.background, ) { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Screen.Login.route + startDestination = Screen.Login.route, ) { composable(route = Screen.Login.route) { LoginScreen() @@ -42,8 +42,8 @@ class MainActivity : ComponentActivity() { deepLinks = listOf( navDeepLink { uriPattern = REDIRECT_URL - } - ) + }, + ), ) { ReposScreen(intent = intent) } diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index bea361018..e4b049bba 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -23,12 +23,12 @@ object GithubModule { @Singleton @Provides fun provideUserRepository( - githubDataSource: GithubDataSource + githubDataSource: GithubDataSource, ): UserRepository = UserRepositoryImpl(githubDataSource) @Singleton @Provides fun provideGithubDataSource( - api: GithubApi + api: GithubApi, ): GithubDataSource = GithubNetworkDataSource(api) } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 02e6e058a..78f0d7a58 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -5,7 +5,7 @@ import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject class UserRepositoryImpl @Inject constructor( - private val githubDataSource: GithubDataSource + private val githubDataSource: GithubDataSource, ) : UserRepository { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = diff --git a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt index 4e7e4d969..6189cdf5c 100644 --- a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt @@ -4,5 +4,5 @@ data class Reviewer( val name: String, val isReviewDone: Boolean, val hoursFromPRStart: Int, - val hoursFromReviewDone: Int? + val hoursFromReviewDone: Int?, ) diff --git a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt index e3f15ee37..7ef8e4fa4 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubApi.kt @@ -14,6 +14,6 @@ interface GithubApi { suspend fun getAccessToken( @Field("client_id") clientId: String, @Field("client_secret") clientSecret: String, - @Field("code") code: String + @Field("code") code: String, ): AccessToken } diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt index beb3fa49b..9150a99b4 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt @@ -12,7 +12,7 @@ interface GithubDataSource { @Singleton class GithubNetworkDataSource @Inject constructor( - private val api: GithubApi + private val api: GithubApi, ) : GithubDataSource { override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt index 094a30b11..395a29af6 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt @@ -2,5 +2,5 @@ package com.appunite.loudius.network.model data class AccessToken( - val accessToken: String + val accessToken: String, ) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index dddbfe466..ee6d6df01 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -5,7 +5,7 @@ import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, - apiCall: suspend () -> T + apiCall: suspend () -> T, ): Result { return try { val response = apiCall() diff --git a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt index 734947705..00f52c49c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt @@ -35,20 +35,20 @@ private fun DetailsScreenStateless(topBarTitle: String, reviewers: List DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) }, - modifier = Modifier.background(MaterialTheme.colorScheme.surface) + modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) } @Composable private fun DetailsScreenContent(reviewers: List, modifier: Modifier) { LazyColumn( - modifier = modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth(), ) { itemsIndexed(reviewers) { index, reviewer -> ReviewerView( reviewer = reviewer, backgroundColor = resolveReviewerBackgroundColor(index), - onNotifyClick = {} + onNotifyClick = {}, ) } } @@ -65,14 +65,14 @@ private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyCli .fillMaxWidth() .background(backgroundColor) .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) - .padding(16.dp) + .padding(16.dp), ) { ReviewerAvatarView(Modifier.align(CenterVertically)) Column( modifier = Modifier .weight(1f) .padding(start = 16.dp) - .align(CenterVertically) + .align(CenterVertically), ) { IsReviewedHeadlineText(reviewer) ReviewerName(reviewer) @@ -86,9 +86,9 @@ private fun ReviewerAvatarView(modifier: Modifier = Modifier) { Image( painter = painterResource(id = R.drawable.person_outline_24px), contentDescription = stringResource( - R.string.details_screen_user_image_description + R.string.details_screen_user_image_description, ), - modifier = modifier + modifier = modifier, ) } @@ -97,7 +97,7 @@ private fun IsReviewedHeadlineText(reviewer: Reviewer) { Text( text = resolveIsReviewedText(reviewer), style = MaterialTheme.typography.labelMedium, - color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error + color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error, ) } @@ -113,7 +113,7 @@ private fun ReviewerName(reviewer: Reviewer) { Text( text = reviewer.name, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } @@ -122,7 +122,7 @@ private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifie OutlinedButton(onClick = onNotifyClick, modifier = modifier) { Text( text = stringResource(R.string.details_notify), - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, ) } } @@ -133,7 +133,7 @@ private fun ReviewerViewPreview() { LoudiusTheme { ReviewerView( reviewer = Reviewer("Kezc", true, 12, 12), - backgroundColor = MaterialTheme.colorScheme.surface + backgroundColor = MaterialTheme.colorScheme.surface, ) {} } } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 4e6725c1e..4eeccec92 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -18,27 +18,27 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusTopAppBar( title: String, - onClickBackArrow: () -> Unit + onClickBackArrow: () -> Unit, ) { TopAppBar( title = { Text( text = title, color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) }, navigationIcon = { IconButton(onClick = onClickBackArrow) { Icon( painter = painterResource(id = R.drawable.arrow_back), - contentDescription = stringResource(R.string.back_button) + contentDescription = stringResource(R.string.back_button), ) } }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( - containerColor = MaterialTheme.colorScheme.surface - ) + containerColor = MaterialTheme.colorScheme.surface, + ), ) } @@ -48,7 +48,7 @@ fun LoudiusTopAppBar() { LoudiusTheme { LoudiusTopAppBar( onClickBackArrow = {}, - title = "Loudius" + title = "Loudius", ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 0b2750d1c..09506a5f6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -34,24 +34,24 @@ fun LoginScreen() { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { Image(painter = painterResource(id = R.drawable.loudius_logo), contentDescription = "Loudius logo") OutlinedButton( onClick = { startAuthorizing(context) }, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 46.dp) + .padding(horizontal = 46.dp), ) { Icon( painter = painterResource(id = R.drawable.ic_github), contentDescription = "Github icon", - tint = Color.Black + tint = Color.Black, ) Text( modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), text = stringResource(id = R.string.login), - color = Pink40 + color = Pink40, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt index 7929464e3..f02ae990d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt @@ -8,7 +8,7 @@ import androidx.hilt.navigation.compose.hiltViewModel @Composable fun ReposScreen( intent: Intent, - viewModel: ReposViewModel = hiltViewModel() + viewModel: ReposViewModel = hiltViewModel(), ) { val code = intent.data?.getQueryParameter("code") Text(text = code ?: "empty code") diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index c7c0bd4bd..00568f474 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -11,7 +11,7 @@ import javax.inject.Inject @HiltViewModel class ReposViewModel @Inject constructor( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) : ViewModel() { fun getAccessToken(code: String) { @@ -20,7 +20,7 @@ class ReposViewModel @Inject constructor( userRepository.getAccessToken( clientId = CLIENT_ID, clientSecret = "", - code = code + code = code, ).onSuccess { token -> Log.i("access_token", token.accessToken) }.onFailure { diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index e17981485..b2eac4cbe 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -18,7 +18,7 @@ import androidx.core.view.ViewCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80 + tertiary = Pink80, ) private val LightColorScheme = lightColorScheme( @@ -29,7 +29,7 @@ private val LightColorScheme = lightColorScheme( onSurface = Black90, onSurfaceVariant = PurpleBlack30, outlineVariant = NeutralVariant30, - error = Error40 + error = Error40, /* Other default colors to override background = Color(0xFFFFFBFE), @@ -46,7 +46,7 @@ fun LoudiusTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { @@ -67,6 +67,6 @@ fun LoudiusTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt index be36ad9be..f08a44f91 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt @@ -13,15 +13,15 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp + letterSpacing = 0.5.sp, ), titleLarge = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 22.sp, lineHeight = 28.sp, - letterSpacing = 0.sp - ) + letterSpacing = 0.sp, + ), /* Other default text styles to override labelSmall = TextStyle( fontFamily = FontFamily.Default, diff --git a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt index c7c8dbe1d..b0f5ebd5b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt +++ b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt @@ -21,8 +21,8 @@ fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed( color = color, start = Offset(x = 0f, y = height), end = Offset(x = width, y = height), - strokeWidth = strokeWidthPx + strokeWidth = strokeWidthPx, ) } - } + }, ) From 1c59169cc23cc4d846f345bda6a38344066e8848 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 28 Feb 2023 09:30:57 +0100 Subject: [PATCH 035/526] SIL-55: code cleanup --- .../main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 6 ++++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 0b2750d1c..c903a7b25 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -36,7 +36,9 @@ fun LoginScreen() { verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally ) { - Image(painter = painterResource(id = R.drawable.loudius_logo), contentDescription = "Loudius logo") + Image(painter = painterResource(id = R.drawable.loudius_logo), contentDescription = stringResource( + R.string.login_screen) + ) OutlinedButton( onClick = { startAuthorizing(context) }, modifier = Modifier @@ -45,7 +47,7 @@ fun LoginScreen() { ) { Icon( painter = painterResource(id = R.drawable.ic_github), - contentDescription = "Github icon", + contentDescription = stringResource(R.string.github_icon), tint = Color.Black ) Text( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f51f37570..5f7b79b00 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,4 +6,6 @@ Notify Reviewed %d h ago. Not reviewed for %d h. + Github icon + Loudius logo From 47487df4b782f7316fa3a0afe68f06c745822026 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 28 Feb 2023 08:35:23 +0000 Subject: [PATCH 036/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/ui/login/LoginScreen.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 765c6e29a..19182a83f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -36,9 +36,12 @@ fun LoginScreen() { verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, ) { - Image(painter = painterResource(id = R.drawable.loudius_logo), contentDescription = stringResource( - R.string.login_screen) - ) + Image( + painter = painterResource(id = R.drawable.loudius_logo), + contentDescription = stringResource( + R.string.login_screen, + ), + ) OutlinedButton( onClick = { startAuthorizing(context) }, modifier = Modifier From e52e05f5274019d8471e516d69ff464d468e249d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Feb 2023 10:11:29 +0100 Subject: [PATCH 037/526] Remove use of jetpack security library - EncryptedSharedPreferences. --- .../loudius/domain/AccessTokenLocalDataSource.kt | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt index be6e53215..7e9477092 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt @@ -2,8 +2,6 @@ package com.appunite.loudius.domain import android.content.Context import android.content.SharedPreferences -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKeys import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -17,16 +15,8 @@ class AccessTokenLocalDataSource @Inject constructor(@ApplicationContext context } - private val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC - private val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) - - private val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create( - FILE_NAME, - mainKeyAlias, - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) + private val sharedPreferences: SharedPreferences = + context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) fun saveAccessToken(accessToken: String) { sharedPreferences.edit().putString(KEY_ACCESS_TOKEN, accessToken).apply() From 35f633901c71c4a1548eff6b2da7aab435c229bb Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Feb 2023 10:12:45 +0100 Subject: [PATCH 038/526] Add missing provide methods for AccessTokenLocalDataSource. --- .../java/com/appunite/loudius/di/GithubModule.kt | 13 +++++++++++-- .../appunite/loudius/domain/UserRepositoryImpl.kt | 1 - 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index e4b049bba..02f13127a 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -1,5 +1,7 @@ package com.appunite.loudius.di +import android.content.Context +import com.appunite.loudius.domain.AccessTokenLocalDataSource import com.appunite.loudius.domain.UserRepository import com.appunite.loudius.domain.UserRepositoryImpl import com.appunite.loudius.network.GithubApi @@ -8,9 +10,10 @@ import com.appunite.loudius.network.GithubNetworkDataSource import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit import javax.inject.Singleton +import retrofit2.Retrofit @InstallIn(SingletonComponent::class) @Module @@ -24,11 +27,17 @@ object GithubModule { @Provides fun provideUserRepository( githubDataSource: GithubDataSource, - ): UserRepository = UserRepositoryImpl(githubDataSource) + accessTokenLocalDataSource: AccessTokenLocalDataSource, + ): UserRepository = UserRepositoryImpl(githubDataSource, accessTokenLocalDataSource) @Singleton @Provides fun provideGithubDataSource( api: GithubApi, ): GithubDataSource = GithubNetworkDataSource(api) + + @Singleton + @Provides + fun provideAccessTokenLocalDataSource(@ApplicationContext context: Context): AccessTokenLocalDataSource = + AccessTokenLocalDataSource(context) } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 46be770f8..77b902792 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -21,7 +21,6 @@ class UserRepositoryImpl @Inject constructor( Result.success(AccessToken(tokenFromLocal)) } else { githubDataSource.getAccessToken(clientId, clientSecret, code) - } } From e0c6d591c2938ef8a8e7bf74fc81815aed8305b5 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Feb 2023 10:13:44 +0100 Subject: [PATCH 039/526] Add tests - AccessTokenLocalDataSourceTest.kt. --- app/build.gradle | 13 +++++--- .../domain/AccessTokenLocalDataSourceTest.kt | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 app/src/test/java/com/appunite/loudius/domain/AccessTokenLocalDataSourceTest.kt diff --git a/app/build.gradle b/app/build.gradle index 14302dfe6..745db6484 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,14 +89,19 @@ dependencies { //gson implementation 'com.google.code.gson:gson:2.10.1' - //security - implementation "androidx.security:security-crypto:1.0.0" - - testImplementation 'junit:junit:4.13.2' + //testing + testImplementation "io.mockk:mockk:1.13.3" + testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0") + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } +tasks.withType(Test) { + useJUnitPlatform() +} + kapt { correctErrorTypes true } diff --git a/app/src/test/java/com/appunite/loudius/domain/AccessTokenLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/AccessTokenLocalDataSourceTest.kt new file mode 100644 index 000000000..a67150d2f --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/domain/AccessTokenLocalDataSourceTest.kt @@ -0,0 +1,33 @@ +package com.appunite.loudius.domain + +import android.content.Context +import android.content.SharedPreferences +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + + +class AccessTokenLocalDataSourceTest { + private val sharedPreferences = mockk(relaxed = true) { + every { getString("access_token", null) } returns "exampleAccessToken" + } + private val context = mockk { + every { getSharedPreferences(any(), any()) } returns sharedPreferences + } + private val accessTokenLocalDataSource = AccessTokenLocalDataSource(context) + + @Test + fun `GIVEN filled data source WHEN getting access token THEN return access token`() { + val result = accessTokenLocalDataSource.getAccessToken() + assertEquals("exampleAccessToken", result) { "Access token should be correct" } + } + + @Test + fun `GIVEN not filled data source WHEN getting access token THEN return null`() { + every { sharedPreferences.getString("access_token", null) } returns null + + val result = accessTokenLocalDataSource.getAccessToken() + assertEquals(null, result) + } +} From 74e5186200bf2c5a042c9606b968b2472dd8a473 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 28 Feb 2023 10:39:39 +0100 Subject: [PATCH 040/526] SIL-55: code cleanup --- .../appunite/loudius/ui/login/LoginScreen.kt | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 19182a83f..394a3b5d0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,35 +31,47 @@ import com.appunite.loudius.ui.theme.Pink40 @Composable fun LoginScreen() { - val context = LocalContext.current Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, ) { - Image( - painter = painterResource(id = R.drawable.loudius_logo), - contentDescription = stringResource( - R.string.login_screen, - ), - ) - OutlinedButton( - onClick = { startAuthorizing(context) }, + LoginImage() + LoginButton( modifier = Modifier .fillMaxWidth() .padding(horizontal = 46.dp), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_github), - contentDescription = stringResource(R.string.github_icon), - tint = Color.Black, - ) - Text( - modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), - text = stringResource(id = R.string.login), - color = Pink40, - ) - } + ) + } +} + +@Composable +fun LoginImage() { + Image( + painter = painterResource(id = R.drawable.loudius_logo), + contentDescription = stringResource( + R.string.login_screen, + ), + ) +} + +@Composable +fun LoginButton(modifier: Modifier) { + val context = LocalContext.current + OutlinedButton( + onClick = { startAuthorizing(context) }, + modifier = modifier + ) { + Icon( + painter = painterResource(id = R.drawable.ic_github), + contentDescription = stringResource(R.string.github_icon), + tint = Color.Black, + ) + Text( + modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), + text = stringResource(id = R.string.login), + color = Pink40, + ) } } @@ -73,5 +86,7 @@ private fun buildAuthorizationUrl() = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_I @Preview(showSystemUi = true, showBackground = true) @Composable fun LoginScreenPreview() { - LoginScreen() + MaterialTheme { + LoginScreen() + } } From 22a51e1105da51e96d50604b1adeefbc6aae2191 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 28 Feb 2023 09:42:50 +0000 Subject: [PATCH 041/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 394a3b5d0..b025db72c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -60,7 +60,7 @@ fun LoginButton(modifier: Modifier) { val context = LocalContext.current OutlinedButton( onClick = { startAuthorizing(context) }, - modifier = modifier + modifier = modifier, ) { Icon( painter = painterResource(id = R.drawable.ic_github), From 7e70cafdef5cc3dfa459431a9cb06c66d4ade558 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Feb 2023 10:57:36 +0100 Subject: [PATCH 042/526] Use lazy column --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 67fe5decd..848744a69 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -2,13 +2,12 @@ package com.appunite.loudius.ui.pullrequests -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -38,13 +37,12 @@ private fun PullRequestsScreenStateless( // TODO: navigation } }, content = { padding -> - Column( + LazyColumn( modifier = Modifier .padding(padding) .fillMaxSize() - .verticalScroll(rememberScrollState()), ) { - pullRequests.forEach { + items(pullRequests) { PullRequestItem( repositoryName = it.fullRepositoryName, pullRequestTitle = it.title, From 98517706de2761190442be50959236144aa3ce28 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Feb 2023 10:59:09 +0100 Subject: [PATCH 043/526] Use cleaner query --- .../appunite/loudius/network/GitHubPullRequestsDataSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt index caba6551d..e90ead83f 100644 --- a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt @@ -10,7 +10,7 @@ class GitHubPullRequestsDataSource @Inject constructor(private val service: Gith suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { service.getPullRequestsForUser( auth_token, - "author%3A$author+type%3Apr+state%3Aopen", + "author:$author type:pr state:open", ) } } From 811d4d773119f19ba0edb6fb51e89898f1459278 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Feb 2023 13:42:47 +0100 Subject: [PATCH 044/526] Change package structure --- app/src/main/java/com/appunite/loudius/di/GithubModule.kt | 7 ++++--- .../{network => domain}/GitHubPullRequestsRepository.kt | 3 ++- .../{ => datasource}/GitHubPullRequestsDataSource.kt | 3 ++- .../loudius/network/{ => datasource}/GithubDataSource.kt | 1 + .../appunite/loudius/network/{ => services}/GithubApi.kt | 2 +- .../network/{ => services}/GithubPullRequestsService.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 2 +- 7 files changed, 12 insertions(+), 8 deletions(-) rename app/src/main/java/com/appunite/loudius/{network => domain}/GitHubPullRequestsRepository.kt (75%) rename app/src/main/java/com/appunite/loudius/network/{ => datasource}/GitHubPullRequestsDataSource.kt (82%) rename app/src/main/java/com/appunite/loudius/network/{ => datasource}/GithubDataSource.kt (92%) rename app/src/main/java/com/appunite/loudius/network/{ => services}/GithubApi.kt (91%) rename app/src/main/java/com/appunite/loudius/network/{ => services}/GithubPullRequestsService.kt (91%) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index a10b95f4c..b72bd1060 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -2,10 +2,10 @@ package com.appunite.loudius.di import com.appunite.loudius.domain.UserRepository import com.appunite.loudius.domain.UserRepositoryImpl -import com.appunite.loudius.network.GithubApi +import com.appunite.loudius.network.services.GithubApi import com.appunite.loudius.network.GithubDataSource import com.appunite.loudius.network.GithubNetworkDataSource -import com.appunite.loudius.network.GithubPullRequestsService +import com.appunite.loudius.network.services.GithubPullRequestsService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,7 +19,8 @@ object GithubModule { @Singleton @Provides - fun provideGithubApi(@NetworkModule.GitHubNonApi retrofit: Retrofit): GithubApi = retrofit.create(GithubApi::class.java) + fun provideGithubApi(@NetworkModule.GitHubNonApi retrofit: Retrofit): GithubApi = retrofit.create( + GithubApi::class.java) @Singleton @Provides diff --git a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt similarity index 75% rename from app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsRepository.kt rename to app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index 68e2bf954..330d61bc5 100644 --- a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -1,5 +1,6 @@ -package com.appunite.loudius.network +package com.appunite.loudius.domain +import com.appunite.loudius.network.datasource.GitHubPullRequestsDataSource import com.appunite.loudius.network.model.PullRequestsResponse import javax.inject.Inject diff --git a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt similarity index 82% rename from app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt rename to app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt index e90ead83f..59f16eadd 100644 --- a/app/src/main/java/com/appunite/loudius/network/GitHubPullRequestsDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt @@ -1,6 +1,7 @@ -package com.appunite.loudius.network +package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/GithubDataSource.kt similarity index 92% rename from app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt rename to app/src/main/java/com/appunite/loudius/network/datasource/GithubDataSource.kt index 9150a99b4..b91d2a46f 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/GithubDataSource.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.network import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.services.GithubApi import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt similarity index 91% rename from app/src/main/java/com/appunite/loudius/network/GithubApi.kt rename to app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt index 7ef8e4fa4..2c1efbf3d 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.network +package com.appunite.loudius.network.services import com.appunite.loudius.network.model.AccessToken import retrofit2.http.Field diff --git a/app/src/main/java/com/appunite/loudius/network/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt similarity index 91% rename from app/src/main/java/com/appunite/loudius/network/GithubPullRequestsService.kt rename to app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt index 027d8af2a..9831ea810 100644 --- a/app/src/main/java/com/appunite/loudius/network/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.network +package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse import retrofit2.http.GET diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index b1a681be0..4be850bcc 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.appunite.loudius.network.GitHubPullRequestsRepository +import com.appunite.loudius.domain.GitHubPullRequestsRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch From 7f487dfe663ca392911cca6b82b4e8d87a43ee89 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Feb 2023 13:50:49 +0100 Subject: [PATCH 045/526] Change starting screen --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 3afa46c37..f092faa9f 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -33,7 +33,7 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() NavHost( navController = navController, - startDestination = Screen.PullRequests.route, + startDestination = Screen.Login.route, ) { composable(route = Screen.Login.route) { LoginScreen() From 9b02b91788f7164c8a52e0eead140eb396ea41d8 Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 28 Feb 2023 12:54:02 +0000 Subject: [PATCH 046/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/di/GithubModule.kt | 5 +++-- .../appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index b72bd1060..38db10a67 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -2,9 +2,9 @@ package com.appunite.loudius.di import com.appunite.loudius.domain.UserRepository import com.appunite.loudius.domain.UserRepositoryImpl -import com.appunite.loudius.network.services.GithubApi import com.appunite.loudius.network.GithubDataSource import com.appunite.loudius.network.GithubNetworkDataSource +import com.appunite.loudius.network.services.GithubApi import com.appunite.loudius.network.services.GithubPullRequestsService import dagger.Module import dagger.Provides @@ -20,7 +20,8 @@ object GithubModule { @Singleton @Provides fun provideGithubApi(@NetworkModule.GitHubNonApi retrofit: Retrofit): GithubApi = retrofit.create( - GithubApi::class.java) + GithubApi::class.java, + ) @Singleton @Provides diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 848744a69..69d302df2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -40,7 +40,7 @@ private fun PullRequestsScreenStateless( LazyColumn( modifier = Modifier .padding(padding) - .fillMaxSize() + .fillMaxSize(), ) { items(pullRequests) { PullRequestItem( From 89f84b920aad1b368dc068dc1c2982bd1a6065ca Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Feb 2023 14:42:42 +0100 Subject: [PATCH 047/526] Add named parameters --- .../ui/pullrequests/PullRequestsScreen.kt | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 69d302df2..50a226ca8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -71,36 +71,36 @@ fun PullRequestsScreenPreview() { PullRequestsScreenStateless( listOf( PullRequest( - 0, - false, - 0, - "${Constants.BASE_API_URL}/appunite/Stefan", - "PR 1", - "2021-11-29T16:31:41Z", + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/appunite/Stefan", + title = "PR 1", + updatedAt = "2021-11-29T16:31:41Z", ), PullRequest( - 1, - true, - 1, - "${Constants.BASE_API_URL}/appunite/Silentus", - "PR 2", - "2022-11-29T16:31:41Z", + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/appunite/Silentus", + title = "PR 2", + updatedAt = "2022-11-29T16:31:41Z", ), PullRequest( - 2, - false, - 2, - "${Constants.BASE_API_URL}/appunite/Loudius", - "PR 3", - "2023-01-29T16:31:41Z", + id = 2, + draft = false, + number = 2, + repositoryUrl = "${Constants.BASE_API_URL}/appunite/Loudius", + title = "PR 3", + updatedAt = "2023-01-29T16:31:41Z", ), PullRequest( - 3, - false, - 3, - "${Constants.BASE_API_URL}/appunite/Blocktrade", - "PR 4", - "2022-01-29T16:31:41Z", + id = 3, + draft = false, + number = 3, + repositoryUrl = "${Constants.BASE_API_URL}/appunite/Blocktrade", + title = "PR 4", + updatedAt = "2022-01-29T16:31:41Z", ), ), ) From 2ee6e4642f97e58edac6185de0714bdf47e77994 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Feb 2023 10:21:32 +0100 Subject: [PATCH 048/526] Rename UserDataStore.kt and remove save access token function from UserRepository.kt. --- .../com/appunite/loudius/di/GithubModule.kt | 18 ++++++------ ...alDataSource.kt => UserLocalDataSource.kt} | 7 ++--- .../appunite/loudius/domain/UserRepository.kt | 4 +-- .../loudius/domain/UserRepositoryImpl.kt | 17 +++++------ .../loudius/network/GithubDataSource.kt | 20 ------------- .../loudius/network/UserDataSource.kt | 28 +++++++++++++++++++ .../presentation/repos/ReposViewModel.kt | 4 +-- ...urceTest.kt => UserLocalDataSourceTest.kt} | 8 +++--- 8 files changed, 53 insertions(+), 53 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{AccessTokenLocalDataSource.kt => UserLocalDataSource.kt} (77%) delete mode 100644 app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/UserDataSource.kt rename app/src/test/java/com/appunite/loudius/domain/{AccessTokenLocalDataSourceTest.kt => UserLocalDataSourceTest.kt} (79%) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 02f13127a..18c0eeda3 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -1,12 +1,12 @@ package com.appunite.loudius.di import android.content.Context -import com.appunite.loudius.domain.AccessTokenLocalDataSource +import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.domain.UserRepository import com.appunite.loudius.domain.UserRepositoryImpl import com.appunite.loudius.network.GithubApi -import com.appunite.loudius.network.GithubDataSource -import com.appunite.loudius.network.GithubNetworkDataSource +import com.appunite.loudius.network.UserDataSource +import com.appunite.loudius.network.UserNetworkDataSource import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -26,18 +26,18 @@ object GithubModule { @Singleton @Provides fun provideUserRepository( - githubDataSource: GithubDataSource, - accessTokenLocalDataSource: AccessTokenLocalDataSource, - ): UserRepository = UserRepositoryImpl(githubDataSource, accessTokenLocalDataSource) + userDataSource: UserDataSource, + userLocalDataSource: UserLocalDataSource, + ): UserRepository = UserRepositoryImpl(userDataSource, userLocalDataSource) @Singleton @Provides fun provideGithubDataSource( api: GithubApi, - ): GithubDataSource = GithubNetworkDataSource(api) + ): UserDataSource = UserNetworkDataSource(api) @Singleton @Provides - fun provideAccessTokenLocalDataSource(@ApplicationContext context: Context): AccessTokenLocalDataSource = - AccessTokenLocalDataSource(context) + fun provideAccessTokenLocalDataSource(@ApplicationContext context: Context): UserLocalDataSource = + UserLocalDataSource(context) } diff --git a/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt similarity index 77% rename from app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt rename to app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt index 7e9477092..edc14079e 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AccessTokenLocalDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt @@ -7,12 +7,11 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class AccessTokenLocalDataSource @Inject constructor(@ApplicationContext context: Context) { +class UserLocalDataSource @Inject constructor(@ApplicationContext context: Context) { companion object { private const val FILE_NAME = "com.appunite.loudius.sharedPreferences" private const val KEY_ACCESS_TOKEN = "access_token" - } private val sharedPreferences: SharedPreferences = @@ -22,7 +21,5 @@ class AccessTokenLocalDataSource @Inject constructor(@ApplicationContext context sharedPreferences.edit().putString(KEY_ACCESS_TOKEN, accessToken).apply() } - fun getAccessToken(): String? = - sharedPreferences.getString(KEY_ACCESS_TOKEN, null) - + fun getAccessToken(): String? = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt index b1a78eb38..cb27ce960 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt @@ -4,11 +4,9 @@ import com.appunite.loudius.network.model.AccessToken interface UserRepository { - suspend fun getAccessToken( + suspend fun getAndSaveAccessToken( clientId: String, clientSecret: String, code: String ): Result - - suspend fun saveAccessToken(accessToken: AccessToken) } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 77b902792..3c17f2b67 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -1,30 +1,27 @@ package com.appunite.loudius.domain -import com.appunite.loudius.network.GithubDataSource +import com.appunite.loudius.network.UserDataSource import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject class UserRepositoryImpl @Inject constructor( - private val githubDataSource: GithubDataSource, - private val accessTokenLocalDataSource: AccessTokenLocalDataSource, + private val userDataSource: UserDataSource, + private val userLocalDataSource: UserLocalDataSource, ) : UserRepository { - override suspend fun getAccessToken( + override suspend fun getAndSaveAccessToken( clientId: String, clientSecret: String, code: String ): Result { - val tokenFromLocal = accessTokenLocalDataSource.getAccessToken() + val tokenFromLocal = userLocalDataSource.getAccessToken() return if (tokenFromLocal != null) { // TODO: Propose removal of AccessToken data class Result.success(AccessToken(tokenFromLocal)) } else { - githubDataSource.getAccessToken(clientId, clientSecret, code) + val result = userDataSource.getAccessToken(clientId, clientSecret, code) + result.onSuccess { userLocalDataSource.saveAccessToken(it.accessToken) } } } - - override suspend fun saveAccessToken(accessToken: AccessToken) { - accessTokenLocalDataSource.saveAccessToken(accessToken.accessToken) - } } diff --git a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt b/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt deleted file mode 100644 index 9150a99b4..000000000 --- a/app/src/main/java/com/appunite/loudius/network/GithubDataSource.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.appunite.loudius.network - -import com.appunite.loudius.network.model.AccessToken -import com.appunite.loudius.network.utils.safeApiCall -import javax.inject.Inject -import javax.inject.Singleton - -interface GithubDataSource { - - suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result -} - -@Singleton -class GithubNetworkDataSource @Inject constructor( - private val api: GithubApi, -) : GithubDataSource { - - override suspend fun getAccessToken(clientId: String, clientSecret: String, code: String): Result = - safeApiCall { api.getAccessToken(clientId, clientSecret, code) } -} diff --git a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt new file mode 100644 index 000000000..d07dcdb4d --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt @@ -0,0 +1,28 @@ +package com.appunite.loudius.network + +import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.utils.safeApiCall +import javax.inject.Inject +import javax.inject.Singleton + +interface UserDataSource { + + suspend fun getAccessToken( + clientId: String, + clientSecret: String, + code: String + ): Result +} + +@Singleton +class UserNetworkDataSource @Inject constructor( + private val api: GithubApi, +) : UserDataSource { + + override suspend fun getAccessToken( + clientId: String, + clientSecret: String, + code: String + ): Result = + safeApiCall { api.getAccessToken(clientId, clientSecret, code) } +} diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt index 75d24242a..a9c154eeb 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt @@ -6,8 +6,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class ReposViewModel @Inject constructor( @@ -17,7 +17,7 @@ class ReposViewModel @Inject constructor( fun getAccessToken(code: String) { viewModelScope.launch { // TODO add client secret [SIL-66] - userRepository.getAccessToken( + userRepository.getAndSaveAccessToken( clientId = CLIENT_ID, clientSecret = "", code = code, diff --git a/app/src/test/java/com/appunite/loudius/domain/AccessTokenLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt similarity index 79% rename from app/src/test/java/com/appunite/loudius/domain/AccessTokenLocalDataSourceTest.kt rename to app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index a67150d2f..eb9cfadfa 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AccessTokenLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -8,18 +8,18 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test -class AccessTokenLocalDataSourceTest { +class UserLocalDataSourceTest { private val sharedPreferences = mockk(relaxed = true) { every { getString("access_token", null) } returns "exampleAccessToken" } private val context = mockk { every { getSharedPreferences(any(), any()) } returns sharedPreferences } - private val accessTokenLocalDataSource = AccessTokenLocalDataSource(context) + private val userLocalDataSource = UserLocalDataSource(context) @Test fun `GIVEN filled data source WHEN getting access token THEN return access token`() { - val result = accessTokenLocalDataSource.getAccessToken() + val result = userLocalDataSource.getAccessToken() assertEquals("exampleAccessToken", result) { "Access token should be correct" } } @@ -27,7 +27,7 @@ class AccessTokenLocalDataSourceTest { fun `GIVEN not filled data source WHEN getting access token THEN return null`() { every { sharedPreferences.getString("access_token", null) } returns null - val result = accessTokenLocalDataSource.getAccessToken() + val result = userLocalDataSource.getAccessToken() assertEquals(null, result) } } From 11d097ab77640e0e32c6cc747860821943b58b38 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Feb 2023 14:53:34 +0100 Subject: [PATCH 049/526] Implement UserRepositoryImplTest.kt. --- .../loudius/domain/UserRepositoryImplTest.kt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt new file mode 100644 index 000000000..2d0f7adc7 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt @@ -0,0 +1,59 @@ +package com.appunite.loudius.domain + +import com.appunite.loudius.network.UserDataSource +import com.appunite.loudius.network.model.AccessToken +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class UserRepositoryImplTest { + private val networkDataSource: UserDataSource = mockk { + coEvery { + getAccessToken(any(), any(), any()) + } returns Result.success(AccessToken("validAccessToken")) + } + private val localDataSource: UserLocalDataSource = mockk { + every { getAccessToken() } returns null + every { saveAccessToken(any()) } returns Unit + } + private val repository = UserRepositoryImpl(networkDataSource, localDataSource) + + @Test + fun `GIVEN not saved token locally WHEN getting token THEN return new token from network`() = + runTest { + val result = repository.getAndSaveAccessToken("clientId", "clientSecret", "code") + + coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } + assertEquals( + Result.success(AccessToken("validAccessToken")), result + ) { "Expected success result with valid access token" } + } + + @Test + fun `GIVEN not saved token locally WHEN getting token THEN new token should be saved`() = + runTest { + repository.getAndSaveAccessToken("clientId", "clientSecret", "code") + + coVerify(exactly = 1) { localDataSource.saveAccessToken(any()) } + } + + @Test + fun `GIVEN saved token locally WHEN getting token THEN return saved token`() = runTest { + every { localDataSource.getAccessToken() } returns "validAccessToken" + + val result = repository.getAndSaveAccessToken("clientId", "clientSecret", "code") + + coVerify(exactly = 0) { networkDataSource.getAccessToken(any(), any(), any()) } + assertEquals( + Result.success(AccessToken("validAccessToken")), result + ) { "Expected success result with valid access token" } + } + + +} From 74cb425789e9c7b3c43d7e032d706585f74699d0 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Feb 2023 17:36:59 +0100 Subject: [PATCH 050/526] Provide new annotation for API instances --- .../com/appunite/loudius/common/Constants.kt | 2 +- .../java/com/appunite/loudius/di/APIScope.kt | 11 +++++++ .../com/appunite/loudius/di/GithubModule.kt | 9 +++--- .../com/appunite/loudius/di/NetworkModule.kt | 32 ++++++++++--------- .../appunite/loudius/ui/login/LoginScreen.kt | 4 +-- 5 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/di/APIScope.kt diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index 30aa5f782..df403cc07 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.common object Constants { - const val BASE_URL = "https://github.com" + const val BASE_AUTH_URL = "https://github.com" const val BASE_API_URL = "https://api.github.com" const val AUTH_PATH = "/login/oauth/authorize" const val NAME_PARAM_CLIENT_ID = "?client_id=" diff --git a/app/src/main/java/com/appunite/loudius/di/APIScope.kt b/app/src/main/java/com/appunite/loudius/di/APIScope.kt new file mode 100644 index 000000000..7ec2c60d4 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/APIScope.kt @@ -0,0 +1,11 @@ +package com.appunite.loudius.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class BaseAPI + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class AuthAPI diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 38db10a67..91ec08374 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -10,8 +10,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit import javax.inject.Singleton +import retrofit2.Retrofit @InstallIn(SingletonComponent::class) @Module @@ -19,9 +19,8 @@ object GithubModule { @Singleton @Provides - fun provideGithubApi(@NetworkModule.GitHubNonApi retrofit: Retrofit): GithubApi = retrofit.create( - GithubApi::class.java, - ) + fun provideGithubApi(@AuthAPI retrofit: Retrofit): GithubApi = + retrofit.create(GithubApi::class.java) @Singleton @Provides @@ -36,6 +35,6 @@ object GithubModule { ): GithubDataSource = GithubNetworkDataSource(api) @Provides - fun provideGithubReposService(retrofit: Retrofit): GithubPullRequestsService = + fun provideGithubReposService(@BaseAPI retrofit: Retrofit): GithubPullRequestsService = retrofit.create(GithubPullRequestsService::class.java) } diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index c0d9d4c34..2ef13ed47 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -8,19 +8,26 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Qualifier -import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module object NetworkModule { + @Provides + @AuthAPI + fun provideBaseAuthUrl() = Constants.BASE_AUTH_URL + + @Provides + @BaseAPI + fun provideBaseAPIUrl() = Constants.BASE_API_URL + @Provides @Singleton - @GitHubNonApi - fun provideRetrofit(gson: Gson, baseUrl: String): Retrofit = + @AuthAPI + fun provideAuthRetrofit(gson: Gson, @AuthAPI baseUrl: String): Retrofit = Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create(gson)) @@ -28,21 +35,16 @@ object NetworkModule { @Provides @Singleton - fun provideGson(): Gson = - GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() - - @Provides - @Singleton - fun provideApiRetrofit(gson: Gson): Retrofit = + @BaseAPI + fun provideBaseRetrofit(gson: Gson, @BaseAPI baseAPIUrl: String): Retrofit = Retrofit.Builder() - .baseUrl(Constants.BASE_API_URL) + .baseUrl(baseAPIUrl) .addConverterFactory(GsonConverterFactory.create(gson)) .build() @Provides - fun provideBaseUrl() = Constants.BASE_URL + @Singleton + fun provideGson(): Gson = + GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() - @Qualifier - @Retention(AnnotationRetention.RUNTIME) - annotation class GitHubNonApi } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index b025db72c..18357b3bc 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTH_PATH -import com.appunite.loudius.common.Constants.BASE_URL +import com.appunite.loudius.common.Constants.BASE_AUTH_URL import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID import com.appunite.loudius.ui.theme.Pink40 @@ -81,7 +81,7 @@ private fun startAuthorizing(context: Context) { context.startActivity(intent) } -private fun buildAuthorizationUrl() = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID +private fun buildAuthorizationUrl() = BASE_AUTH_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID @Preview(showSystemUi = true, showBackground = true) @Composable From 066c57c43aface62a33d1146b6b88a40ce2b0ee2 Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 28 Feb 2023 16:57:57 +0000 Subject: [PATCH 051/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/di/GithubModule.kt | 2 +- app/src/main/java/com/appunite/loudius/di/NetworkModule.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 91ec08374..a8b733325 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -10,8 +10,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import retrofit2.Retrofit +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 2ef13ed47..34893cbaa 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -8,9 +8,9 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -46,5 +46,4 @@ object NetworkModule { @Singleton fun provideGson(): Gson = GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() - } From 90521c9fefddbf83b768cd3fb3252e1ee8894e12 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Feb 2023 18:14:15 +0100 Subject: [PATCH 052/526] Requests repos when logging in --- app/src/main/java/com/appunite/loudius/common/Constants.kt | 1 + .../java/com/appunite/loudius/network/services/GithubApi.kt | 2 +- app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index 30aa5f782..12527d765 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -6,6 +6,7 @@ object Constants { const val BASE_API_URL = "https://api.github.com" const val AUTH_PATH = "/login/oauth/authorize" const val NAME_PARAM_CLIENT_ID = "?client_id=" + const val SCOPE_PARAM = "&scope=repo" const val CLIENT_ID = "91131449e417c7e29912" const val REDIRECT_URL = "loudius://callback" } diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt index 2c1efbf3d..ec0c0941e 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt @@ -9,7 +9,7 @@ import retrofit2.http.POST interface GithubApi { @Headers("Accept: application/json") - @POST("login/oauth/access_token") + @POST("login/oauth/access_token?scope=repo") @FormUrlEncoded suspend fun getAccessToken( @Field("client_id") clientId: String, diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index b025db72c..9b286b81c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -27,6 +27,7 @@ import com.appunite.loudius.common.Constants.AUTH_PATH import com.appunite.loudius.common.Constants.BASE_URL import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID +import com.appunite.loudius.common.Constants.SCOPE_PARAM import com.appunite.loudius.ui.theme.Pink40 @Composable @@ -81,7 +82,7 @@ private fun startAuthorizing(context: Context) { context.startActivity(intent) } -private fun buildAuthorizationUrl() = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID +private fun buildAuthorizationUrl() = BASE_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + SCOPE_PARAM @Preview(showSystemUi = true, showBackground = true) @Composable From 4cf4888ef3f4fb875fe99469c3781e58586fd924 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 1 Mar 2023 09:06:25 +0100 Subject: [PATCH 053/526] SIL-66: add client secret to build config --- app/build.gradle | 1 + .../com/appunite/loudius/presentation/repos/ReposViewModel.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d6def0969..15842db7d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,7 @@ android { vectorDrawables { useSupportLibrary true } + buildConfigField "String", "CLIENT_SECRET", "\"${System.env.CLIENT_SECRET}\"" } buildTypes { diff --git a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt index 75d24242a..fe4e23e84 100644 --- a/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/presentation/repos/ReposViewModel.kt @@ -3,6 +3,7 @@ package com.appunite.loudius.presentation.repos import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -19,7 +20,7 @@ class ReposViewModel @Inject constructor( // TODO add client secret [SIL-66] userRepository.getAccessToken( clientId = CLIENT_ID, - clientSecret = "", + clientSecret = BuildConfig.CLIENT_SECRET, code = code, ).onSuccess { token -> Log.i("access_token", token.accessToken) From 645d1d22030b8e98deeab34b297507af68700cae Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 1 Mar 2023 11:37:44 +0100 Subject: [PATCH 054/526] Correct after develop merge with conflicts. --- .../com/appunite/loudius/di/GithubModule.kt | 23 ++++++++++--------- .../loudius/network/UserDataSource.kt | 1 + 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 433d86e9a..f63074818 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -4,10 +4,10 @@ import android.content.Context import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.domain.UserRepository import com.appunite.loudius.domain.UserRepositoryImpl -import com.appunite.loudius.network.services.GithubApi -import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.UserDataSource import com.appunite.loudius.network.UserNetworkDataSource +import com.appunite.loudius.network.services.GithubApi +import com.appunite.loudius.network.services.GithubPullRequestsService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -22,9 +22,14 @@ object GithubModule { @Singleton @Provides - fun provideGithubApi(@NetworkModule.GitHubNonApi retrofit: Retrofit): GithubApi = retrofit.create( - GithubApi::class.java, - ) + fun provideGithubApi(@NetworkModule.GitHubNonApi retrofit: Retrofit): GithubApi = + retrofit.create( + GithubApi::class.java, + ) + + @Provides + fun provideGithubReposService(retrofit: Retrofit): GithubPullRequestsService = + retrofit.create(GithubPullRequestsService::class.java) @Singleton @Provides @@ -35,16 +40,12 @@ object GithubModule { @Singleton @Provides - fun provideGithubDataSource( + fun provideUserDataSource( api: GithubApi, ): UserDataSource = UserNetworkDataSource(api) - @Provides - fun provideGithubReposService(retrofit: Retrofit): GithubPullRequestsService = - retrofit.create(GithubPullRequestsService::class.java) - @Singleton @Provides - fun provideAccessTokenLocalDataSource(@ApplicationContext context: Context): UserLocalDataSource = + fun provideUserLocalDataSource(@ApplicationContext context: Context): UserLocalDataSource = UserLocalDataSource(context) } diff --git a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt index d07dcdb4d..0456bc239 100644 --- a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.network import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.services.GithubApi import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject import javax.inject.Singleton From e7e8f5937efba94ae4f459cf9275b282296f17f6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 1 Mar 2023 11:38:14 +0100 Subject: [PATCH 055/526] Add additional test for saving access token in UserLocalDataSourceTest.kt. --- .../appunite/loudius/domain/UserLocalDataSourceTest.kt | 10 ++++++++++ .../appunite/loudius/domain/UserRepositoryImplTest.kt | 2 -- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index eb9cfadfa..866e3ddec 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import io.mockk.every import io.mockk.mockk +import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -30,4 +31,13 @@ class UserLocalDataSourceTest { val result = userLocalDataSource.getAccessToken() assertEquals(null, result) } + + @Test + fun `GIVEN access token WHEN saving access token THEN shared preferences are edited`() { + userLocalDataSource.saveAccessToken("exampleAccessToken") + + verify(exactly = 1) { + sharedPreferences.edit().putString("access_token", "exampleAccessToken") + } + } } diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt index 2d0f7adc7..d4177a3b5 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt @@ -54,6 +54,4 @@ class UserRepositoryImplTest { Result.success(AccessToken("validAccessToken")), result ) { "Expected success result with valid access token" } } - - } From aedb4d03ca106f9af3c7031c33cdfc6aec7c195c Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Wed, 1 Mar 2023 10:41:23 +0000 Subject: [PATCH 056/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/di/GithubModule.kt | 2 +- .../main/java/com/appunite/loudius/domain/UserRepository.kt | 2 +- .../java/com/appunite/loudius/domain/UserRepositoryImpl.kt | 2 +- .../java/com/appunite/loudius/network/UserDataSource.kt | 4 ++-- .../java/com/appunite/loudius/ui/repos/ReposViewModel.kt | 2 +- .../com/appunite/loudius/domain/UserLocalDataSourceTest.kt | 1 - .../com/appunite/loudius/domain/UserRepositoryImplTest.kt | 6 ++++-- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index f63074818..c8a8a51a9 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -13,8 +13,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import retrofit2.Retrofit +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt index cb27ce960..5fa7d8333 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt @@ -7,6 +7,6 @@ interface UserRepository { suspend fun getAndSaveAccessToken( clientId: String, clientSecret: String, - code: String + code: String, ): Result } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 3c17f2b67..78760263e 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -12,7 +12,7 @@ class UserRepositoryImpl @Inject constructor( override suspend fun getAndSaveAccessToken( clientId: String, clientSecret: String, - code: String + code: String, ): Result { val tokenFromLocal = userLocalDataSource.getAccessToken() diff --git a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt index 0456bc239..972539e3b 100644 --- a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt @@ -11,7 +11,7 @@ interface UserDataSource { suspend fun getAccessToken( clientId: String, clientSecret: String, - code: String + code: String, ): Result } @@ -23,7 +23,7 @@ class UserNetworkDataSource @Inject constructor( override suspend fun getAccessToken( clientId: String, clientSecret: String, - code: String + code: String, ): Result = safeApiCall { api.getAccessToken(clientId, clientSecret, code) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index 76db6b428..8343ebc89 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -6,8 +6,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class ReposViewModel @Inject constructor( diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index 866e3ddec..6370b2669 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -8,7 +8,6 @@ import io.mockk.verify import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test - class UserLocalDataSourceTest { private val sharedPreferences = mockk(relaxed = true) { every { getString("access_token", null) } returns "exampleAccessToken" diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt index d4177a3b5..cf8e7d792 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt @@ -31,7 +31,8 @@ class UserRepositoryImplTest { coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( - Result.success(AccessToken("validAccessToken")), result + Result.success(AccessToken("validAccessToken")), + result, ) { "Expected success result with valid access token" } } @@ -51,7 +52,8 @@ class UserRepositoryImplTest { coVerify(exactly = 0) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( - Result.success(AccessToken("validAccessToken")), result + Result.success(AccessToken("validAccessToken")), + result, ) { "Expected success result with valid access token" } } } From 05ce5474099240a6d3208bcc32e4df74deae142e Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 1 Mar 2023 11:42:26 +0100 Subject: [PATCH 057/526] Remove unnecessary scope in access token path --- .../java/com/appunite/loudius/network/services/GithubApi.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt index ec0c0941e..2c1efbf3d 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt @@ -9,7 +9,7 @@ import retrofit2.http.POST interface GithubApi { @Headers("Accept: application/json") - @POST("login/oauth/access_token?scope=repo") + @POST("login/oauth/access_token") @FormUrlEncoded suspend fun getAccessToken( @Field("client_id") clientId: String, From 1f921fc95560383a21d335c858fec6a6ff372bd9 Mon Sep 17 00:00:00 2001 From: nowakweronika <72873966+nowakweronika@users.noreply.github.com> Date: Wed, 1 Mar 2023 11:46:19 +0100 Subject: [PATCH 058/526] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 881a64e36..7eadb06b7 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ -# Loudius \ No newline at end of file +# Loudius + +### How to set environmental variable on mac? +1. Launch zsh (command `zsh`) +2. `$ echo 'export CLIENT_SECRET=you know what' >> ~/.zshenv` +3. `$ echo $CLIENT_SECRET` +4. Restart your computer. From d1e2e756a4f6c3fae90c52e33b8589fa4f6b5f57 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 1 Mar 2023 12:19:53 +0100 Subject: [PATCH 059/526] SIL-66: check if client secret key is empty and clean code --- app/build.gradle | 3 +++ .../main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 15842db7d..57c12cc60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,9 @@ android { vectorDrawables { useSupportLibrary true } + if (System.env.CLIENT_SECRET.isEmpty()) { + throw RuntimeException("You need to set CLIENT_SECRET in your environment variables") + } buildConfigField "String", "CLIENT_SECRET", "\"${System.env.CLIENT_SECRET}\"" } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index 8e8813e9e..5bb9f7d7d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -17,7 +17,6 @@ class ReposViewModel @Inject constructor( fun getAccessToken(code: String) { viewModelScope.launch { - // TODO add client secret [SIL-66] userRepository.getAccessToken( clientId = CLIENT_ID, clientSecret = BuildConfig.CLIENT_SECRET, From c5b9959b9676cd3142e06414f35412fba167825d Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 1 Mar 2023 12:29:42 +0100 Subject: [PATCH 060/526] SIL-66: check if client secret key is null --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 57c12cc60..01cd8ef78 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ android { vectorDrawables { useSupportLibrary true } - if (System.env.CLIENT_SECRET.isEmpty()) { + if (System.env.CLIENT_SECRET.isEmpty() || System.env.CLIENT_SECRET == null) { throw RuntimeException("You need to set CLIENT_SECRET in your environment variables") } buildConfigField "String", "CLIENT_SECRET", "\"${System.env.CLIENT_SECRET}\"" From 78bd84579eb66b2eac78148ec68e02db2dfb1e91 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 1 Mar 2023 12:32:28 +0100 Subject: [PATCH 061/526] Add junit 5 test platform. --- app/build.gradle | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d6def0969..745db6484 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,11 +89,19 @@ dependencies { //gson implementation 'com.google.code.gson:gson:2.10.1' - testImplementation 'junit:junit:4.13.2' + //testing + testImplementation "io.mockk:mockk:1.13.3" + testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0") + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' } +tasks.withType(Test) { + useJUnitPlatform() +} + kapt { correctErrorTypes true } From 97853a7a531f0bfc7bcd4e55cb1d71cd41c0c76f Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 1 Mar 2023 12:33:09 +0100 Subject: [PATCH 062/526] SIL-66: check if client secret key is null --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 01cd8ef78..8a48a5ed4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ android { vectorDrawables { useSupportLibrary true } - if (System.env.CLIENT_SECRET.isEmpty() || System.env.CLIENT_SECRET == null) { + if (System.env.CLIENT_SECRET == null || System.env.CLIENT_SECRET.isEmpty()) { throw RuntimeException("You need to set CLIENT_SECRET in your environment variables") } buildConfigField "String", "CLIENT_SECRET", "\"${System.env.CLIENT_SECRET}\"" From f9429df442bee5a7c6807f92dd64e66e0da8e8fc Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 1 Mar 2023 12:35:34 +0100 Subject: [PATCH 063/526] Move request functions to the proper API interface. --- .../loudius/network/services/GithubApi.kt | 21 ------------------- .../services/GithubPullRequestsService.kt | 19 +++++++++++++++++ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt index ca413e95e..2c1efbf3d 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt @@ -1,15 +1,10 @@ package com.appunite.loudius.network.services import com.appunite.loudius.network.model.AccessToken -import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.Reviewer import retrofit2.http.Field import retrofit2.http.FormUrlEncoded -import retrofit2.http.GET -import retrofit2.http.Header import retrofit2.http.Headers import retrofit2.http.POST -import retrofit2.http.Path interface GithubApi { @@ -21,20 +16,4 @@ interface GithubApi { @Field("client_secret") clientSecret: String, @Field("code") code: String, ): AccessToken - - @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers") - suspend fun getReviewers( - @Path("owner") owner: String, - @Path("repo") repo: String, - @Path("pull_number") pullRequestNumber: String, - @Header("Authorization") token: String, - ): List - - @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/reviews") - suspend fun getReviews( - @Path("owner") owner: String, - @Path("repo") repo: String, - @Path("pull_number") pullRequestNumber: String, - @Header("Authorization") token: String, - ): List } diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt index 9831ea810..89195c1ee 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt @@ -1,8 +1,11 @@ package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.Reviewer import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Path import retrofit2.http.Query interface GithubPullRequestsService { @@ -13,4 +16,20 @@ interface GithubPullRequestsService { @Query("page") page: Int = 0, @Query("per_page") perPage: Int = 100, ): PullRequestsResponse + + @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers") + suspend fun getReviewers( + @Path("owner") owner: String, + @Path("repo") repo: String, + @Path("pull_number") pullRequestNumber: String, + @Header("Authorization") token: String, + ): List + + @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/reviews") + suspend fun getReviews( + @Path("owner") owner: String, + @Path("repo") repo: String, + @Path("pull_number") pullRequestNumber: String, + @Header("Authorization") token: String, + ): List } From 1b2bb3e292a8ffe68529d6c5bde2c9589d5605e0 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 1 Mar 2023 12:42:50 +0100 Subject: [PATCH 064/526] Merge data source functions to the existing GitHubPullRequestsDataSource.kt. --- .../network/PullRequestNetworkDataSource.kt | 47 ------------------- .../GitHubPullRequestsDataSource.kt | 40 +++++++++++++++- 2 files changed, 39 insertions(+), 48 deletions(-) delete mode 100644 app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt diff --git a/app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt deleted file mode 100644 index e1116b12a..000000000 --- a/app/src/main/java/com/appunite/loudius/network/PullRequestNetworkDataSource.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.appunite.loudius.network - -import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.Reviewer -import com.appunite.loudius.network.utils.safeApiCall -import javax.inject.Inject -import javax.inject.Singleton - -interface PullRequestDataSource { - suspend fun getReviewers( - owner: String, - repository: String, - pullRequestNumber: String, - accessToken: String - ): Result> - - suspend fun getReviews( - owner: String, - repository: String, - pullRequestNumber: String, - accessToken: String - ): Result> -} - -@Singleton -class PullRequestNetworkDataSource @Inject constructor( - private val api: GithubApi, -) : PullRequestDataSource { - - override suspend fun getReviewers( - owner: String, - repository: String, - pullRequestNumber: String, - accessToken: String - ): Result> = safeApiCall { - api.getReviewers(owner, repository, pullRequestNumber, accessToken) - } - - override suspend fun getReviews( - owner: String, - repository: String, - pullRequestNumber: String, - accessToken: String - ): Result> = safeApiCall { - api.getReviews(owner, repository, pullRequestNumber, accessToken) - } -} diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt index 59f16eadd..382166062 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt @@ -1,17 +1,55 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.Reviewer import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject +interface PullRequestDataSource { + suspend fun getReviewers( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> + + suspend fun getReviews( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> + +} + const val auth_token = "BEARER xxxxxxx" // temporary solution -class GitHubPullRequestsDataSource @Inject constructor(private val service: GithubPullRequestsService) { +class GitHubPullRequestsDataSource @Inject constructor(private val service: GithubPullRequestsService) : + PullRequestDataSource { suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { service.getPullRequestsForUser( auth_token, "author:$author type:pr state:open", ) } + + override suspend fun getReviewers( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> = safeApiCall { + service.getReviewers(owner, repository, pullRequestNumber, accessToken) + } + + override suspend fun getReviews( + owner: String, + repository: String, + pullRequestNumber: String, + accessToken: String + ): Result> = safeApiCall { + service.getReviews(owner, repository, pullRequestNumber, accessToken) + } } From 3b75ca31d15e01c77e446d171f1bd815655152d2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 1 Mar 2023 12:45:00 +0100 Subject: [PATCH 065/526] Rename GitHubPullRequestsDataSource.kt into PullRequestsNetworkDataSource.kt. --- .../loudius/domain/GitHubPullRequestsRepository.kt | 4 ++-- ...uestsDataSource.kt => PullRequestsNetworkDataSource.kt} | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) rename app/src/main/java/com/appunite/loudius/network/datasource/{GitHubPullRequestsDataSource.kt => PullRequestsNetworkDataSource.kt} (86%) diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index 330d61bc5..0c3ad7b3c 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -1,10 +1,10 @@ package com.appunite.loudius.domain -import com.appunite.loudius.network.datasource.GitHubPullRequestsDataSource +import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource import com.appunite.loudius.network.model.PullRequestsResponse import javax.inject.Inject -class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSource: GitHubPullRequestsDataSource) { +class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSource: PullRequestsNetworkDataSource) { suspend fun getPullRequestsForUser(author: String): Result = remoteDataSource.getPullRequestsForUser(author) } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt similarity index 86% rename from app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt rename to app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 382166062..1dbde0b43 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/GitHubPullRequestsDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -26,13 +26,10 @@ interface PullRequestDataSource { const val auth_token = "BEARER xxxxxxx" // temporary solution -class GitHubPullRequestsDataSource @Inject constructor(private val service: GithubPullRequestsService) : +class PullRequestsNetworkDataSource @Inject constructor(private val service: GithubPullRequestsService) : PullRequestDataSource { suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { - service.getPullRequestsForUser( - auth_token, - "author:$author type:pr state:open", - ) + service.getPullRequestsForUser(auth_token, "author:$author type:pr state:open") } override suspend fun getReviewers( From 22e7d4534f49cbc3e1149d0d2643b55f64afb90f Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 1 Mar 2023 13:14:12 +0100 Subject: [PATCH 066/526] Change BASE_AUTH_URL to AUTH_API_URL --- app/src/main/java/com/appunite/loudius/common/Constants.kt | 2 +- app/src/main/java/com/appunite/loudius/di/NetworkModule.kt | 2 +- .../main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index df403cc07..9c66c8f82 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.common object Constants { - const val BASE_AUTH_URL = "https://github.com" + const val AUTH_API_URL = "https://github.com" const val BASE_API_URL = "https://api.github.com" const val AUTH_PATH = "/login/oauth/authorize" const val NAME_PARAM_CLIENT_ID = "?client_id=" diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 34893cbaa..f7846cf0e 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -18,7 +18,7 @@ object NetworkModule { @Provides @AuthAPI - fun provideBaseAuthUrl() = Constants.BASE_AUTH_URL + fun provideBaseAuthUrl() = Constants.AUTH_API_URL @Provides @BaseAPI diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 18357b3bc..bba14068a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTH_PATH -import com.appunite.loudius.common.Constants.BASE_AUTH_URL +import com.appunite.loudius.common.Constants.AUTH_API_URL import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID import com.appunite.loudius.ui.theme.Pink40 @@ -81,7 +81,7 @@ private fun startAuthorizing(context: Context) { context.startActivity(intent) } -private fun buildAuthorizationUrl() = BASE_AUTH_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID +private fun buildAuthorizationUrl() = AUTH_API_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID @Preview(showSystemUi = true, showBackground = true) @Composable From cff9b2a9ccb59a7e7cd38e923c36d55fb769a02b Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 1 Mar 2023 13:14:50 +0100 Subject: [PATCH 067/526] Rename APIScope file to APIQualifiers --- .../com/appunite/loudius/di/{APIScope.kt => APIQualifiers.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/appunite/loudius/di/{APIScope.kt => APIQualifiers.kt} (100%) diff --git a/app/src/main/java/com/appunite/loudius/di/APIScope.kt b/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt similarity index 100% rename from app/src/main/java/com/appunite/loudius/di/APIScope.kt rename to app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt From 904d9ffc0770f6cd8f63e76e38988791c91247cd Mon Sep 17 00:00:00 2001 From: kezc Date: Wed, 1 Mar 2023 12:31:46 +0000 Subject: [PATCH 068/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index c8a7100c7..4f6227832 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R -import com.appunite.loudius.common.Constants.AUTH_PATH import com.appunite.loudius.common.Constants.AUTH_API_URL +import com.appunite.loudius.common.Constants.AUTH_PATH import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID import com.appunite.loudius.common.Constants.SCOPE_PARAM From 79625d17789f327ddd4360535b564aa2e9b54565 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 1 Mar 2023 14:29:21 +0100 Subject: [PATCH 069/526] SIL-66: add warning instead of error --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 8a48a5ed4..d2fce7f21 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { useSupportLibrary true } if (System.env.CLIENT_SECRET == null || System.env.CLIENT_SECRET.isEmpty()) { - throw RuntimeException("You need to set CLIENT_SECRET in your environment variables") + logger.warn("You need to set CLIENT_SECRET in your environment variables") } buildConfigField "String", "CLIENT_SECRET", "\"${System.env.CLIENT_SECRET}\"" } From 70fbdb4f371aed3a02efa050b9ee34d1815463c2 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 1 Mar 2023 15:11:07 +0100 Subject: [PATCH 070/526] SIL-66: disable REPOSITORY_TRIVY --- .mega-linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.mega-linter.yml b/.mega-linter.yml index 071d16777..3e1d26c81 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -4,3 +4,4 @@ DISABLE_LINTERS: - SPELL_CSPELL - GROOVY_NPM_GROOVY_LINT + - REPOSITORY_TRIVY From a12ae9458909e98ed7b436e6cc661d9aa34a29b7 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 1 Mar 2023 14:45:05 +0100 Subject: [PATCH 071/526] Update designs according to Figma --- .../ui/pullrequests/PullRequestsScreen.kt | 66 +++++++++++++++---- app/src/main/res/drawable/ic_share.xml | 5 ++ 2 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/drawable/ic_share.xml diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 50a226ca8..6442d600f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -2,17 +2,25 @@ package com.appunite.loudius.ui.pullrequests -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -42,10 +50,13 @@ private fun PullRequestsScreenStateless( .padding(padding) .fillMaxSize(), ) { - items(pullRequests) { + itemsIndexed(pullRequests) { index, item -> + val isIndexEven = index % 2 == 0 PullRequestItem( - repositoryName = it.fullRepositoryName, - pullRequestTitle = it.title, + repositoryName = item.fullRepositoryName, + pullRequestTitle = item.title, + darkBackground = isIndexEven, + onClick = {} ) } } @@ -53,10 +64,37 @@ private fun PullRequestsScreenStateless( } @Composable -private fun PullRequestItem(repositoryName: String, pullRequestTitle: String) { - Text(text = repositoryName) - Text(text = pullRequestTitle) - Spacer(modifier = Modifier.height(8.dp)) +private fun PullRequestItem( + repositoryName: String, pullRequestTitle: String, darkBackground: Boolean, onClick: () -> Unit +) { + val backgroundColor = if (darkBackground) { + MaterialTheme.colorScheme.onSurface.copy(0.08f) + } else { + MaterialTheme.colorScheme.surface + } + Row( + modifier = Modifier + .fillMaxWidth() + .background(backgroundColor) + .clickable(onClick = onClick) + ) { + Image( + painter = painterResource(id = R.drawable.ic_share), + contentDescription = null, + modifier = Modifier + .padding(start = 19.dp, top = 10.dp) + .size(width = 18.dp, height = 20.dp) + ) + Column(Modifier.padding(start = 19.dp, top = 8.dp, bottom = 8.dp)) { + Text(text = pullRequestTitle, style = MaterialTheme.typography.bodyLarge) + Text( + text = repositoryName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Divider(color = MaterialTheme.colorScheme.outlineVariant) } @Preview("Pull requests - empty list") @@ -75,7 +113,7 @@ fun PullRequestsScreenPreview() { draft = false, number = 0, repositoryUrl = "${Constants.BASE_API_URL}/appunite/Stefan", - title = "PR 1", + title = "[SIL-67] Details screen - network layer", updatedAt = "2021-11-29T16:31:41Z", ), PullRequest( @@ -83,7 +121,7 @@ fun PullRequestsScreenPreview() { draft = true, number = 1, repositoryUrl = "${Constants.BASE_API_URL}/appunite/Silentus", - title = "PR 2", + title = "[SIL-66] Add client secret to build config", updatedAt = "2022-11-29T16:31:41Z", ), PullRequest( @@ -91,7 +129,7 @@ fun PullRequestsScreenPreview() { draft = false, number = 2, repositoryUrl = "${Constants.BASE_API_URL}/appunite/Loudius", - title = "PR 3", + title = "[SIL-73] Storing access token", updatedAt = "2023-01-29T16:31:41Z", ), PullRequest( @@ -99,7 +137,7 @@ fun PullRequestsScreenPreview() { draft = false, number = 3, repositoryUrl = "${Constants.BASE_API_URL}/appunite/Blocktrade", - title = "PR 4", + title = "[SIL-62/SIL-75] Provide new annotation for API instances", updatedAt = "2022-01-29T16:31:41Z", ), ), diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..43178a392 --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,5 @@ + + + From 05ffc41cf9cf93fef6cd308354b8ea320d07159a Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 1 Mar 2023 14:47:46 +0100 Subject: [PATCH 072/526] Remove back button from Pull Requests Screen --- .../loudius/ui/components/LoudiusTopAppBar.kt | 14 ++++++++------ .../loudius/ui/pullrequests/PullRequestsScreen.kt | 4 +--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 4eeccec92..e2ead0f34 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -18,7 +18,7 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusTopAppBar( title: String, - onClickBackArrow: () -> Unit, + onClickBackArrow: (() -> Unit)? = null, ) { TopAppBar( title = { @@ -29,11 +29,13 @@ fun LoudiusTopAppBar( ) }, navigationIcon = { - IconButton(onClick = onClickBackArrow) { - Icon( - painter = painterResource(id = R.drawable.arrow_back), - contentDescription = stringResource(R.string.back_button), - ) + if (onClickBackArrow != null) { + IconButton(onClick = onClickBackArrow) { + Icon( + painter = painterResource(id = R.drawable.arrow_back), + contentDescription = stringResource(R.string.back_button), + ) + } } }, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 6442d600f..b67bba851 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -41,9 +41,7 @@ private fun PullRequestsScreenStateless( pullRequests: List, ) { Scaffold(topBar = { - LoudiusTopAppBar(title = stringResource(R.string.app_name)) { - // TODO: navigation - } + LoudiusTopAppBar(title = stringResource(R.string.app_name)) }, content = { padding -> LazyColumn( modifier = Modifier From 2da148671dc4b3af20131be96735e4a10776c1fc Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 2 Mar 2023 10:32:47 +0100 Subject: [PATCH 073/526] Update colors --- app/src/main/java/com/appunite/loudius/ui/theme/Color.kt | 2 +- app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt index a62f4449b..418ce37d1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt @@ -12,7 +12,7 @@ val Pink40 = Color(0xFF7D5260) val White99 = Color(0xFFFFFBFE) val Black90 = Color(0xFF1C1B1F) -val PurpleBlack30 = Color(0xFF49454F) val NeutralVariant30 = Color(0xFF49454F) +val NeutralVariant80 = Color(0xFFCAC4D0) val Error40 = Color(0xFFB3261E) diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index b2eac4cbe..8457cf418 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -27,8 +27,8 @@ private val LightColorScheme = lightColorScheme( tertiary = Pink40, surface = White99, onSurface = Black90, - onSurfaceVariant = PurpleBlack30, - outlineVariant = NeutralVariant30, + onSurfaceVariant = NeutralVariant30, + outlineVariant = NeutralVariant80, error = Error40, /* Other default colors to override From e1338587a199e1125d9d3466863f30c9ce89337d Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 2 Mar 2023 10:33:05 +0100 Subject: [PATCH 074/526] Fix getting full repository name --- .../com/appunite/loudius/network/model/PullRequest.kt | 6 +++++- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt index c5617f05b..10ab6122a 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt @@ -11,5 +11,9 @@ data class PullRequest( val updatedAt: String, ) { val fullRepositoryName: String - get() = repositoryUrl.removePrefix(Constants.BASE_API_URL + "/") + get() = repositoryUrl.removePrefix(REPOSITORY_PATH) + + companion object { + private const val REPOSITORY_PATH = Constants.BASE_API_URL + "/repos/" + } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index b67bba851..c45dcc698 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -110,7 +110,7 @@ fun PullRequestsScreenPreview() { id = 0, draft = false, number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/appunite/Stefan", + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", title = "[SIL-67] Details screen - network layer", updatedAt = "2021-11-29T16:31:41Z", ), @@ -118,7 +118,7 @@ fun PullRequestsScreenPreview() { id = 1, draft = true, number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/appunite/Silentus", + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", title = "[SIL-66] Add client secret to build config", updatedAt = "2022-11-29T16:31:41Z", ), @@ -126,7 +126,7 @@ fun PullRequestsScreenPreview() { id = 2, draft = false, number = 2, - repositoryUrl = "${Constants.BASE_API_URL}/appunite/Loudius", + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", title = "[SIL-73] Storing access token", updatedAt = "2023-01-29T16:31:41Z", ), @@ -134,7 +134,7 @@ fun PullRequestsScreenPreview() { id = 3, draft = false, number = 3, - repositoryUrl = "${Constants.BASE_API_URL}/appunite/Blocktrade", + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", title = "[SIL-62/SIL-75] Provide new annotation for API instances", updatedAt = "2022-01-29T16:31:41Z", ), From 7c71904d1c17aa18fc8ef1e5bfcaad5a5b9f5aa5 Mon Sep 17 00:00:00 2001 From: kezc Date: Thu, 2 Mar 2023 09:42:40 +0000 Subject: [PATCH 075/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index c45dcc698..7ca2571ad 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -54,7 +54,7 @@ private fun PullRequestsScreenStateless( repositoryName = item.fullRepositoryName, pullRequestTitle = item.title, darkBackground = isIndexEven, - onClick = {} + onClick = {}, ) } } @@ -63,7 +63,10 @@ private fun PullRequestsScreenStateless( @Composable private fun PullRequestItem( - repositoryName: String, pullRequestTitle: String, darkBackground: Boolean, onClick: () -> Unit + repositoryName: String, + pullRequestTitle: String, + darkBackground: Boolean, + onClick: () -> Unit, ) { val backgroundColor = if (darkBackground) { MaterialTheme.colorScheme.onSurface.copy(0.08f) @@ -74,21 +77,21 @@ private fun PullRequestItem( modifier = Modifier .fillMaxWidth() .background(backgroundColor) - .clickable(onClick = onClick) + .clickable(onClick = onClick), ) { Image( painter = painterResource(id = R.drawable.ic_share), contentDescription = null, modifier = Modifier .padding(start = 19.dp, top = 10.dp) - .size(width = 18.dp, height = 20.dp) + .size(width = 18.dp, height = 20.dp), ) Column(Modifier.padding(start = 19.dp, top = 8.dp, bottom = 8.dp)) { Text(text = pullRequestTitle, style = MaterialTheme.typography.bodyLarge) Text( text = repositoryName, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } } From b511d131cc44c41f3c1a6fd03e83354e7bf924ae Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 2 Mar 2023 11:56:08 +0100 Subject: [PATCH 076/526] Divide functions getAndSaveAccessToken into 2 separate functions. --- .../loudius/domain/UserLocalDataSource.kt | 2 +- .../appunite/loudius/domain/UserRepository.kt | 4 +++- .../loudius/domain/UserRepositoryImpl.kt | 16 ++++++---------- .../appunite/loudius/ui/repos/ReposViewModel.kt | 4 ++-- .../loudius/domain/UserRepositoryImplTest.kt | 8 ++++---- 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt index edc14079e..6254e8de5 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt @@ -21,5 +21,5 @@ class UserLocalDataSource @Inject constructor(@ApplicationContext context: Conte sharedPreferences.edit().putString(KEY_ACCESS_TOKEN, accessToken).apply() } - fun getAccessToken(): String? = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) + fun getAccessToken(): String = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) ?: "" } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt index 5fa7d8333..ae6eab7d3 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt @@ -4,9 +4,11 @@ import com.appunite.loudius.network.model.AccessToken interface UserRepository { - suspend fun getAndSaveAccessToken( + suspend fun fetchAccessToken( clientId: String, clientSecret: String, code: String, ): Result + + fun getAccessToken(): String } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index 78760263e..ad3682c8f 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -9,19 +9,15 @@ class UserRepositoryImpl @Inject constructor( private val userLocalDataSource: UserLocalDataSource, ) : UserRepository { - override suspend fun getAndSaveAccessToken( + override suspend fun fetchAccessToken( clientId: String, clientSecret: String, code: String, ): Result { - val tokenFromLocal = userLocalDataSource.getAccessToken() - - return if (tokenFromLocal != null) { - // TODO: Propose removal of AccessToken data class - Result.success(AccessToken(tokenFromLocal)) - } else { - val result = userDataSource.getAccessToken(clientId, clientSecret, code) - result.onSuccess { userLocalDataSource.saveAccessToken(it.accessToken) } - } + val result = userDataSource.getAccessToken(clientId, clientSecret, code) + result.onSuccess { userLocalDataSource.saveAccessToken(it.accessToken) } + return result } + + override fun getAccessToken(): String = userLocalDataSource.getAccessToken() } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index 3d869d43f..d059df7f2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -7,8 +7,8 @@ import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch @HiltViewModel class ReposViewModel @Inject constructor( @@ -17,7 +17,7 @@ class ReposViewModel @Inject constructor( fun getAccessToken(code: String) { viewModelScope.launch { - userRepository.getAndSaveAccessToken( + userRepository.fetchAccessToken( clientId = CLIENT_ID, clientSecret = BuildConfig.CLIENT_SECRET, code = code, diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt index cf8e7d792..d8daa790d 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt @@ -19,7 +19,7 @@ class UserRepositoryImplTest { } returns Result.success(AccessToken("validAccessToken")) } private val localDataSource: UserLocalDataSource = mockk { - every { getAccessToken() } returns null + every { getAccessToken() } returns "" every { saveAccessToken(any()) } returns Unit } private val repository = UserRepositoryImpl(networkDataSource, localDataSource) @@ -27,7 +27,7 @@ class UserRepositoryImplTest { @Test fun `GIVEN not saved token locally WHEN getting token THEN return new token from network`() = runTest { - val result = repository.getAndSaveAccessToken("clientId", "clientSecret", "code") + val result = repository.fetchAccessToken("clientId", "clientSecret", "code") coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( @@ -39,7 +39,7 @@ class UserRepositoryImplTest { @Test fun `GIVEN not saved token locally WHEN getting token THEN new token should be saved`() = runTest { - repository.getAndSaveAccessToken("clientId", "clientSecret", "code") + repository.fetchAccessToken("clientId", "clientSecret", "code") coVerify(exactly = 1) { localDataSource.saveAccessToken(any()) } } @@ -48,7 +48,7 @@ class UserRepositoryImplTest { fun `GIVEN saved token locally WHEN getting token THEN return saved token`() = runTest { every { localDataSource.getAccessToken() } returns "validAccessToken" - val result = repository.getAndSaveAccessToken("clientId", "clientSecret", "code") + val result = repository.fetchAccessToken("clientId", "clientSecret", "code") coVerify(exactly = 0) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( From 255b73f77e43c4e8e4a59479351de88a2e74ad6e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 2 Mar 2023 12:40:26 +0100 Subject: [PATCH 077/526] Update UserRepositoryImplTest.kt. --- .../loudius/domain/UserRepositoryImplTest.kt | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt index d8daa790d..999b0a098 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt @@ -19,13 +19,13 @@ class UserRepositoryImplTest { } returns Result.success(AccessToken("validAccessToken")) } private val localDataSource: UserLocalDataSource = mockk { - every { getAccessToken() } returns "" + every { getAccessToken() } returns "validAccessToken" every { saveAccessToken(any()) } returns Unit } private val repository = UserRepositoryImpl(networkDataSource, localDataSource) @Test - fun `GIVEN not saved token locally WHEN getting token THEN return new token from network`() = + fun `GIVEN fetch access token function WHEN processing THEN return success with new valid token`() = runTest { val result = repository.fetchAccessToken("clientId", "clientSecret", "code") @@ -37,7 +37,7 @@ class UserRepositoryImplTest { } @Test - fun `GIVEN not saved token locally WHEN getting token THEN new token should be saved`() = + fun `GIVEN fetch access token WHEN processing THEN new token should be saved`() = runTest { repository.fetchAccessToken("clientId", "clientSecret", "code") @@ -45,15 +45,19 @@ class UserRepositoryImplTest { } @Test - fun `GIVEN saved token locally WHEN getting token THEN return saved token`() = runTest { - every { localDataSource.getAccessToken() } returns "validAccessToken" + fun `GIVEN token stored WHEN getting access token THEN return stored access token`() = runTest { + val result = repository.getAccessToken() - val result = repository.fetchAccessToken("clientId", "clientSecret", "code") - - coVerify(exactly = 0) { networkDataSource.getAccessToken(any(), any(), any()) } - assertEquals( - Result.success(AccessToken("validAccessToken")), - result, - ) { "Expected success result with valid access token" } + assertEquals("validAccessToken", result) { "Expected valid access token" } } + + @Test + fun `GIVEN not stored access token WHEN getting access token THEN return empty string`() = + runTest { + every { repository.getAccessToken() } returns "" + + val result = repository.getAccessToken() + + assertEquals("", result) { "Expected empty string" } + } } From 5f2b3ef84b11f76e1084a88057dc0cd7a380c46a Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 2 Mar 2023 12:41:36 +0100 Subject: [PATCH 078/526] Update UserLocalDataSourceTest.kt. --- .../com/appunite/loudius/domain/UserLocalDataSourceTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index 6370b2669..0d3db01b8 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -24,11 +24,11 @@ class UserLocalDataSourceTest { } @Test - fun `GIVEN not filled data source WHEN getting access token THEN return null`() { + fun `GIVEN not filled data source WHEN getting access token THEN return empty string`() { every { sharedPreferences.getString("access_token", null) } returns null val result = userLocalDataSource.getAccessToken() - assertEquals(null, result) + assertEquals("", result) } @Test From fd708f5090c1128f845be17b57ae5e869c373338 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 2 Mar 2023 12:44:12 +0100 Subject: [PATCH 079/526] Update UserRepositoryImplTest.kt with additional check for a value. --- .../java/com/appunite/loudius/domain/UserRepositoryImplTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt index 999b0a098..6bf59aa30 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt @@ -41,7 +41,7 @@ class UserRepositoryImplTest { runTest { repository.fetchAccessToken("clientId", "clientSecret", "code") - coVerify(exactly = 1) { localDataSource.saveAccessToken(any()) } + coVerify(exactly = 1) { localDataSource.saveAccessToken("validAccessToken") } } @Test From 6bd53cde490ce2d329dc94941ef5a649dd98d34d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 2 Mar 2023 13:01:34 +0100 Subject: [PATCH 080/526] Add AccessToken typealias and rename AccessToken.kt to AccessTokenResponse.kt. --- .../java/com/appunite/loudius/domain/UserRepositoryImpl.kt | 2 +- .../main/java/com/appunite/loudius/network/UserDataSource.kt | 2 +- .../network/model/{AccessToken.kt => AccessTokenResponse.kt} | 4 +++- .../java/com/appunite/loudius/network/services/GithubApi.kt | 4 ++-- .../java/com/appunite/loudius/ui/repos/ReposViewModel.kt | 2 +- .../com/appunite/loudius/domain/UserRepositoryImplTest.kt | 5 ++--- 6 files changed, 10 insertions(+), 9 deletions(-) rename app/src/main/java/com/appunite/loudius/network/model/{AccessToken.kt => AccessTokenResponse.kt} (54%) diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt index ad3682c8f..1d0a16cb5 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt @@ -15,7 +15,7 @@ class UserRepositoryImpl @Inject constructor( code: String, ): Result { val result = userDataSource.getAccessToken(clientId, clientSecret, code) - result.onSuccess { userLocalDataSource.saveAccessToken(it.accessToken) } + result.onSuccess { userLocalDataSource.saveAccessToken(it) } return result } diff --git a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt index 972539e3b..f1bbe804b 100644 --- a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt @@ -25,5 +25,5 @@ class UserNetworkDataSource @Inject constructor( clientSecret: String, code: String, ): Result = - safeApiCall { api.getAccessToken(clientId, clientSecret, code) } + safeApiCall { api.getAccessToken(clientId, clientSecret, code).accessToken } } diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt similarity index 54% rename from app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt rename to app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt index 395a29af6..ea21b8f43 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessToken.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt @@ -1,6 +1,8 @@ package com.appunite.loudius.network.model -data class AccessToken( +typealias AccessToken = String + +data class AccessTokenResponse( val accessToken: String, ) diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt index 2c1efbf3d..ccfefc0e7 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.network.services -import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.model.AccessTokenResponse import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.Headers @@ -15,5 +15,5 @@ interface GithubApi { @Field("client_id") clientId: String, @Field("client_secret") clientSecret: String, @Field("code") code: String, - ): AccessToken + ): AccessTokenResponse } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index d059df7f2..80fa17df2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -22,7 +22,7 @@ class ReposViewModel @Inject constructor( clientSecret = BuildConfig.CLIENT_SECRET, code = code, ).onSuccess { token -> - Log.i("access_token", token.accessToken) + Log.i("access_token", token) }.onFailure { Log.i("access_token", it.message.toString()) } diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt index 6bf59aa30..a8acfbee6 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt @@ -1,7 +1,6 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.UserDataSource -import com.appunite.loudius.network.model.AccessToken import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -16,7 +15,7 @@ class UserRepositoryImplTest { private val networkDataSource: UserDataSource = mockk { coEvery { getAccessToken(any(), any(), any()) - } returns Result.success(AccessToken("validAccessToken")) + } returns Result.success("validAccessToken") } private val localDataSource: UserLocalDataSource = mockk { every { getAccessToken() } returns "validAccessToken" @@ -31,7 +30,7 @@ class UserRepositoryImplTest { coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( - Result.success(AccessToken("validAccessToken")), + Result.success("validAccessToken"), result, ) { "Expected success result with valid access token" } } From 7dd5652caf6b32855266bf86b470355e47ff727c Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 2 Mar 2023 12:05:59 +0000 Subject: [PATCH 081/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index 80fa17df2..60087423c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -7,8 +7,8 @@ import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class ReposViewModel @Inject constructor( From 08e3c6e924aa89cdfe16beb3023de01efce43f2e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 2 Mar 2023 14:08:50 +0100 Subject: [PATCH 082/526] Merge GitHubPullRequestsRepository.kt with DefaultPullRequestRepository.kt. --- .../appunite/loudius/di/PullRequestModule.kt | 20 ++++++++++ .../domain/DefaultPullRequestRepository.kt | 40 ------------------- .../domain/GitHubPullRequestsRepository.kt | 37 ++++++++++++++++- .../PullRequestsNetworkDataSource.kt | 2 + 4 files changed, 57 insertions(+), 42 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt delete mode 100644 app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt diff --git a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt new file mode 100644 index 000000000..e8723131e --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt @@ -0,0 +1,20 @@ +package com.appunite.loudius.di + +import com.appunite.loudius.network.datasource.PullRequestDataSource +import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource +import com.appunite.loudius.network.services.GithubPullRequestsService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object PullRequestModule { + + @Provides + @Singleton + fun providePullRequestNetworkDataSource(service: GithubPullRequestsService): PullRequestDataSource = + PullRequestsNetworkDataSource(service) +} diff --git a/app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt b/app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt deleted file mode 100644 index b567a6920..000000000 --- a/app/src/main/java/com/appunite/loudius/domain/DefaultPullRequestRepository.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.appunite.loudius.domain - -import com.appunite.loudius.network.PullRequestDataSource -import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.Reviewer -import javax.inject.Inject -import javax.inject.Singleton - -interface PullRequestRepository { - suspend fun getReviews( - owner: String, - repo: String, - pullRequestNumber: String - ): Result> - - suspend fun getReviewers( - owner: String, - repo: String, - pullRequestNumber: String - ): Result> -} - -@Singleton -class DefaultPullRequestRepository @Inject constructor( - private val pullRequestDataSource: PullRequestDataSource -) : PullRequestRepository { - override suspend fun getReviews( - owner: String, - repo: String, - pullRequestNumber: String - ): Result> = - pullRequestDataSource.getReviews(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") - - override suspend fun getReviewers( - owner: String, - repo: String, - pullRequestNumber: String - ): Result> = - pullRequestDataSource.getReviewers(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") -} diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index 0c3ad7b3c..57d8e2e22 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -2,9 +2,42 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.Reviewer import javax.inject.Inject -class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSource: PullRequestsNetworkDataSource) { - suspend fun getPullRequestsForUser(author: String): Result = +interface PullRequestRepository { + suspend fun getReviews( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> + + suspend fun getReviewers( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> + + suspend fun getPullRequestsForUser(author: String): Result +} + +class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSource: PullRequestsNetworkDataSource) : + PullRequestRepository { + override suspend fun getPullRequestsForUser(author: String): Result = remoteDataSource.getPullRequestsForUser(author) + + override suspend fun getReviews( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> = + remoteDataSource.getReviews(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") + + override suspend fun getReviewers( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> = + remoteDataSource.getReviewers(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 1dbde0b43..20372782b 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -6,6 +6,7 @@ import com.appunite.loudius.network.model.Reviewer import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject +import javax.inject.Singleton interface PullRequestDataSource { suspend fun getReviewers( @@ -26,6 +27,7 @@ interface PullRequestDataSource { const val auth_token = "BEARER xxxxxxx" // temporary solution +@Singleton class PullRequestsNetworkDataSource @Inject constructor(private val service: GithubPullRequestsService) : PullRequestDataSource { suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { From f6609be55029d85b7659edabda4ea96a0b42bb48 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 2 Mar 2023 14:21:22 +0100 Subject: [PATCH 083/526] Use even value padding --- .../appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 7ca2571ad..87879e9bd 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -29,6 +29,7 @@ import com.appunite.loudius.R import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusTopAppBar +import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun PullRequestsScreen(viewModel: PullRequestsViewModel = hiltViewModel()) { @@ -83,10 +84,10 @@ private fun PullRequestItem( painter = painterResource(id = R.drawable.ic_share), contentDescription = null, modifier = Modifier - .padding(start = 19.dp, top = 10.dp) + .padding(start = 18.dp, top = 10.dp) .size(width = 18.dp, height = 20.dp), ) - Column(Modifier.padding(start = 19.dp, top = 8.dp, bottom = 8.dp)) { + Column(Modifier.padding(start = 18.dp, top = 8.dp, bottom = 8.dp)) { Text(text = pullRequestTitle, style = MaterialTheme.typography.bodyLarge) Text( text = repositoryName, From 10c722855a10851aa29471e3a762a61d4a93c566 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 2 Mar 2023 14:21:35 +0100 Subject: [PATCH 084/526] Use LoudiusTheme in preview --- .../ui/pullrequests/PullRequestsScreen.kt | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 87879e9bd..cd7ebb0c9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -102,46 +102,50 @@ private fun PullRequestItem( @Preview("Pull requests - empty list") @Composable fun PullRequestsScreenEmptyListPreview() { - PullRequestsScreenStateless(emptyList()) + LoudiusTheme { + PullRequestsScreenStateless(emptyList()) + } } @Preview("Pull requests - filled list") @Composable fun PullRequestsScreenPreview() { - PullRequestsScreenStateless( - listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - updatedAt = "2021-11-29T16:31:41Z", - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - updatedAt = "2022-11-29T16:31:41Z", - ), - PullRequest( - id = 2, - draft = false, - number = 2, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", - title = "[SIL-73] Storing access token", - updatedAt = "2023-01-29T16:31:41Z", + LoudiusTheme { + PullRequestsScreenStateless( + listOf( + PullRequest( + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", + title = "[SIL-67] Details screen - network layer", + updatedAt = "2021-11-29T16:31:41Z", + ), + PullRequest( + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", + title = "[SIL-66] Add client secret to build config", + updatedAt = "2022-11-29T16:31:41Z", + ), + PullRequest( + id = 2, + draft = false, + number = 2, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", + title = "[SIL-73] Storing access token", + updatedAt = "2023-01-29T16:31:41Z", + ), + PullRequest( + id = 3, + draft = false, + number = 3, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", + title = "[SIL-62/SIL-75] Provide new annotation for API instances", + updatedAt = "2022-01-29T16:31:41Z", + ), ), - PullRequest( - id = 3, - draft = false, - number = 3, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", - title = "[SIL-62/SIL-75] Provide new annotation for API instances", - updatedAt = "2022-01-29T16:31:41Z", - ), - ), - ) + ) + } } From f77885df4b9f425a7dcb8ee65f34d2b8cdf7c2e4 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 2 Mar 2023 14:22:02 +0100 Subject: [PATCH 085/526] Split PullRequestItem into smaller composables --- .../ui/pullrequests/PullRequestsScreen.kt | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index cd7ebb0c9..74e3867a8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -80,25 +80,35 @@ private fun PullRequestItem( .background(backgroundColor) .clickable(onClick = onClick), ) { - Image( - painter = painterResource(id = R.drawable.ic_share), - contentDescription = null, - modifier = Modifier - .padding(start = 18.dp, top = 10.dp) - .size(width = 18.dp, height = 20.dp), - ) - Column(Modifier.padding(start = 18.dp, top = 8.dp, bottom = 8.dp)) { - Text(text = pullRequestTitle, style = MaterialTheme.typography.bodyLarge) - Text( - text = repositoryName, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + PullRequestIcon() + RepoDetails(pullRequestTitle = pullRequestTitle, repositoryName = repositoryName) } Divider(color = MaterialTheme.colorScheme.outlineVariant) } +@Composable +private fun PullRequestIcon() { + Image( + painter = painterResource(id = R.drawable.ic_share), + contentDescription = null, + modifier = Modifier + .padding(start = 18.dp, top = 10.dp) + .size(width = 18.dp, height = 20.dp), + ) +} + +@Composable +private fun RepoDetails(pullRequestTitle: String, repositoryName: String) { + Column(Modifier.padding(start = 18.dp, top = 8.dp, bottom = 8.dp)) { + Text(text = pullRequestTitle, style = MaterialTheme.typography.bodyLarge) + Text( + text = repositoryName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} + @Preview("Pull requests - empty list") @Composable fun PullRequestsScreenEmptyListPreview() { From 7de5e67c12defff82d7321a14edda2fae7fd10ba Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 3 Mar 2023 12:26:02 +0100 Subject: [PATCH 086/526] Correct return type of the requestedReviewers request. --- .../loudius/domain/GitHubPullRequestsRepository.kt | 6 +++--- .../network/datasource/PullRequestsNetworkDataSource.kt | 6 +++--- .../loudius/network/model/RequestedReviewersResponse.kt | 5 +++++ .../loudius/network/services/GithubPullRequestsService.kt | 8 ++++---- 4 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index 57d8e2e22..58f8a3616 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -2,8 +2,8 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.Reviewer import javax.inject.Inject interface PullRequestRepository { @@ -17,7 +17,7 @@ interface PullRequestRepository { owner: String, repo: String, pullRequestNumber: String - ): Result> + ): Result suspend fun getPullRequestsForUser(author: String): Result } @@ -38,6 +38,6 @@ class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSou owner: String, repo: String, pullRequestNumber: String - ): Result> = + ): Result = remoteDataSource.getReviewers(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 20372782b..dbde9c86a 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -1,8 +1,8 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.Reviewer import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject @@ -14,7 +14,7 @@ interface PullRequestDataSource { repository: String, pullRequestNumber: String, accessToken: String - ): Result> + ): Result suspend fun getReviews( owner: String, @@ -39,7 +39,7 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Git repository: String, pullRequestNumber: String, accessToken: String - ): Result> = safeApiCall { + ): Result = safeApiCall { service.getReviewers(owner, repository, pullRequestNumber, accessToken) } diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt new file mode 100644 index 000000000..5936cd6cb --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt @@ -0,0 +1,5 @@ +package com.appunite.loudius.network.model + +data class RequestedReviewersResponse( + val users: List +) diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt index 89195c1ee..321fd299f 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt @@ -1,8 +1,8 @@ package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.Reviewer import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Path @@ -17,15 +17,15 @@ interface GithubPullRequestsService { @Query("per_page") perPage: Int = 100, ): PullRequestsResponse - @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers") + @GET("/repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers") suspend fun getReviewers( @Path("owner") owner: String, @Path("repo") repo: String, @Path("pull_number") pullRequestNumber: String, @Header("Authorization") token: String, - ): List + ): RequestedReviewersResponse - @GET("https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}/reviews") + @GET("/repos/{owner}/{repo}/pulls/{pull_number}/reviews") suspend fun getReviews( @Path("owner") owner: String, @Path("repo") repo: String, From bdf535b4f0cf5772249be7123dec496c9f05b148 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 3 Mar 2023 12:26:38 +0100 Subject: [PATCH 087/526] Add NetworkTestDoubles.kt for testing RetrofitClient. --- .../loudius/network/NetworkTestDoubles.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt new file mode 100644 index 000000000..cfc034026 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -0,0 +1,29 @@ +package com.appunite.loudius.network + +import com.google.gson.FieldNamingPolicy +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import java.util.concurrent.TimeUnit +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockWebServer +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +private fun testOkHttpClient() = OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(1, TimeUnit.SECONDS) + .writeTimeout(1, TimeUnit.SECONDS) + .build() + +private fun testGson() = + GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() + +fun retrofitTestDouble( + client: OkHttpClient = testOkHttpClient(), + gson: Gson = testGson(), + mockWebServer: MockWebServer +): Retrofit = Retrofit.Builder() + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .baseUrl(mockWebServer.url("/")) + .build() From 7e2370ade4b13f0cd90ba9b0c6b6152e60c1970c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 3 Mar 2023 12:27:03 +0100 Subject: [PATCH 088/526] Add first implementation of tests for PullRequestsNetworkDataSource. --- .../PullRequestsNetworkDataSourceTest.kt | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt new file mode 100644 index 000000000..d2a28d27c --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -0,0 +1,156 @@ +package com.appunite.loudius.network.datasource + +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.Reviewer +import com.appunite.loudius.network.retrofitTestDouble +import com.appunite.loudius.network.services.GithubPullRequestsService +import com.appunite.loudius.network.utils.WebException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@ExperimentalCoroutinesApi +class PullRequestsNetworkDataSourceTest { + + private val mockWebServer: MockWebServer = MockWebServer().apply { start(8080) } + private val userApi = + retrofitTestDouble(mockWebServer = mockWebServer).create(GithubPullRequestsService::class.java) + private val pullRequestDataSource = PullRequestsNetworkDataSource(userApi) + + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + } + + @Nested + inner class GetReviewersRequestsTest { + + @Test + fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = + runTest { + mockWebServer.enqueue( + MockResponse() + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + ) + + val actualResponse = pullRequestDataSource.getReviewers( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "validAccessToken" + ) + Assertions.assertInstanceOf( + WebException.NetworkError::class.java, + actualResponse.exceptionOrNull() + ) + } + + @Test + fun `Given correct params WHEN successful response THEN return success`() = + runTest { + //language=JSON + val jsonResponse = """ + { + "users": [ + { + "login": "exampleLogin", + "id": 1, + "node_id": "example_node_id", + "avatar_url": "https://example/avatar", + "gravatar_id": "", + "url": "https://api.github.com/users/exampleUser", + "html_url": "https://github.com/exampleUser", + "followers_url": "https://api.github.com/users/exampleUser/followers", + "following_url": "https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url": "https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/exampleUser/subscriptions", + "organizations_url": "https://api.github.com/users/exampleUser/orgs", + "repos_url": "https://api.github.com/users/exampleUser/repos", + "events_url": "https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url": "https://api.github.com/users/exampleUser/received_events", + "type": "User", + "site_admin": false + } + ], + "teams": [] + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .setBody(jsonResponse) + ) + + val actualResponse = pullRequestDataSource.getReviewers( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "validAccessToken" + ) + + val expected = Result.success( + RequestedReviewersResponse( + listOf( + Reviewer( + "1", + "exampleLogin", + "https://example/avatar" + ) + ) + ) + ) + + assertEquals(expected, actualResponse) { "Data should be correct" } + } + + + @Test + fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + runTest { + // language=JSON + val jsonResponse = """ + { + "message": "Bad credentials", + "documentation_url": "https://docs.github.com/rest" + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(401) + .setBody(jsonResponse) + ) + + val actualResponse = pullRequestDataSource.getReviewers( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "validAccessToken" + ) + + val expected = Result.failure( + WebException.UnknownError( + 401, + "Bad credentials" + ) + ) + + + assertEquals(expected, actualResponse) + } + + + } + + +} From 337f64712783ebdb7cde6a6157d352bc8c74f9e2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 3 Mar 2023 12:57:15 +0100 Subject: [PATCH 089/526] Add parsing api error messages to SafeApiCall method. --- .../loudius/network/utils/ApiCallUtil.kt | 17 +++++++++++++++-- .../network/utils/DefaultErrorResponse.kt | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/network/utils/DefaultErrorResponse.kt diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index ee6d6df01..ebe839faf 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,7 +1,9 @@ package com.appunite.loudius.network.utils -import retrofit2.HttpException +import com.google.gson.Gson import java.io.IOException +import org.json.JSONException +import retrofit2.HttpException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, @@ -11,12 +13,23 @@ suspend fun safeApiCall( val response = apiCall() Result.success(response) } catch (throwable: HttpException) { - Result.failure(errorParser(throwable.code(), throwable.message())) + val message = getApiErrorMessageIfExist(throwable) + Result.failure(errorParser(throwable.code(), message ?: throwable.message())) } catch (throwable: IOException) { Result.failure(WebException.NetworkError(throwable)) } } +private fun getApiErrorMessageIfExist(throwable: HttpException) = try { + val errorResponse = Gson().fromJson( + throwable.response()?.errorBody()?.string(), + DefaultErrorResponse::class.java + ) + errorResponse.message +} catch (throwable: JSONException) { + null +} + object DefaultErrorParser : RequestErrorParser { override fun invoke(responseCode: Int, responseMessage: String): Exception = diff --git a/app/src/main/java/com/appunite/loudius/network/utils/DefaultErrorResponse.kt b/app/src/main/java/com/appunite/loudius/network/utils/DefaultErrorResponse.kt new file mode 100644 index 000000000..cdbc1bd27 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/utils/DefaultErrorResponse.kt @@ -0,0 +1,6 @@ +package com.appunite.loudius.network.utils + +data class DefaultErrorResponse( + val message: String, + val documentationUrl: String, +) From f403d29d058eb5c8427b45f4d7cab4b6850f7faf Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 08:23:26 +0100 Subject: [PATCH 090/526] Implement tests for the PullRequestsNetworkDataSource. --- app/build.gradle | 4 + .../com/appunite/loudius/di/NetworkModule.kt | 9 +- .../appunite/loudius/network/model/Review.kt | 6 +- .../utils/LocalDateTimeDeserializer.kt | 28 ++++ .../loudius/network/NetworkTestDoubles.kt | 7 +- .../PullRequestsNetworkDataSourceTest.kt | 138 +++++++++++++++++- 6 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt diff --git a/app/build.gradle b/app/build.gradle index 481d0da4b..e7ea42c54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,6 +34,7 @@ android { } } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -54,6 +55,9 @@ android { } dependencies { + //Desugaring for use of java.time in api lower then 26 + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + //Base android deps implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index f7846cf0e..7adc55897 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.di import com.appunite.loudius.common.Constants +import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -8,9 +9,10 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import java.time.LocalDateTime +import javax.inject.Singleton import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -45,5 +47,8 @@ object NetworkModule { @Provides @Singleton fun provideGson(): Gson = - GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() + GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) + .create() } + diff --git a/app/src/main/java/com/appunite/loudius/network/model/Review.kt b/app/src/main/java/com/appunite/loudius/network/model/Review.kt index 7cd3f8d24..858ade875 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/Review.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/Review.kt @@ -4,7 +4,11 @@ import java.time.LocalDateTime data class Review( val id: String, - val userId: String, + val user: User, val state: ReviewState, val submittedAt: LocalDateTime ) + +data class User( + val id: Int +) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt new file mode 100644 index 000000000..42b7c04b9 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt @@ -0,0 +1,28 @@ +package com.appunite.loudius.network.utils + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import java.lang.reflect.Type +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +class LocalDateTimeDeserializer : JsonDeserializer { + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): LocalDateTime { + try { + val dateString = json?.asJsonPrimitive?.asString + val offsetDateTime = + OffsetDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + return offsetDateTime.toLocalDateTime() + } catch (e: Exception) { + throw JsonParseException(e) + } + } +} diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index cfc034026..4f2159ce9 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -1,14 +1,17 @@ package com.appunite.loudius.network +import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import java.time.LocalDateTime import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory + private fun testOkHttpClient() = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) @@ -16,7 +19,9 @@ private fun testOkHttpClient() = OkHttpClient.Builder() .build() private fun testGson() = - GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() + GsonBuilder() + .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() fun retrofitTestDouble( client: OkHttpClient = testOkHttpClient(), diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index d2a28d27c..977b6c44c 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -1,10 +1,14 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.Reviewer +import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -31,7 +35,7 @@ class PullRequestsNetworkDataSourceTest { } @Nested - inner class GetReviewersRequestsTest { + inner class GetReviewersRequestTest { @Test fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = @@ -148,8 +152,140 @@ class PullRequestsNetworkDataSourceTest { assertEquals(expected, actualResponse) } + } + + @Nested + inner class GetReviewsRequestTest { + + @Test + fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = + runTest { + mockWebServer.enqueue( + MockResponse() + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + ) + + val actualResponse = pullRequestDataSource.getReviews( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "validAccessToken" + ) + Assertions.assertInstanceOf( + WebException.NetworkError::class.java, + actualResponse.exceptionOrNull() + ) + } + @Test + fun `Given correct params WHEN successful response THEN return success`() = + runTest { + //language=JSON + val jsonResponse = """ + [ + { + "id": 1, + "node_id": "exampleId", + "user": { + "login": "exampleUser", + "id": 33498031, + "node_id": "exampleNodeId", + "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/exampleUser", + "html_url": "https://github.com/exampleUser", + "followers_url": "https://api.github.com/users/exampleUser/followers", + "following_url": "https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url": "https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/exampleUser/subscriptions", + "organizations_url": "https://api.github.com/users/exampleUser/orgs", + "repos_url": "https://api.github.com/users/exampleUser/repos", + "events_url": "https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url": "https://api.github.com/users/exampleUser/received_events", + "type": "User", + "site_admin": false + }, + "body": "", + "state": "COMMENTED", + "html_url": "https://github.com/exampleOwner/exampleRepo/pull/20#pullrequestreview-1321494756", + "pull_request_url": "https://api.github.com/repos/exampleOwner/exampleRepo/pulls/20", + "author_association": "COLLABORATOR", + "_links": { + "html": { + "href": "https://github.com/exampleOwner/exampleRepo/pull/20#pullrequestreview-1321494756" + }, + "pull_request": { + "href": "https://api.github.com/repos/exampleOwner/exampleRepo/pulls/20" + } + }, + "submitted_at": "2023-03-02T10:21:36Z", + "commit_id": "exampleCommitId" + }] + """.trimIndent() + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .setBody(jsonResponse) + ) + + val actualResponse = pullRequestDataSource.getReviews( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "validAccessToken" + ) + val expected = Result.success( + listOf( + Review( + "1", + User(33498031), + ReviewState.COMMENTED, + LocalDateTime.parse("2023-03-02T10:21:36") + ) + ) + ) + + + assertEquals(expected, actualResponse) { "Data should be correct" } + } + + + @Test + fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + runTest { + // language=JSON + val jsonResponse = """ + { + "message": "Bad credentials", + "documentation_url": "https://docs.github.com/rest" + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(401) + .setBody(jsonResponse) + ) + + val actualResponse = pullRequestDataSource.getReviews( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "validAccessToken" + ) + + val expected = Result.failure( + WebException.UnknownError( + 401, + "Bad credentials" + ) + ) + + + assertEquals(expected, actualResponse) + } } From b0d9585a8c6cb1fd9ce597b7d2f8770fa1153a16 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 08:40:54 +0100 Subject: [PATCH 091/526] Change permissions to the workflow files - fixing linter issues. --- .github/workflows/run-code-quality-check.yml | 6 +++++- .github/workflows/run-unit-test.yml | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml index 404526d41..8d82d8309 100644 --- a/.github/workflows/run-code-quality-check.yml +++ b/.github/workflows/run-code-quality-check.yml @@ -17,7 +17,11 @@ concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true -permissions: write-all +permissions: + checks: write + contents: write + statuses: write + pull-requests: write jobs: build: diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index a245d2956..881ba3b12 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -7,6 +7,8 @@ on: - 'develop' - 'main' +permissions: read-all + jobs: unit-tests: runs-on: ubuntu-20.04 From 9deee1dda8db0193f19a137faa2dc746eefa5837 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 08:53:43 +0100 Subject: [PATCH 092/526] Configure jscpd linter to ignore files ending with "Test" --- .jscpd.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.jscpd.json b/.jscpd.json index 0e07c4553..ff82ff3ad 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,7 +1,16 @@ { "threshold": 0, - "reporters": ["html", "console", "xml"], - "ignore": ["**/__snapshots__/**"], - "ignorePattern": ["import .*"], + "reporters": [ + "html", + "console", + "xml" + ], + "ignore": [ + "**/__snapshots__/**", + "**/src/test/java/**" + ], + "ignorePattern": [ + "import .*" + ], "absolute": true } From 9902c230bfcb0986f453796f4526baafd9497181 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 11:27:35 +0100 Subject: [PATCH 093/526] Perform minor cleaning before CR. --- .../utils/LocalDateTimeDeserializer.kt | 5 +- .../PullRequestsNetworkDataSourceTest.kt | 114 ++++++++---------- 2 files changed, 53 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt index 42b7c04b9..64b31cf5e 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt @@ -7,7 +7,7 @@ import com.google.gson.JsonParseException import java.lang.reflect.Type import java.time.LocalDateTime import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME class LocalDateTimeDeserializer : JsonDeserializer { @@ -18,8 +18,7 @@ class LocalDateTimeDeserializer : JsonDeserializer { ): LocalDateTime { try { val dateString = json?.asJsonPrimitive?.asString - val offsetDateTime = - OffsetDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + val offsetDateTime = OffsetDateTime.parse(dateString, ISO_OFFSET_DATE_TIME) return offsetDateTime.toLocalDateTime() } catch (e: Exception) { throw JsonParseException(e) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 977b6c44c..93444e0d6 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -23,10 +23,10 @@ import org.junit.jupiter.api.Test @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { - private val mockWebServer: MockWebServer = MockWebServer().apply { start(8080) } - private val userApi = - retrofitTestDouble(mockWebServer = mockWebServer).create(GithubPullRequestsService::class.java) - private val pullRequestDataSource = PullRequestsNetworkDataSource(userApi) + private val mockWebServer: MockWebServer = MockWebServer() + private val pullRequestsService = retrofitTestDouble(mockWebServer = mockWebServer) + .create(GithubPullRequestsService::class.java) + private val pullRequestDataSource = PullRequestsNetworkDataSource(pullRequestsService) @AfterEach @@ -54,7 +54,7 @@ class PullRequestsNetworkDataSourceTest { Assertions.assertInstanceOf( WebException.NetworkError::class.java, actualResponse.exceptionOrNull() - ) + ) { "Exception thrown should be NetworkError type" } } @Test @@ -102,19 +102,10 @@ class PullRequestsNetworkDataSourceTest { "validAccessToken" ) - val expected = Result.success( - RequestedReviewersResponse( - listOf( - Reviewer( - "1", - "exampleLogin", - "https://example/avatar" - ) - ) - ) - ) + val reviewer = Reviewer("1", "exampleLogin", "https://example/avatar") + val expected = Result.success(RequestedReviewersResponse(listOf(reviewer))) - assertEquals(expected, actualResponse) { "Data should be correct" } + assertEquals(expected, actualResponse) { "Data should be valid" } } @@ -150,7 +141,7 @@ class PullRequestsNetworkDataSourceTest { ) - assertEquals(expected, actualResponse) + assertEquals(expected, actualResponse) { "Data should be valid" } } } @@ -174,7 +165,7 @@ class PullRequestsNetworkDataSourceTest { Assertions.assertInstanceOf( WebException.NetworkError::class.java, actualResponse.exceptionOrNull() - ) + ) { "Exception thrown should be NetworkError type" } } @Test @@ -182,47 +173,46 @@ class PullRequestsNetworkDataSourceTest { runTest { //language=JSON val jsonResponse = """ - [ - { - "id": 1, - "node_id": "exampleId", - "user": { - "login": "exampleUser", - "id": 33498031, - "node_id": "exampleNodeId", - "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/exampleUser", - "html_url": "https://github.com/exampleUser", - "followers_url": "https://api.github.com/users/exampleUser/followers", - "following_url": "https://api.github.com/users/exampleUser/following{/other_user}", - "gists_url": "https://api.github.com/users/exampleUser/gists{/gist_id}", - "starred_url": "https://api.github.com/users/exampleUser/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/exampleUser/subscriptions", - "organizations_url": "https://api.github.com/users/exampleUser/orgs", - "repos_url": "https://api.github.com/users/exampleUser/repos", - "events_url": "https://api.github.com/users/exampleUser/events{/privacy}", - "received_events_url": "https://api.github.com/users/exampleUser/received_events", - "type": "User", - "site_admin": false - }, - "body": "", - "state": "COMMENTED", - "html_url": "https://github.com/exampleOwner/exampleRepo/pull/20#pullrequestreview-1321494756", - "pull_request_url": "https://api.github.com/repos/exampleOwner/exampleRepo/pulls/20", - "author_association": "COLLABORATOR", - "_links": { - "html": { - "href": "https://github.com/exampleOwner/exampleRepo/pull/20#pullrequestreview-1321494756" - }, - "pull_request": { - "href": "https://api.github.com/repos/exampleOwner/exampleRepo/pulls/20" - } - }, - "submitted_at": "2023-03-02T10:21:36Z", - "commit_id": "exampleCommitId" - }] - """.trimIndent() + [ + { + "id": 1, + "node_id": "exampleId", + "user": { + "login": "exampleUser", + "id": 33498031, + "node_id": "exampleNodeId", + "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/exampleUser", + "html_url": "https://github.com/exampleUser", + "followers_url": "https://api.github.com/users/exampleUser/followers", + "following_url": "https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url": "https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/exampleUser/subscriptions", + "organizations_url": "https://api.github.com/users/exampleUser/orgs", + "repos_url": "https://api.github.com/users/exampleUser/repos", + "events_url": "https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url": "https://api.github.com/users/exampleUser/received_events", + "type": "User", + "site_admin": false + }, + "body": "", + "state": "COMMENTED", + "html_url": "https://github.com/exampleOwner/exampleRepo/pull/20#pullrequestreview-1321494756", + "pull_request_url": "https://api.github.com/repos/exampleOwner/exampleRepo/pulls/20", + "author_association": "COLLABORATOR", + "_links": { + "html": { + "href": "https://github.com/exampleOwner/exampleRepo/pull/20#pullrequestreview-1321494756" + }, + "pull_request": { + "href": "https://api.github.com/repos/exampleOwner/exampleRepo/pulls/20" + } + }, + "submitted_at": "2023-03-02T10:21:36Z", + "commit_id": "exampleCommitId" + }]""".trimIndent() mockWebServer.enqueue( MockResponse() .setResponseCode(200) @@ -247,8 +237,7 @@ class PullRequestsNetworkDataSourceTest { ) ) - - assertEquals(expected, actualResponse) { "Data should be correct" } + assertEquals(expected, actualResponse) { "Data should be valid" } } @@ -283,7 +272,6 @@ class PullRequestsNetworkDataSourceTest { ) ) - assertEquals(expected, actualResponse) } } From c58062478f77c91692b9533c0adf53943827e0a7 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 6 Mar 2023 10:31:26 +0000 Subject: [PATCH 094/526] [MegaLinter] Apply linters fixes --- .github/workflows/run-unit-test.yml | 8 +-- .jscpd.json | 15 +---- .../com/appunite/loudius/di/NetworkModule.kt | 5 +- .../domain/GitHubPullRequestsRepository.kt | 8 +-- .../PullRequestsNetworkDataSource.kt | 9 ++- .../model/RequestedReviewersResponse.kt | 2 +- .../appunite/loudius/network/model/Review.kt | 4 +- .../loudius/network/model/ReviewState.kt | 2 +- .../loudius/network/utils/ApiCallUtil.kt | 4 +- .../utils/LocalDateTimeDeserializer.kt | 2 +- .../loudius/network/NetworkTestDoubles.kt | 7 +-- .../PullRequestsNetworkDataSourceTest.kt | 55 +++++++++---------- github_conf/branch_protection_rules.json | 4 -- 13 files changed, 52 insertions(+), 73 deletions(-) delete mode 100644 github_conf/branch_protection_rules.json diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 881ba3b12..7e4285dbe 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -4,8 +4,8 @@ on: pull_request: push: branches: - - 'develop' - - 'main' + - "develop" + - "main" permissions: read-all @@ -25,8 +25,8 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: '11' - distribution: 'adopt' + java-version: "11" + distribution: "adopt" cache: gradle - name: Validate Gradle wrapper diff --git a/.jscpd.json b/.jscpd.json index ff82ff3ad..c4ce4ed0e 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,16 +1,7 @@ { "threshold": 0, - "reporters": [ - "html", - "console", - "xml" - ], - "ignore": [ - "**/__snapshots__/**", - "**/src/test/java/**" - ], - "ignorePattern": [ - "import .*" - ], + "reporters": ["html", "console", "xml"], + "ignore": ["**/__snapshots__/**", "**/src/test/java/**"], + "ignorePattern": ["import .*"], "absolute": true } diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 7adc55897..2360ef51c 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -9,10 +9,10 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import java.time.LocalDateTime -import javax.inject.Singleton import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -51,4 +51,3 @@ object NetworkModule { .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) .create() } - diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index 58f8a3616..117ad2392 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -10,13 +10,13 @@ interface PullRequestRepository { suspend fun getReviews( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result> suspend fun getReviewers( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result suspend fun getPullRequestsForUser(author: String): Result @@ -30,14 +30,14 @@ class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSou override suspend fun getReviews( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result> = remoteDataSource.getReviews(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") override suspend fun getReviewers( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result = remoteDataSource.getReviewers(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index dbde9c86a..b32e5dc55 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -13,16 +13,15 @@ interface PullRequestDataSource { owner: String, repository: String, pullRequestNumber: String, - accessToken: String + accessToken: String, ): Result suspend fun getReviews( owner: String, repository: String, pullRequestNumber: String, - accessToken: String + accessToken: String, ): Result> - } const val auth_token = "BEARER xxxxxxx" // temporary solution @@ -38,7 +37,7 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Git owner: String, repository: String, pullRequestNumber: String, - accessToken: String + accessToken: String, ): Result = safeApiCall { service.getReviewers(owner, repository, pullRequestNumber, accessToken) } @@ -47,7 +46,7 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Git owner: String, repository: String, pullRequestNumber: String, - accessToken: String + accessToken: String, ): Result> = safeApiCall { service.getReviews(owner, repository, pullRequestNumber, accessToken) } diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt index 5936cd6cb..7ca494832 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt @@ -1,5 +1,5 @@ package com.appunite.loudius.network.model data class RequestedReviewersResponse( - val users: List + val users: List, ) diff --git a/app/src/main/java/com/appunite/loudius/network/model/Review.kt b/app/src/main/java/com/appunite/loudius/network/model/Review.kt index 858ade875..4c4396406 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/Review.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/Review.kt @@ -6,9 +6,9 @@ data class Review( val id: String, val user: User, val state: ReviewState, - val submittedAt: LocalDateTime + val submittedAt: LocalDateTime, ) data class User( - val id: Int + val id: Int, ) diff --git a/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt b/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt index 4a7e2e4ba..1ea1bb9f7 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt @@ -3,5 +3,5 @@ package com.appunite.loudius.network.model enum class ReviewState { APPROVED, CHANGES_REQUESTED, - COMMENTED + COMMENTED, } diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index ebe839faf..8f6580100 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.network.utils import com.google.gson.Gson -import java.io.IOException import org.json.JSONException import retrofit2.HttpException +import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, @@ -23,7 +23,7 @@ suspend fun safeApiCall( private fun getApiErrorMessageIfExist(throwable: HttpException) = try { val errorResponse = Gson().fromJson( throwable.response()?.errorBody()?.string(), - DefaultErrorResponse::class.java + DefaultErrorResponse::class.java, ) errorResponse.message } catch (throwable: JSONException) { diff --git a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt index 64b31cf5e..500fa25f2 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt @@ -14,7 +14,7 @@ class LocalDateTimeDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement?, typeOfT: Type?, - context: JsonDeserializationContext? + context: JsonDeserializationContext?, ): LocalDateTime { try { val dateString = json?.asJsonPrimitive?.asString diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 4f2159ce9..0e4969044 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -4,13 +4,12 @@ import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory - +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit private fun testOkHttpClient() = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) @@ -26,7 +25,7 @@ private fun testGson() = fun retrofitTestDouble( client: OkHttpClient = testOkHttpClient(), gson: Gson = testGson(), - mockWebServer: MockWebServer + mockWebServer: MockWebServer, ): Retrofit = Retrofit.Builder() .client(client) .addConverterFactory(GsonConverterFactory.create(gson)) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 93444e0d6..d5622bc8d 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -8,7 +8,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -19,6 +18,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -28,7 +28,6 @@ class PullRequestsNetworkDataSourceTest { .create(GithubPullRequestsService::class.java) private val pullRequestDataSource = PullRequestsNetworkDataSource(pullRequestsService) - @AfterEach fun tearDown() { mockWebServer.shutdown() @@ -42,18 +41,18 @@ class PullRequestsNetworkDataSourceTest { runTest { mockWebServer.enqueue( MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) val actualResponse = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken" + "validAccessToken", ) Assertions.assertInstanceOf( WebException.NetworkError::class.java, - actualResponse.exceptionOrNull() + actualResponse.exceptionOrNull(), ) { "Exception thrown should be NetworkError type" } } @@ -87,19 +86,19 @@ class PullRequestsNetworkDataSourceTest { ], "teams": [] } - """.trimIndent() + """.trimIndent() mockWebServer.enqueue( MockResponse() .setResponseCode(200) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val actualResponse = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken" + "validAccessToken", ) val reviewer = Reviewer("1", "exampleLogin", "https://example/avatar") @@ -108,7 +107,6 @@ class PullRequestsNetworkDataSourceTest { assertEquals(expected, actualResponse) { "Data should be valid" } } - @Test fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = runTest { @@ -123,24 +121,23 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val actualResponse = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken" + "validAccessToken", ) val expected = Result.failure( WebException.UnknownError( 401, - "Bad credentials" - ) + "Bad credentials", + ), ) - assertEquals(expected, actualResponse) { "Data should be valid" } } } @@ -153,18 +150,18 @@ class PullRequestsNetworkDataSourceTest { runTest { mockWebServer.enqueue( MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) val actualResponse = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken" + "validAccessToken", ) Assertions.assertInstanceOf( WebException.NetworkError::class.java, - actualResponse.exceptionOrNull() + actualResponse.exceptionOrNull(), ) { "Exception thrown should be NetworkError type" } } @@ -212,18 +209,19 @@ class PullRequestsNetworkDataSourceTest { }, "submitted_at": "2023-03-02T10:21:36Z", "commit_id": "exampleCommitId" - }]""".trimIndent() + }] + """.trimIndent() mockWebServer.enqueue( MockResponse() .setResponseCode(200) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val actualResponse = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken" + "validAccessToken", ) val expected = Result.success( @@ -232,15 +230,14 @@ class PullRequestsNetworkDataSourceTest { "1", User(33498031), ReviewState.COMMENTED, - LocalDateTime.parse("2023-03-02T10:21:36") - ) - ) + LocalDateTime.parse("2023-03-02T10:21:36"), + ), + ), ) assertEquals(expected, actualResponse) { "Data should be valid" } } - @Test fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = runTest { @@ -255,26 +252,24 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val actualResponse = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken" + "validAccessToken", ) val expected = Result.failure( WebException.UnknownError( 401, - "Bad credentials" - ) + "Bad credentials", + ), ) assertEquals(expected, actualResponse) } } - - } diff --git a/github_conf/branch_protection_rules.json b/github_conf/branch_protection_rules.json deleted file mode 100644 index 8e614b55c..000000000 --- a/github_conf/branch_protection_rules.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Not Found", - "documentation_url": "https://docs.github.com/rest" -} \ No newline at end of file From c55a5e5fb1d2aec2cc5d9e7579712099c843d470 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 13:10:04 +0100 Subject: [PATCH 095/526] Move DetailsScreen.kt to the new package - collaborators. --- .../appunite/loudius/ui/{ => collaborators}/DetailsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/main/java/com/appunite/loudius/ui/{ => collaborators}/DetailsScreen.kt (99%) diff --git a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/collaborators/DetailsScreen.kt similarity index 99% rename from app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt rename to app/src/main/java/com/appunite/loudius/ui/collaborators/DetailsScreen.kt index 00f52c49c..28895858c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/DetailsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/collaborators/DetailsScreen.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.ui +package com.appunite.loudius.ui.collaborators import androidx.compose.foundation.Image import androidx.compose.foundation.background From 148783aae59d5dbb6819795c104a50b3147f174c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 13:11:18 +0100 Subject: [PATCH 096/526] Rename DetailsScreen.kt to CollaboratorsScreen.kt. --- .../ui/collaborators/{DetailsScreen.kt => CollaboratorsScreen.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/appunite/loudius/ui/collaborators/{DetailsScreen.kt => CollaboratorsScreen.kt} (100%) diff --git a/app/src/main/java/com/appunite/loudius/ui/collaborators/DetailsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/collaborators/CollaboratorsScreen.kt similarity index 100% rename from app/src/main/java/com/appunite/loudius/ui/collaborators/DetailsScreen.kt rename to app/src/main/java/com/appunite/loudius/ui/collaborators/CollaboratorsScreen.kt From ae4692d9e11f2a9bcc99112a9929aba52813a755 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 13:15:58 +0100 Subject: [PATCH 097/526] Rename CollaboratorsScreen.kt into ReviewersScreen.kt and package. --- .../CollaboratorsScreen.kt => reviewers/ReviewersScreen.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/main/java/com/appunite/loudius/ui/{collaborators/CollaboratorsScreen.kt => reviewers/ReviewersScreen.kt} (99%) diff --git a/app/src/main/java/com/appunite/loudius/ui/collaborators/CollaboratorsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt similarity index 99% rename from app/src/main/java/com/appunite/loudius/ui/collaborators/CollaboratorsScreen.kt rename to app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 28895858c..b0a47f1c3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/collaborators/CollaboratorsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.ui.collaborators +package com.appunite.loudius.ui.reviewers import androidx.compose.foundation.Image import androidx.compose.foundation.background From 57b11cd22c8003aa04de890cf3d7b7631bbe1585 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 6 Mar 2023 13:19:06 +0100 Subject: [PATCH 098/526] SIL-83: error dialog ui --- .../ui/components/LoudiusErrorDialog.kt | 57 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 + 2 files changed, 60 insertions(+) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt new file mode 100644 index 000000000..abc627cd2 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -0,0 +1,57 @@ +package com.appunite.loudius.ui.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.appunite.loudius.R +import com.appunite.loudius.ui.theme.LoudiusTheme + +@Composable +fun LoudiusErrorDialog( + onConfirmButtonClick: () -> Unit +) { + val openDialog = remember { mutableStateOf(true) } + if (openDialog.value) { + AlertDialog( + onDismissRequest = { openDialog.value = false }, + title = { Text(text = stringResource(R.string.error_dialog_title)) }, + text = { Text(text = stringResource(R.string.error_dialog_text)) }, + confirmButton = { + ConfirmButton { + onConfirmButtonClick() + } + }, + containerColor = MaterialTheme.colorScheme.surface + ) + } +} + +@Composable +private fun ConfirmButton( + confirm: () -> Unit +) { + Button( + onClick = { confirm() }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.tertiary + ) + ) { + Text(text = stringResource(R.string.ok)) + } +} + +@Preview +@Composable +fun LoudiusErrorDialogPreview() { + LoudiusTheme { + LoudiusErrorDialog { } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5f7b79b00..fee6e5124 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,4 +8,7 @@ Not reviewed for %d h. Github icon Loudius logo + Error + Something went wrong… + OK From b6847bcb5dbeb6e0a0b583b0a5a6b2827448f403 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 08:40:54 +0100 Subject: [PATCH 099/526] Change permissions to the workflow files - fixing linter issues. --- .github/workflows/run-code-quality-check.yml | 6 +++++- .github/workflows/run-unit-test.yml | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml index 404526d41..8d82d8309 100644 --- a/.github/workflows/run-code-quality-check.yml +++ b/.github/workflows/run-code-quality-check.yml @@ -17,7 +17,11 @@ concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true -permissions: write-all +permissions: + checks: write + contents: write + statuses: write + pull-requests: write jobs: build: diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index a245d2956..881ba3b12 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -7,6 +7,8 @@ on: - 'develop' - 'main' +permissions: read-all + jobs: unit-tests: runs-on: ubuntu-20.04 From 8cfeedd131f275aaf5a69e139d92ff5f5d5938d9 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 6 Mar 2023 12:56:14 +0000 Subject: [PATCH 100/526] [MegaLinter] Apply linters fixes --- .github/workflows/run-unit-test.yml | 8 ++++---- .../loudius/ui/components/LoudiusErrorDialog.kt | 10 +++++----- github_conf/branch_protection_rules.json | 4 ---- 3 files changed, 9 insertions(+), 13 deletions(-) delete mode 100644 github_conf/branch_protection_rules.json diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 881ba3b12..7e4285dbe 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -4,8 +4,8 @@ on: pull_request: push: branches: - - 'develop' - - 'main' + - "develop" + - "main" permissions: read-all @@ -25,8 +25,8 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: '11' - distribution: 'adopt' + java-version: "11" + distribution: "adopt" cache: gradle - name: Validate Gradle wrapper diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index abc627cd2..3d062db9a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -15,7 +15,7 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusErrorDialog( - onConfirmButtonClick: () -> Unit + onConfirmButtonClick: () -> Unit, ) { val openDialog = remember { mutableStateOf(true) } if (openDialog.value) { @@ -28,21 +28,21 @@ fun LoudiusErrorDialog( onConfirmButtonClick() } }, - containerColor = MaterialTheme.colorScheme.surface + containerColor = MaterialTheme.colorScheme.surface, ) } } @Composable private fun ConfirmButton( - confirm: () -> Unit + confirm: () -> Unit, ) { Button( onClick = { confirm() }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.tertiary - ) + contentColor = MaterialTheme.colorScheme.tertiary, + ), ) { Text(text = stringResource(R.string.ok)) } diff --git a/github_conf/branch_protection_rules.json b/github_conf/branch_protection_rules.json deleted file mode 100644 index 8e614b55c..000000000 --- a/github_conf/branch_protection_rules.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Not Found", - "documentation_url": "https://docs.github.com/rest" -} \ No newline at end of file From cb41b3e79d19657e183b030bfb3ea5a6a1aa4424 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 6 Mar 2023 15:35:27 +0100 Subject: [PATCH 101/526] SIL-83: add dialog title and text as function parameters --- .../loudius/ui/components/LoudiusErrorDialog.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index 3d062db9a..b6d4f3e5a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -16,13 +16,15 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusErrorDialog( onConfirmButtonClick: () -> Unit, + dialogTitle: String, + dialogText: String ) { val openDialog = remember { mutableStateOf(true) } if (openDialog.value) { AlertDialog( onDismissRequest = { openDialog.value = false }, - title = { Text(text = stringResource(R.string.error_dialog_title)) }, - text = { Text(text = stringResource(R.string.error_dialog_text)) }, + title = { Text(text = dialogTitle) }, + text = { Text(text = dialogText) }, confirmButton = { ConfirmButton { onConfirmButtonClick() @@ -52,6 +54,10 @@ private fun ConfirmButton( @Composable fun LoudiusErrorDialogPreview() { LoudiusTheme { - LoudiusErrorDialog { } + LoudiusErrorDialog( + onConfirmButtonClick = { }, + dialogTitle = "Example title", + dialogText = "Example text" + ) } } From fc898217cb8e548a45c5bfc8bd4b234323e5d945 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 6 Mar 2023 14:38:19 +0000 Subject: [PATCH 102/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/components/LoudiusErrorDialog.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index b6d4f3e5a..5fb3f17a1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -17,7 +17,7 @@ import com.appunite.loudius.ui.theme.LoudiusTheme fun LoudiusErrorDialog( onConfirmButtonClick: () -> Unit, dialogTitle: String, - dialogText: String + dialogText: String, ) { val openDialog = remember { mutableStateOf(true) } if (openDialog.value) { @@ -57,7 +57,7 @@ fun LoudiusErrorDialogPreview() { LoudiusErrorDialog( onConfirmButtonClick = { }, dialogTitle = "Example title", - dialogText = "Example text" + dialogText = "Example text", ) } } From e372bb5acd0429434c3efc69430a8f9e242a1d07 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 15:51:01 +0100 Subject: [PATCH 103/526] Add Launched effect for getting access token. --- .../com/appunite/loudius/ui/repos/ReposScreen.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt index f02ae990d..ab180fbfd 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt @@ -1,18 +1,28 @@ package com.appunite.loudius.ui.repos import android.content.Intent +import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberUpdatedState import androidx.hilt.navigation.compose.hiltViewModel + @Composable fun ReposScreen( intent: Intent, viewModel: ReposViewModel = hiltViewModel(), ) { val code = intent.data?.getQueryParameter("code") - Text(text = code ?: "empty code") - code?.let { - viewModel.getAccessToken(code) + val rememberedCode = rememberUpdatedState(newValue = code) + Column { + Text(text = code ?: "code is already consumed") + } + LaunchedEffect(key1 = rememberedCode) { + rememberedCode.value?.let { + viewModel.getAccessToken(it) + intent.data = null + } } } From d156760ccb2bce375af5b58b1f45b1ccf076139a Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 6 Mar 2023 15:58:04 +0100 Subject: [PATCH 104/526] SIL-83: add defaults to params --- .../ui/components/LoudiusErrorDialog.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index 5fb3f17a1..6e7472e36 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -16,8 +16,9 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusErrorDialog( onConfirmButtonClick: () -> Unit, - dialogTitle: String, - dialogText: String, + dialogTitle: String = stringResource(id = R.string.error_dialog_title), + dialogText: String = stringResource(id = R.string.error_dialog_text), + confirmText: String = stringResource(R.string.ok) ) { val openDialog = remember { mutableStateOf(true) } if (openDialog.value) { @@ -26,9 +27,10 @@ fun LoudiusErrorDialog( title = { Text(text = dialogTitle) }, text = { Text(text = dialogText) }, confirmButton = { - ConfirmButton { - onConfirmButtonClick() - } + ConfirmButton( + confirmText = confirmText, + confirm = onConfirmButtonClick + ) }, containerColor = MaterialTheme.colorScheme.surface, ) @@ -37,6 +39,7 @@ fun LoudiusErrorDialog( @Composable private fun ConfirmButton( + confirmText: String, confirm: () -> Unit, ) { Button( @@ -46,7 +49,7 @@ private fun ConfirmButton( contentColor = MaterialTheme.colorScheme.tertiary, ), ) { - Text(text = stringResource(R.string.ok)) + Text(text = confirmText) } } @@ -54,10 +57,6 @@ private fun ConfirmButton( @Composable fun LoudiusErrorDialogPreview() { LoudiusTheme { - LoudiusErrorDialog( - onConfirmButtonClick = { }, - dialogTitle = "Example title", - dialogText = "Example text", - ) + LoudiusErrorDialog(onConfirmButtonClick = {}) } } From b68c5d2ae0deaa1e3db34198f2a6407b0fb6b7f0 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 6 Mar 2023 08:40:54 +0100 Subject: [PATCH 105/526] Change permissions to the workflow files - fixing linter issues. --- .github/workflows/run-code-quality-check.yml | 6 +++++- .github/workflows/run-unit-test.yml | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml index 404526d41..8d82d8309 100644 --- a/.github/workflows/run-code-quality-check.yml +++ b/.github/workflows/run-code-quality-check.yml @@ -17,7 +17,11 @@ concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true -permissions: write-all +permissions: + checks: write + contents: write + statuses: write + pull-requests: write jobs: build: diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index a245d2956..881ba3b12 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -7,6 +7,8 @@ on: - 'develop' - 'main' +permissions: read-all + jobs: unit-tests: runs-on: ubuntu-20.04 From fab4d1e0beeebe763e2e096f1c152329c00b571c Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 6 Mar 2023 15:01:03 +0000 Subject: [PATCH 106/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/components/LoudiusErrorDialog.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index 6e7472e36..37fb00ed4 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -18,7 +18,7 @@ fun LoudiusErrorDialog( onConfirmButtonClick: () -> Unit, dialogTitle: String = stringResource(id = R.string.error_dialog_title), dialogText: String = stringResource(id = R.string.error_dialog_text), - confirmText: String = stringResource(R.string.ok) + confirmText: String = stringResource(R.string.ok), ) { val openDialog = remember { mutableStateOf(true) } if (openDialog.value) { @@ -29,7 +29,7 @@ fun LoudiusErrorDialog( confirmButton = { ConfirmButton( confirmText = confirmText, - confirm = onConfirmButtonClick + confirm = onConfirmButtonClick, ) }, containerColor = MaterialTheme.colorScheme.surface, From 8a57b5ef4285c08d4c03d011616675138b7d4265 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 6 Mar 2023 15:01:50 +0000 Subject: [PATCH 107/526] [MegaLinter] Apply linters fixes --- .github/workflows/run-unit-test.yml | 8 ++++---- .../java/com/appunite/loudius/ui/repos/ReposScreen.kt | 1 - github_conf/branch_protection_rules.json | 4 ---- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 github_conf/branch_protection_rules.json diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 881ba3b12..7e4285dbe 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -4,8 +4,8 @@ on: pull_request: push: branches: - - 'develop' - - 'main' + - "develop" + - "main" permissions: read-all @@ -25,8 +25,8 @@ jobs: - name: Set up JDK uses: actions/setup-java@v3 with: - java-version: '11' - distribution: 'adopt' + java-version: "11" + distribution: "adopt" cache: gradle - name: Validate Gradle wrapper diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt index ab180fbfd..ba2253425 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberUpdatedState import androidx.hilt.navigation.compose.hiltViewModel - @Composable fun ReposScreen( intent: Intent, diff --git a/github_conf/branch_protection_rules.json b/github_conf/branch_protection_rules.json deleted file mode 100644 index 8e614b55c..000000000 --- a/github_conf/branch_protection_rules.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "message": "Not Found", - "documentation_url": "https://docs.github.com/rest" -} \ No newline at end of file From daabe7eb786d2fd0baebd87a2e3ddd56594c41ac Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 7 Mar 2023 09:26:56 +0100 Subject: [PATCH 108/526] SIL-83: code cleanup --- .../loudius/ui/components/LoudiusErrorDialog.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index 37fb00ed4..a9cb7c500 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -6,8 +6,10 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.appunite.loudius.R @@ -20,10 +22,10 @@ fun LoudiusErrorDialog( dialogText: String = stringResource(id = R.string.error_dialog_text), confirmText: String = stringResource(R.string.ok), ) { - val openDialog = remember { mutableStateOf(true) } - if (openDialog.value) { + var openDialog by remember { mutableStateOf(true) } + if (openDialog) { AlertDialog( - onDismissRequest = { openDialog.value = false }, + onDismissRequest = { openDialog = false }, title = { Text(text = dialogTitle) }, text = { Text(text = dialogText) }, confirmButton = { @@ -43,7 +45,7 @@ private fun ConfirmButton( confirm: () -> Unit, ) { Button( - onClick = { confirm() }, + onClick = confirm, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.tertiary, From aba0f60d7cdfa3c42c2df7597df62e61e2b5fe89 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 2 Mar 2023 13:27:27 +0100 Subject: [PATCH 109/526] Add logger to retrofit --- app/build.gradle | 4 ++- .../com/appunite/loudius/di/NetworkModule.kt | 29 ++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e7ea42c54..e57c36bb8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,7 +90,9 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" - //retrofit + //retrofit & okhttp + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0' diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 2360ef51c..c0c9d536f 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -9,14 +9,25 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import java.time.LocalDateTime import javax.inject.Singleton +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory @InstallIn(SingletonComponent::class) @Module object NetworkModule { + @Provides + @Singleton + fun provideOkHttp(): OkHttpClient { + val logging = HttpLoggingInterceptor() + logging.setLevel(HttpLoggingInterceptor.Level.BASIC) + return OkHttpClient.Builder() + .addInterceptor(logging) + .build() + } @Provides @AuthAPI @@ -29,18 +40,28 @@ object NetworkModule { @Provides @Singleton @AuthAPI - fun provideAuthRetrofit(gson: Gson, @AuthAPI baseUrl: String): Retrofit = + fun provideAuthRetrofit( + gson: Gson, + @AuthAPI baseUrl: String, + okHttpClient: OkHttpClient + ): Retrofit = Retrofit.Builder() .baseUrl(baseUrl) + .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() @Provides @Singleton @BaseAPI - fun provideBaseRetrofit(gson: Gson, @BaseAPI baseAPIUrl: String): Retrofit = + fun provideBaseRetrofit( + gson: Gson, + @BaseAPI baseAPIUrl: String, + okHttpClient: OkHttpClient + ): Retrofit = Retrofit.Builder() .baseUrl(baseAPIUrl) + .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() From 6cb8fc08af87af4ef61443370824ae047810ce98 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 2 Mar 2023 13:52:10 +0100 Subject: [PATCH 110/526] Download pull requests for current user --- .../java/com/appunite/loudius/MainActivity.kt | 4 +- .../loudius/common/ResultExtension.kt | 4 ++ .../com/appunite/loudius/di/NetworkModule.kt | 33 +++++++----- .../domain/GitHubPullRequestsRepository.kt | 13 +++-- .../PullRequestsNetworkDataSource.kt | 17 +++---- .../appunite/loudius/network/model/Review.kt | 4 -- .../appunite/loudius/network/model/User.kt | 3 ++ .../services/GithubPullRequestsService.kt | 10 ++-- .../loudius/network/utils/ApiCallUtil.kt | 2 +- .../loudius/network/utils/AuthInterceptor.kt | 17 +++++++ .../ui/pullrequests/PullRequestsViewModel.kt | 2 +- .../appunite/loudius/ui/repos/ReposScreen.kt | 4 ++ .../loudius/fakes/FakeUserRepository.kt | 18 +++++++ .../loudius/network/AuthInterceptorTest.kt | 50 +++++++++++++++++++ .../loudius/network/NetworkTestDoubles.kt | 19 ++++--- .../PullRequestsNetworkDataSourceTest.kt | 8 +-- 16 files changed, 157 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/common/ResultExtension.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/model/User.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt create mode 100644 app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt create mode 100644 app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index f092faa9f..77b6ceb1e 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -46,7 +46,9 @@ class MainActivity : ComponentActivity() { }, ), ) { - ReposScreen(intent = intent) + ReposScreen(intent = intent) { + navController.navigate(Screen.PullRequests.route) + } } composable(route = Screen.PullRequests.route) { PullRequestsScreen() diff --git a/app/src/main/java/com/appunite/loudius/common/ResultExtension.kt b/app/src/main/java/com/appunite/loudius/common/ResultExtension.kt new file mode 100644 index 000000000..05ab0c91c --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/common/ResultExtension.kt @@ -0,0 +1,4 @@ +package com.appunite.loudius.common + +inline fun Result.flatMap(mapper: (value: T) -> Result): Result = + fold(onSuccess = mapper, onFailure = { Result.failure(it) }) diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index c0c9d536f..3963c55ed 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -2,6 +2,7 @@ package com.appunite.loudius.di import com.appunite.loudius.common.Constants import com.appunite.loudius.network.utils.LocalDateTimeDeserializer +import com.appunite.loudius.network.utils.AuthInterceptor import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -21,12 +22,8 @@ import retrofit2.converter.gson.GsonConverterFactory object NetworkModule { @Provides @Singleton - fun provideOkHttp(): OkHttpClient { - val logging = HttpLoggingInterceptor() - logging.setLevel(HttpLoggingInterceptor.Level.BASIC) - return OkHttpClient.Builder() - .addInterceptor(logging) - .build() + fun provideLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BASIC } @Provides @@ -43,13 +40,18 @@ object NetworkModule { fun provideAuthRetrofit( gson: Gson, @AuthAPI baseUrl: String, - okHttpClient: OkHttpClient - ): Retrofit = - Retrofit.Builder() + loggingInterceptor: HttpLoggingInterceptor + ): Retrofit { + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .build() + + return Retrofit.Builder() .baseUrl(baseUrl) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() + } @Provides @Singleton @@ -57,13 +59,20 @@ object NetworkModule { fun provideBaseRetrofit( gson: Gson, @BaseAPI baseAPIUrl: String, - okHttpClient: OkHttpClient - ): Retrofit = - Retrofit.Builder() + loggingInterceptor: HttpLoggingInterceptor, + authInterceptor: AuthInterceptor + ): Retrofit { + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .addInterceptor(loggingInterceptor) + .build() + + return Retrofit.Builder() .baseUrl(baseAPIUrl) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() + } @Provides @Singleton diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index 117ad2392..b6dea4761 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource +import com.appunite.loudius.common.flatMap import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review @@ -19,25 +20,27 @@ interface PullRequestRepository { pullRequestNumber: String, ): Result - suspend fun getPullRequestsForUser(author: String): Result + suspend fun getCurrentUserPullRequests(): Result } class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSource: PullRequestsNetworkDataSource) : PullRequestRepository { - override suspend fun getPullRequestsForUser(author: String): Result = - remoteDataSource.getPullRequestsForUser(author) + override suspend fun getCurrentUserPullRequests(): Result { + val currentUser = remoteDataSource.getUser() + return currentUser.flatMap { remoteDataSource.getPullRequestsForUser(it.login) } + } override suspend fun getReviews( owner: String, repo: String, pullRequestNumber: String, ): Result> = - remoteDataSource.getReviews(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") + remoteDataSource.getReviews(owner, repo, pullRequestNumber) override suspend fun getReviewers( owner: String, repo: String, pullRequestNumber: String, ): Result = - remoteDataSource.getReviewers(owner, repo, pullRequestNumber, "TODO ACCESS TOKEN") + remoteDataSource.getReviewers(owner, repo, pullRequestNumber) } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index b32e5dc55..984daacfd 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -3,6 +3,7 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.User import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject @@ -13,41 +14,39 @@ interface PullRequestDataSource { owner: String, repository: String, pullRequestNumber: String, - accessToken: String, ): Result suspend fun getReviews( owner: String, repository: String, pullRequestNumber: String, - accessToken: String, ): Result> } -const val auth_token = "BEARER xxxxxxx" // temporary solution - @Singleton class PullRequestsNetworkDataSource @Inject constructor(private val service: GithubPullRequestsService) : PullRequestDataSource { suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { - service.getPullRequestsForUser(auth_token, "author:$author type:pr state:open") + service.getPullRequestsForUser("author:$author type:pr state:open") } override suspend fun getReviewers( owner: String, repository: String, pullRequestNumber: String, - accessToken: String, ): Result = safeApiCall { - service.getReviewers(owner, repository, pullRequestNumber, accessToken) + service.getReviewers(owner, repository, pullRequestNumber) } override suspend fun getReviews( owner: String, repository: String, pullRequestNumber: String, - accessToken: String, ): Result> = safeApiCall { - service.getReviews(owner, repository, pullRequestNumber, accessToken) + service.getReviews(owner, repository, pullRequestNumber) + } + + suspend fun getUser(): Result = safeApiCall { + service.getUser() } } diff --git a/app/src/main/java/com/appunite/loudius/network/model/Review.kt b/app/src/main/java/com/appunite/loudius/network/model/Review.kt index 4c4396406..020a6c903 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/Review.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/Review.kt @@ -8,7 +8,3 @@ data class Review( val state: ReviewState, val submittedAt: LocalDateTime, ) - -data class User( - val id: Int, -) diff --git a/app/src/main/java/com/appunite/loudius/network/model/User.kt b/app/src/main/java/com/appunite/loudius/network/model/User.kt new file mode 100644 index 000000000..d5f1e6b38 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/User.kt @@ -0,0 +1,3 @@ +package com.appunite.loudius.network.model + +data class User(val id: Int, val login: String) diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt index 321fd299f..afc86a565 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt @@ -3,15 +3,15 @@ package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.User import retrofit2.http.GET -import retrofit2.http.Header +import retrofit2.http.Headers import retrofit2.http.Path import retrofit2.http.Query interface GithubPullRequestsService { @GET("/search/issues") suspend fun getPullRequestsForUser( - @Header("Authorization") authorization: String, @Query("q", encoded = true) query: String, @Query("page") page: Int = 0, @Query("per_page") perPage: Int = 100, @@ -22,7 +22,6 @@ interface GithubPullRequestsService { @Path("owner") owner: String, @Path("repo") repo: String, @Path("pull_number") pullRequestNumber: String, - @Header("Authorization") token: String, ): RequestedReviewersResponse @GET("/repos/{owner}/{repo}/pulls/{pull_number}/reviews") @@ -30,6 +29,9 @@ interface GithubPullRequestsService { @Path("owner") owner: String, @Path("repo") repo: String, @Path("pull_number") pullRequestNumber: String, - @Header("Authorization") token: String, ): List + + @Headers("Accept: application/json") + @GET("user") + suspend fun getUser(): User } diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index 8f6580100..8cffbb064 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.network.utils import com.google.gson.Gson +import java.io.IOException import org.json.JSONException import retrofit2.HttpException -import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt new file mode 100644 index 000000000..0b1e302ee --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt @@ -0,0 +1,17 @@ +package com.appunite.loudius.network.utils + +import com.appunite.loudius.domain.UserRepository +import javax.inject.Inject +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor @Inject constructor( + private val userRepository: UserRepository +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val authenticatedRequest = chain.request().newBuilder() + .addHeader("Authorization", "Bearer ${userRepository.getAccessToken()}") + .build() + return chain.proceed(authenticatedRequest) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 4be850bcc..da51d17e2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,7 +24,7 @@ class PullRequestsViewModel @Inject constructor( init { viewModelScope.launch { - gitHubReposRepository.getPullRequestsForUser("kezc") // TODO get logged user + gitHubReposRepository.getCurrentUserPullRequests() .onSuccess { state = state.copy(pullRequests = it.items) } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt index ba2253425..386c63ed5 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt @@ -7,11 +7,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberUpdatedState import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.delay @Composable fun ReposScreen( intent: Intent, viewModel: ReposViewModel = hiltViewModel(), + onNavigateToPullRequest: () -> Unit, ) { val code = intent.data?.getQueryParameter("code") val rememberedCode = rememberUpdatedState(newValue = code) @@ -22,6 +24,8 @@ fun ReposScreen( rememberedCode.value?.let { viewModel.getAccessToken(it) intent.data = null + delay(1000) + onNavigateToPullRequest() } } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt new file mode 100644 index 000000000..9d13ab81a --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt @@ -0,0 +1,18 @@ +package com.appunite.loudius.fakes + +import com.appunite.loudius.domain.UserRepository +import com.appunite.loudius.network.model.AccessToken + +class FakeUserRepository : UserRepository { + override suspend fun fetchAccessToken( + clientId: String, + clientSecret: String, + code: String + ): Result { + return Result.success("validToken") + } + + override fun getAccessToken(): String { + return "validToken" + } +} diff --git a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt new file mode 100644 index 000000000..aab2bb2c3 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt @@ -0,0 +1,50 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.appunite.loudius.network + +import com.appunite.loudius.fakes.FakeUserRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import retrofit2.http.GET + +class AuthInterceptorTest { + private val fakeUserRepository = FakeUserRepository() + private val testOkHttpClient = testOkHttpClient(fakeUserRepository) + private val mockWebServer: MockWebServer = MockWebServer() + private val service = retrofitTestDouble( + mockWebServer = mockWebServer, client = testOkHttpClient + ).create(TestApi::class.java) + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun `GIVEN saved token WHEN making an api call THEN authorization token should be added`() { + runTest { + val testDataJson = "{\"name\":\"test\"}" + val successResponse = MockResponse().setBody(testDataJson) + mockWebServer.enqueue(successResponse) + + service.test() + val request = mockWebServer.takeRequest() + val header = request.getHeader("authorization") + + assertEquals("Bearer validToken", header) + } + } + + private interface TestApi { + + @GET("/test") + suspend fun test(): TestData + } + + private data class TestData(val name: String) +} diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 0e4969044..728b12f23 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -1,21 +1,26 @@ package com.appunite.loudius.network +import com.appunite.loudius.domain.UserRepository +import com.appunite.loudius.fakes.FakeUserRepository +import com.appunite.loudius.network.utils.AuthInterceptor import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit -private fun testOkHttpClient() = OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.SECONDS) - .readTimeout(1, TimeUnit.SECONDS) - .writeTimeout(1, TimeUnit.SECONDS) - .build() +fun testOkHttpClient(userRepository: UserRepository = FakeUserRepository()) = + OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(1, TimeUnit.SECONDS) + .writeTimeout(1, TimeUnit.SECONDS) + .addInterceptor(AuthInterceptor(userRepository)) + .build() private fun testGson() = GsonBuilder() diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index d5622bc8d..da7e69e63 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -48,7 +48,6 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken", ) Assertions.assertInstanceOf( WebException.NetworkError::class.java, @@ -98,7 +97,6 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken", ) val reviewer = Reviewer("1", "exampleLogin", "https://example/avatar") @@ -128,7 +126,6 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken", ) val expected = Result.failure( @@ -157,7 +154,6 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken", ) Assertions.assertInstanceOf( WebException.NetworkError::class.java, @@ -221,14 +217,13 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken", ) val expected = Result.success( listOf( Review( "1", - User(33498031), + User(33498031, "exampleUser"), ReviewState.COMMENTED, LocalDateTime.parse("2023-03-02T10:21:36"), ), @@ -259,7 +254,6 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "validAccessToken", ) val expected = Result.failure( From 7c3f2f02b0e169aa80155e097e302428bdc5e98b Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 7 Mar 2023 10:36:37 +0000 Subject: [PATCH 111/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/di/NetworkModule.kt | 10 +++++----- .../loudius/domain/GitHubPullRequestsRepository.kt | 2 +- .../com/appunite/loudius/network/utils/ApiCallUtil.kt | 2 +- .../appunite/loudius/network/utils/AuthInterceptor.kt | 4 ++-- .../com/appunite/loudius/fakes/FakeUserRepository.kt | 2 +- .../appunite/loudius/network/AuthInterceptorTest.kt | 3 ++- .../com/appunite/loudius/network/NetworkTestDoubles.kt | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 3963c55ed..e20a73a55 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -1,8 +1,8 @@ package com.appunite.loudius.di import com.appunite.loudius.common.Constants -import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.appunite.loudius.network.utils.AuthInterceptor +import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -10,12 +10,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import java.time.LocalDateTime -import javax.inject.Singleton import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -40,7 +40,7 @@ object NetworkModule { fun provideAuthRetrofit( gson: Gson, @AuthAPI baseUrl: String, - loggingInterceptor: HttpLoggingInterceptor + loggingInterceptor: HttpLoggingInterceptor, ): Retrofit { val okHttpClient = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) @@ -60,7 +60,7 @@ object NetworkModule { gson: Gson, @BaseAPI baseAPIUrl: String, loggingInterceptor: HttpLoggingInterceptor, - authInterceptor: AuthInterceptor + authInterceptor: AuthInterceptor, ): Retrofit { val okHttpClient = OkHttpClient.Builder() .addInterceptor(authInterceptor) diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index b6dea4761..16dd1a65e 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -1,7 +1,7 @@ package com.appunite.loudius.domain -import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource import com.appunite.loudius.common.flatMap +import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index 8cffbb064..8f6580100 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.network.utils import com.google.gson.Gson -import java.io.IOException import org.json.JSONException import retrofit2.HttpException +import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt index 0b1e302ee..b67248125 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt @@ -1,12 +1,12 @@ package com.appunite.loudius.network.utils import com.appunite.loudius.domain.UserRepository -import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Response +import javax.inject.Inject class AuthInterceptor @Inject constructor( - private val userRepository: UserRepository + private val userRepository: UserRepository, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val authenticatedRequest = chain.request().newBuilder() diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt index 9d13ab81a..883c1153b 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt @@ -7,7 +7,7 @@ class FakeUserRepository : UserRepository { override suspend fun fetchAccessToken( clientId: String, clientSecret: String, - code: String + code: String, ): Result { return Result.success("validToken") } diff --git a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt index aab2bb2c3..dbf15098e 100644 --- a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt @@ -17,7 +17,8 @@ class AuthInterceptorTest { private val testOkHttpClient = testOkHttpClient(fakeUserRepository) private val mockWebServer: MockWebServer = MockWebServer() private val service = retrofitTestDouble( - mockWebServer = mockWebServer, client = testOkHttpClient + mockWebServer = mockWebServer, + client = testOkHttpClient, ).create(TestApi::class.java) @AfterEach diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 728b12f23..bbe849551 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -7,12 +7,12 @@ import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit fun testOkHttpClient(userRepository: UserRepository = FakeUserRepository()) = OkHttpClient.Builder() From 4626be2c2f3dfdd067b5557a466bffb2a3a55c16 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 7 Mar 2023 11:59:55 +0100 Subject: [PATCH 112/526] Add stateful composable to ReviewersScreen.kt and rename few composables. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index b0a47f1c3..c91f8ea1d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -21,31 +21,46 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.ui.utils.bottomBorder +@Composable +fun ReviewersScreen(navigateBack: () -> Unit, viewModel: ReviewersViewModel = hiltViewModel()) { + val state = viewModel.state + ReviewersScreenStateless( + topBarTitle = "Pull request #19", + reviewers = state.reviewers, + onClickBackArrow = navigateBack + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun DetailsScreenStateless(topBarTitle: String, reviewers: List) { +private fun ReviewersScreenStateless( + topBarTitle: String, + reviewers: List, + onClickBackArrow: () -> Unit +) { Scaffold( - topBar = { LoudiusTopAppBar(onClickBackArrow = {}, title = topBarTitle) }, + topBar = { LoudiusTopAppBar(onClickBackArrow = onClickBackArrow, title = topBarTitle) }, content = { padding -> - DetailsScreenContent(reviewers, modifier = Modifier.padding(padding)) + ReviewersScreenContent(reviewers, modifier = Modifier.padding(padding)) }, modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) } @Composable -private fun DetailsScreenContent(reviewers: List, modifier: Modifier) { +private fun ReviewersScreenContent(reviewers: List, modifier: Modifier) { LazyColumn( modifier = modifier.fillMaxWidth(), ) { itemsIndexed(reviewers) { index, reviewer -> - ReviewerView( + ReviewerItem( reviewer = reviewer, backgroundColor = resolveReviewerBackgroundColor(index), onNotifyClick = {}, @@ -59,7 +74,7 @@ private fun resolveReviewerBackgroundColor(index: Int) = if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface @Composable -private fun ReviewerView(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { +private fun ReviewerItem(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -111,7 +126,7 @@ private fun resolveIsReviewedText(reviewer: Reviewer) = if (reviewer.isReviewDon @Composable private fun ReviewerName(reviewer: Reviewer) { Text( - text = reviewer.name, + text = reviewer.login, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, ) @@ -131,8 +146,8 @@ private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifie @Composable private fun ReviewerViewPreview() { LoudiusTheme { - ReviewerView( - reviewer = Reviewer("Kezc", true, 12, 12), + ReviewerItem( + reviewer = Reviewer(1, "Kezc", true, 12, 12), backgroundColor = MaterialTheme.colorScheme.surface, ) {} } @@ -141,12 +156,12 @@ private fun ReviewerViewPreview() { @Preview @Composable fun DetailsScreenPreview() { - val reviewer1 = Reviewer("Kezc", true, 24, 12) - val reviewer2 = Reviewer("Krzysiudan", false, 24, 0) - val reviewer3 = Reviewer("Weronika", false, 24, 0) - val reviewer4 = Reviewer("Jacek", false, 24, 0) + val reviewer1 = Reviewer(1, "Kezc", true, 24, 12) + val reviewer2 = Reviewer(2, "Krzysiudan", false, 24, 0) + val reviewer3 = Reviewer(3, "Weronika", false, 24, 0) + val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { - DetailsScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers) + ReviewersScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers, {}) } } From 9819e552f5fa3efce38947b029591492d1bafb5a Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 7 Mar 2023 12:00:51 +0100 Subject: [PATCH 113/526] Implement ReviewersScreenViewModel. --- .../appunite/loudius/domain/model/Reviewer.kt | 3 +- .../{Reviewer.kt => RequestedReviewer.kt} | 6 ++- .../model/RequestedReviewersResponse.kt | 2 +- .../appunite/loudius/network/model/Review.kt | 2 + .../ui/reviewers/ReviewersViewModel.kt | 52 +++++++++++++++++++ .../PullRequestsNetworkDataSourceTest.kt | 15 +++--- 6 files changed, 69 insertions(+), 11 deletions(-) rename app/src/main/java/com/appunite/loudius/network/model/{Reviewer.kt => RequestedReviewer.kt} (53%) create mode 100644 app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt diff --git a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt index 6189cdf5c..9e0f92e65 100644 --- a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt @@ -1,7 +1,8 @@ package com.appunite.loudius.domain.model data class Reviewer( - val name: String, + val id: Int, + val login: String, val isReviewDone: Boolean, val hoursFromPRStart: Int, val hoursFromReviewDone: Int?, diff --git a/app/src/main/java/com/appunite/loudius/network/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt similarity index 53% rename from app/src/main/java/com/appunite/loudius/network/model/Reviewer.kt rename to app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt index bb7795bd7..96362eedb 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt @@ -1,7 +1,9 @@ package com.appunite.loudius.network.model -data class Reviewer( - val id: String, +data class RequestedReviewer( + val id: Int, val login: String, val avatarUrl: String, ) + +//reviewer id z Review -> user -> id diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt index 7ca494832..908ed5c36 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt @@ -1,5 +1,5 @@ package com.appunite.loudius.network.model data class RequestedReviewersResponse( - val users: List, + val users: List, ) diff --git a/app/src/main/java/com/appunite/loudius/network/model/Review.kt b/app/src/main/java/com/appunite/loudius/network/model/Review.kt index 4c4396406..fbb0d1e63 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/Review.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/Review.kt @@ -11,4 +11,6 @@ data class Review( data class User( val id: Int, + val login: String, + val avatarUrl: String, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt new file mode 100644 index 000000000..398ed3db4 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -0,0 +1,52 @@ +package com.appunite.loudius.ui.reviewers + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.appunite.loudius.domain.GitHubPullRequestsRepository +import com.appunite.loudius.domain.model.Reviewer +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +data class ReviewersState( + val reviewers: List = emptyList() +) + +@HiltViewModel +class ReviewersViewModel @Inject constructor( + private val repository: GitHubPullRequestsRepository +) : ViewModel() { + var state by mutableStateOf(ReviewersState()) + private set + + init { + + viewModelScope.launch { + fetchRequestedReviewers() + fetchReviews() + } + } + + + private suspend fun fetchRequestedReviewers() { + repository.getReviewers("Appunite", "Loudius", "19").onSuccess { response -> + val reviewers = response.users.map { + Reviewer(it.id, it.login, false, 10, null) + } + state = state.copy(reviewers = state.reviewers + reviewers) + } + } + + private suspend fun fetchReviews() { + repository.getReviews("Appunite", "Loudius", "19").onSuccess { reviews -> + reviews.groupBy { it.user.id }.map { singleUser -> + val latestReview = singleUser.value.minBy { it.submittedAt } + + Reviewer(latestReview.user.id, latestReview.user.login, true, 10, 10) + } + } + } +} diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index d5622bc8d..6c705e18f 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -1,13 +1,14 @@ package com.appunite.loudius.network.datasource +import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.ReviewState -import com.appunite.loudius.network.model.Reviewer import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.GithubPullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -18,7 +19,6 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -101,8 +101,9 @@ class PullRequestsNetworkDataSourceTest { "validAccessToken", ) - val reviewer = Reviewer("1", "exampleLogin", "https://example/avatar") - val expected = Result.success(RequestedReviewersResponse(listOf(reviewer))) + val requestedReviewer = + RequestedReviewer(1, "exampleLogin", "https://example/avatar") + val expected = Result.success(RequestedReviewersResponse(listOf(requestedReviewer))) assertEquals(expected, actualResponse) { "Data should be valid" } } @@ -176,9 +177,9 @@ class PullRequestsNetworkDataSourceTest { "node_id": "exampleId", "user": { "login": "exampleUser", - "id": 33498031, + "id": 10000000, "node_id": "exampleNodeId", - "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", + "avatar_url": "https://avatars.com/u/10000000", "gravatar_id": "", "url": "https://api.github.com/users/exampleUser", "html_url": "https://github.com/exampleUser", @@ -228,7 +229,7 @@ class PullRequestsNetworkDataSourceTest { listOf( Review( "1", - User(33498031), + User(10000000, "exampleUser", "https://avatars.com/u/10000000"), ReviewState.COMMENTED, LocalDateTime.parse("2023-03-02T10:21:36"), ), From 34ed2a0e11ada924524b830a6a55873558d84668 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 7 Mar 2023 12:02:31 +0100 Subject: [PATCH 114/526] Add navigation destination to the ReviewerScreen. --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 4 ++++ app/src/main/java/com/appunite/loudius/common/Screen.kt | 2 ++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index f092faa9f..ff99e84d1 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -16,6 +16,7 @@ import com.appunite.loudius.common.Screen import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.repos.ReposScreen +import com.appunite.loudius.ui.reviewers.ReviewersScreen import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.AndroidEntryPoint @@ -51,6 +52,9 @@ class MainActivity : ComponentActivity() { composable(route = Screen.PullRequests.route) { PullRequestsScreen() } + composable(route = Screen.Reviewers.route) { + ReviewersScreen({ navController.popBackStack() }) + } } } } diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index bb5bd3502..6bbfa3b60 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -7,4 +7,6 @@ sealed class Screen(val route: String) { object Repos : Screen("repos_screen") object PullRequests : Screen("pull_requests_screen") + + object Reviewers : Screen("reviewers_screen") } From 1d59b27b4b27a372ef0486f19b34046b1f9c8271 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 7 Mar 2023 12:52:08 +0100 Subject: [PATCH 115/526] Remove comment. --- .../com/appunite/loudius/network/model/RequestedReviewer.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt index 96362eedb..ae37c9a6f 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt @@ -5,5 +5,3 @@ data class RequestedReviewer( val login: String, val avatarUrl: String, ) - -//reviewer id z Review -> user -> id From d07660cd2a5984fafce09b5a9c24467d03c4c2da Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 7 Mar 2023 13:01:36 +0100 Subject: [PATCH 116/526] Remove unnecessary header --- .../loudius/network/services/GithubPullRequestsService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt index afc86a565..ab22702fa 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt @@ -31,7 +31,6 @@ interface GithubPullRequestsService { @Path("pull_number") pullRequestNumber: String, ): List - @Headers("Accept: application/json") @GET("user") suspend fun getUser(): User } From 87b4f283e10fdf39666644431c8106ac9970fb1e Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 7 Mar 2023 12:08:33 +0000 Subject: [PATCH 117/526] [MegaLinter] Apply linters fixes --- .../loudius/network/services/GithubPullRequestsService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt index ab22702fa..12e8b551a 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt @@ -5,7 +5,6 @@ import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.User import retrofit2.http.GET -import retrofit2.http.Headers import retrofit2.http.Path import retrofit2.http.Query From 4cbb61e0d1c98926f22fd27199321dc17653c433 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 7 Mar 2023 15:31:21 +0100 Subject: [PATCH 118/526] SIL-85: add error screen and extract LoudiusOutlinedButton --- .../ui/components/LoudiusErrorScreen.kt | 67 ++++++++++++++++++ .../ui/components/LoudiusOutlinedButton.kt | 41 +++++++++++ .../appunite/loudius/ui/login/LoginScreen.kt | 39 ++-------- .../com/appunite/loudius/ui/theme/Color.kt | 2 +- .../com/appunite/loudius/ui/theme/Theme.kt | 2 +- .../main/res/drawable-hdpi/error_image.png | Bin 0 -> 57401 bytes .../main/res/drawable-mdpi/error_image.png | Bin 0 -> 32335 bytes .../main/res/drawable-xhdpi/error_image.png | Bin 0 -> 85872 bytes .../main/res/drawable-xxhdpi/error_image.png | Bin 0 -> 159582 bytes .../main/res/drawable-xxxhdpi/error_image.png | Bin 0 -> 232172 bytes app/src/main/res/values/strings.xml | 2 + 11 files changed, 119 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt create mode 100644 app/src/main/res/drawable-hdpi/error_image.png create mode 100644 app/src/main/res/drawable-mdpi/error_image.png create mode 100644 app/src/main/res/drawable-xhdpi/error_image.png create mode 100644 app/src/main/res/drawable-xxhdpi/error_image.png create mode 100644 app/src/main/res/drawable-xxxhdpi/error_image.png diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt new file mode 100644 index 000000000..613fe6be8 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -0,0 +1,67 @@ +package com.appunite.loudius.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.appunite.loudius.R +import com.appunite.loudius.ui.theme.LoudiusTheme + +@Composable +fun LoudiusErrorScreen( + errorText: String, + buttonText: String, + onButtonClick: () -> Unit +) { + Column( + modifier = Modifier.padding(top = 142.dp).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(57.dp) + ) { + ErrorImage() + ErrorText(text = errorText) + LoudiusOutlinedButton( + onClick = onButtonClick, + text = buttonText + ) + } +} + +@Composable +private fun ErrorImage() { + Image( + painter = painterResource(id = R.drawable.error_image), + contentDescription = stringResource(R.string.error_image_desc), + ) +} + +@Composable +private fun ErrorText(text: String) { + Text( + text = text, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleLarge + ) +} + +@Preview(showSystemUi = true) +@Composable +fun LoudiusErrorScreenPreview() { + LoudiusTheme { + LoudiusErrorScreen( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(R.string.try_again_text), + onButtonClick = {} + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt new file mode 100644 index 000000000..f75c8b2c3 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt @@ -0,0 +1,41 @@ +package com.appunite.loudius.ui.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +@Composable +fun LoudiusOutlinedButton( + onClick: () -> Unit, + text: String, + iconPainter: Painter? = null, + iconDescription: String? = null +) { + OutlinedButton( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 46.dp), + ) { + if (iconPainter != null) { + Icon( + painter = iconPainter, + contentDescription = iconDescription, + tint = Color.Black, + ) + } + Text( + modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), + text = text, + color = MaterialTheme.colorScheme.tertiary, + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 4f6227832..db3a30888 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -7,41 +7,36 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTH_API_URL import com.appunite.loudius.common.Constants.AUTH_PATH import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID import com.appunite.loudius.common.Constants.SCOPE_PARAM -import com.appunite.loudius.ui.theme.Pink40 +import com.appunite.loudius.ui.components.LoudiusOutlinedButton @Composable fun LoginScreen() { + val context = LocalContext.current Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally, ) { LoginImage() - LoginButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 46.dp), + LoudiusOutlinedButton( + onClick = { startAuthorizing(context) }, + text = stringResource(id = R.string.login), + iconPainter = painterResource(id = R.drawable.ic_github), + iconDescription = stringResource(R.string.github_icon) ) } } @@ -56,26 +51,6 @@ fun LoginImage() { ) } -@Composable -fun LoginButton(modifier: Modifier) { - val context = LocalContext.current - OutlinedButton( - onClick = { startAuthorizing(context) }, - modifier = modifier, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_github), - contentDescription = stringResource(R.string.github_icon), - tint = Color.Black, - ) - Text( - modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), - text = stringResource(id = R.string.login), - color = Pink40, - ) - } -} - private fun startAuthorizing(context: Context) { val url = buildAuthorizationUrl() val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt index 418ce37d1..dd0bade0b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt @@ -15,4 +15,4 @@ val Black90 = Color(0xFF1C1B1F) val NeutralVariant30 = Color(0xFF49454F) val NeutralVariant80 = Color(0xFFCAC4D0) -val Error40 = Color(0xFFB3261E) +val Error20 = Color(0xFF601410) diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index 8457cf418..37e2714bc 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -29,7 +29,7 @@ private val LightColorScheme = lightColorScheme( onSurface = Black90, onSurfaceVariant = NeutralVariant30, outlineVariant = NeutralVariant80, - error = Error40, + error = Error20, /* Other default colors to override background = Color(0xFFFFFBFE), diff --git a/app/src/main/res/drawable-hdpi/error_image.png b/app/src/main/res/drawable-hdpi/error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..c97cedea2aa453087b16d11732ef47abcfa8fbe7 GIT binary patch literal 57401 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 Y)>q4(_iDHA_cP#9me-K0gPVu{4+1|AssI20 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/error_image.png b/app/src/main/res/drawable-mdpi/error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..330aa7f5a647bb25a3224312d8c9416f46ac4e24 GIT binary patch literal 32335 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

EEQe({cN-Yst;m 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 LNkvXXu0mjf!(@2; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/error_image.png b/app/src/main/res/drawable-xxhdpi/error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..df5d5e1cb65ea952035dc22215bcc4c9852156f5 GIT binary patch literal 159582 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 z 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/error_image.png b/app/src/main/res/drawable-xxxhdpi/error_image.png new file mode 100644 index 0000000000000000000000000000000000000000..b40bd3bb51983a0b755a79a94c5559b330b067ed GIT binary patch literal 232172 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�_ZXjMxZI1yhI2+$K1LAFsdiFB^m2P*%;i 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 z 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�K8(~a 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 Something went wrong… OK + error image + Try again From 0baf6f5d6ef38d8152b2908c41a71e9301af7260 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 7 Mar 2023 14:34:44 +0000 Subject: [PATCH 119/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/components/LoudiusErrorScreen.kt | 10 +++++----- .../loudius/ui/components/LoudiusOutlinedButton.kt | 2 +- .../java/com/appunite/loudius/ui/login/LoginScreen.kt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index 613fe6be8..f7e14a1c3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -21,18 +21,18 @@ import com.appunite.loudius.ui.theme.LoudiusTheme fun LoudiusErrorScreen( errorText: String, buttonText: String, - onButtonClick: () -> Unit + onButtonClick: () -> Unit, ) { Column( modifier = Modifier.padding(top = 142.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(57.dp) + verticalArrangement = Arrangement.spacedBy(57.dp), ) { ErrorImage() ErrorText(text = errorText) LoudiusOutlinedButton( onClick = onButtonClick, - text = buttonText + text = buttonText, ) } } @@ -50,7 +50,7 @@ private fun ErrorText(text: String) { Text( text = text, color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.titleLarge + style = MaterialTheme.typography.titleLarge, ) } @@ -61,7 +61,7 @@ fun LoudiusErrorScreenPreview() { LoudiusErrorScreen( errorText = stringResource(id = R.string.error_dialog_text), buttonText = stringResource(R.string.try_again_text), - onButtonClick = {} + onButtonClick = {}, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt index f75c8b2c3..7f0752a7b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt @@ -17,7 +17,7 @@ fun LoudiusOutlinedButton( onClick: () -> Unit, text: String, iconPainter: Painter? = null, - iconDescription: String? = null + iconDescription: String? = null, ) { OutlinedButton( onClick = onClick, diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index db3a30888..74bfcbee2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -36,7 +36,7 @@ fun LoginScreen() { onClick = { startAuthorizing(context) }, text = stringResource(id = R.string.login), iconPainter = painterResource(id = R.drawable.ic_github), - iconDescription = stringResource(R.string.github_icon) + iconDescription = stringResource(R.string.github_icon), ) } } From 921725e2d35ed62942e80960c18905d66225677a Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 8 Mar 2023 09:17:57 +0100 Subject: [PATCH 120/526] SIL-85: code cleanup --- .../com/appunite/loudius/ui/components/LoudiusErrorScreen.kt | 2 +- .../com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index f7e14a1c3..178968310 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -26,7 +26,7 @@ fun LoudiusErrorScreen( Column( modifier = Modifier.padding(top = 142.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(57.dp), + verticalArrangement = Arrangement.spacedBy(56.dp), ) { ErrorImage() ErrorText(text = errorText) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt index 7f0752a7b..ddfff695b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt @@ -14,10 +14,10 @@ import androidx.compose.ui.unit.dp @Composable fun LoudiusOutlinedButton( - onClick: () -> Unit, text: String, iconPainter: Painter? = null, iconDescription: String? = null, + onClick: () -> Unit, ) { OutlinedButton( onClick = onClick, From d240bd679c9ff181b1a0f8ec3b3da8fa59292ae5 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 7 Mar 2023 12:58:19 +0100 Subject: [PATCH 121/526] Rename user and auth network classes --- .../com/appunite/loudius/di/GithubModule.kt | 33 +++++++++++-------- .../{UserRepository.kt => AuthRepository.kt} | 2 +- ...epositoryImpl.kt => AuthRepositoryImpl.kt} | 10 +++--- .../domain/GitHubPullRequestsRepository.kt | 14 +++++--- .../AuthDataSource.kt} | 14 ++++---- .../PullRequestsNetworkDataSource.kt | 4 --- .../network/datasource/UserDataSource.kt | 12 +++++++ .../{GithubApi.kt => GithubAuthService.kt} | 2 +- .../services/GithubPullRequestsService.kt | 4 --- .../network/services/GithubUserService.kt | 11 +++++++ .../loudius/network/utils/AuthInterceptor.kt | 6 ++-- .../loudius/ui/repos/ReposViewModel.kt | 6 ++-- ...yImplTest.kt => AuthRepositoryImplTest.kt} | 8 ++--- ...serRepository.kt => FakeAuthRepository.kt} | 4 +-- .../loudius/network/AuthInterceptorTest.kt | 4 +-- .../loudius/network/NetworkTestDoubles.kt | 8 ++--- 16 files changed, 84 insertions(+), 58 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{UserRepository.kt => AuthRepository.kt} (90%) rename app/src/main/java/com/appunite/loudius/domain/{UserRepositoryImpl.kt => AuthRepositoryImpl.kt} (70%) rename app/src/main/java/com/appunite/loudius/network/{UserDataSource.kt => datasource/AuthDataSource.kt} (57%) create mode 100644 app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt rename app/src/main/java/com/appunite/loudius/network/services/{GithubApi.kt => GithubAuthService.kt} (94%) create mode 100644 app/src/main/java/com/appunite/loudius/network/services/GithubUserService.kt rename app/src/test/java/com/appunite/loudius/domain/{UserRepositoryImplTest.kt => AuthRepositoryImplTest.kt} (90%) rename app/src/test/java/com/appunite/loudius/fakes/{FakeUserRepository.kt => FakeAuthRepository.kt} (79%) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index b0eb8adfc..b44886969 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -2,12 +2,13 @@ package com.appunite.loudius.di import android.content.Context import com.appunite.loudius.domain.UserLocalDataSource -import com.appunite.loudius.domain.UserRepository -import com.appunite.loudius.domain.UserRepositoryImpl -import com.appunite.loudius.network.UserDataSource -import com.appunite.loudius.network.UserNetworkDataSource -import com.appunite.loudius.network.services.GithubApi +import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.domain.AuthRepositoryImpl +import com.appunite.loudius.network.datasource.AuthDataSource +import com.appunite.loudius.network.datasource.AuthNetworkDataSource +import com.appunite.loudius.network.services.GithubAuthService import com.appunite.loudius.network.services.GithubPullRequestsService +import com.appunite.loudius.network.services.GithubUserService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -22,25 +23,31 @@ object GithubModule { @Singleton @Provides - fun provideGithubApi(@AuthAPI retrofit: Retrofit): GithubApi = - retrofit.create(GithubApi::class.java) + fun provideGithubAuthService(@AuthAPI retrofit: Retrofit): GithubAuthService = + retrofit.create(GithubAuthService::class.java) + @Singleton + @Provides + fun provideGithubUserService(@BaseAPI retrofit: Retrofit): GithubUserService = + retrofit.create(GithubUserService::class.java) + + @Singleton @Provides fun provideGithubReposService(@BaseAPI retrofit: Retrofit): GithubPullRequestsService = retrofit.create(GithubPullRequestsService::class.java) @Singleton @Provides - fun provideUserRepository( - userDataSource: UserDataSource, + fun provideAuthRepository( + authDataSource: AuthDataSource, userLocalDataSource: UserLocalDataSource, - ): UserRepository = UserRepositoryImpl(userDataSource, userLocalDataSource) + ): AuthRepository = AuthRepositoryImpl(authDataSource, userLocalDataSource) @Singleton @Provides - fun provideUserDataSource( - api: GithubApi, - ): UserDataSource = UserNetworkDataSource(api) + fun provideAuthServiceDataSource( + service: GithubAuthService, + ): AuthDataSource = AuthNetworkDataSource(service) @Singleton @Provides diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt similarity index 90% rename from app/src/main/java/com/appunite/loudius/domain/UserRepository.kt rename to app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt index ae6eab7d3..5e8b72edb 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.model.AccessToken -interface UserRepository { +interface AuthRepository { suspend fun fetchAccessToken( clientId: String, diff --git a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt similarity index 70% rename from app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt rename to app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt index 8b0dbe889..bae1813db 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt @@ -1,22 +1,22 @@ package com.appunite.loudius.domain -import com.appunite.loudius.network.UserDataSource +import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject import javax.inject.Singleton @Singleton -class UserRepositoryImpl @Inject constructor( - private val userDataSource: UserDataSource, +class AuthRepositoryImpl @Inject constructor( + private val authDataSource: AuthDataSource, private val userLocalDataSource: UserLocalDataSource, -) : UserRepository { +) : AuthRepository { override suspend fun fetchAccessToken( clientId: String, clientSecret: String, code: String, ): Result { - val result = userDataSource.getAccessToken(clientId, clientSecret, code) + val result = authDataSource.getAccessToken(clientId, clientSecret, code) result.onSuccess { userLocalDataSource.saveAccessToken(it) } return result } diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt index 16dd1a65e..e4d317a40 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt @@ -2,6 +2,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.common.flatMap import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource +import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review @@ -23,11 +24,14 @@ interface PullRequestRepository { suspend fun getCurrentUserPullRequests(): Result } -class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSource: PullRequestsNetworkDataSource) : +class GitHubPullRequestsRepository @Inject constructor( + private val pullRequestsNetworkDataSource: PullRequestsNetworkDataSource, + private val userDataSource: UserDataSource, +) : PullRequestRepository { override suspend fun getCurrentUserPullRequests(): Result { - val currentUser = remoteDataSource.getUser() - return currentUser.flatMap { remoteDataSource.getPullRequestsForUser(it.login) } + val currentUser = userDataSource.getUser() + return currentUser.flatMap { pullRequestsNetworkDataSource.getPullRequestsForUser(it.login) } } override suspend fun getReviews( @@ -35,12 +39,12 @@ class GitHubPullRequestsRepository @Inject constructor(private val remoteDataSou repo: String, pullRequestNumber: String, ): Result> = - remoteDataSource.getReviews(owner, repo, pullRequestNumber) + pullRequestsNetworkDataSource.getReviews(owner, repo, pullRequestNumber) override suspend fun getReviewers( owner: String, repo: String, pullRequestNumber: String, ): Result = - remoteDataSource.getReviewers(owner, repo, pullRequestNumber) + pullRequestsNetworkDataSource.getReviewers(owner, repo, pullRequestNumber) } diff --git a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt similarity index 57% rename from app/src/main/java/com/appunite/loudius/network/UserDataSource.kt rename to app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index f1bbe804b..927f38097 100644 --- a/app/src/main/java/com/appunite/loudius/network/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -1,12 +1,12 @@ -package com.appunite.loudius.network +package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.AccessToken -import com.appunite.loudius.network.services.GithubApi +import com.appunite.loudius.network.services.GithubAuthService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject import javax.inject.Singleton -interface UserDataSource { +interface AuthDataSource { suspend fun getAccessToken( clientId: String, @@ -16,14 +16,14 @@ interface UserDataSource { } @Singleton -class UserNetworkDataSource @Inject constructor( - private val api: GithubApi, -) : UserDataSource { +class AuthNetworkDataSource @Inject constructor( + private val authService: GithubAuthService, +) : AuthDataSource { override suspend fun getAccessToken( clientId: String, clientSecret: String, code: String, ): Result = - safeApiCall { api.getAccessToken(clientId, clientSecret, code).accessToken } + safeApiCall { authService.getAccessToken(clientId, clientSecret, code).accessToken } } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 984daacfd..191171fdb 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -45,8 +45,4 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Git ): Result> = safeApiCall { service.getReviews(owner, repository, pullRequestNumber) } - - suspend fun getUser(): Result = safeApiCall { - service.getUser() - } } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt new file mode 100644 index 000000000..a4be6c9e1 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt @@ -0,0 +1,12 @@ +package com.appunite.loudius.network.datasource + +import com.appunite.loudius.network.model.User +import com.appunite.loudius.network.services.GithubUserService +import com.appunite.loudius.network.utils.safeApiCall +import javax.inject.Inject + +class UserDataSource @Inject constructor(private val userService: GithubUserService) { + suspend fun getUser(): Result = safeApiCall { + userService.getUser() + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubAuthService.kt similarity index 94% rename from app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt rename to app/src/main/java/com/appunite/loudius/network/services/GithubAuthService.kt index ccfefc0e7..2003aeb95 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubApi.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubAuthService.kt @@ -6,7 +6,7 @@ import retrofit2.http.FormUrlEncoded import retrofit2.http.Headers import retrofit2.http.POST -interface GithubApi { +interface GithubAuthService { @Headers("Accept: application/json") @POST("login/oauth/access_token") diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt index 12e8b551a..7eefa3234 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt @@ -3,7 +3,6 @@ package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.User import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query @@ -29,7 +28,4 @@ interface GithubPullRequestsService { @Path("repo") repo: String, @Path("pull_number") pullRequestNumber: String, ): List - - @GET("user") - suspend fun getUser(): User } diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubUserService.kt b/app/src/main/java/com/appunite/loudius/network/services/GithubUserService.kt new file mode 100644 index 000000000..5ade4ce21 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/services/GithubUserService.kt @@ -0,0 +1,11 @@ +package com.appunite.loudius.network.services + +import com.appunite.loudius.network.model.User +import retrofit2.http.GET +import retrofit2.http.Headers + +interface GithubUserService { + @Headers("Accept: application/json") + @GET("user") + suspend fun getUser(): User +} diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt index b67248125..3a904863f 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt @@ -1,16 +1,16 @@ package com.appunite.loudius.network.utils -import com.appunite.loudius.domain.UserRepository +import com.appunite.loudius.domain.AuthRepository import okhttp3.Interceptor import okhttp3.Response import javax.inject.Inject class AuthInterceptor @Inject constructor( - private val userRepository: UserRepository, + private val authRepository: AuthRepository, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val authenticatedRequest = chain.request().newBuilder() - .addHeader("Authorization", "Bearer ${userRepository.getAccessToken()}") + .addHeader("Authorization", "Bearer ${authRepository.getAccessToken()}") .build() return chain.proceed(authenticatedRequest) } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index 60087423c..60fbe8208 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -5,19 +5,19 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID -import com.appunite.loudius.domain.UserRepository +import com.appunite.loudius.domain.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ReposViewModel @Inject constructor( - private val userRepository: UserRepository, + private val authRepository: AuthRepository, ) : ViewModel() { fun getAccessToken(code: String) { viewModelScope.launch { - userRepository.fetchAccessToken( + authRepository.fetchAccessToken( clientId = CLIENT_ID, clientSecret = BuildConfig.CLIENT_SECRET, code = code, diff --git a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt similarity index 90% rename from app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt rename to app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index a8acfbee6..18e33c32b 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.domain -import com.appunite.loudius.network.UserDataSource +import com.appunite.loudius.network.datasource.AuthDataSource import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -11,8 +11,8 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @OptIn(ExperimentalCoroutinesApi::class) -class UserRepositoryImplTest { - private val networkDataSource: UserDataSource = mockk { +class AuthRepositoryImplTest { + private val networkDataSource: AuthDataSource = mockk { coEvery { getAccessToken(any(), any(), any()) } returns Result.success("validAccessToken") @@ -21,7 +21,7 @@ class UserRepositoryImplTest { every { getAccessToken() } returns "validAccessToken" every { saveAccessToken(any()) } returns Unit } - private val repository = UserRepositoryImpl(networkDataSource, localDataSource) + private val repository = AuthRepositoryImpl(networkDataSource, localDataSource) @Test fun `GIVEN fetch access token function WHEN processing THEN return success with new valid token`() = diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt similarity index 79% rename from app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt rename to app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 883c1153b..453464d7a 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeUserRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.fakes -import com.appunite.loudius.domain.UserRepository +import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.network.model.AccessToken -class FakeUserRepository : UserRepository { +class FakeAuthRepository : AuthRepository { override suspend fun fetchAccessToken( clientId: String, clientSecret: String, diff --git a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt index dbf15098e..cc72e854a 100644 --- a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.network -import com.appunite.loudius.fakes.FakeUserRepository +import com.appunite.loudius.fakes.FakeAuthRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test import retrofit2.http.GET class AuthInterceptorTest { - private val fakeUserRepository = FakeUserRepository() + private val fakeUserRepository = FakeAuthRepository() private val testOkHttpClient = testOkHttpClient(fakeUserRepository) private val mockWebServer: MockWebServer = MockWebServer() private val service = retrofitTestDouble( diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index bbe849551..b91fac13f 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -1,7 +1,7 @@ package com.appunite.loudius.network -import com.appunite.loudius.domain.UserRepository -import com.appunite.loudius.fakes.FakeUserRepository +import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.fakes.FakeAuthRepository import com.appunite.loudius.network.utils.AuthInterceptor import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy @@ -14,12 +14,12 @@ import retrofit2.converter.gson.GsonConverterFactory import java.time.LocalDateTime import java.util.concurrent.TimeUnit -fun testOkHttpClient(userRepository: UserRepository = FakeUserRepository()) = +fun testOkHttpClient(authRepository: AuthRepository = FakeAuthRepository()) = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) .writeTimeout(1, TimeUnit.SECONDS) - .addInterceptor(AuthInterceptor(userRepository)) + .addInterceptor(AuthInterceptor(authRepository)) .build() private fun testGson() = From 65f32cb5434af224a2af32a3fffc18d14a9341f9 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 8 Mar 2023 12:53:59 +0100 Subject: [PATCH 122/526] Remove 'Github' prefix --- .../com/appunite/loudius/di/GithubModule.kt | 20 +++++++++---------- .../appunite/loudius/di/PullRequestModule.kt | 4 ++-- ...epository.kt => PullRequestsRepository.kt} | 2 +- .../network/datasource/AuthDataSource.kt | 4 ++-- .../PullRequestsNetworkDataSource.kt | 5 ++--- .../network/datasource/UserDataSource.kt | 4 ++-- .../{GithubAuthService.kt => AuthService.kt} | 2 +- ...uestsService.kt => PullRequestsService.kt} | 2 +- .../{GithubUserService.kt => UserService.kt} | 2 +- .../ui/pullrequests/PullRequestsViewModel.kt | 6 +++--- .../PullRequestsNetworkDataSourceTest.kt | 4 ++-- 11 files changed, 27 insertions(+), 28 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{GitHubPullRequestsRepository.kt => PullRequestsRepository.kt} (96%) rename app/src/main/java/com/appunite/loudius/network/services/{GithubAuthService.kt => AuthService.kt} (94%) rename app/src/main/java/com/appunite/loudius/network/services/{GithubPullRequestsService.kt => PullRequestsService.kt} (96%) rename app/src/main/java/com/appunite/loudius/network/services/{GithubUserService.kt => UserService.kt} (89%) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index b44886969..63a3a06ee 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -6,9 +6,9 @@ import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.domain.AuthRepositoryImpl import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource -import com.appunite.loudius.network.services.GithubAuthService -import com.appunite.loudius.network.services.GithubPullRequestsService -import com.appunite.loudius.network.services.GithubUserService +import com.appunite.loudius.network.services.AuthService +import com.appunite.loudius.network.services.PullRequestsService +import com.appunite.loudius.network.services.UserService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,18 +23,18 @@ object GithubModule { @Singleton @Provides - fun provideGithubAuthService(@AuthAPI retrofit: Retrofit): GithubAuthService = - retrofit.create(GithubAuthService::class.java) + fun provideAuthService(@AuthAPI retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java) @Singleton @Provides - fun provideGithubUserService(@BaseAPI retrofit: Retrofit): GithubUserService = - retrofit.create(GithubUserService::class.java) + fun provideUserService(@BaseAPI retrofit: Retrofit): UserService = + retrofit.create(UserService::class.java) @Singleton @Provides - fun provideGithubReposService(@BaseAPI retrofit: Retrofit): GithubPullRequestsService = - retrofit.create(GithubPullRequestsService::class.java) + fun provideReposService(@BaseAPI retrofit: Retrofit): PullRequestsService = + retrofit.create(PullRequestsService::class.java) @Singleton @Provides @@ -46,7 +46,7 @@ object GithubModule { @Singleton @Provides fun provideAuthServiceDataSource( - service: GithubAuthService, + service: AuthService, ): AuthDataSource = AuthNetworkDataSource(service) @Singleton diff --git a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt index e8723131e..9acf23eaf 100644 --- a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt @@ -2,7 +2,7 @@ package com.appunite.loudius.di import com.appunite.loudius.network.datasource.PullRequestDataSource import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource -import com.appunite.loudius.network.services.GithubPullRequestsService +import com.appunite.loudius.network.services.PullRequestsService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,6 +15,6 @@ object PullRequestModule { @Provides @Singleton - fun providePullRequestNetworkDataSource(service: GithubPullRequestsService): PullRequestDataSource = + fun providePullRequestNetworkDataSource(service: PullRequestsService): PullRequestDataSource = PullRequestsNetworkDataSource(service) } diff --git a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestsRepository.kt similarity index 96% rename from app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt rename to app/src/main/java/com/appunite/loudius/domain/PullRequestsRepository.kt index e4d317a40..32189e770 100644 --- a/app/src/main/java/com/appunite/loudius/domain/GitHubPullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestsRepository.kt @@ -24,7 +24,7 @@ interface PullRequestRepository { suspend fun getCurrentUserPullRequests(): Result } -class GitHubPullRequestsRepository @Inject constructor( +class PullRequestsRepository @Inject constructor( private val pullRequestsNetworkDataSource: PullRequestsNetworkDataSource, private val userDataSource: UserDataSource, ) : diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index 927f38097..e3e4d92ed 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -1,7 +1,7 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.AccessToken -import com.appunite.loudius.network.services.GithubAuthService +import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject import javax.inject.Singleton @@ -17,7 +17,7 @@ interface AuthDataSource { @Singleton class AuthNetworkDataSource @Inject constructor( - private val authService: GithubAuthService, + private val authService: AuthService, ) : AuthDataSource { override suspend fun getAccessToken( diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 191171fdb..f0a16084a 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -3,8 +3,7 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.User -import com.appunite.loudius.network.services.GithubPullRequestsService +import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject import javax.inject.Singleton @@ -24,7 +23,7 @@ interface PullRequestDataSource { } @Singleton -class PullRequestsNetworkDataSource @Inject constructor(private val service: GithubPullRequestsService) : +class PullRequestsNetworkDataSource @Inject constructor(private val service: PullRequestsService) : PullRequestDataSource { suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { service.getPullRequestsForUser("author:$author type:pr state:open") diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt index a4be6c9e1..1bc062674 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt @@ -1,11 +1,11 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.User -import com.appunite.loudius.network.services.GithubUserService +import com.appunite.loudius.network.services.UserService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject -class UserDataSource @Inject constructor(private val userService: GithubUserService) { +class UserDataSource @Inject constructor(private val userService: UserService) { suspend fun getUser(): Result = safeApiCall { userService.getUser() } diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubAuthService.kt b/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt similarity index 94% rename from app/src/main/java/com/appunite/loudius/network/services/GithubAuthService.kt rename to app/src/main/java/com/appunite/loudius/network/services/AuthService.kt index 2003aeb95..2ca324698 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubAuthService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt @@ -6,7 +6,7 @@ import retrofit2.http.FormUrlEncoded import retrofit2.http.Headers import retrofit2.http.POST -interface GithubAuthService { +interface AuthService { @Headers("Accept: application/json") @POST("login/oauth/access_token") diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt similarity index 96% rename from app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt rename to app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt index 7eefa3234..ad6f1f657 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubPullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt @@ -7,7 +7,7 @@ import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query -interface GithubPullRequestsService { +interface PullRequestsService { @GET("/search/issues") suspend fun getPullRequestsForUser( @Query("q", encoded = true) query: String, diff --git a/app/src/main/java/com/appunite/loudius/network/services/GithubUserService.kt b/app/src/main/java/com/appunite/loudius/network/services/UserService.kt similarity index 89% rename from app/src/main/java/com/appunite/loudius/network/services/GithubUserService.kt rename to app/src/main/java/com/appunite/loudius/network/services/UserService.kt index 5ade4ce21..6d9f62933 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/GithubUserService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/UserService.kt @@ -4,7 +4,7 @@ import com.appunite.loudius.network.model.User import retrofit2.http.GET import retrofit2.http.Headers -interface GithubUserService { +interface UserService { @Headers("Accept: application/json") @GET("user") suspend fun getUser(): User diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index da51d17e2..10a9468ec 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.appunite.loudius.domain.GitHubPullRequestsRepository +import com.appunite.loudius.domain.PullRequestsRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -17,14 +17,14 @@ data class PullRequestState( @HiltViewModel class PullRequestsViewModel @Inject constructor( - private val gitHubReposRepository: GitHubPullRequestsRepository, + private val pullRequestsRepository: PullRequestsRepository, ) : ViewModel() { var state by mutableStateOf(PullRequestState()) private set init { viewModelScope.launch { - gitHubReposRepository.getCurrentUserPullRequests() + pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { state = state.copy(pullRequests = it.items) } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index da7e69e63..97277fc0b 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -6,7 +6,7 @@ import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.Reviewer import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble -import com.appunite.loudius.network.services.GithubPullRequestsService +import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -25,7 +25,7 @@ class PullRequestsNetworkDataSourceTest { private val mockWebServer: MockWebServer = MockWebServer() private val pullRequestsService = retrofitTestDouble(mockWebServer = mockWebServer) - .create(GithubPullRequestsService::class.java) + .create(PullRequestsService::class.java) private val pullRequestDataSource = PullRequestsNetworkDataSource(pullRequestsService) @AfterEach From ff07ab6ed36b6e0b2a1a779aa56053e757b6415d Mon Sep 17 00:00:00 2001 From: kezc Date: Wed, 8 Mar 2023 11:57:48 +0000 Subject: [PATCH 123/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/di/GithubModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 63a3a06ee..fcb968e44 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.di import android.content.Context -import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.domain.AuthRepositoryImpl +import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource import com.appunite.loudius.network.services.AuthService From 09497142a4ba7f874ee9ed0f2ff0ed34c133dbe4 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 8 Mar 2023 13:25:22 +0100 Subject: [PATCH 124/526] Add arguments for passing data to the ReviewersScreen. --- .../java/com/appunite/loudius/MainActivity.kt | 21 +++++++++-- .../com/appunite/loudius/common/Screen.kt | 23 +++++++++++- .../loudius/ui/reviewers/ReviewersScreen.kt | 11 ++++-- .../ui/reviewers/ReviewersViewModel.kt | 36 ++++++++++++++----- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index ff99e84d1..392fe3913 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -52,8 +52,25 @@ class MainActivity : ComponentActivity() { composable(route = Screen.PullRequests.route) { PullRequestsScreen() } - composable(route = Screen.Reviewers.route) { - ReviewersScreen({ navController.popBackStack() }) + composable( + route = Screen.Reviewers.route, arguments = Screen.Reviewers.arguments + ) { navBackStackEntry -> + val pullRequestNumber = + navBackStackEntry.arguments?.getString(Screen.Reviewers.pullRequestNumberArg) + val owner = + navBackStackEntry.arguments?.getString(Screen.Reviewers.ownerArg) + val repo = + navBackStackEntry.arguments?.getString(Screen.Reviewers.repoArg) + val submissionDate = + navBackStackEntry.arguments?.getString(Screen.Reviewers.submissionDateArg) + + // TODO: Handle the case with null arguments + ReviewersScreen( + pullRequestNumber, + owner, + repo, + submissionDate, + { navController.popBackStack() }) } } } diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index 6bbfa3b60..6590afbb6 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -1,6 +1,11 @@ package com.appunite.loudius.common +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavType +import androidx.navigation.navArgument + sealed class Screen(val route: String) { + open val arguments: List = emptyList() object Login : Screen("login_screen") @@ -8,5 +13,21 @@ sealed class Screen(val route: String) { object PullRequests : Screen("pull_requests_screen") - object Reviewers : Screen("reviewers_screen") + object Reviewers : + Screen("reviewers_screen/{pullRequestNumber}/{owner}/{repo}/{submissionDate}") { + const val pullRequestNumberArg = "pull_request_number" + const val ownerArg = "pull_request_number" + const val repoArg = "pull_request_number" + const val submissionDateArg = "pull_request_number" + + override val arguments: List + get() { + return listOf( + navArgument(pullRequestNumberArg) { type = NavType.StringType }, + navArgument(ownerArg) { type = NavType.StringType }, + navArgument(repoArg) { type = NavType.StringType }, + navArgument(submissionDateArg) { type = NavType.StringType }, + ) + } + } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index c91f8ea1d..783f8db70 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -29,10 +29,17 @@ import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.ui.utils.bottomBorder @Composable -fun ReviewersScreen(navigateBack: () -> Unit, viewModel: ReviewersViewModel = hiltViewModel()) { +fun ReviewersScreen( + pullRequestNumber: String?, + owner: String?, + repo: String?, + submissionDate: String?, + navigateBack: () -> Unit, + viewModel: ReviewersViewModel = hiltViewModel() +) { val state = viewModel.state ReviewersScreenStateless( - topBarTitle = "Pull request #19", + topBarTitle = "Pull request $pullRequestNumber", reviewers = state.reviewers, onClickBackArrow = navigateBack ) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 398ed3db4..a9f57dbb8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -3,8 +3,10 @@ package com.appunite.loudius.ui.reviewers import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.GitHubPullRequestsRepository import com.appunite.loudius.domain.model.Reviewer import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,27 +14,39 @@ import javax.inject.Inject import kotlinx.coroutines.launch data class ReviewersState( - val reviewers: List = emptyList() + val reviewers: List = emptyList(), + val pullRequestNumber: String = "", ) @HiltViewModel class ReviewersViewModel @Inject constructor( - private val repository: GitHubPullRequestsRepository + private val repository: GitHubPullRequestsRepository, + savedStateHandle: SavedStateHandle, ) : ViewModel() { + private val pullRequestNumber: String = + checkNotNull(savedStateHandle[Screen.Reviewers.pullRequestNumberArg]) + private val owner: String = checkNotNull(savedStateHandle[Screen.Reviewers.ownerArg]) + private val repo: String = checkNotNull(savedStateHandle[Screen.Reviewers.repoArg]) + private val submissionDate: String = + checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]) + var state by mutableStateOf(ReviewersState()) private set init { - viewModelScope.launch { - fetchRequestedReviewers() - fetchReviews() + fetchRequestedReviewers(owner, repo, pullRequestNumber) + fetchReviews(owner, repo, pullRequestNumber) } } - private suspend fun fetchRequestedReviewers() { - repository.getReviewers("Appunite", "Loudius", "19").onSuccess { response -> + private suspend fun fetchRequestedReviewers( + owner: String, + repo: String, + pullRequestNumber: String + ) { + repository.getReviewers(owner, repo, pullRequestNumber).onSuccess { response -> val reviewers = response.users.map { Reviewer(it.id, it.login, false, 10, null) } @@ -40,8 +54,12 @@ class ReviewersViewModel @Inject constructor( } } - private suspend fun fetchReviews() { - repository.getReviews("Appunite", "Loudius", "19").onSuccess { reviews -> + private suspend fun fetchReviews( + owner: String, + repo: String, + pullRequestNumber: String + ) { + repository.getReviews(owner, repo, pullRequestNumber).onSuccess { reviews -> reviews.groupBy { it.user.id }.map { singleUser -> val latestReview = singleUser.value.minBy { it.submittedAt } From 860d6bf55015778a40c167ae041bb8136f9db93c Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 8 Mar 2023 15:15:50 +0100 Subject: [PATCH 125/526] Add fetching pull request list test --- .../loudius/network/model/PullRequest.kt | 3 +- .../ui/pullrequests/PullRequestsScreen.kt | 10 +- .../PullRequestsNetworkDataSourceTest.kt | 176 +++++++++++++++++- 3 files changed, 177 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt index 10ab6122a..ea4739744 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.network.model import com.appunite.loudius.common.Constants +import java.time.LocalDateTime data class PullRequest( val id: Int, @@ -8,7 +9,7 @@ data class PullRequest( val number: Int, val repositoryUrl: String, val title: String, - val updatedAt: String, + val updatedAt: LocalDateTime, ) { val fullRepositoryName: String get() = repositoryUrl.removePrefix(REPOSITORY_PATH) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 74e3867a8..28d115bb7 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -30,6 +30,8 @@ import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme +import java.time.LocalDateTime +import java.time.ZonedDateTime @Composable fun PullRequestsScreen(viewModel: PullRequestsViewModel = hiltViewModel()) { @@ -129,7 +131,7 @@ fun PullRequestsScreenPreview() { number = 0, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", title = "[SIL-67] Details screen - network layer", - updatedAt = "2021-11-29T16:31:41Z", + updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), ), PullRequest( id = 1, @@ -137,7 +139,7 @@ fun PullRequestsScreenPreview() { number = 1, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", title = "[SIL-66] Add client secret to build config", - updatedAt = "2022-11-29T16:31:41Z", + updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), ), PullRequest( id = 2, @@ -145,7 +147,7 @@ fun PullRequestsScreenPreview() { number = 2, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", title = "[SIL-73] Storing access token", - updatedAt = "2023-01-29T16:31:41Z", + updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), ), PullRequest( id = 3, @@ -153,7 +155,7 @@ fun PullRequestsScreenPreview() { number = 3, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", title = "[SIL-62/SIL-75] Provide new annotation for API instances", - updatedAt = "2022-01-29T16:31:41Z", + updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), ), ), ) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 97277fc0b..6fae0afa4 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -1,5 +1,7 @@ package com.appunite.loudius.network.datasource +import com.appunite.loudius.network.model.PullRequest +import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.ReviewState @@ -8,6 +10,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.ZonedDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -18,14 +21,13 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { private val mockWebServer: MockWebServer = MockWebServer() - private val pullRequestsService = retrofitTestDouble(mockWebServer = mockWebServer) - .create(PullRequestsService::class.java) + private val pullRequestsService = + retrofitTestDouble(mockWebServer = mockWebServer).create(PullRequestsService::class.java) private val pullRequestDataSource = PullRequestsNetworkDataSource(pullRequestsService) @AfterEach @@ -33,6 +35,166 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.shutdown() } + @Nested + inner class GetPullRequestsForUserTest { + @Test + fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = + runTest { + mockWebServer.enqueue( + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), + ) + + val actualResponse = pullRequestDataSource.getPullRequestsForUser( + "exampleUser" + ) + Assertions.assertInstanceOf( + WebException.NetworkError::class.java, + actualResponse.exceptionOrNull(), + ) { "Exception thrown should be NetworkError type" } + } + + + @Test + fun `Given correct params WHEN successful response THEN return success`() = runTest { + //language=JSON + val jsonResponse = """ + { + "total_count":1, + "incomplete_results":false, + "items":[ + { + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", + "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", + "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", + "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", + "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "id":1, + "node_id":"example_node_id", + "number":1, + "title":"example title", + "user":{ + "login":"exampleUser", + "id":1, + "node_id":"example_user_node_id", + "avatar_url":"https://avatars.githubusercontent.com/u/1", + "gravatar_id":"", + "url":"https://api.github.com/users/exampleUser", + "html_url":"https://github.com/exampleUser", + "followers_url":"https://api.github.com/users/exampleUser/followers", + "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", + "organizations_url":"https://api.github.com/users/exampleUser/orgs", + "repos_url":"https://api.github.com/users/exampleUser/repos", + "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url":"https://api.github.com/users/exampleUser/received_events", + "type":"User", + "site_admin":false + }, + "labels":[ + + ], + "state":"open", + "locked":false, + "assignee":null, + "assignees":[ + + ], + "milestone":null, + "comments":1, + "created_at":"2023-03-07T09:21:45Z", + "updated_at":"2023-03-07T09:24:24Z", + "closed_at":null, + "author_association":"COLLABORATOR", + "active_lock_reason":null, + "draft":false, + "pull_request":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", + "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", + "merged_at":null + }, + "body":"pr only for demonstration purposes . . . .", + "reactions":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", + "total_count":0, + "+1":0, + "-1":0, + "laugh":0, + "hooray":0, + "confused":0, + "heart":0, + "rocket":0, + "eyes":0 + }, + "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", + "performed_via_github_app":null, + "state_reason":null, + "score":1.0 + } + ] + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(200).setBody(jsonResponse), + ) + + val actualResponse = pullRequestDataSource.getPullRequestsForUser("exampleUser") + + val expected = Result.success( + PullRequestsResponse( + incompleteResults = false, + totalCount = 1, + items = listOf( + PullRequest( + id = 1, + draft = false, + number = 1, + repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", + title = "example title", + ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), + ) + ), + ) + ) + + assertEquals(expected, actualResponse) { "Data should be valid" } + } + + @Test + fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + runTest { + // language=JSON + val jsonResponse = """ + { + "message": "Bad credentials", + "documentation_url": "https://docs.github.com/rest" + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(401).setBody(jsonResponse), + ) + + val actualResponse = pullRequestDataSource.getPullRequestsForUser("exampleUser") + + val expected = Result.failure( + WebException.UnknownError( + 401, + "Bad credentials", + ), + ) + + assertEquals(expected, actualResponse) { "Data should be valid" } + } + + + } + @Nested inner class GetReviewersRequestTest { @@ -40,8 +202,7 @@ class PullRequestsNetworkDataSourceTest { fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = runTest { mockWebServer.enqueue( - MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) val actualResponse = pullRequestDataSource.getReviewers( @@ -97,7 +258,8 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - ) + + ) val reviewer = Reviewer("1", "exampleLogin", "https://example/avatar") val expected = Result.success(RequestedReviewersResponse(listOf(reviewer))) @@ -225,7 +387,7 @@ class PullRequestsNetworkDataSourceTest { "1", User(33498031, "exampleUser"), ReviewState.COMMENTED, - LocalDateTime.parse("2023-03-02T10:21:36"), + ZonedDateTime.parse("2023-03-02T10:21:36Z").toLocalDateTime(), ), ), ) From 72dd8624bbb610deb32fd6c7d184ed8e76576d08 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 9 Mar 2023 08:20:44 +0100 Subject: [PATCH 126/526] Implement proper navigation to the ReviewersScreen.kt. --- .../java/com/appunite/loudius/MainActivity.kt | 29 ++++----- .../com/appunite/loudius/common/Screen.kt | 15 +++-- .../appunite/loudius/domain/model/Reviewer.kt | 4 +- .../loudius/network/model/PullRequest.kt | 6 ++ .../ui/pullrequests/PullRequestsScreen.kt | 39 ++++++++---- .../loudius/ui/reviewers/ReviewersScreen.kt | 17 +++--- .../ui/reviewers/ReviewersViewModel.kt | 61 +++++++++++++------ app/src/main/res/values/strings.xml | 1 + 8 files changed, 110 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index f7709d540..d34d16b98 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -52,27 +52,20 @@ class MainActivity : ComponentActivity() { } } composable(route = Screen.PullRequests.route) { - PullRequestsScreen() + PullRequestsScreen { owner, repo, pullRequestNumber, submissionTime -> + val route = Screen.Reviewers.constructRoute( + owner = owner, + repo = repo, + pullRequestNumber = pullRequestNumber, + submissionDate = submissionTime + ) + navController.navigate(route) + } } composable( route = Screen.Reviewers.route, arguments = Screen.Reviewers.arguments - ) { navBackStackEntry -> - val pullRequestNumber = - navBackStackEntry.arguments?.getString(Screen.Reviewers.pullRequestNumberArg) - val owner = - navBackStackEntry.arguments?.getString(Screen.Reviewers.ownerArg) - val repo = - navBackStackEntry.arguments?.getString(Screen.Reviewers.repoArg) - val submissionDate = - navBackStackEntry.arguments?.getString(Screen.Reviewers.submissionDateArg) - - // TODO: Handle the case with null arguments - ReviewersScreen( - pullRequestNumber, - owner, - repo, - submissionDate, - { navController.popBackStack() }) + ) { + ReviewersScreen({ navController.popBackStack() }) } } } diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index 6590afbb6..b682ae9f5 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -14,11 +14,11 @@ sealed class Screen(val route: String) { object PullRequests : Screen("pull_requests_screen") object Reviewers : - Screen("reviewers_screen/{pullRequestNumber}/{owner}/{repo}/{submissionDate}") { + Screen("reviewers_screen/{pull_request_number}/{owner}/{repo}/{submission_date}") { const val pullRequestNumberArg = "pull_request_number" - const val ownerArg = "pull_request_number" - const val repoArg = "pull_request_number" - const val submissionDateArg = "pull_request_number" + const val ownerArg = "owner" + const val repoArg = "repo" + const val submissionDateArg = "submission_date" override val arguments: List get() { @@ -29,5 +29,12 @@ sealed class Screen(val route: String) { navArgument(submissionDateArg) { type = NavType.StringType }, ) } + + fun constructRoute( + owner: String, + repo: String, + pullRequestNumber: String, + submissionDate: String + ): String = "reviewers_screen/$pullRequestNumber/$owner/$repo/$submissionDate" } } diff --git a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt index 9e0f92e65..b76c0d1db 100644 --- a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt @@ -4,6 +4,6 @@ data class Reviewer( val id: Int, val login: String, val isReviewDone: Boolean, - val hoursFromPRStart: Int, - val hoursFromReviewDone: Int?, + val hoursFromPRStart: Long, + val hoursFromReviewDone: Long?, ) diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt index 10ab6122a..2f4ee5c97 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt @@ -13,6 +13,12 @@ data class PullRequest( val fullRepositoryName: String get() = repositoryUrl.removePrefix(REPOSITORY_PATH) + val shortRepositoryName: String + get() = fullRepositoryName.substringAfter('/') + + val owner: String + get() = fullRepositoryName.substringBefore('/') + companion object { private const val REPOSITORY_PATH = Constants.BASE_API_URL + "/repos/" } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 74e3867a8..4049d091f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -30,16 +30,24 @@ import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme +import java.time.LocalDateTime @Composable -fun PullRequestsScreen(viewModel: PullRequestsViewModel = hiltViewModel()) { +fun PullRequestsScreen( + viewModel: PullRequestsViewModel = hiltViewModel(), + navigateToReviewers: (String, String, String, String) -> Unit +) { val state = viewModel.state - PullRequestsScreenStateless(pullRequests = state.pullRequests) + PullRequestsScreenStateless( + pullRequests = state.pullRequests, + onItemClick = navigateToReviewers + ) } @Composable private fun PullRequestsScreenStateless( pullRequests: List, + onItemClick: (String, String, String, String) -> Unit ) { Scaffold(topBar = { LoudiusTopAppBar(title = stringResource(R.string.app_name)) @@ -52,10 +60,9 @@ private fun PullRequestsScreenStateless( itemsIndexed(pullRequests) { index, item -> val isIndexEven = index % 2 == 0 PullRequestItem( - repositoryName = item.fullRepositoryName, - pullRequestTitle = item.title, + data = item, darkBackground = isIndexEven, - onClick = {}, + onClick = onItemClick, ) } } @@ -64,10 +71,9 @@ private fun PullRequestsScreenStateless( @Composable private fun PullRequestItem( - repositoryName: String, - pullRequestTitle: String, + data: PullRequest, darkBackground: Boolean, - onClick: () -> Unit, + onClick: (String, String, String, String) -> Unit, ) { val backgroundColor = if (darkBackground) { MaterialTheme.colorScheme.onSurface.copy(0.08f) @@ -78,10 +84,19 @@ private fun PullRequestItem( modifier = Modifier .fillMaxWidth() .background(backgroundColor) - .clickable(onClick = onClick), + .clickable(onClick = { + onClick( + data.owner, + data.shortRepositoryName, + data.number.toString(), + LocalDateTime + .now() + .toString() + ) + }), ) { PullRequestIcon() - RepoDetails(pullRequestTitle = pullRequestTitle, repositoryName = repositoryName) + RepoDetails(pullRequestTitle = data.title, repositoryName = data.fullRepositoryName) } Divider(color = MaterialTheme.colorScheme.outlineVariant) } @@ -113,7 +128,7 @@ private fun RepoDetails(pullRequestTitle: String, repositoryName: String) { @Composable fun PullRequestsScreenEmptyListPreview() { LoudiusTheme { - PullRequestsScreenStateless(emptyList()) + PullRequestsScreenStateless(emptyList()) { _, _, _, _ -> } } } @@ -156,6 +171,6 @@ fun PullRequestsScreenPreview() { updatedAt = "2022-01-29T16:31:41Z", ), ), - ) + ) { _, _, _, _ -> } } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 783f8db70..28da8a932 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -30,16 +30,12 @@ import com.appunite.loudius.ui.utils.bottomBorder @Composable fun ReviewersScreen( - pullRequestNumber: String?, - owner: String?, - repo: String?, - submissionDate: String?, navigateBack: () -> Unit, viewModel: ReviewersViewModel = hiltViewModel() ) { val state = viewModel.state ReviewersScreenStateless( - topBarTitle = "Pull request $pullRequestNumber", + pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, onClickBackArrow = navigateBack ) @@ -48,12 +44,17 @@ fun ReviewersScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReviewersScreenStateless( - topBarTitle: String, + pullRequestNumber: String, reviewers: List, onClickBackArrow: () -> Unit ) { Scaffold( - topBar = { LoudiusTopAppBar(onClickBackArrow = onClickBackArrow, title = topBarTitle) }, + topBar = { + LoudiusTopAppBar( + onClickBackArrow = onClickBackArrow, + title = stringResource(id = R.string.details_title, pullRequestNumber) + ) + }, content = { padding -> ReviewersScreenContent(reviewers, modifier = Modifier.padding(padding)) }, @@ -169,6 +170,6 @@ fun DetailsScreenPreview() { val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { - ReviewersScreenStateless(topBarTitle = "Pull request #1", reviewers = reviewers, {}) + ReviewersScreenStateless(pullRequestNumber = "Pull request #1", reviewers = reviewers, {}) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index a9f57dbb8..9b54a25d2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -10,6 +10,8 @@ import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.GitHubPullRequestsRepository import com.appunite.loudius.domain.model.Reviewer import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.Duration +import java.time.LocalDateTime import javax.inject.Inject import kotlinx.coroutines.launch @@ -23,48 +25,71 @@ class ReviewersViewModel @Inject constructor( private val repository: GitHubPullRequestsRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val pullRequestNumber: String = - checkNotNull(savedStateHandle[Screen.Reviewers.pullRequestNumberArg]) - private val owner: String = checkNotNull(savedStateHandle[Screen.Reviewers.ownerArg]) - private val repo: String = checkNotNull(savedStateHandle[Screen.Reviewers.repoArg]) - private val submissionDate: String = - checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]) var state by mutableStateOf(ReviewersState()) private set init { + val initialValues = getInitialValues(savedStateHandle) + + state = state.copy(pullRequestNumber = initialValues.pullRequestNumber) + viewModelScope.launch { - fetchRequestedReviewers(owner, repo, pullRequestNumber) - fetchReviews(owner, repo, pullRequestNumber) + fetchRequestedReviewers(initialValues) + fetchReviews(initialValues) } } + private fun getInitialValues(savedStateHandle: SavedStateHandle): InitialValues { + val owner: String = checkNotNull(savedStateHandle[Screen.Reviewers.ownerArg]) + val repo: String = checkNotNull(savedStateHandle[Screen.Reviewers.repoArg]) + val pullRequestNumber: String = + checkNotNull(savedStateHandle[Screen.Reviewers.pullRequestNumberArg]) + val submissionDate: String = + checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]) + return InitialValues(owner, repo, pullRequestNumber, submissionDate) + } private suspend fun fetchRequestedReviewers( - owner: String, - repo: String, - pullRequestNumber: String + initialValues: InitialValues ) { + val (owner, repo, pullRequestNumber, submissionTime) = initialValues + val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) repository.getReviewers(owner, repo, pullRequestNumber).onSuccess { response -> val reviewers = response.users.map { - Reviewer(it.id, it.login, false, 10, null) + Reviewer(it.id, it.login, false, hoursFromPRStart, null) } state = state.copy(reviewers = state.reviewers + reviewers) } } - private suspend fun fetchReviews( - owner: String, - repo: String, - pullRequestNumber: String - ) { + private suspend fun fetchReviews(initialValues: InitialValues) { + val (owner, repo, pullRequestNumber, submissionTime) = initialValues + val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) repository.getReviews(owner, repo, pullRequestNumber).onSuccess { reviews -> reviews.groupBy { it.user.id }.map { singleUser -> val latestReview = singleUser.value.minBy { it.submittedAt } + val hoursFromReviewDone = countHoursTillNow(latestReview.submittedAt) - Reviewer(latestReview.user.id, latestReview.user.login, true, 10, 10) + Reviewer( + latestReview.user.id, + latestReview.user.login, + true, + hoursFromPRStart, + hoursFromReviewDone + ) } } } + + private fun countHoursTillNow(submissionTime: LocalDateTime): Long = + Duration.between(submissionTime, LocalDateTime.now()).toHours() + + + private data class InitialValues( + val owner: String, + val repo: String, + val pullRequestNumber: String, + val submissionTime: String, + ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7fec3e177..d44dc5082 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,6 +6,7 @@ Notify Reviewed %d h ago. Not reviewed for %d h. + Pull request # %s Github icon Loudius logo Error From 7884fce26c2b6627aedfb1d150ded2275814bef2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 9 Mar 2023 08:26:25 +0100 Subject: [PATCH 127/526] Fix test - remove extra param - avatar url. --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index f50d2879b..7bf74c4db 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -224,7 +224,7 @@ class PullRequestsNetworkDataSourceTest { listOf( Review( "1", - User(10000000, "exampleUser", "https://avatars.com/u/10000000"), + User(10000000, "exampleUser"), ReviewState.COMMENTED, LocalDateTime.parse("2023-03-02T10:21:36"), ), From 521ec73b90585a585ba98e2423b88c64633ab946 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 9 Mar 2023 09:15:26 +0100 Subject: [PATCH 128/526] Refactor ReviewersViewModel.kt code. --- .../ui/reviewers/ReviewersViewModel.kt | 75 ++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 9b54a25d2..88b0f73e0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -7,11 +7,13 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Screen -import com.appunite.loudius.domain.GitHubPullRequestsRepository +import com.appunite.loudius.domain.PullRequestsRepository import com.appunite.loudius.domain.model.Reviewer +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.Duration import java.time.LocalDateTime +import java.time.temporal.ChronoUnit import javax.inject.Inject import kotlinx.coroutines.launch @@ -22,7 +24,7 @@ data class ReviewersState( @HiltViewModel class ReviewersViewModel @Inject constructor( - private val repository: GitHubPullRequestsRepository, + private val repository: PullRequestsRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -40,51 +42,56 @@ class ReviewersViewModel @Inject constructor( } } - private fun getInitialValues(savedStateHandle: SavedStateHandle): InitialValues { - val owner: String = checkNotNull(savedStateHandle[Screen.Reviewers.ownerArg]) - val repo: String = checkNotNull(savedStateHandle[Screen.Reviewers.repoArg]) - val pullRequestNumber: String = - checkNotNull(savedStateHandle[Screen.Reviewers.pullRequestNumberArg]) - val submissionDate: String = - checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]) - return InitialValues(owner, repo, pullRequestNumber, submissionDate) - } + private fun getInitialValues(savedStateHandle: SavedStateHandle) = InitialValues( + checkNotNull(savedStateHandle[Screen.Reviewers.ownerArg]), + checkNotNull(savedStateHandle[Screen.Reviewers.repoArg]), + checkNotNull(savedStateHandle[Screen.Reviewers.pullRequestNumberArg]), + checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]) + ) - private suspend fun fetchRequestedReviewers( - initialValues: InitialValues - ) { + + private suspend fun fetchRequestedReviewers(initialValues: InitialValues) { val (owner, repo, pullRequestNumber, submissionTime) = initialValues val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) - repository.getReviewers(owner, repo, pullRequestNumber).onSuccess { response -> - val reviewers = response.users.map { - Reviewer(it.id, it.login, false, hoursFromPRStart, null) + + repository.getReviewers(owner, repo, pullRequestNumber) + .onSuccess { response -> + val reviewers = response.mapToReviewers(hoursFromPRStart) + state = state.copy(reviewers = state.reviewers + reviewers) } - state = state.copy(reviewers = state.reviewers + reviewers) - } + } + + private fun RequestedReviewersResponse.mapToReviewers(hoursFromPRStart: Long) = users.map { + Reviewer(it.id, it.login, false, hoursFromPRStart, null) } private suspend fun fetchReviews(initialValues: InitialValues) { val (owner, repo, pullRequestNumber, submissionTime) = initialValues val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) + repository.getReviews(owner, repo, pullRequestNumber).onSuccess { reviews -> - reviews.groupBy { it.user.id }.map { singleUser -> - val latestReview = singleUser.value.minBy { it.submittedAt } - val hoursFromReviewDone = countHoursTillNow(latestReview.submittedAt) - - Reviewer( - latestReview.user.id, - latestReview.user.login, - true, - hoursFromPRStart, - hoursFromReviewDone - ) - } + val reviewersAfterReview = reviews.mapToReviewers(hoursFromPRStart) + state = state.copy(reviewers = state.reviewers + reviewersAfterReview) + } } - private fun countHoursTillNow(submissionTime: LocalDateTime): Long = - Duration.between(submissionTime, LocalDateTime.now()).toHours() + private fun List.mapToReviewers(hoursFromPRStart: Long) = groupBy { it.user.id } + .map { reviewsForSingleUser -> + val latestReview = reviewsForSingleUser.value.minBy { it.submittedAt } + val hoursFromReviewDone = countHoursTillNow(latestReview.submittedAt) + Reviewer( + latestReview.user.id, + latestReview.user.login, + true, + hoursFromPRStart, + hoursFromReviewDone + ) + } + + private fun countHoursTillNow(submissionTime: LocalDateTime): Long = + ChronoUnit.HOURS.between(submissionTime, LocalDateTime.now()) private data class InitialValues( val owner: String, From cdbcc3bd1875a8c6594178b3dff7f645aced3a93 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 9 Mar 2023 09:54:22 +0100 Subject: [PATCH 129/526] Move to the separate method on item click in PullRequestsScreen.kt. --- .../ui/pullrequests/PullRequestsScreen.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 4049d091f..2df2c5730 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -84,16 +84,7 @@ private fun PullRequestItem( modifier = Modifier .fillMaxWidth() .background(backgroundColor) - .clickable(onClick = { - onClick( - data.owner, - data.shortRepositoryName, - data.number.toString(), - LocalDateTime - .now() - .toString() - ) - }), + .clickable { onItemClick(onClick, data) }, ) { PullRequestIcon() RepoDetails(pullRequestTitle = data.title, repositoryName = data.fullRepositoryName) @@ -101,6 +92,20 @@ private fun PullRequestItem( Divider(color = MaterialTheme.colorScheme.outlineVariant) } +private fun onItemClick( + onClick: (String, String, String, String) -> Unit, + data: PullRequest +) { + onClick( + data.owner, + data.shortRepositoryName, + data.number.toString(), + LocalDateTime + .now() + .toString() + ) +} + @Composable private fun PullRequestIcon() { Image( From e711f1b98dea6c1996fe996d7b33ab004bce027a Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 9 Mar 2023 08:57:34 +0000 Subject: [PATCH 130/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 5 +++-- .../main/java/com/appunite/loudius/common/Screen.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 10 +++++----- .../appunite/loudius/ui/reviewers/ReviewersScreen.kt | 8 ++++---- .../loudius/ui/reviewers/ReviewersViewModel.kt | 8 +++----- .../datasource/PullRequestsNetworkDataSourceTest.kt | 2 +- 6 files changed, 17 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index d34d16b98..20bec6148 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -57,13 +57,14 @@ class MainActivity : ComponentActivity() { owner = owner, repo = repo, pullRequestNumber = pullRequestNumber, - submissionDate = submissionTime + submissionDate = submissionTime, ) navController.navigate(route) } } composable( - route = Screen.Reviewers.route, arguments = Screen.Reviewers.arguments + route = Screen.Reviewers.route, + arguments = Screen.Reviewers.arguments, ) { ReviewersScreen({ navController.popBackStack() }) } diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index b682ae9f5..87fd303b9 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -34,7 +34,7 @@ sealed class Screen(val route: String) { owner: String, repo: String, pullRequestNumber: String, - submissionDate: String + submissionDate: String, ): String = "reviewers_screen/$pullRequestNumber/$owner/$repo/$submissionDate" } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 2df2c5730..4c39d7a2f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -35,19 +35,19 @@ import java.time.LocalDateTime @Composable fun PullRequestsScreen( viewModel: PullRequestsViewModel = hiltViewModel(), - navigateToReviewers: (String, String, String, String) -> Unit + navigateToReviewers: (String, String, String, String) -> Unit, ) { val state = viewModel.state PullRequestsScreenStateless( pullRequests = state.pullRequests, - onItemClick = navigateToReviewers + onItemClick = navigateToReviewers, ) } @Composable private fun PullRequestsScreenStateless( pullRequests: List, - onItemClick: (String, String, String, String) -> Unit + onItemClick: (String, String, String, String) -> Unit, ) { Scaffold(topBar = { LoudiusTopAppBar(title = stringResource(R.string.app_name)) @@ -94,7 +94,7 @@ private fun PullRequestItem( private fun onItemClick( onClick: (String, String, String, String) -> Unit, - data: PullRequest + data: PullRequest, ) { onClick( data.owner, @@ -102,7 +102,7 @@ private fun onItemClick( data.number.toString(), LocalDateTime .now() - .toString() + .toString(), ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 28da8a932..ffbcb29a8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -31,13 +31,13 @@ import com.appunite.loudius.ui.utils.bottomBorder @Composable fun ReviewersScreen( navigateBack: () -> Unit, - viewModel: ReviewersViewModel = hiltViewModel() + viewModel: ReviewersViewModel = hiltViewModel(), ) { val state = viewModel.state ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, - onClickBackArrow = navigateBack + onClickBackArrow = navigateBack, ) } @@ -46,13 +46,13 @@ fun ReviewersScreen( private fun ReviewersScreenStateless( pullRequestNumber: String, reviewers: List, - onClickBackArrow: () -> Unit + onClickBackArrow: () -> Unit, ) { Scaffold( topBar = { LoudiusTopAppBar( onClickBackArrow = onClickBackArrow, - title = stringResource(id = R.string.details_title, pullRequestNumber) + title = stringResource(id = R.string.details_title, pullRequestNumber), ) }, content = { padding -> diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 88b0f73e0..4e825a843 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,10 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject -import kotlinx.coroutines.launch data class ReviewersState( val reviewers: List = emptyList(), @@ -46,10 +46,9 @@ class ReviewersViewModel @Inject constructor( checkNotNull(savedStateHandle[Screen.Reviewers.ownerArg]), checkNotNull(savedStateHandle[Screen.Reviewers.repoArg]), checkNotNull(savedStateHandle[Screen.Reviewers.pullRequestNumberArg]), - checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]) + checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]), ) - private suspend fun fetchRequestedReviewers(initialValues: InitialValues) { val (owner, repo, pullRequestNumber, submissionTime) = initialValues val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) @@ -72,7 +71,6 @@ class ReviewersViewModel @Inject constructor( repository.getReviews(owner, repo, pullRequestNumber).onSuccess { reviews -> val reviewersAfterReview = reviews.mapToReviewers(hoursFromPRStart) state = state.copy(reviewers = state.reviewers + reviewersAfterReview) - } } @@ -86,7 +84,7 @@ class ReviewersViewModel @Inject constructor( latestReview.user.login, true, hoursFromPRStart, - hoursFromReviewDone + hoursFromReviewDone, ) } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 186e0ae04..4ac2287ac 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -8,7 +8,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -19,6 +18,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { From a9189c43b165eb5c6d56ea6ce8973ea807caeea2 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 9 Mar 2023 12:39:55 +0100 Subject: [PATCH 131/526] SIL-80: helpers --- .../com/appunite/loudius/domain/AuthRepository.kt | 4 ++-- .../appunite/loudius/domain/AuthRepositoryImpl.kt | 15 ++++++++++++--- .../loudius/network/datasource/AuthDataSource.kt | 8 ++++---- .../loudius/network/model/AccessTokenResponse.kt | 5 ++--- .../appunite/loudius/ui/repos/ReposViewModel.kt | 2 +- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt index 5e8b72edb..526cc4090 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.domain -import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.model.AccessTokenResponse interface AuthRepository { @@ -8,7 +8,7 @@ interface AuthRepository { clientId: String, clientSecret: String, code: String, - ): Result + ): Result fun getAccessToken(): String } diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt index bae1813db..f27e859c0 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt @@ -1,7 +1,8 @@ package com.appunite.loudius.domain +import android.util.Log import com.appunite.loudius.network.datasource.AuthDataSource -import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.model.AccessTokenResponse import javax.inject.Inject import javax.inject.Singleton @@ -15,9 +16,17 @@ class AuthRepositoryImpl @Inject constructor( clientId: String, clientSecret: String, code: String, - ): Result { + ): Result { val result = authDataSource.getAccessToken(clientId, clientSecret, code) - result.onSuccess { userLocalDataSource.saveAccessToken(it) } + result + .onSuccess { + if (it.accessToken != null) { + userLocalDataSource.saveAccessToken(it.accessToken) + } else { + Log.i("failure", it.toString() + "bad_verification_code") + } + } + .onFailure { Log.i("failure", "incorrect_client_credientals") } return result } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index e3e4d92ed..d382a01eb 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.network.datasource -import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.model.AccessTokenResponse import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject @@ -12,7 +12,7 @@ interface AuthDataSource { clientId: String, clientSecret: String, code: String, - ): Result + ): Result } @Singleton @@ -24,6 +24,6 @@ class AuthNetworkDataSource @Inject constructor( clientId: String, clientSecret: String, code: String, - ): Result = - safeApiCall { authService.getAccessToken(clientId, clientSecret, code).accessToken } + ): Result = + safeApiCall { authService.getAccessToken(clientId, clientSecret, code) } } diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt index ea21b8f43..9c5cc1ad5 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt @@ -1,8 +1,7 @@ package com.appunite.loudius.network.model -typealias AccessToken = String - data class AccessTokenResponse( - val accessToken: String, + val accessToken: String?, + val error: String? = null ) diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt index 60fbe8208..e917c7395 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt @@ -22,7 +22,7 @@ class ReposViewModel @Inject constructor( clientSecret = BuildConfig.CLIENT_SECRET, code = code, ).onSuccess { token -> - Log.i("access_token", token) + Log.i("access_token", token.accessToken.toString()) }.onFailure { Log.i("access_token", it.message.toString()) } From 04e41dfef04ee0c3fbf4d29067e95cd7a92d5f87 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 9 Mar 2023 11:44:10 +0000 Subject: [PATCH 132/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt | 2 +- .../com/appunite/loudius/network/model/AccessTokenResponse.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt index f27e859c0..55e81e671 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt @@ -26,7 +26,7 @@ class AuthRepositoryImpl @Inject constructor( Log.i("failure", it.toString() + "bad_verification_code") } } - .onFailure { Log.i("failure", "incorrect_client_credientals") } + .onFailure { Log.i("failure", "incorrect_client_credentials") } return result } diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt index 9c5cc1ad5..78dce89ee 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt @@ -3,5 +3,5 @@ package com.appunite.loudius.network.model data class AccessTokenResponse( val accessToken: String?, - val error: String? = null + val error: String? = null, ) From 3e0e4d7e6e5f013e8107029603e192983bcf9741 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 9 Mar 2023 10:18:52 +0100 Subject: [PATCH 133/526] Change PullRequest model to store localDateTime, and change updatedAt to createdAt. --- .../appunite/loudius/network/model/PullRequest.kt | 3 ++- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 12 +++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt index 2f4ee5c97..50180c904 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.network.model import com.appunite.loudius.common.Constants +import java.time.LocalDateTime data class PullRequest( val id: Int, @@ -8,7 +9,7 @@ data class PullRequest( val number: Int, val repositoryUrl: String, val title: String, - val updatedAt: String, + val createdAt: LocalDateTime, ) { val fullRepositoryName: String get() = repositoryUrl.removePrefix(REPOSITORY_PATH) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 4c39d7a2f..60fef5ebb 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -100,9 +100,7 @@ private fun onItemClick( data.owner, data.shortRepositoryName, data.number.toString(), - LocalDateTime - .now() - .toString(), + data.createdAt.toString(), ) } @@ -149,7 +147,7 @@ fun PullRequestsScreenPreview() { number = 0, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", title = "[SIL-67] Details screen - network layer", - updatedAt = "2021-11-29T16:31:41Z", + createdAt = LocalDateTime.parse("2021-11-29T16:31:41Z"), ), PullRequest( id = 1, @@ -157,7 +155,7 @@ fun PullRequestsScreenPreview() { number = 1, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", title = "[SIL-66] Add client secret to build config", - updatedAt = "2022-11-29T16:31:41Z", + createdAt = LocalDateTime.parse("2022-11-29T16:31:41Z"), ), PullRequest( id = 2, @@ -165,7 +163,7 @@ fun PullRequestsScreenPreview() { number = 2, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", title = "[SIL-73] Storing access token", - updatedAt = "2023-01-29T16:31:41Z", + createdAt = LocalDateTime.parse("2023-01-29T16:31:41Z"), ), PullRequest( id = 3, @@ -173,7 +171,7 @@ fun PullRequestsScreenPreview() { number = 3, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", title = "[SIL-62/SIL-75] Provide new annotation for API instances", - updatedAt = "2022-01-29T16:31:41Z", + createdAt = LocalDateTime.parse("2022-01-29T16:31:41Z"), ), ), ) { _, _, _, _ -> } From 5677d97e3ab4249047b5fb45cfcce117072630d3 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 9 Mar 2023 15:21:06 +0100 Subject: [PATCH 134/526] Rename PullRequestsRepository.kt to the PullRequestRepositoryImpl. --- ...llRequestsRepository.kt => PullRequestRepositoryImpl.kt} | 5 ++--- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 6 +++--- .../com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 6 +++--- 3 files changed, 8 insertions(+), 9 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{PullRequestsRepository.kt => PullRequestRepositoryImpl.kt} (95%) diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestsRepository.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt similarity index 95% rename from app/src/main/java/com/appunite/loudius/domain/PullRequestsRepository.kt rename to app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index 32189e770..d2b159eec 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestsRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -24,11 +24,10 @@ interface PullRequestRepository { suspend fun getCurrentUserPullRequests(): Result } -class PullRequestsRepository @Inject constructor( +class PullRequestRepositoryImpl @Inject constructor( private val pullRequestsNetworkDataSource: PullRequestsNetworkDataSource, private val userDataSource: UserDataSource, -) : - PullRequestRepository { +) : PullRequestRepository { override suspend fun getCurrentUserPullRequests(): Result { val currentUser = userDataSource.getUser() return currentUser.flatMap { pullRequestsNetworkDataSource.getPullRequestsForUser(it.login) } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 10a9468ec..cfd6a3200 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -5,11 +5,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.appunite.loudius.domain.PullRequestsRepository +import com.appunite.loudius.domain.PullRequestRepositoryImpl import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch data class PullRequestState( val pullRequests: List = emptyList(), @@ -17,7 +17,7 @@ data class PullRequestState( @HiltViewModel class PullRequestsViewModel @Inject constructor( - private val pullRequestsRepository: PullRequestsRepository, + private val pullRequestsRepository: PullRequestRepositoryImpl, ) : ViewModel() { var state by mutableStateOf(PullRequestState()) private set diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 4e825a843..81f1852b6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -7,15 +7,15 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Screen -import com.appunite.loudius.domain.PullRequestsRepository +import com.appunite.loudius.domain.PullRequestRepositoryImpl import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.launch data class ReviewersState( val reviewers: List = emptyList(), @@ -24,7 +24,7 @@ data class ReviewersState( @HiltViewModel class ReviewersViewModel @Inject constructor( - private val repository: PullRequestsRepository, + private val repository: PullRequestRepositoryImpl, savedStateHandle: SavedStateHandle, ) : ViewModel() { From 264f6ec66ba54ba17828b5d360cd6c65d327e256 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 10:21:38 +0100 Subject: [PATCH 135/526] Change the order of the parameters in the ReviewersScreen.kt constructor. --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 2 +- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 20bec6148..69e375998 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -66,7 +66,7 @@ class MainActivity : ComponentActivity() { route = Screen.Reviewers.route, arguments = Screen.Reviewers.arguments, ) { - ReviewersScreen({ navController.popBackStack() }) + ReviewersScreen { navController.popBackStack() } } } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index ffbcb29a8..4d1373f8a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -30,8 +30,8 @@ import com.appunite.loudius.ui.utils.bottomBorder @Composable fun ReviewersScreen( - navigateBack: () -> Unit, viewModel: ReviewersViewModel = hiltViewModel(), + navigateBack: () -> Unit, ) { val state = viewModel.state ReviewersScreenStateless( From 4251de98a81dcde380509d91af2cf0835438ca21 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 10:22:02 +0100 Subject: [PATCH 136/526] Add typealias for (String, String, String, String) -> Unit. --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 60fef5ebb..5fe13fdc9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -32,10 +32,12 @@ import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import java.time.LocalDateTime +typealias NavigateToReviewers = (String, String, String, String) -> Unit + @Composable fun PullRequestsScreen( viewModel: PullRequestsViewModel = hiltViewModel(), - navigateToReviewers: (String, String, String, String) -> Unit, + navigateToReviewers: NavigateToReviewers, ) { val state = viewModel.state PullRequestsScreenStateless( @@ -47,7 +49,7 @@ fun PullRequestsScreen( @Composable private fun PullRequestsScreenStateless( pullRequests: List, - onItemClick: (String, String, String, String) -> Unit, + onItemClick: NavigateToReviewers, ) { Scaffold(topBar = { LoudiusTopAppBar(title = stringResource(R.string.app_name)) @@ -73,7 +75,7 @@ private fun PullRequestsScreenStateless( private fun PullRequestItem( data: PullRequest, darkBackground: Boolean, - onClick: (String, String, String, String) -> Unit, + onClick: NavigateToReviewers, ) { val backgroundColor = if (darkBackground) { MaterialTheme.colorScheme.onSurface.copy(0.08f) @@ -93,7 +95,7 @@ private fun PullRequestItem( } private fun onItemClick( - onClick: (String, String, String, String) -> Unit, + onClick: NavigateToReviewers, data: PullRequest, ) { onClick( From 20a610c46903574e992399c551a56f0a3b2b04a4 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 10:26:27 +0100 Subject: [PATCH 137/526] Move counting the hours from PRStart to the mapToReviewers function. --- .../loudius/ui/reviewers/ReviewersViewModel.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 4e825a843..dd45f2ac5 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,10 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.launch data class ReviewersState( val reviewers: List = emptyList(), @@ -51,17 +51,20 @@ class ReviewersViewModel @Inject constructor( private suspend fun fetchRequestedReviewers(initialValues: InitialValues) { val (owner, repo, pullRequestNumber, submissionTime) = initialValues - val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) repository.getReviewers(owner, repo, pullRequestNumber) .onSuccess { response -> - val reviewers = response.mapToReviewers(hoursFromPRStart) + val reviewers = response.mapToReviewers(submissionTime) state = state.copy(reviewers = state.reviewers + reviewers) } } - private fun RequestedReviewersResponse.mapToReviewers(hoursFromPRStart: Long) = users.map { - Reviewer(it.id, it.login, false, hoursFromPRStart, null) + private fun RequestedReviewersResponse.mapToReviewers(submissionTime: String): List { + val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) + + return users.map { + Reviewer(it.id, it.login, false, hoursFromPRStart, null) + } } private suspend fun fetchReviews(initialValues: InitialValues) { From 5d5a3b65c28b2aceda22499c768f83395eacfe19 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 8 Mar 2023 16:15:40 +0100 Subject: [PATCH 138/526] Add user data source tests --- .../network/datasource/UserDataSourceTest.kt | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt new file mode 100644 index 000000000..497e47762 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -0,0 +1,127 @@ +package com.appunite.loudius.network.datasource + +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.User +import com.appunite.loudius.network.retrofitTestDouble +import com.appunite.loudius.network.services.UserService +import com.appunite.loudius.network.utils.WebException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@ExperimentalCoroutinesApi +class UserDataSourceTest { + + private val mockWebServer: MockWebServer = MockWebServer() + private val userService = + retrofitTestDouble(mockWebServer = mockWebServer).create(UserService::class.java) + private val userDataSource = UserDataSource(userService) + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = + runTest { + mockWebServer.enqueue( + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), + ) + + val actualResponse = userDataSource.getUser() + Assertions.assertInstanceOf( + WebException.NetworkError::class.java, + actualResponse.exceptionOrNull(), + ) { "Exception thrown should be NetworkError type" } + } + + + @Test + fun `Given correct params WHEN successful response THEN return success`() = runTest { + //language=JSON + val jsonResponse = """ + { + "login": "exampleUser", + "id": 1, + "node_id": "MDQ6VXNlcjE4MTAyNzc1", + "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/exampleUser", + "html_url": "https://github.com/exampleUser", + "followers_url": "https://api.github.com/users/exampleUser/followers", + "following_url": "https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url": "https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url": "https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/exampleUser/subscriptions", + "organizations_url": "https://api.github.com/users/exampleUser/orgs", + "repos_url": "https://api.github.com/users/exampleUser/repos", + "events_url": "https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url": "https://api.github.com/users/exampleUser/received_events", + "type": "User", + "site_admin": false, + "name": "Name Surname", + "company": null, + "blog": "", + "location": null, + "email": null, + "hireable": null, + "bio": "bio description", + "twitter_username": null, + "public_repos": 8, + "public_gists": 2, + "followers": 16, + "following": 14, + "created_at": "2016-03-27T17:03:48Z", + "updated_at": "2023-02-15T13:22:50Z" + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(200).setBody(jsonResponse), + ) + + val actualResponse = userDataSource.getUser() + + val expected = Result.success( + User( + id = 1, + login = "exampleUser", + ) + ) + + Assertions.assertEquals(expected, actualResponse) { "Data should be valid" } + } + + @Test + fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + runTest { + // language=JSON + val jsonResponse = """ + { + "message": "Bad credentials", + "documentation_url": "https://docs.github.com/rest" + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(401).setBody(jsonResponse), + ) + + val actualResponse = userDataSource.getUser() + + val expected = Result.failure( + WebException.UnknownError( + 401, + "Bad credentials", + ), + ) + + Assertions.assertEquals(expected, actualResponse) { "Data should be valid" } + } +} From 930d100c081c86e9265fcb26680b8c9bdd8784ff Mon Sep 17 00:00:00 2001 From: kezc Date: Fri, 10 Mar 2023 09:57:23 +0000 Subject: [PATCH 139/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 1 - .../datasource/PullRequestsNetworkDataSourceTest.kt | 13 +++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 28d115bb7..476355f39 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -30,7 +30,6 @@ import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme -import java.time.LocalDateTime import java.time.ZonedDateTime @Composable diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 6fae0afa4..6fec9169c 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,7 +10,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.ZonedDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.ZonedDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -45,7 +45,7 @@ class PullRequestsNetworkDataSourceTest { ) val actualResponse = pullRequestDataSource.getPullRequestsForUser( - "exampleUser" + "exampleUser", ) Assertions.assertInstanceOf( WebException.NetworkError::class.java, @@ -53,7 +53,6 @@ class PullRequestsNetworkDataSourceTest { ) { "Exception thrown should be NetworkError type" } } - @Test fun `Given correct params WHEN successful response THEN return success`() = runTest { //language=JSON @@ -157,9 +156,9 @@ class PullRequestsNetworkDataSourceTest { repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), - ) + ), ), - ) + ), ) assertEquals(expected, actualResponse) { "Data should be valid" } @@ -191,8 +190,6 @@ class PullRequestsNetworkDataSourceTest { assertEquals(expected, actualResponse) { "Data should be valid" } } - - } @Nested @@ -259,7 +256,7 @@ class PullRequestsNetworkDataSourceTest { "exampleRepo", "exampleNumber", - ) + ) val reviewer = Reviewer("1", "exampleLogin", "https://example/avatar") val expected = Result.success(RequestedReviewersResponse(listOf(reviewer))) From 3234ff60069dd402455b3b25b5662c29fa7a358a Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 11:05:59 +0100 Subject: [PATCH 140/526] Implement tests for ReviewersViewModel.kt. --- .../appunite/loudius/di/PullRequestModule.kt | 10 ++ .../domain/PullRequestRepositoryImpl.kt | 8 +- .../PullRequestsNetworkDataSource.kt | 9 +- .../network/model/RequestedReviewer.kt | 1 - .../ui/reviewers/ReviewersViewModel.kt | 6 +- .../fakes/FakePullRequestRepository.kt | 59 ++++++++ .../PullRequestsNetworkDataSourceTest.kt | 4 +- .../ui/reviewers/ReviewersViewModelTest.kt | 142 ++++++++++++++++++ .../loudius/util/MainDispatcherExtension.kt | 24 +++ 9 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt create mode 100644 app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt create mode 100644 app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt diff --git a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt index 9acf23eaf..6eb11d60d 100644 --- a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt @@ -1,7 +1,10 @@ package com.appunite.loudius.di +import com.appunite.loudius.domain.PullRequestRepository +import com.appunite.loudius.domain.PullRequestRepositoryImpl import com.appunite.loudius.network.datasource.PullRequestDataSource import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource +import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.services.PullRequestsService import dagger.Module import dagger.Provides @@ -17,4 +20,11 @@ object PullRequestModule { @Singleton fun providePullRequestNetworkDataSource(service: PullRequestsService): PullRequestDataSource = PullRequestsNetworkDataSource(service) + + @Provides + @Singleton + fun providePullRequestRepository( + dataSource: PullRequestDataSource, + userDataSource: UserDataSource + ): PullRequestRepository = PullRequestRepositoryImpl(dataSource, userDataSource) } diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index d2b159eec..352fae67c 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -1,7 +1,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.common.flatMap -import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource +import com.appunite.loudius.network.datasource.PullRequestDataSource import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse @@ -15,7 +15,7 @@ interface PullRequestRepository { pullRequestNumber: String, ): Result> - suspend fun getReviewers( + suspend fun getRequestedReviewers( owner: String, repo: String, pullRequestNumber: String, @@ -25,7 +25,7 @@ interface PullRequestRepository { } class PullRequestRepositoryImpl @Inject constructor( - private val pullRequestsNetworkDataSource: PullRequestsNetworkDataSource, + private val pullRequestsNetworkDataSource: PullRequestDataSource, private val userDataSource: UserDataSource, ) : PullRequestRepository { override suspend fun getCurrentUserPullRequests(): Result { @@ -40,7 +40,7 @@ class PullRequestRepositoryImpl @Inject constructor( ): Result> = pullRequestsNetworkDataSource.getReviews(owner, repo, pullRequestNumber) - override suspend fun getReviewers( + override suspend fun getRequestedReviewers( owner: String, repo: String, pullRequestNumber: String, diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index f0a16084a..0417d8369 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -20,14 +20,17 @@ interface PullRequestDataSource { repository: String, pullRequestNumber: String, ): Result> + + suspend fun getPullRequestsForUser(author: String): Result } @Singleton class PullRequestsNetworkDataSource @Inject constructor(private val service: PullRequestsService) : PullRequestDataSource { - suspend fun getPullRequestsForUser(author: String): Result = safeApiCall { - service.getPullRequestsForUser("author:$author type:pr state:open") - } + override suspend fun getPullRequestsForUser(author: String): Result = + safeApiCall { + service.getPullRequestsForUser("author:$author type:pr state:open") + } override suspend fun getReviewers( owner: String, diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt index ae37c9a6f..3e5533f8f 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt @@ -3,5 +3,4 @@ package com.appunite.loudius.network.model data class RequestedReviewer( val id: Int, val login: String, - val avatarUrl: String, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 81f1852b6..d71a86fea 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Screen -import com.appunite.loudius.domain.PullRequestRepositoryImpl +import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review @@ -24,7 +24,7 @@ data class ReviewersState( @HiltViewModel class ReviewersViewModel @Inject constructor( - private val repository: PullRequestRepositoryImpl, + private val repository: PullRequestRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -53,7 +53,7 @@ class ReviewersViewModel @Inject constructor( val (owner, repo, pullRequestNumber, submissionTime) = initialValues val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) - repository.getReviewers(owner, repo, pullRequestNumber) + repository.getRequestedReviewers(owner, repo, pullRequestNumber) .onSuccess { response -> val reviewers = response.mapToReviewers(hoursFromPRStart) state = state.copy(reviewers = state.reviewers + reviewers) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt new file mode 100644 index 000000000..72dcd4187 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -0,0 +1,59 @@ +package com.appunite.loudius.fakes + +import com.appunite.loudius.domain.PullRequestRepository +import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.RequestedReviewer +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.ReviewState.APPROVED +import com.appunite.loudius.network.model.ReviewState.CHANGES_REQUESTED +import com.appunite.loudius.network.model.ReviewState.COMMENTED +import com.appunite.loudius.network.model.User +import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime + +class FakePullRequestRepository : PullRequestRepository { + private val date1 = LocalDateTime.parse("2022-01-29T10:00:00") + private val date2 = LocalDateTime.parse("2022-01-29T11:00:00") + private val date3 = LocalDateTime.parse("2022-01-29T12:00:00") + + override suspend fun getReviews( + owner: String, + repo: String, + pullRequestNumber: String + ): Result> = when (pullRequestNumber) { + "correctPullRequestNumber", "onlyReviewsNumber" -> Result.success( + listOf( + Review("1", User(1, "user1"), CHANGES_REQUESTED, date1), + Review("2", User(1, "user1"), COMMENTED, date2), + Review("3", User(1, "user1"), APPROVED, date3), + Review("4", User(2, "user2"), COMMENTED, date1), + Review("5", User(2, "user2"), COMMENTED, date2), + Review("6", User(2, "user2"), APPROVED, date3), + ) + ) + "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + else -> Result.success(emptyList()) + } + + override suspend fun getRequestedReviewers( + owner: String, + repo: String, + pullRequestNumber: String + ): Result = when (pullRequestNumber) { + "correctPullRequestNumber", "onlyRequestedReviewers" -> Result.success( + RequestedReviewersResponse( + listOf( + RequestedReviewer(3, "user3"), + RequestedReviewer(4, "user4"), + ) + ) + ) + "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + else -> Result.success(RequestedReviewersResponse(emptyList())) + } + + override suspend fun getCurrentUserPullRequests(): Result { + TODO("Not yet implemented") + } +} diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 4ac2287ac..2ec205cda 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -8,6 +8,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -18,7 +19,6 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -100,7 +100,7 @@ class PullRequestsNetworkDataSourceTest { ) val requestedReviewer = - RequestedReviewer(1, "exampleLogin", "https://example/avatar") + RequestedReviewer(1, "exampleLogin") val expected = Result.success(RequestedReviewersResponse(listOf(requestedReviewer))) assertEquals(expected, actualResponse) { "Data should be valid" } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt new file mode 100644 index 000000000..516239641 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -0,0 +1,142 @@ +package com.appunite.loudius.ui.reviewers + +import androidx.lifecycle.SavedStateHandle +import com.appunite.loudius.domain.PullRequestRepository +import com.appunite.loudius.domain.model.Reviewer +import com.appunite.loudius.fakes.FakePullRequestRepository +import com.appunite.loudius.util.MainDispatcherExtension +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(MainDispatcherExtension::class) +class ReviewersViewModelTest { + + private val repository: PullRequestRepository = FakePullRequestRepository() + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) + private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") + private val systemClockFixed = + Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.systemDefault()) + + private fun createViewModel(): ReviewersViewModel = + ReviewersViewModel(repository, savedStateHandle) + + @BeforeEach + fun setup() { + mockkStatic(Clock::class) + every { Clock.systemDefaultZone() } returns systemClockFixed + } + + + @Test + fun `GIVEN no values in saved state WHEN init THEN throw IllegalArgumentException`() { + every { savedStateHandle.get(any()) } returns null + + assertThrows { + createViewModel() + } + } + + @Test + fun `GIVEN correct initial values WHEN init THEN state is correct`() { + every { savedStateHandle.get(any()) } returns "example" + every { savedStateHandle.get("submission_date") } returns "2022-01-29T12:00:00" + every { savedStateHandle.get("pull_request_number") } returns "correctPullRequestNumber" + + val viewModel = createViewModel() + + verify(exactly = 1) { savedStateHandle.get("owner") } + verify(exactly = 1) { savedStateHandle.get("repo") } + verify(exactly = 1) { savedStateHandle.get("pull_request_number") } + verify(exactly = 1) { savedStateHandle.get("submission_date") } + + assertEquals("correctPullRequestNumber", viewModel.state.pullRequestNumber) + } + + @Test + fun `GIVEN empty reviewers source WHEN init THEN state is correct, no reviewers`() { + every { savedStateHandle.get(any()) } returns "example" + every { savedStateHandle.get("submission_date") } returns "2022-01-29T12:00:00" + + val viewModel = createViewModel() + + assertEquals("example", viewModel.state.pullRequestNumber) + assertEquals(emptyList(), viewModel.state.reviewers) + } + + @Test + fun `GIVEN mix reviewers WHEN init THEN list of reviewers is fetched`() = + runTest { + every { savedStateHandle.get(any()) } returns "example" + every { savedStateHandle.get("submission_date") } returns "2022-01-29T08:00:00" + every { savedStateHandle.get("pull_request_number") } returns "correctPullRequestNumber" + + val viewModel = createViewModel() + + val expected = listOf( + Reviewer(1, "user1", true, 8, 6), + Reviewer(2, "user2", true, 8, 6), + Reviewer(3, "user3", false, 8, null), + Reviewer(4, "user4", false, 8, null), + ) + val actual = viewModel.state.reviewers + + assertTrue( + actual.containsAll(expected) && expected.containsAll(actual) + ) + } + + @Test + fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = + runTest { + every { savedStateHandle.get(any()) } returns "example" + every { savedStateHandle.get("submission_date") } returns "2022-01-29T08:00:00" + every { savedStateHandle.get("pull_request_number") } returns "onlyRequestedReviewers" + + val viewModel = createViewModel() + + val expected = listOf( + Reviewer(3, "user3", false, 8, null), + Reviewer(4, "user4", false, 8, null), + ) + val actual = viewModel.state.reviewers + + assertTrue( + actual.containsAll(expected) && expected.containsAll(actual) + ) + } + + @Test + fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = + runTest { + every { savedStateHandle.get(any()) } returns "example" + every { savedStateHandle.get("submission_date") } returns "2022-01-29T08:00:00" + every { savedStateHandle.get("pull_request_number") } returns "onlyReviewsNumber" + + val viewModel = createViewModel() + + val expected = listOf( + Reviewer(1, "user1", true, 8, 6), + Reviewer(2, "user2", true, 8, 6), + ) + val actual = viewModel.state.reviewers + + assertTrue( + actual.containsAll(expected) && expected.containsAll(actual) + ) + } +} diff --git a/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt new file mode 100644 index 000000000..e429203e4 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt @@ -0,0 +1,24 @@ +package com.appunite.loudius.util + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.jupiter.api.extension.AfterTestExecutionCallback +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback +import org.junit.jupiter.api.extension.ExtensionContext + +@OptIn(ExperimentalCoroutinesApi::class) +class MainDispatcherExtension( + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) : BeforeTestExecutionCallback, AfterTestExecutionCallback { + override fun beforeTestExecution(context: ExtensionContext?) { + Dispatchers.setMain(testDispatcher) + } + + override fun afterTestExecution(context: ExtensionContext?) { + Dispatchers.resetMain() + } +} From 701e05c33416151b6dc028c96d86d63715937ae5 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 11:07:29 +0100 Subject: [PATCH 141/526] Remove ExampleUnitTest.kt. --- .../java/com/appunite/loudius/ExampleUnitTest.kt | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt diff --git a/app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt b/app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt deleted file mode 100644 index d8c0d4425..000000000 --- a/app/src/test/java/com/appunite/loudius/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.appunite.loudius - -import junit.framework.TestCase.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} From dcdedee971706aea04422717c2e07815685a44ae Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 11:18:47 +0100 Subject: [PATCH 142/526] Perform cleaning and shortening tests. --- .../ui/reviewers/ReviewersViewModelTest.kt | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 516239641..df27ccdb7 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -26,14 +26,18 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MainDispatcherExtension::class) class ReviewersViewModelTest { - private val repository: PullRequestRepository = FakePullRequestRepository() - private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") private val systemClockFixed = Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.systemDefault()) - private fun createViewModel(): ReviewersViewModel = - ReviewersViewModel(repository, savedStateHandle) + private val repository: PullRequestRepository = FakePullRequestRepository() + private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) { + every { get(any()) } returns "example" + every { get("submission_date") } returns "2022-01-29T08:00:00" + every { get("pull_request_number") } returns "correctPullRequestNumber" + } + + private fun createViewModel() = ReviewersViewModel(repository, savedStateHandle) @BeforeEach fun setup() { @@ -41,9 +45,8 @@ class ReviewersViewModelTest { every { Clock.systemDefaultZone() } returns systemClockFixed } - @Test - fun `GIVEN no values in saved state WHEN init THEN throw IllegalArgumentException`() { + fun `GIVEN no values in saved state WHEN init THEN throw IllegalStateException`() { every { savedStateHandle.get(any()) } returns null assertThrows { @@ -53,10 +56,6 @@ class ReviewersViewModelTest { @Test fun `GIVEN correct initial values WHEN init THEN state is correct`() { - every { savedStateHandle.get(any()) } returns "example" - every { savedStateHandle.get("submission_date") } returns "2022-01-29T12:00:00" - every { savedStateHandle.get("pull_request_number") } returns "correctPullRequestNumber" - val viewModel = createViewModel() verify(exactly = 1) { savedStateHandle.get("owner") } @@ -69,22 +68,17 @@ class ReviewersViewModelTest { @Test fun `GIVEN empty reviewers source WHEN init THEN state is correct, no reviewers`() { - every { savedStateHandle.get(any()) } returns "example" - every { savedStateHandle.get("submission_date") } returns "2022-01-29T12:00:00" + every { savedStateHandle.get("pull_request_number") } returns "pullRequestWithNoReviewers" val viewModel = createViewModel() - assertEquals("example", viewModel.state.pullRequestNumber) + assertEquals("pullRequestWithNoReviewers", viewModel.state.pullRequestNumber) assertEquals(emptyList(), viewModel.state.reviewers) } @Test fun `GIVEN mix reviewers WHEN init THEN list of reviewers is fetched`() = runTest { - every { savedStateHandle.get(any()) } returns "example" - every { savedStateHandle.get("submission_date") } returns "2022-01-29T08:00:00" - every { savedStateHandle.get("pull_request_number") } returns "correctPullRequestNumber" - val viewModel = createViewModel() val expected = listOf( @@ -103,8 +97,6 @@ class ReviewersViewModelTest { @Test fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = runTest { - every { savedStateHandle.get(any()) } returns "example" - every { savedStateHandle.get("submission_date") } returns "2022-01-29T08:00:00" every { savedStateHandle.get("pull_request_number") } returns "onlyRequestedReviewers" val viewModel = createViewModel() @@ -123,8 +115,6 @@ class ReviewersViewModelTest { @Test fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = runTest { - every { savedStateHandle.get(any()) } returns "example" - every { savedStateHandle.get("submission_date") } returns "2022-01-29T08:00:00" every { savedStateHandle.get("pull_request_number") } returns "onlyReviewsNumber" val viewModel = createViewModel() From d246222fb858ac4763eb24804bb0b2f3046d9cd6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 12:10:29 +0100 Subject: [PATCH 143/526] Move counting hours from PR start to mapper. --- .../ui/reviewers/ReviewersViewModel.kt | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index dd45f2ac5..0ecd0e258 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -69,27 +69,30 @@ class ReviewersViewModel @Inject constructor( private suspend fun fetchReviews(initialValues: InitialValues) { val (owner, repo, pullRequestNumber, submissionTime) = initialValues - val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) repository.getReviews(owner, repo, pullRequestNumber).onSuccess { reviews -> - val reviewersAfterReview = reviews.mapToReviewers(hoursFromPRStart) + val reviewersAfterReview = reviews.mapToReviewers(submissionTime) state = state.copy(reviewers = state.reviewers + reviewersAfterReview) } } - private fun List.mapToReviewers(hoursFromPRStart: Long) = groupBy { it.user.id } - .map { reviewsForSingleUser -> - val latestReview = reviewsForSingleUser.value.minBy { it.submittedAt } - val hoursFromReviewDone = countHoursTillNow(latestReview.submittedAt) - - Reviewer( - latestReview.user.id, - latestReview.user.login, - true, - hoursFromPRStart, - hoursFromReviewDone, - ) - } + private fun List.mapToReviewers(submissionTime: String): List { + val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) + + return groupBy { it.user.id } + .map { reviewsForSingleUser -> + val latestReview = reviewsForSingleUser.value.minBy { it.submittedAt } + val hoursFromReviewDone = countHoursTillNow(latestReview.submittedAt) + + Reviewer( + latestReview.user.id, + latestReview.user.login, + true, + hoursFromPRStart, + hoursFromReviewDone, + ) + } + } private fun countHoursTillNow(submissionTime: LocalDateTime): Long = ChronoUnit.HOURS.between(submissionTime, LocalDateTime.now()) From 7d4dadc3bdedfff7c28e514ac293b80005ebfe73 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 10 Mar 2023 12:22:28 +0100 Subject: [PATCH 144/526] Remove unnecessary line break --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 6fec9169c..366c847bb 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -255,7 +255,6 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - ) val reviewer = Reviewer("1", "exampleLogin", "https://example/avatar") From 39b21ec527667bf1738bfc34b02c108b587d4634 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 10 Mar 2023 14:45:25 +0100 Subject: [PATCH 145/526] Rename tests with incorrect access token --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 6fae0afa4..51bec785c 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -166,7 +166,7 @@ class PullRequestsNetworkDataSourceTest { } @Test - fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + fun `Given incorrect access token WHEN processing request THEN return failure with Unknown error`() = runTest { // language=JSON val jsonResponse = """ @@ -268,7 +268,7 @@ class PullRequestsNetworkDataSourceTest { } @Test - fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + fun `Given incorrect access token WHEN processing request THEN return failure with Unknown error`() = runTest { // language=JSON val jsonResponse = """ @@ -396,7 +396,7 @@ class PullRequestsNetworkDataSourceTest { } @Test - fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + fun `Given incorrect access token WHEN processing request THEN return failure with Unknown error`() = runTest { // language=JSON val jsonResponse = """ From 8dcc1ccc16a18a797edb839bd9efacc8483e2a3a Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 10 Mar 2023 14:46:06 +0100 Subject: [PATCH 146/526] Use LocalDateTime instead of ZonedDateTime --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 51bec785c..c31d148aa 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,6 +10,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import java.time.ZonedDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -387,7 +388,7 @@ class PullRequestsNetworkDataSourceTest { "1", User(33498031, "exampleUser"), ReviewState.COMMENTED, - ZonedDateTime.parse("2023-03-02T10:21:36Z").toLocalDateTime(), + LocalDateTime.parse("2023-03-02T10:21:36Z"), ), ), ) From d9a1a0c2b97613258a0410a72b7ca20989b6085a Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 10 Mar 2023 14:37:53 +0100 Subject: [PATCH 147/526] Rename tests with incorrect access token --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 366c847bb..1af506510 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -264,7 +264,7 @@ class PullRequestsNetworkDataSourceTest { } @Test - fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + fun `Given incorrect access token WHEN processing request THEN return failure with UnknownError error`() = runTest { // language=JSON val jsonResponse = """ @@ -392,7 +392,7 @@ class PullRequestsNetworkDataSourceTest { } @Test - fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + fun `Given incorrect access token WHEN processing request THEN return failure with UnknownError error`() = runTest { // language=JSON val jsonResponse = """ From 1b01d0c44694498f4a05446e53337063699e89ea Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 10 Mar 2023 14:40:55 +0100 Subject: [PATCH 148/526] Use LocalDateTime instead of ZonedDateTime --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 9 +++++---- .../datasource/PullRequestsNetworkDataSourceTest.kt | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 476355f39..a35945775 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -30,6 +30,7 @@ import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme +import java.time.LocalDateTime import java.time.ZonedDateTime @Composable @@ -130,7 +131,7 @@ fun PullRequestsScreenPreview() { number = 0, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", title = "[SIL-67] Details screen - network layer", - updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), + updatedAt = LocalDateTime.parse("2023-03-07T09:24:24"), ), PullRequest( id = 1, @@ -138,7 +139,7 @@ fun PullRequestsScreenPreview() { number = 1, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", title = "[SIL-66] Add client secret to build config", - updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), + updatedAt = LocalDateTime.parse("2023-03-07T09:24:24"), ), PullRequest( id = 2, @@ -146,7 +147,7 @@ fun PullRequestsScreenPreview() { number = 2, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", title = "[SIL-73] Storing access token", - updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), + updatedAt = LocalDateTime.parse("2023-03-07T09:24:24"), ), PullRequest( id = 3, @@ -154,7 +155,7 @@ fun PullRequestsScreenPreview() { number = 3, repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", title = "[SIL-62/SIL-75] Provide new annotation for API instances", - updatedAt = ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), + updatedAt = LocalDateTime.parse("2023-03-07T09:24:24"), ), ), ) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 1af506510..759610f6e 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,6 +10,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -20,7 +21,6 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.ZonedDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -155,8 +155,8 @@ class PullRequestsNetworkDataSourceTest { number = 1, repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", - ZonedDateTime.parse("2023-03-07T09:24:24Z").toLocalDateTime(), - ), + LocalDateTime.parse("2023-03-07T09:24:24") + ) ), ), ) @@ -383,7 +383,7 @@ class PullRequestsNetworkDataSourceTest { "1", User(33498031, "exampleUser"), ReviewState.COMMENTED, - ZonedDateTime.parse("2023-03-02T10:21:36Z").toLocalDateTime(), + LocalDateTime.parse("2023-03-02T10:21:36"), ), ), ) From 84ea2abdd7a3451696186ebe0ee899421c32b0b5 Mon Sep 17 00:00:00 2001 From: kezc Date: Fri, 10 Mar 2023 13:57:01 +0000 Subject: [PATCH 149/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 1 - .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index a35945775..37c4d346a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -31,7 +31,6 @@ import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import java.time.LocalDateTime -import java.time.ZonedDateTime @Composable fun PullRequestsScreen(viewModel: PullRequestsViewModel = hiltViewModel()) { diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 759610f6e..a9bfa64d9 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,7 +10,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -155,8 +155,8 @@ class PullRequestsNetworkDataSourceTest { number = 1, repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", - LocalDateTime.parse("2023-03-07T09:24:24") - ) + LocalDateTime.parse("2023-03-07T09:24:24"), + ), ), ), ) From b1b2a0e406bb981b402cda45e7052f4586cc0177 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 10 Mar 2023 14:56:53 +0100 Subject: [PATCH 150/526] Move navigation action handle from screen to the view model. --- .../ui/pullrequests/PullRequestsScreen.kt | 31 +++++++--------- .../ui/pullrequests/PullRequestsViewModel.kt | 37 ++++++++++++++++++- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 5fe13fdc9..717611822 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -40,16 +41,22 @@ fun PullRequestsScreen( navigateToReviewers: NavigateToReviewers, ) { val state = viewModel.state + LaunchedEffect(state.navigateToReviewers) { + state.navigateToReviewers?.let { + navigateToReviewers(it.owner, it.repo, it.pullRequestNumber, it.submissionTime) + viewModel.onAction(PulLRequestsAction.OnNavigateToReviewers) + } + } PullRequestsScreenStateless( pullRequests = state.pullRequests, - onItemClick = navigateToReviewers, + onItemClick = viewModel::onAction, ) } @Composable private fun PullRequestsScreenStateless( pullRequests: List, - onItemClick: NavigateToReviewers, + onItemClick: (PulLRequestsAction) -> Unit, ) { Scaffold(topBar = { LoudiusTopAppBar(title = stringResource(R.string.app_name)) @@ -75,7 +82,7 @@ private fun PullRequestsScreenStateless( private fun PullRequestItem( data: PullRequest, darkBackground: Boolean, - onClick: NavigateToReviewers, + onClick: (PulLRequestsAction) -> Unit, ) { val backgroundColor = if (darkBackground) { MaterialTheme.colorScheme.onSurface.copy(0.08f) @@ -86,7 +93,7 @@ private fun PullRequestItem( modifier = Modifier .fillMaxWidth() .background(backgroundColor) - .clickable { onItemClick(onClick, data) }, + .clickable { onClick(PulLRequestsAction.ItemClick(data.id)) }, ) { PullRequestIcon() RepoDetails(pullRequestTitle = data.title, repositoryName = data.fullRepositoryName) @@ -94,18 +101,6 @@ private fun PullRequestItem( Divider(color = MaterialTheme.colorScheme.outlineVariant) } -private fun onItemClick( - onClick: NavigateToReviewers, - data: PullRequest, -) { - onClick( - data.owner, - data.shortRepositoryName, - data.number.toString(), - data.createdAt.toString(), - ) -} - @Composable private fun PullRequestIcon() { Image( @@ -133,7 +128,7 @@ private fun RepoDetails(pullRequestTitle: String, repositoryName: String) { @Composable fun PullRequestsScreenEmptyListPreview() { LoudiusTheme { - PullRequestsScreenStateless(emptyList()) { _, _, _, _ -> } + PullRequestsScreenStateless(emptyList()) { } } } @@ -176,6 +171,6 @@ fun PullRequestsScreenPreview() { createdAt = LocalDateTime.parse("2022-01-29T16:31:41Z"), ), ), - ) { _, _, _, _ -> } + ) { } } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 10a9468ec..edbac5a30 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -8,11 +8,24 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.PullRequestsRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch + +sealed class PulLRequestsAction { + data class ItemClick(val id: Int) : PulLRequestsAction() + object OnNavigateToReviewers : PulLRequestsAction() +} data class PullRequestState( val pullRequests: List = emptyList(), + val navigateToReviewers: NavigationPayload? = null +) + +data class NavigationPayload( + val owner: String, + val repo: String, + val pullRequestNumber: String, + val submissionTime: String, ) @HiltViewModel @@ -30,4 +43,26 @@ class PullRequestsViewModel @Inject constructor( } } } + + fun onAction(action: PulLRequestsAction) = when (action) { + is PulLRequestsAction.ItemClick -> navigateToReviewers(action.id) + is PulLRequestsAction.OnNavigateToReviewers -> resetNavigationState() + } + + private fun navigateToReviewers(itemClickedId: Int) { + val index = state.pullRequests.indexOfFirst { it.id == itemClickedId } + val itemClickedData = state.pullRequests[index] + state = state.copy( + navigateToReviewers = NavigationPayload( + itemClickedData.owner, + itemClickedData.shortRepositoryName, + itemClickedData.number.toString(), + itemClickedData.createdAt.toString() + ) + ) + } + + private fun resetNavigationState() { + state = state.copy(navigateToReviewers = null) + } } From 6971273c3edbe00377d8194246a64487ff6f2c45 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Sun, 12 Mar 2023 17:12:03 +0100 Subject: [PATCH 151/526] Rename tests with incorrect access token --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index a9bfa64d9..29f0ebf45 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -165,7 +165,7 @@ class PullRequestsNetworkDataSourceTest { } @Test - fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + fun `Given incorrect access token WHEN processing request THEN return failure with Unknown error`() = runTest { // language=JSON val jsonResponse = """ From 07976be6898dab74f1ce646b03994bd4ad871b51 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Sun, 12 Mar 2023 18:36:46 +0100 Subject: [PATCH 152/526] Rename tests with incorrect access token --- .../appunite/loudius/network/datasource/UserDataSourceTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index 497e47762..eac6021a9 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -99,7 +99,7 @@ class UserDataSourceTest { } @Test - fun `Given incorrect access token WHEN processing request THEN return failure with Network error`() = + fun `Given incorrect access token WHEN processing request THEN return failure with Unknown error`() = runTest { // language=JSON val jsonResponse = """ From 622f62e8ffc54ded43f8fe45e759c5f5ff59f5a1 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 13 Mar 2023 00:39:37 +0100 Subject: [PATCH 153/526] SIL-80: add error handling --- .../java/com/appunite/loudius/MainActivity.kt | 4 +- .../ui/components/LoudiusErrorScreen.kt | 2 +- .../loudius/ui/loading/LoadingScreen.kt | 65 +++++++++++++++++++ .../LoadingViewModel.kt} | 24 +++++-- .../appunite/loudius/ui/repos/ReposScreen.kt | 31 --------- app/src/main/res/values/strings.xml | 2 +- 6 files changed, 88 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt rename app/src/main/java/com/appunite/loudius/ui/{repos/ReposViewModel.kt => loading/LoadingViewModel.kt} (52%) delete mode 100644 app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 77b6ceb1e..460ee5c0e 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -13,9 +13,9 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import com.appunite.loudius.common.Constants.REDIRECT_URL import com.appunite.loudius.common.Screen +import com.appunite.loudius.ui.loading.LoadingScreen import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.pullrequests.PullRequestsScreen -import com.appunite.loudius.ui.repos.ReposScreen import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.AndroidEntryPoint @@ -46,7 +46,7 @@ class MainActivity : ComponentActivity() { }, ), ) { - ReposScreen(intent = intent) { + LoadingScreen(intent = intent) { navController.navigate(Screen.PullRequests.route) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index 178968310..095a30337 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -60,7 +60,7 @@ fun LoudiusErrorScreenPreview() { LoudiusTheme { LoudiusErrorScreen( errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again_text), + buttonText = stringResource(R.string.try_again), onButtonClick = {}, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt new file mode 100644 index 000000000..9caba0a71 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -0,0 +1,65 @@ +package com.appunite.loudius.ui.loading + +import android.content.Intent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.appunite.loudius.R +import com.appunite.loudius.ui.components.LoudiusErrorScreen +import kotlinx.coroutines.delay + +@Composable +fun LoadingScreen( + intent: Intent, + viewModel: LoadingViewModel = hiltViewModel(), + onNavigateToPullRequest: () -> Unit, +) { + viewModel.state.let { state -> + val code = intent.data?.getQueryParameter("code") + val rememberedCode = rememberUpdatedState(newValue = code) + LaunchedEffect(key1 = rememberedCode) { + rememberedCode.value?.let { + viewModel.getAccessToken(it) + delay(1000) + } + } + if (state.showErrorScreen) { + ShowLoudiusErrorScreen { + code?.let { + viewModel.getAccessToken(it) + } + } + } else { + ShowLoadingIndicator(code = code) + } + if (state.accessToken != null) { + LaunchedEffect(key1 = null) { + onNavigateToPullRequest() + } + } + } +} + +@Composable +private fun ShowLoudiusErrorScreen( + onTryAgainClick: () -> Unit +) { + LoudiusErrorScreen( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(id = R.string.try_again) + ) { + onTryAgainClick() + } +} + +@Composable +private fun ShowLoadingIndicator(code: String?) { + //TODO add loading indicator + Column { + Text(text = code ?: "code is already consumed") + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt similarity index 52% rename from app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt rename to app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index e917c7395..551412939 100644 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -1,6 +1,8 @@ -package com.appunite.loudius.ui.repos +package com.appunite.loudius.ui.loading -import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.BuildConfig @@ -10,11 +12,19 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +data class LoadingState( + val accessToken: String? = null, + val showErrorScreen: Boolean = false +) + @HiltViewModel -class ReposViewModel @Inject constructor( +class LoadingViewModel @Inject constructor( private val authRepository: AuthRepository, ) : ViewModel() { + var state by mutableStateOf(LoadingState()) + private set + fun getAccessToken(code: String) { viewModelScope.launch { authRepository.fetchAccessToken( @@ -22,9 +32,13 @@ class ReposViewModel @Inject constructor( clientSecret = BuildConfig.CLIENT_SECRET, code = code, ).onSuccess { token -> - Log.i("access_token", token.accessToken.toString()) + state = if (token.accessToken != null) { + state.copy(accessToken = token.accessToken) + } else { + state.copy(showErrorScreen = true) + } }.onFailure { - Log.i("access_token", it.message.toString()) + state = state.copy(showErrorScreen = true) } } } diff --git a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt b/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt deleted file mode 100644 index 386c63ed5..000000000 --- a/app/src/main/java/com/appunite/loudius/ui/repos/ReposScreen.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.appunite.loudius.ui.repos - -import android.content.Intent -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberUpdatedState -import androidx.hilt.navigation.compose.hiltViewModel -import kotlinx.coroutines.delay - -@Composable -fun ReposScreen( - intent: Intent, - viewModel: ReposViewModel = hiltViewModel(), - onNavigateToPullRequest: () -> Unit, -) { - val code = intent.data?.getQueryParameter("code") - val rememberedCode = rememberUpdatedState(newValue = code) - Column { - Text(text = code ?: "code is already consumed") - } - LaunchedEffect(key1 = rememberedCode) { - rememberedCode.value?.let { - viewModel.getAccessToken(it) - intent.data = null - delay(1000) - onNavigateToPullRequest() - } - } -} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7fec3e177..b53aed4ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,5 +12,5 @@ Something went wrong… OK error image - Try again + Try again From b2436862dd4bd2b3737245f21eecb36a7360369c Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Sun, 12 Mar 2023 23:42:39 +0000 Subject: [PATCH 154/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/ui/loading/LoadingScreen.kt | 6 +++--- .../com/appunite/loudius/ui/loading/LoadingViewModel.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 9caba0a71..386961e76 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -46,11 +46,11 @@ fun LoadingScreen( @Composable private fun ShowLoudiusErrorScreen( - onTryAgainClick: () -> Unit + onTryAgainClick: () -> Unit, ) { LoudiusErrorScreen( errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(id = R.string.try_again) + buttonText = stringResource(id = R.string.try_again), ) { onTryAgainClick() } @@ -58,7 +58,7 @@ private fun ShowLoudiusErrorScreen( @Composable private fun ShowLoadingIndicator(code: String?) { - //TODO add loading indicator + // TODO add loading indicator Column { Text(text = code ?: "code is already consumed") } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index 551412939..91f77be35 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -14,7 +14,7 @@ import javax.inject.Inject data class LoadingState( val accessToken: String? = null, - val showErrorScreen: Boolean = false + val showErrorScreen: Boolean = false, ) @HiltViewModel From bef3ffd96ee18d2be4804517a5a8947bb4c1f036 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 13 Mar 2023 00:50:21 +0100 Subject: [PATCH 155/526] SIL-80: adjust tests --- .../com/appunite/loudius/domain/AuthRepositoryImplTest.kt | 5 +++-- .../java/com/appunite/loudius/fakes/FakeAuthRepository.kt | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index 18e33c32b..b77e9f65e 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.datasource.AuthDataSource +import com.appunite.loudius.network.model.AccessTokenResponse import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -15,7 +16,7 @@ class AuthRepositoryImplTest { private val networkDataSource: AuthDataSource = mockk { coEvery { getAccessToken(any(), any(), any()) - } returns Result.success("validAccessToken") + } returns Result.success(AccessTokenResponse("validAccessToken")) } private val localDataSource: UserLocalDataSource = mockk { every { getAccessToken() } returns "validAccessToken" @@ -30,7 +31,7 @@ class AuthRepositoryImplTest { coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( - Result.success("validAccessToken"), + Result.success(AccessTokenResponse("validAccessToken")), result, ) { "Expected success result with valid access token" } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 453464d7a..0cc2de388 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -1,15 +1,15 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.AuthRepository -import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.model.AccessTokenResponse class FakeAuthRepository : AuthRepository { override suspend fun fetchAccessToken( clientId: String, clientSecret: String, code: String, - ): Result { - return Result.success("validToken") + ): Result { + return Result.success(AccessTokenResponse("validToken")) } override fun getAccessToken(): String { From 13a96dbd4791b63d561667b9cae3aad46190297c Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 13 Mar 2023 01:02:25 +0100 Subject: [PATCH 156/526] SIL-80: code cleanup --- .../appunite/loudius/domain/AuthRepositoryImpl.kt | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt index 55e81e671..ea626aeac 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt @@ -1,6 +1,5 @@ package com.appunite.loudius.domain -import android.util.Log import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.model.AccessTokenResponse import javax.inject.Inject @@ -18,15 +17,11 @@ class AuthRepositoryImpl @Inject constructor( code: String, ): Result { val result = authDataSource.getAccessToken(clientId, clientSecret, code) - result - .onSuccess { - if (it.accessToken != null) { - userLocalDataSource.saveAccessToken(it.accessToken) - } else { - Log.i("failure", it.toString() + "bad_verification_code") - } + result.onSuccess { response -> + response.accessToken?.let { + userLocalDataSource.saveAccessToken(it) } - .onFailure { Log.i("failure", "incorrect_client_credentials") } + } return result } From e0826944a25801369a9212c787829240b74682d8 Mon Sep 17 00:00:00 2001 From: kezc Date: Mon, 13 Mar 2023 06:59:38 +0000 Subject: [PATCH 157/526] [MegaLinter] Apply linters fixes --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 2 +- .../loudius/network/datasource/UserDataSourceTest.kt | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 40b91dc61..eb114d499 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,7 +10,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index eac6021a9..d5fa2bc4b 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -41,7 +41,6 @@ class UserDataSourceTest { ) { "Exception thrown should be NetworkError type" } } - @Test fun `Given correct params WHEN successful response THEN return success`() = runTest { //language=JSON @@ -92,7 +91,7 @@ class UserDataSourceTest { User( id = 1, login = "exampleUser", - ) + ), ) Assertions.assertEquals(expected, actualResponse) { "Data should be valid" } @@ -107,7 +106,7 @@ class UserDataSourceTest { "message": "Bad credentials", "documentation_url": "https://docs.github.com/rest" } - """.trimIndent() + """.trimIndent() mockWebServer.enqueue( MockResponse().setResponseCode(401).setBody(jsonResponse), From ae2d3cd40539a7ef713eab41e990ce610d10c800 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 13 Mar 2023 09:04:02 +0100 Subject: [PATCH 158/526] Fix tests - after merging and changing updated at with created at param in PullRequestResponse. --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 06b75603a..5bbc77fc5 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -1,8 +1,8 @@ package com.appunite.loudius.network.datasource -import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.ReviewState @@ -10,6 +10,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -20,7 +21,6 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -155,7 +155,7 @@ class PullRequestsNetworkDataSourceTest { number = 1, repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", - LocalDateTime.parse("2023-03-07T09:24:24"), + LocalDateTime.parse("2023-03-07T09:21:45"), ), ), ), From 49f2f5e723c3df6a132885c989ba71cadb8f3837 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 13 Mar 2023 08:07:28 +0000 Subject: [PATCH 159/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 8 ++++---- .../appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 2 +- .../datasource/PullRequestsNetworkDataSourceTest.kt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index edbac5a30..e01893bd0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -8,8 +8,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.PullRequestsRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() @@ -18,7 +18,7 @@ sealed class PulLRequestsAction { data class PullRequestState( val pullRequests: List = emptyList(), - val navigateToReviewers: NavigationPayload? = null + val navigateToReviewers: NavigationPayload? = null, ) data class NavigationPayload( @@ -57,8 +57,8 @@ class PullRequestsViewModel @Inject constructor( itemClickedData.owner, itemClickedData.shortRepositoryName, itemClickedData.number.toString(), - itemClickedData.createdAt.toString() - ) + itemClickedData.createdAt.toString(), + ), ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 0ecd0e258..a6996437a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,10 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject -import kotlinx.coroutines.launch data class ReviewersState( val reviewers: List = emptyList(), diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 5bbc77fc5..d46fc51c1 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,7 +10,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { From 7ba6c1433764b67e32588a600f93c5528a35d0fd Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 13 Mar 2023 08:18:55 +0000 Subject: [PATCH 160/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/di/PullRequestModule.kt | 2 +- .../ui/pullrequests/PullRequestsViewModel.kt | 2 +- .../loudius/fakes/FakePullRequestRepository.kt | 10 +++++----- .../PullRequestsNetworkDataSourceTest.kt | 2 +- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 14 +++++++------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt index 6eb11d60d..9c6d9a659 100644 --- a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt @@ -25,6 +25,6 @@ object PullRequestModule { @Singleton fun providePullRequestRepository( dataSource: PullRequestDataSource, - userDataSource: UserDataSource + userDataSource: UserDataSource, ): PullRequestRepository = PullRequestRepositoryImpl(dataSource, userDataSource) } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 9f2646669..f05eba0b6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -8,8 +8,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.PullRequestRepositoryImpl import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 72dcd4187..f0c9d2952 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -20,7 +20,7 @@ class FakePullRequestRepository : PullRequestRepository { override suspend fun getReviews( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result> = when (pullRequestNumber) { "correctPullRequestNumber", "onlyReviewsNumber" -> Result.success( listOf( @@ -30,7 +30,7 @@ class FakePullRequestRepository : PullRequestRepository { Review("4", User(2, "user2"), COMMENTED, date1), Review("5", User(2, "user2"), COMMENTED, date2), Review("6", User(2, "user2"), APPROVED, date3), - ) + ), ) "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) else -> Result.success(emptyList()) @@ -39,15 +39,15 @@ class FakePullRequestRepository : PullRequestRepository { override suspend fun getRequestedReviewers( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber", "onlyRequestedReviewers" -> Result.success( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), RequestedReviewer(4, "user4"), - ) - ) + ), + ), ) "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) else -> Result.success(RequestedReviewersResponse(emptyList())) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 00424cc47..2cb186abb 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,7 +10,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index df27ccdb7..ab51d4fc8 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,10 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -21,6 +17,10 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -90,7 +90,7 @@ class ReviewersViewModelTest { val actual = viewModel.state.reviewers assertTrue( - actual.containsAll(expected) && expected.containsAll(actual) + actual.containsAll(expected) && expected.containsAll(actual), ) } @@ -108,7 +108,7 @@ class ReviewersViewModelTest { val actual = viewModel.state.reviewers assertTrue( - actual.containsAll(expected) && expected.containsAll(actual) + actual.containsAll(expected) && expected.containsAll(actual), ) } @@ -126,7 +126,7 @@ class ReviewersViewModelTest { val actual = viewModel.state.reviewers assertTrue( - actual.containsAll(expected) && expected.containsAll(actual) + actual.containsAll(expected) && expected.containsAll(actual), ) } } From b66610742ce49b37d4f705cc7b3d291f098cb2af Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 13 Mar 2023 11:02:10 +0100 Subject: [PATCH 161/526] Repair test by not using system default time zone. --- .../fakes/FakePullRequestRepository.kt | 4 +- .../ui/reviewers/ReviewersViewModelTest.kt | 56 +++++++++---------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index f0c9d2952..ef5033556 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -22,7 +22,7 @@ class FakePullRequestRepository : PullRequestRepository { repo: String, pullRequestNumber: String, ): Result> = when (pullRequestNumber) { - "correctPullRequestNumber", "onlyReviewsNumber" -> Result.success( + "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( listOf( Review("1", User(1, "user1"), CHANGES_REQUESTED, date1), Review("2", User(1, "user1"), COMMENTED, date2), @@ -41,7 +41,7 @@ class FakePullRequestRepository : PullRequestRepository { repo: String, pullRequestNumber: String, ): Result = when (pullRequestNumber) { - "correctPullRequestNumber", "onlyRequestedReviewers" -> Result.success( + "correctPullRequestNumber", "onlyRequestedReviewersPullNumber" -> Result.success( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index ab51d4fc8..0e2944e6a 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,18 +9,17 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -28,14 +27,14 @@ class ReviewersViewModelTest { private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") private val systemClockFixed = - Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.systemDefault()) - + Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) private val repository: PullRequestRepository = FakePullRequestRepository() private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) { every { get(any()) } returns "example" every { get("submission_date") } returns "2022-01-29T08:00:00" every { get("pull_request_number") } returns "correctPullRequestNumber" } + private lateinit var viewModel: ReviewersViewModel private fun createViewModel() = ReviewersViewModel(repository, savedStateHandle) @@ -56,7 +55,7 @@ class ReviewersViewModelTest { @Test fun `GIVEN correct initial values WHEN init THEN state is correct`() { - val viewModel = createViewModel() + viewModel = createViewModel() verify(exactly = 1) { savedStateHandle.get("owner") } verify(exactly = 1) { savedStateHandle.get("repo") } @@ -70,7 +69,7 @@ class ReviewersViewModelTest { fun `GIVEN empty reviewers source WHEN init THEN state is correct, no reviewers`() { every { savedStateHandle.get("pull_request_number") } returns "pullRequestWithNoReviewers" - val viewModel = createViewModel() + viewModel = createViewModel() assertEquals("pullRequestWithNoReviewers", viewModel.state.pullRequestNumber) assertEquals(emptyList(), viewModel.state.reviewers) @@ -79,54 +78,49 @@ class ReviewersViewModelTest { @Test fun `GIVEN mix reviewers WHEN init THEN list of reviewers is fetched`() = runTest { - val viewModel = createViewModel() + viewModel = createViewModel() val expected = listOf( - Reviewer(1, "user1", true, 8, 6), - Reviewer(2, "user2", true, 8, 6), - Reviewer(3, "user3", false, 8, null), - Reviewer(4, "user4", false, 8, null), + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), ) val actual = viewModel.state.reviewers - assertTrue( - actual.containsAll(expected) && expected.containsAll(actual), - ) + assertEquals(expected, actual) } @Test fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = runTest { - every { savedStateHandle.get("pull_request_number") } returns "onlyRequestedReviewers" + every { savedStateHandle.get("pull_request_number") } returns "onlyRequestedReviewersPullNumber" - val viewModel = createViewModel() + viewModel = createViewModel() val expected = listOf( - Reviewer(3, "user3", false, 8, null), - Reviewer(4, "user4", false, 8, null), + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), ) val actual = viewModel.state.reviewers - assertTrue( - actual.containsAll(expected) && expected.containsAll(actual), - ) + assertEquals(expected, actual) } @Test fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = runTest { - every { savedStateHandle.get("pull_request_number") } returns "onlyReviewsNumber" + every { savedStateHandle.get("pull_request_number") } returns "onlyReviewsPullNumber" - val viewModel = createViewModel() + viewModel = createViewModel() val expected = listOf( - Reviewer(1, "user1", true, 8, 6), - Reviewer(2, "user2", true, 8, 6), + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), ) val actual = viewModel.state.reviewers - assertTrue( - actual.containsAll(expected) && expected.containsAll(actual), - ) + assertEquals(expected, actual) + } } From 89038a970096840d0d674005dcf1c1c10cd2036b Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 13 Mar 2023 13:58:23 +0100 Subject: [PATCH 162/526] Correct naming at the ReviewersViewModelTest.kt. --- .../appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 0e2944e6a..d5318d07d 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -66,7 +66,7 @@ class ReviewersViewModelTest { } @Test - fun `GIVEN empty reviewers source WHEN init THEN state is correct, no reviewers`() { + fun `GIVEN no reviewers WHEN init THEN state is correct with no reviewers`() { every { savedStateHandle.get("pull_request_number") } returns "pullRequestWithNoReviewers" viewModel = createViewModel() @@ -76,7 +76,7 @@ class ReviewersViewModelTest { } @Test - fun `GIVEN mix reviewers WHEN init THEN list of reviewers is fetched`() = + fun `GIVEN mixed reviewers WHEN init THEN list of reviewers is fetched`() = runTest { viewModel = createViewModel() @@ -121,6 +121,5 @@ class ReviewersViewModelTest { val actual = viewModel.state.reviewers assertEquals(expected, actual) - } } From a560c4aafe39d58267f46bad9a73a3931954b3cb Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 13 Mar 2023 13:02:24 +0000 Subject: [PATCH 163/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index d5318d07d..d75abb5d2 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,10 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -20,6 +16,10 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From d6b58646a7783d6a5df341a2d68af8d6616a6726 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 14 Mar 2023 08:08:16 +0100 Subject: [PATCH 164/526] Exclude current user from the reviewers list. --- .../com/appunite/loudius/di/UserModule.kt | 21 +++++++++++++++++++ .../domain/PullRequestRepositoryImpl.kt | 15 +++++++++++-- .../network/datasource/UserDataSource.kt | 12 ----------- .../network/datasource/UserDataSourceImpl.kt | 19 +++++++++++++++++ .../network/datasource/UserDataSourceTest.kt | 2 +- 5 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/di/UserModule.kt delete mode 100644 app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/datasource/UserDataSourceImpl.kt diff --git a/app/src/main/java/com/appunite/loudius/di/UserModule.kt b/app/src/main/java/com/appunite/loudius/di/UserModule.kt new file mode 100644 index 000000000..7c6931c16 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/UserModule.kt @@ -0,0 +1,21 @@ +package com.appunite.loudius.di + +import com.appunite.loudius.network.datasource.UserDataSource +import com.appunite.loudius.network.datasource.UserDataSourceImpl +import com.appunite.loudius.network.services.UserService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object UserModule { + + @Provides + @Singleton + fun provideUserDataSource(userService: UserService): UserDataSource = + UserDataSourceImpl(userService) + +} diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index 352fae67c..13a743253 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -6,6 +6,7 @@ import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.User import javax.inject.Inject interface PullRequestRepository { @@ -37,8 +38,18 @@ class PullRequestRepositoryImpl @Inject constructor( owner: String, repo: String, pullRequestNumber: String, - ): Result> = - pullRequestsNetworkDataSource.getReviews(owner, repo, pullRequestNumber) + ): Result> { + val currentUser = userDataSource.getUser() + return currentUser.flatMap { user -> + pullRequestsNetworkDataSource.getReviews(owner, repo, pullRequestNumber) + .map { excludeCurrentUserReviews(it, user) } + } + } + + private fun excludeCurrentUserReviews( + it: List, + user: User + ) = it.filter { review -> review.user.id != user.id } override suspend fun getRequestedReviewers( owner: String, diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt deleted file mode 100644 index 1bc062674..000000000 --- a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.appunite.loudius.network.datasource - -import com.appunite.loudius.network.model.User -import com.appunite.loudius.network.services.UserService -import com.appunite.loudius.network.utils.safeApiCall -import javax.inject.Inject - -class UserDataSource @Inject constructor(private val userService: UserService) { - suspend fun getUser(): Result = safeApiCall { - userService.getUser() - } -} diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSourceImpl.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSourceImpl.kt new file mode 100644 index 000000000..a5eaa1b93 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSourceImpl.kt @@ -0,0 +1,19 @@ +package com.appunite.loudius.network.datasource + +import com.appunite.loudius.network.model.User +import com.appunite.loudius.network.services.UserService +import com.appunite.loudius.network.utils.safeApiCall +import javax.inject.Inject +import javax.inject.Singleton + +interface UserDataSource { + suspend fun getUser(): Result +} + +@Singleton +class UserDataSourceImpl @Inject constructor(private val userService: UserService) : + UserDataSource { + override suspend fun getUser(): Result = safeApiCall { + userService.getUser() + } +} diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index d5fa2bc4b..a41a25b75 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -20,7 +20,7 @@ class UserDataSourceTest { private val mockWebServer: MockWebServer = MockWebServer() private val userService = retrofitTestDouble(mockWebServer = mockWebServer).create(UserService::class.java) - private val userDataSource = UserDataSource(userService) + private val userDataSource = UserDataSourceImpl(userService) @AfterEach fun tearDown() { From 0d0e29cb03e692077c18c02420b8f5f571288a48 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 14 Mar 2023 08:09:12 +0100 Subject: [PATCH 165/526] Write tests for the success cases at the PullRequestRepositoryImpTest.kt. --- .../domain/PullRequestRepositoryImpTest.kt | 80 +++++++++++++++++++ .../fakes/FakePullRequestDataSource.kt | 56 +++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt create mode 100644 app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt new file mode 100644 index 000000000..e8e5e57c3 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -0,0 +1,80 @@ +package com.appunite.loudius.domain + +import com.appunite.loudius.fakes.FakePullRequestDataSource +import com.appunite.loudius.network.datasource.UserDataSource +import com.appunite.loudius.network.model.RequestedReviewer +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.ReviewState +import com.appunite.loudius.network.model.User +import io.mockk.coEvery +import io.mockk.mockk +import java.time.LocalDateTime +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class PullRequestRepositoryImpTest { + + private val pullRequestDataSource = FakePullRequestDataSource() + private val userDataSource: UserDataSource = mockk { + coEvery { getUser() } returns Result.success(User(1, "user1")) + } + private val repository = PullRequestRepositoryImpl(pullRequestDataSource, userDataSource) + + @Nested + inner class GetReviewsFunctionTest { + + @Test + fun `GIVEN correct values WHEN get reviews THEN return result with reviews excluding ones from current user`() = + runTest { + val actual = repository.getReviews("example", "example", "correctPullRequestNumber") + + val date1 = LocalDateTime.parse("2022-01-29T10:00:00") + + val expected = Result.success( + listOf + ( + Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), + Review("5", User(2, "user2"), ReviewState.COMMENTED, date1), + Review("6", User(2, "user2"), ReviewState.APPROVED, date1), + ) + ) + + assertEquals(expected, actual) + } + + // TODO: Write tests with failure cases + } + + @Nested + inner class GetRequestedReviewersTest { + @Test + fun `GIVEN correct values WHEN get requested reviewers THEN return result with requested reviewers`() = + runTest { + val actual = repository.getRequestedReviewers( + "example", + "example", + "correctPullRequestNumber" + ) + + val expected = Result.success( + RequestedReviewersResponse( + listOf + ( + RequestedReviewer(3, "user3"), + RequestedReviewer(4, "user4"), + ) + ) + ) + + assertEquals(expected, actual) + } + // TODO: Write tests with failure cases + + } + +} diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt new file mode 100644 index 000000000..44f7fddd1 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -0,0 +1,56 @@ +package com.appunite.loudius.fakes + +import com.appunite.loudius.network.datasource.PullRequestDataSource +import com.appunite.loudius.network.model.PullRequestsResponse +import com.appunite.loudius.network.model.RequestedReviewer +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.ReviewState +import com.appunite.loudius.network.model.User +import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime + +class FakePullRequestDataSource : PullRequestDataSource { + + private val date1 = LocalDateTime.parse("2022-01-29T10:00:00") + + override suspend fun getReviews( + owner: String, + repository: String, + pullRequestNumber: String + ): Result> = when (pullRequestNumber) { + "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( + listOf( + Review("1", User(1, "user1"), ReviewState.CHANGES_REQUESTED, date1), + Review("2", User(1, "user1"), ReviewState.COMMENTED, date1), + Review("3", User(1, "user1"), ReviewState.APPROVED, date1), + Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), + Review("5", User(2, "user2"), ReviewState.COMMENTED, date1), + Review("6", User(2, "user2"), ReviewState.APPROVED, date1), + ), + ) + "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + else -> Result.success(emptyList()) + } + + override suspend fun getReviewers( + owner: String, + repository: String, + pullRequestNumber: String + ): Result = when (pullRequestNumber) { + "correctPullRequestNumber", "onlyRequestedReviewersPullNumber" -> Result.success( + RequestedReviewersResponse( + listOf( + RequestedReviewer(3, "user3"), + RequestedReviewer(4, "user4"), + ), + ), + ) + "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + else -> Result.success(RequestedReviewersResponse(emptyList())) + } + + override suspend fun getPullRequestsForUser(author: String): Result { + TODO("Not yet implemented") + } +} From e1ca4169afdcffef327fc7d3730d8acc6f9bf904 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 14 Mar 2023 10:44:51 +0100 Subject: [PATCH 166/526] Divide test into 2 tests - reading initial values. --- .../ui/reviewers/ReviewersViewModelTest.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index d75abb5d2..bb4a839cd 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,6 +9,10 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -16,10 +20,6 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -54,13 +54,18 @@ class ReviewersViewModelTest { } @Test - fun `GIVEN correct initial values WHEN init THEN state is correct`() { + fun `GIVEN correct initial values WHEN init THEN all initial values are read once`() { viewModel = createViewModel() verify(exactly = 1) { savedStateHandle.get("owner") } verify(exactly = 1) { savedStateHandle.get("repo") } verify(exactly = 1) { savedStateHandle.get("pull_request_number") } verify(exactly = 1) { savedStateHandle.get("submission_date") } + } + + @Test + fun `GIVEN correct initial values WHEN init THEN pull request number is correct`() { + viewModel = createViewModel() assertEquals("correctPullRequestNumber", viewModel.state.pullRequestNumber) } From ec9352ca515cf4a73fc3ad7ea14d4a528a54442f Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Tue, 14 Mar 2023 09:56:04 +0000 Subject: [PATCH 167/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index bb4a839cd..6c5da6750 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,10 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -20,6 +16,10 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From abb4c5706edb4481603c11a2b356bd67bc5be8e1 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 14 Mar 2023 11:11:32 +0100 Subject: [PATCH 168/526] Make getUser and getReviews requests run in parallel. --- .../domain/PullRequestRepositoryImpl.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index 13a743253..7146786c0 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -8,6 +8,9 @@ import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.User import javax.inject.Inject +import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext interface PullRequestRepository { suspend fun getReviews( @@ -30,6 +33,7 @@ class PullRequestRepositoryImpl @Inject constructor( private val userDataSource: UserDataSource, ) : PullRequestRepository { override suspend fun getCurrentUserPullRequests(): Result { + val currentUser = userDataSource.getUser() return currentUser.flatMap { pullRequestsNetworkDataSource.getPullRequestsForUser(it.login) } } @@ -38,14 +42,20 @@ class PullRequestRepositoryImpl @Inject constructor( owner: String, repo: String, pullRequestNumber: String, - ): Result> { - val currentUser = userDataSource.getUser() - return currentUser.flatMap { user -> + ): Result> = withContext(coroutineContext) { + val currentUserDeferred = async { userDataSource.getUser() } + val reviewsDeferred = async { pullRequestsNetworkDataSource.getReviews(owner, repo, pullRequestNumber) - .map { excludeCurrentUserReviews(it, user) } + } + val currentUser = currentUserDeferred.await() + val reviews = reviewsDeferred.await() + + return@withContext currentUser.flatMap { user -> + reviews.map { excludeCurrentUserReviews(it, user) } } } + private fun excludeCurrentUserReviews( it: List, user: User From fb5f4acf87750849bcc8c7a49330118d6caa19e6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 14 Mar 2023 11:15:49 +0100 Subject: [PATCH 169/526] Rename function to excludeUserReviews and make it extension function. --- .../appunite/loudius/domain/PullRequestRepositoryImpl.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index 7146786c0..246605ac8 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -51,15 +51,13 @@ class PullRequestRepositoryImpl @Inject constructor( val reviews = reviewsDeferred.await() return@withContext currentUser.flatMap { user -> - reviews.map { excludeCurrentUserReviews(it, user) } + reviews.map { it.excludeUserReviews(user) } } } - - private fun excludeCurrentUserReviews( - it: List, + private fun List.excludeUserReviews( user: User - ) = it.filter { review -> review.user.id != user.id } + ) = filter { review -> review.user.id != user.id } override suspend fun getRequestedReviewers( owner: String, From f53342be2101d59b6e244d09ad073e7455f6ed16 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 14 Mar 2023 13:06:21 +0100 Subject: [PATCH 170/526] SIL-80: add onAction functionality --- .../loudius/ui/loading/LoadingScreen.kt | 20 +++++----- .../loudius/ui/loading/LoadingViewModel.kt | 40 ++++++++++++++++++- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 386961e76..3388c7a39 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.ui.components.LoudiusErrorScreen -import kotlinx.coroutines.delay @Composable fun LoadingScreen( @@ -23,24 +22,23 @@ fun LoadingScreen( val rememberedCode = rememberUpdatedState(newValue = code) LaunchedEffect(key1 = rememberedCode) { rememberedCode.value?.let { - viewModel.getAccessToken(it) - delay(1000) + viewModel.setCodeAndGetAccessToken(it) } } + LaunchedEffect(key1 = state.navigateToPullRequests) { + state.navigateToPullRequests?.let { + onNavigateToPullRequest() + viewModel.onAction(LoadingAction.OnNavigateToPullRequests) + } + } + if (state.showErrorScreen) { ShowLoudiusErrorScreen { - code?.let { - viewModel.getAccessToken(it) - } + viewModel.onAction(LoadingAction.OnTryAgainClick) } } else { ShowLoadingIndicator(code = code) } - if (state.accessToken != null) { - LaunchedEffect(key1 = null) { - onNavigateToPullRequest() - } - } } } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index 91f77be35..d9dc60425 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -12,11 +12,21 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +sealed class LoadingAction { + + object OnNavigateToPullRequests: LoadingAction() + + object OnTryAgainClick: LoadingAction() +} data class LoadingState( val accessToken: String? = null, + val code: String? = null, + val navigateToPullRequests: NavigateToPullRequests? = null, val showErrorScreen: Boolean = false, ) +object NavigateToPullRequests + @HiltViewModel class LoadingViewModel @Inject constructor( private val authRepository: AuthRepository, @@ -25,7 +35,30 @@ class LoadingViewModel @Inject constructor( var state by mutableStateOf(LoadingState()) private set - fun getAccessToken(code: String) { + fun setCodeAndGetAccessToken(code: String?) { + state = state.copy(code = code) + code?.let { + getAccessToken(it) + } + } + + fun onAction(action: LoadingAction) = when (action) { + is LoadingAction.OnTryAgainClick -> onTryAgain() + is LoadingAction.OnNavigateToPullRequests -> onNavigateToPullRequests() + } + + private fun onTryAgain() { + state = state.copy(showErrorScreen = false) + state.code?.let { + getAccessToken(it) + } + } + + private fun onNavigateToPullRequests() { + state = state.copy(navigateToPullRequests = null) + } + + private fun getAccessToken(code: String) { viewModelScope.launch { authRepository.fetchAccessToken( clientId = CLIENT_ID, @@ -33,7 +66,10 @@ class LoadingViewModel @Inject constructor( code = code, ).onSuccess { token -> state = if (token.accessToken != null) { - state.copy(accessToken = token.accessToken) + state.copy( + accessToken = token.accessToken, + navigateToPullRequests = NavigateToPullRequests + ) } else { state.copy(showErrorScreen = true) } From f6500c2a2df947f4f823c250662bf9eaada881a9 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 14 Mar 2023 12:09:44 +0000 Subject: [PATCH 171/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/loading/LoadingViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index d9dc60425..7d6b16f6e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -14,9 +14,9 @@ import javax.inject.Inject sealed class LoadingAction { - object OnNavigateToPullRequests: LoadingAction() + object OnNavigateToPullRequests : LoadingAction() - object OnTryAgainClick: LoadingAction() + object OnTryAgainClick : LoadingAction() } data class LoadingState( val accessToken: String? = null, @@ -68,7 +68,7 @@ class LoadingViewModel @Inject constructor( state = if (token.accessToken != null) { state.copy( accessToken = token.accessToken, - navigateToPullRequests = NavigateToPullRequests + navigateToPullRequests = NavigateToPullRequests, ) } else { state.copy(showErrorScreen = true) From 934911a1647635908f4e0461b648aa1c85d196bf Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 14 Mar 2023 14:40:52 +0100 Subject: [PATCH 172/526] SIL-80: add LoadingViewModel tests --- .../ui/loading/LoadingViewModelTest.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt new file mode 100644 index 000000000..ed3556d63 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -0,0 +1,74 @@ +package com.appunite.loudius.ui.loading + +import com.appunite.loudius.fakes.FakeAuthRepository +import com.appunite.loudius.util.MainDispatcherExtension +import io.mockk.mockkStatic +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock + +@ExtendWith(MainDispatcherExtension::class) +class LoadingViewModelTest { + + companion object { + private const val EXAMPLE_CODE = "code" + private const val EXAMPLE_ACCESS_TOKEN = "validToken" + } + + private val repository: FakeAuthRepository = FakeAuthRepository() + private lateinit var viewModel: LoadingViewModel + + @BeforeEach + fun setup() { + mockkStatic(Clock::class) + viewModel = LoadingViewModel(repository) + } + + @Test + fun `GIVEN example valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigateToPullRequests`() { + //when + viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) + + //then + assert(viewModel.state.code == EXAMPLE_CODE) + assert(viewModel.state.accessToken == EXAMPLE_ACCESS_TOKEN) + assert(viewModel.state.navigateToPullRequests == NavigateToPullRequests) + } + + @Test + fun `GIVEN null as code WHEN setCodeAndGetAccessToken THEN set null as code`() { + //when + viewModel.setCodeAndGetAccessToken(null) + + //then + assert(viewModel.state.code == null) + } + + @Test + fun `GIVEN OnTryAgain action WHEN onAction THEN set showErrorScreen and get access token`() { + //given + val action = LoadingAction.OnTryAgainClick + viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) + + //when + viewModel.onAction(action) + + //then + assert(!viewModel.state.showErrorScreen) + assert(viewModel.state.accessToken == EXAMPLE_ACCESS_TOKEN) + } + + @Test + fun `GIVEN OnNavigateToPullRequests action WHEN onAction THEN set navigateToPullRequests as null`() { + //given + val action = LoadingAction.OnNavigateToPullRequests + viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) + + //when + viewModel.onAction(action) + + //then + assert(viewModel.state.navigateToPullRequests == null) + } +} From c4a53ab4ef53bd744f9bdd739275590eca5bdcf4 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 14 Mar 2023 13:44:50 +0000 Subject: [PATCH 173/526] [MegaLinter] Apply linters fixes --- .../ui/loading/LoadingViewModelTest.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt index ed3556d63..bca8e5cf1 100644 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -27,10 +27,10 @@ class LoadingViewModelTest { @Test fun `GIVEN example valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigateToPullRequests`() { - //when + // when viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - //then + // then assert(viewModel.state.code == EXAMPLE_CODE) assert(viewModel.state.accessToken == EXAMPLE_ACCESS_TOKEN) assert(viewModel.state.navigateToPullRequests == NavigateToPullRequests) @@ -38,37 +38,37 @@ class LoadingViewModelTest { @Test fun `GIVEN null as code WHEN setCodeAndGetAccessToken THEN set null as code`() { - //when + // when viewModel.setCodeAndGetAccessToken(null) - //then + // then assert(viewModel.state.code == null) } @Test fun `GIVEN OnTryAgain action WHEN onAction THEN set showErrorScreen and get access token`() { - //given + // given val action = LoadingAction.OnTryAgainClick viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - //when + // when viewModel.onAction(action) - //then + // then assert(!viewModel.state.showErrorScreen) assert(viewModel.state.accessToken == EXAMPLE_ACCESS_TOKEN) } @Test fun `GIVEN OnNavigateToPullRequests action WHEN onAction THEN set navigateToPullRequests as null`() { - //given + // given val action = LoadingAction.OnNavigateToPullRequests viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - //when + // when viewModel.onAction(action) - //then + // then assert(viewModel.state.navigateToPullRequests == null) } } From 269d0e4a01b9049c438e6eec07706cab49f62ea2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 14 Mar 2023 15:35:15 +0100 Subject: [PATCH 174/526] Base implementation of the notify mechanism. --- .../domain/PullRequestRepositoryImpl.kt | 15 ++++++++++ .../PullRequestsNetworkDataSource.kt | 16 ++++++++++ .../network/services/PullRequestsService.kt | 10 +++++++ .../loudius/ui/reviewers/ReviewersScreen.kt | 29 +++++++++++++++---- .../ui/reviewers/ReviewersViewModel.kt | 20 +++++++++++-- 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index 246605ac8..7e37d47bc 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -26,6 +26,13 @@ interface PullRequestRepository { ): Result suspend fun getCurrentUserPullRequests(): Result + + suspend fun notify( + owner: String, + repo: String, + pullRequestNumber: String, + message: String + ): Result } class PullRequestRepositoryImpl @Inject constructor( @@ -38,6 +45,14 @@ class PullRequestRepositoryImpl @Inject constructor( return currentUser.flatMap { pullRequestsNetworkDataSource.getPullRequestsForUser(it.login) } } + override suspend fun notify( + owner: String, + repo: String, + pullRequestNumber: String, + message: String + ): Result = pullRequestsNetworkDataSource.notify(owner, repo, pullRequestNumber, message) + + override suspend fun getReviews( owner: String, repo: String, diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 0417d8369..0a92fe0af 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -22,6 +22,13 @@ interface PullRequestDataSource { ): Result> suspend fun getPullRequestsForUser(author: String): Result + + suspend fun notify( + owner: String, + repository: String, + pullRequestNumber: String, + message: String + ): Result } @Singleton @@ -47,4 +54,13 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Pul ): Result> = safeApiCall { service.getReviews(owner, repository, pullRequestNumber) } + + override suspend fun notify( + owner: String, + repository: String, + pullRequestNumber: String, + message: String + ): Result = safeApiCall { + service.notify(owner, repository, pullRequestNumber, message) + } } diff --git a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt index ad6f1f657..29b689ef0 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt @@ -3,7 +3,9 @@ package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -28,4 +30,12 @@ interface PullRequestsService { @Path("repo") repo: String, @Path("pull_number") pullRequestNumber: String, ): List + + @POST("/repos/{owner}/{repo}/issues/{issues_number}/comments") + suspend fun notify( + @Path("owner") owner: String, + @Path("repo") repo: String, + @Path("issue_number") pullRequestNumber: String, + @Body message: String + ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 4d1373f8a..95c6cd201 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -38,6 +38,7 @@ fun ReviewersScreen( pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, onClickBackArrow = navigateBack, + onNotifyClick = viewModel::onAction ) } @@ -47,6 +48,7 @@ private fun ReviewersScreenStateless( pullRequestNumber: String, reviewers: List, onClickBackArrow: () -> Unit, + onNotifyClick: (ReviewersAction) -> Unit ) { Scaffold( topBar = { @@ -56,14 +58,18 @@ private fun ReviewersScreenStateless( ) }, content = { padding -> - ReviewersScreenContent(reviewers, modifier = Modifier.padding(padding)) + ReviewersScreenContent(reviewers, modifier = Modifier.padding(padding), onNotifyClick) }, modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) } @Composable -private fun ReviewersScreenContent(reviewers: List, modifier: Modifier) { +private fun ReviewersScreenContent( + reviewers: List, + modifier: Modifier, + onNotifyClick: (ReviewersAction) -> Unit +) { LazyColumn( modifier = modifier.fillMaxWidth(), ) { @@ -71,7 +77,7 @@ private fun ReviewersScreenContent(reviewers: List, modifier: Modifier ReviewerItem( reviewer = reviewer, backgroundColor = resolveReviewerBackgroundColor(index), - onNotifyClick = {}, + onNotifyClick = onNotifyClick, ) } } @@ -82,7 +88,11 @@ private fun resolveReviewerBackgroundColor(index: Int) = if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface @Composable -private fun ReviewerItem(reviewer: Reviewer, backgroundColor: Color, onNotifyClick: () -> Unit) { +private fun ReviewerItem( + reviewer: Reviewer, + backgroundColor: Color, + onNotifyClick: (ReviewersAction) -> Unit +) { Row( modifier = Modifier .fillMaxWidth() @@ -100,7 +110,10 @@ private fun ReviewerItem(reviewer: Reviewer, backgroundColor: Color, onNotifyCli IsReviewedHeadlineText(reviewer) ReviewerName(reviewer) } - NotifyButton(onNotifyClick, Modifier.align(CenterVertically)) + NotifyButton( + { onNotifyClick(ReviewersAction.Notify(reviewer.login)) }, + Modifier.align(CenterVertically) + ) } } @@ -170,6 +183,10 @@ fun DetailsScreenPreview() { val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { - ReviewersScreenStateless(pullRequestNumber = "Pull request #1", reviewers = reviewers, {}) + ReviewersScreenStateless( + pullRequestNumber = "Pull request #1", + reviewers = reviewers, + {}, + {}) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 3f56600e1..4886f0258 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,14 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.launch + +sealed class ReviewersAction { + data class Notify(val userLogin: String) : ReviewersAction() +} data class ReviewersState( val reviewers: List = emptyList(), @@ -25,7 +29,7 @@ data class ReviewersState( @HiltViewModel class ReviewersViewModel @Inject constructor( private val repository: PullRequestRepository, - savedStateHandle: SavedStateHandle, + private val savedStateHandle: SavedStateHandle, ) : ViewModel() { var state by mutableStateOf(ReviewersState()) @@ -97,6 +101,18 @@ class ReviewersViewModel @Inject constructor( private fun countHoursTillNow(submissionTime: LocalDateTime): Long = ChronoUnit.HOURS.between(submissionTime, LocalDateTime.now()) + fun onAction(action: ReviewersAction) = when (action) { + is ReviewersAction.Notify -> notifyUser(action.userLogin) + } + + private fun notifyUser(userLogin: String) { + val (owner, repo, pullRequestNumber) = getInitialValues(savedStateHandle) + + viewModelScope.launch { + repository.notify(owner, repo, pullRequestNumber, "@$userLogin") + } + } + private data class InitialValues( val owner: String, val repo: String, From 006faaf3b10f8bf151acee180bcfcfc2ffb8dbc6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 15 Mar 2023 08:24:06 +0100 Subject: [PATCH 175/526] Change return type to the Unit from notify request. --- .../loudius/domain/PullRequestRepositoryImpl.kt | 17 +++++++++-------- .../datasource/PullRequestsNetworkDataSource.kt | 3 ++- .../network/model/request/NotifyRequestBody.kt | 6 ++++++ .../network/services/PullRequestsService.kt | 8 +++++--- 4 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index 7e37d47bc..8d2b6d60e 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -45,14 +45,6 @@ class PullRequestRepositoryImpl @Inject constructor( return currentUser.flatMap { pullRequestsNetworkDataSource.getPullRequestsForUser(it.login) } } - override suspend fun notify( - owner: String, - repo: String, - pullRequestNumber: String, - message: String - ): Result = pullRequestsNetworkDataSource.notify(owner, repo, pullRequestNumber, message) - - override suspend fun getReviews( owner: String, repo: String, @@ -70,6 +62,7 @@ class PullRequestRepositoryImpl @Inject constructor( } } + private fun List.excludeUserReviews( user: User ) = filter { review -> review.user.id != user.id } @@ -80,4 +73,12 @@ class PullRequestRepositoryImpl @Inject constructor( pullRequestNumber: String, ): Result = pullRequestsNetworkDataSource.getReviewers(owner, repo, pullRequestNumber) + + override suspend fun notify( + owner: String, + repo: String, + pullRequestNumber: String, + message: String + ): Result = + pullRequestsNetworkDataSource.notify(owner, repo, pullRequestNumber, message) } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 0a92fe0af..672d6ec31 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -3,6 +3,7 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.request.NotifyRequestBody import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject @@ -61,6 +62,6 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Pul pullRequestNumber: String, message: String ): Result = safeApiCall { - service.notify(owner, repository, pullRequestNumber, message) + service.notify(owner, repository, pullRequestNumber, NotifyRequestBody(message)) } } diff --git a/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt new file mode 100644 index 000000000..6c3bc0daf --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt @@ -0,0 +1,6 @@ +package com.appunite.loudius.network.model.request + + +data class NotifyRequestBody( + val body: String +) diff --git a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt index 29b689ef0..46bcb3204 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt @@ -3,6 +3,7 @@ package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.request.NotifyRequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -31,11 +32,12 @@ interface PullRequestsService { @Path("pull_number") pullRequestNumber: String, ): List - @POST("/repos/{owner}/{repo}/issues/{issues_number}/comments") + @POST("/repos/{owner}/{repo}/issues/{issue_number}/comments") suspend fun notify( @Path("owner") owner: String, @Path("repo") repo: String, - @Path("issue_number") pullRequestNumber: String, - @Body message: String + @Path("issue_number") issueNumber: String, + @Body body: NotifyRequestBody ) + } From 40bad8691058afff4c23a60698d60dad86500011 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 15 Mar 2023 08:41:05 +0100 Subject: [PATCH 176/526] SIL-80: code cleanup --- .../com/appunite/loudius/network/model/AccessTokenResponse.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt index 78dce89ee..86f35aba1 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt @@ -1,7 +1,6 @@ package com.appunite.loudius.network.model data class AccessTokenResponse( - val accessToken: String?, val error: String? = null, ) From 501100d6d1067258e5953823eb44d42d3f5e0e5a Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 15 Mar 2023 11:22:03 +0100 Subject: [PATCH 177/526] SIL-80: code cleanup --- .../loudius/ui/loading/LoadingScreen.kt | 35 +++++++++---------- .../loudius/ui/loading/LoadingViewModel.kt | 27 +++++++------- .../ui/loading/LoadingViewModelTest.kt | 27 +++++--------- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 3388c7a39..310a45590 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -17,28 +17,27 @@ fun LoadingScreen( viewModel: LoadingViewModel = hiltViewModel(), onNavigateToPullRequest: () -> Unit, ) { - viewModel.state.let { state -> - val code = intent.data?.getQueryParameter("code") - val rememberedCode = rememberUpdatedState(newValue = code) - LaunchedEffect(key1 = rememberedCode) { - rememberedCode.value?.let { - viewModel.setCodeAndGetAccessToken(it) - } + val state = viewModel.state + val code = intent.data?.getQueryParameter("code") + val rememberedCode = rememberUpdatedState(newValue = code) + LaunchedEffect(key1 = rememberedCode) { + rememberedCode.value?.let { + viewModel.setCodeAndGetAccessToken(it) } - LaunchedEffect(key1 = state.navigateToPullRequests) { - state.navigateToPullRequests?.let { - onNavigateToPullRequest() - viewModel.onAction(LoadingAction.OnNavigateToPullRequests) - } + } + LaunchedEffect(key1 = state.navigateToPullRequests) { + state.navigateToPullRequests?.let { + onNavigateToPullRequest() + viewModel.onAction(LoadingAction.OnNavigateToPullRequests) } + } - if (state.showErrorScreen) { - ShowLoudiusErrorScreen { - viewModel.onAction(LoadingAction.OnTryAgainClick) - } - } else { - ShowLoadingIndicator(code = code) + if (state.showErrorScreen) { + ShowLoudiusErrorScreen { + viewModel.onAction(LoadingAction.OnTryAgainClick) } + } else { + ShowLoadingIndicator(code = code) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index 7d6b16f6e..6089808f3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.network.model.AccessTokenResponse import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,6 +19,7 @@ sealed class LoadingAction { object OnTryAgainClick : LoadingAction() } + data class LoadingState( val accessToken: String? = null, val code: String? = null, @@ -35,11 +37,9 @@ class LoadingViewModel @Inject constructor( var state by mutableStateOf(LoadingState()) private set - fun setCodeAndGetAccessToken(code: String?) { + fun setCodeAndGetAccessToken(code: String) { state = state.copy(code = code) - code?.let { - getAccessToken(it) - } + getAccessToken(code) } fun onAction(action: LoadingAction) = when (action) { @@ -65,17 +65,20 @@ class LoadingViewModel @Inject constructor( clientSecret = BuildConfig.CLIENT_SECRET, code = code, ).onSuccess { token -> - state = if (token.accessToken != null) { - state.copy( - accessToken = token.accessToken, - navigateToPullRequests = NavigateToPullRequests, - ) - } else { - state.copy(showErrorScreen = true) - } + state = handleGetAccessTokenSuccess(token) }.onFailure { state = state.copy(showErrorScreen = true) } } } + + private fun handleGetAccessTokenSuccess(token: AccessTokenResponse) = + if (token.accessToken != null) { + state.copy( + accessToken = token.accessToken, + navigateToPullRequests = NavigateToPullRequests, + ) + } else { + state.copy(showErrorScreen = true) + } } diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt index bca8e5cf1..0526ad990 100644 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -2,11 +2,10 @@ package com.appunite.loudius.ui.loading import com.appunite.loudius.fakes.FakeAuthRepository import com.appunite.loudius.util.MainDispatcherExtension -import io.mockk.mockkStatic +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock @ExtendWith(MainDispatcherExtension::class) class LoadingViewModelTest { @@ -21,28 +20,18 @@ class LoadingViewModelTest { @BeforeEach fun setup() { - mockkStatic(Clock::class) viewModel = LoadingViewModel(repository) } @Test - fun `GIVEN example valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigateToPullRequests`() { + fun `GIVEN valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigateToPullRequests`() { // when viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) // then - assert(viewModel.state.code == EXAMPLE_CODE) - assert(viewModel.state.accessToken == EXAMPLE_ACCESS_TOKEN) - assert(viewModel.state.navigateToPullRequests == NavigateToPullRequests) - } - - @Test - fun `GIVEN null as code WHEN setCodeAndGetAccessToken THEN set null as code`() { - // when - viewModel.setCodeAndGetAccessToken(null) - - // then - assert(viewModel.state.code == null) + assertEquals(viewModel.state.code, EXAMPLE_CODE) + assertEquals(viewModel.state.accessToken, EXAMPLE_ACCESS_TOKEN) + assertEquals(viewModel.state.navigateToPullRequests, NavigateToPullRequests) } @Test @@ -55,8 +44,8 @@ class LoadingViewModelTest { viewModel.onAction(action) // then - assert(!viewModel.state.showErrorScreen) - assert(viewModel.state.accessToken == EXAMPLE_ACCESS_TOKEN) + assertEquals(viewModel.state.showErrorScreen, false) + assertEquals(viewModel.state.accessToken, EXAMPLE_ACCESS_TOKEN) } @Test @@ -69,6 +58,6 @@ class LoadingViewModelTest { viewModel.onAction(action) // then - assert(viewModel.state.navigateToPullRequests == null) + assertEquals(viewModel.state.navigateToPullRequests, null) } } From cf10ad9c0e8a5ebcdcaf2f3fe124503863eea318 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 15 Mar 2023 11:23:52 +0100 Subject: [PATCH 178/526] Add tests to the PullRequestsNetworkDataSourceTest.kt. --- .../fakes/FakePullRequestDataSource.kt | 9 ++ .../fakes/FakePullRequestRepository.kt | 9 ++ .../PullRequestsNetworkDataSourceTest.kt | 111 +++++++++++++++++- 3 files changed, 128 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index 44f7fddd1..39514f9d0 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -53,4 +53,13 @@ class FakePullRequestDataSource : PullRequestDataSource { override suspend fun getPullRequestsForUser(author: String): Result { TODO("Not yet implemented") } + + override suspend fun notify( + owner: String, + repository: String, + pullRequestNumber: String, + message: String + ): Result { + TODO("Not yet implemented") + } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index ef5033556..3a0e3f8f6 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -56,4 +56,13 @@ class FakePullRequestRepository : PullRequestRepository { override suspend fun getCurrentUserPullRequests(): Result { TODO("Not yet implemented") } + + override suspend fun notify( + owner: String, + repo: String, + pullRequestNumber: String, + message: String + ): Result { + TODO("Not yet implemented") + } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 2cb186abb..d3e879e75 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,6 +10,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -20,7 +21,6 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -425,4 +425,113 @@ class PullRequestsNetworkDataSourceTest { assertEquals(expected, actualResponse) } } + + @Nested + inner class NotifyRequestTest { + + @Test + fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = + runTest { + mockWebServer.enqueue( + MockResponse() + .setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST), + ) + + val actualResponse = pullRequestDataSource.notify( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "@ExampleUser" + ) + Assertions.assertInstanceOf( + WebException.NetworkError::class.java, + actualResponse.exceptionOrNull(), + ) { "Exception thrown should be NetworkError type" } + } + + @Test + fun `GIVEN correct params WHEN successful response THEN return success result`() = runTest { + // language=JSON + val jsonResponse = """ + { + "id": 1, + "node_id": "MDEyOklzc3VlQ29tbWVudDE=", + "url": "https://api.github.com/repos/octocat/Hello-World/issues/comments/1", + "html_url": "https://github.com/octocat/Hello-World/issues/1347#issuecomment-1", + "body": "Me too", + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "issue_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", + "author_association": "COLLABORATOR" + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(200).setBody(jsonResponse), + ) + + val actual = pullRequestDataSource.notify( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "@ExampleUser" + ) + + assertEquals(Result.success(Unit), actual) + } + + @Test + fun `GIVEN auth error WHEN processing request THEN return failure with Unknown error`() = + runTest { + // language=JSON + val jsonResponse = """ + { + "message": "Bad credentials", + "documentation_url": "https://docs.github.com/rest" + } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(401) + .setBody(jsonResponse), + ) + + val actualResponse = pullRequestDataSource.notify( + "exampleOwner", + "exampleRepo", + "exampleNumber", + "@ExampleUser" + ) + + val expected = Result.failure( + WebException.UnknownError( + 401, + "Bad credentials", + ), + ) + + assertEquals(expected, actualResponse) + } + } } From e1a7732831331744dc886193565469115efc36cb Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 15 Mar 2023 11:33:12 +0100 Subject: [PATCH 179/526] Add tests to the PullRequestRepositoryImpTest.kt.kt. --- .../domain/PullRequestRepositoryImpTest.kt | 18 ++++++++++++++++++ .../loudius/fakes/FakePullRequestDataSource.kt | 6 ++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index e8e5e57c3..4fa9b4376 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -74,7 +74,25 @@ class PullRequestRepositoryImpTest { assertEquals(expected, actual) } // TODO: Write tests with failure cases + } + + @Nested + inner class NotifyTest { + @Test + fun `GIVEN correct values WHEN notify THEN return success result`() = runTest { + val actual = repository.notify( + "exampleOwner", + "exampleRepo", + "correctPullRequestNumber", + "@ExampleUser" + ) + + val expected = Result.success(Unit) + + assertEquals(expected, actual) + } + // TODO: Write tests with failure cases } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index 39514f9d0..21361f7f0 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -59,7 +59,9 @@ class FakePullRequestDataSource : PullRequestDataSource { repository: String, pullRequestNumber: String, message: String - ): Result { - TODO("Not yet implemented") + ): Result = when (pullRequestNumber) { + "correctPullRequestNumber" -> Result.success(Unit) + "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + else -> Result.failure(WebException.NetworkError()) } } From 1f8838a623bf28f4a1125299d15bf2ee27e72f7f Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 8 Mar 2023 13:54:15 +0100 Subject: [PATCH 180/526] Add error handling and loader to PRs list screen --- .../com/appunite/loudius/di/GithubModule.kt | 5 +- .../ui/pullrequests/PullRequestsScreen.kt | 120 ++++++++++++++---- .../ui/pullrequests/PullRequestsViewModel.kt | 19 ++- .../fakes/FakePullRequestRepository.kt | 31 ++++- .../pullrequests/PullRequestsViewModelTest.kt | 84 ++++++++++++ .../loudius/utils/CoroutinesHelpers.kt | 8 ++ 6 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt create mode 100644 app/src/test/java/com/appunite/loudius/utils/CoroutinesHelpers.kt diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index fcb968e44..afb3fb5ea 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -3,9 +3,12 @@ package com.appunite.loudius.di import android.content.Context import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.domain.AuthRepositoryImpl +import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource +import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource +import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.services.UserService @@ -14,8 +17,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit import javax.inject.Singleton +import retrofit2.Retrofit @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index f968c9ec8..dfc9293e5 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -1,10 +1,11 @@ -@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class) package com.appunite.loudius.ui.pullrequests import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -20,6 +22,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -29,6 +32,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest +import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import java.time.LocalDateTime @@ -49,35 +53,65 @@ fun PullRequestsScreen( } PullRequestsScreenStateless( pullRequests = state.pullRequests, - onItemClick = viewModel::onAction, + onAction = viewModel::onAction, + isLoading = state.isLoading, + isError = state.isError, ) } @Composable private fun PullRequestsScreenStateless( pullRequests: List, - onItemClick: (PulLRequestsAction) -> Unit, + onAction: (PulLRequestsAction) -> Unit, + isLoading: Boolean, + isError: Boolean, ) { Scaffold(topBar = { LoudiusTopAppBar(title = stringResource(R.string.app_name)) }, content = { padding -> - LazyColumn( - modifier = Modifier - .padding(padding) - .fillMaxSize(), - ) { - itemsIndexed(pullRequests) { index, item -> - val isIndexEven = index % 2 == 0 - PullRequestItem( - data = item, - darkBackground = isIndexEven, - onClick = onItemClick, - ) - } + when { + isError -> LoudiusErrorScreen( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(R.string.try_again_text), + onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, + ) + isLoading -> LoadingIndicator(modifier = Modifier.padding(padding)) + else -> PullRequestsList( + pullRequests = pullRequests, + modifier = Modifier.padding(padding), + onItemClick = onAction, + ) } }) } +@Composable +private fun LoadingIndicator(modifier: Modifier) { + Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) { + CircularProgressIndicator() + } +} + +@Composable +private fun PullRequestsList( + pullRequests: List, + modifier: Modifier, + onItemClick: (PulLRequestsAction) -> Unit, +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + ) { + itemsIndexed(pullRequests) { index, item -> + val isIndexEven = index % 2 == 0 + PullRequestItem( + data = item, + darkBackground = isIndexEven, + onClick = onItemClick, + ) + } + } +} + @Composable private fun PullRequestItem( data: PullRequest, @@ -124,20 +158,14 @@ private fun RepoDetails(pullRequestTitle: String, repositoryName: String) { } } -@Preview("Pull requests - empty list") -@Composable -fun PullRequestsScreenEmptyListPreview() { - LoudiusTheme { - PullRequestsScreenStateless(emptyList()) { } - } -} - @Preview("Pull requests - filled list") @Composable fun PullRequestsScreenPreview() { LoudiusTheme { PullRequestsScreenStateless( - listOf( + isLoading = false, + isError = false, + pullRequests = listOf( PullRequest( id = 0, draft = false, @@ -171,6 +199,46 @@ fun PullRequestsScreenPreview() { createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), ), ), - ) { } + onAction = {} + ) + } +} + +@Preview("Pull requests - empty list") +@Composable +fun PullRequestsScreenEmptyListPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + emptyList(), + isLoading = false, + isError = false, + onAction = {}, + ) + } +} + +@Preview("Pull requests - Loading") +@Composable +fun PullRequestsScreenLoadingPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + emptyList(), + isLoading = true, + isError = false, + onAction = {}, + ) + } +} + +@Preview("Pull requests - Error") +@Composable +fun PullRequestsScreenErrorPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + emptyList(), + isLoading = false, + isError = true, + onAction = {}, + ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index f05eba0b6..7f13752e0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -6,19 +6,24 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.PullRequestRepositoryImpl +import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() object OnNavigateToReviewers : PulLRequestsAction() + object RetryClick : PulLRequestsAction() } data class PullRequestState( val pullRequests: List = emptyList(), val navigateToReviewers: NavigationPayload? = null, + val isLoading: Boolean = false, + val isError: Boolean = false, ) data class NavigationPayload( @@ -30,16 +35,23 @@ data class NavigationPayload( @HiltViewModel class PullRequestsViewModel @Inject constructor( - private val pullRequestsRepository: PullRequestRepositoryImpl, + private val pullRequestsRepository: PullRequestRepository, ) : ViewModel() { var state by mutableStateOf(PullRequestState()) private set init { + fetchData() + } + + private fun fetchData() { viewModelScope.launch { + state = state.copy(isLoading = true, isError = false) pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { - state = state.copy(pullRequests = it.items) + state = state.copy(pullRequests = it.items, isLoading = false) + }.onFailure { + state = state.copy(isLoading = false, isError = true) } } } @@ -47,6 +59,7 @@ class PullRequestsViewModel @Inject constructor( fun onAction(action: PulLRequestsAction) = when (action) { is PulLRequestsAction.ItemClick -> navigateToReviewers(action.id) is PulLRequestsAction.OnNavigateToReviewers -> resetNavigationState() + is PulLRequestsAction.RetryClick -> fetchData() } private fun navigateToReviewers(itemClickedId: Int) { diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index ef5033556..fd4ef2a8c 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.PullRequestRepository +import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse @@ -53,7 +54,35 @@ class FakePullRequestRepository : PullRequestRepository { else -> Result.success(RequestedReviewersResponse(emptyList())) } + private val initialPullRequestAnswer = Result.success( + PullRequestsResponse( + incompleteResults = false, + totalCount = 1, + items = listOf( + PullRequest( + id = 1, + draft = false, + number = 1, + repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", + title = "example title", + LocalDateTime.parse("2023-03-07T09:21:45"), + ), + ), + ), + ) + + private var lazyCurrentUserPullRequests: suspend () -> Result = + { initialPullRequestAnswer } + + fun setCurrentUserPullRequests(result: suspend () -> Result) { + lazyCurrentUserPullRequests = result + } + + fun resetCurrentUserPullRequestAnswer() { + lazyCurrentUserPullRequests = { initialPullRequestAnswer } + } + override suspend fun getCurrentUserPullRequests(): Result { - TODO("Not yet implemented") + return lazyCurrentUserPullRequests() } } diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt new file mode 100644 index 000000000..17e742fed --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -0,0 +1,84 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.appunite.loudius.ui.pullrequests + +import com.appunite.loudius.fakes.FakePullRequestRepository +import com.appunite.loudius.network.model.PullRequest +import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.util.MainDispatcherExtension +import com.appunite.loudius.utils.neverCompletingSuspension +import java.time.LocalDateTime +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MainDispatcherExtension::class) +class PullRequestsViewModelTest { + private val pullRequestRepository = FakePullRequestRepository() + private fun getViewModel() = PullRequestsViewModel(pullRequestRepository) + + @Test + fun `GIVEN logged in user WHEN init THEN display loading`() = runTest { + pullRequestRepository.setCurrentUserPullRequests { neverCompletingSuspension() } + val viewModel = getViewModel() + + assertTrue(viewModel.state.isLoading) + } + + @Test + fun `GIVEN logged in user WHEN init THEN display pull requests list`() { + val viewModel = getViewModel() + + assertEquals( + listOf( + PullRequest( + id = 1, + draft = false, + number = 1, + repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", + title = "example title", + LocalDateTime.parse("2023-03-07T09:21:45"), + ) + ), viewModel.state.pullRequests + ) + assertFalse(viewModel.state.isLoading) + } + + + @Test + fun `GIVEN logged in user WHEN fetching failed THEN display error`() = runTest { + pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } + val viewModel = getViewModel() + + assertTrue(viewModel.state.isError) + } + + @Test + fun `GIVEN logged in user WHEN retry THEN fetch pull requests list again`() { + pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } + val viewModel = getViewModel() + assertTrue(viewModel.state.isError) + assertEquals(emptyList(), viewModel.state.pullRequests) + + pullRequestRepository.resetCurrentUserPullRequestAnswer() + viewModel.onAction(PulLRequestsAction.RetryClick) + + assertEquals( + listOf( + PullRequest( + id = 1, + draft = false, + number = 1, + repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", + title = "example title", + LocalDateTime.parse("2023-03-07T09:21:45"), + ) + ), viewModel.state.pullRequests + ) + assertFalse(viewModel.state.isLoading) + } +} diff --git a/app/src/test/java/com/appunite/loudius/utils/CoroutinesHelpers.kt b/app/src/test/java/com/appunite/loudius/utils/CoroutinesHelpers.kt new file mode 100644 index 000000000..7a7d6155b --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/utils/CoroutinesHelpers.kt @@ -0,0 +1,8 @@ +package com.appunite.loudius.utils + +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * A suspend function that never completes + */ +suspend fun neverCompletingSuspension(): T = suspendCancellableCoroutine { } From 96510a923d9846ab424d6bedae0421e710e42452 Mon Sep 17 00:00:00 2001 From: kezc Date: Wed, 15 Mar 2023 10:39:29 +0000 Subject: [PATCH 181/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/di/GithubModule.kt | 5 +---- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 2 +- .../ui/pullrequests/PullRequestsViewModel.kt | 4 +--- .../ui/pullrequests/PullRequestsViewModelTest.kt | 13 +++++++------ 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index afb3fb5ea..fcb968e44 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -3,12 +3,9 @@ package com.appunite.loudius.di import android.content.Context import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.domain.AuthRepositoryImpl -import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource -import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource -import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.services.UserService @@ -17,8 +14,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import retrofit2.Retrofit +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index dfc9293e5..00ee4a9d8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -199,7 +199,7 @@ fun PullRequestsScreenPreview() { createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), ), ), - onAction = {} + onAction = {}, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 7f13752e0..d053feaba 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -5,13 +5,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.appunite.loudius.domain.PullRequestRepositoryImpl import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 17e742fed..e897dcf75 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -7,7 +7,6 @@ import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.MainDispatcherExtension import com.appunite.loudius.utils.neverCompletingSuspension -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -15,6 +14,7 @@ import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import java.time.LocalDateTime @ExtendWith(MainDispatcherExtension::class) class PullRequestsViewModelTest { @@ -42,13 +42,13 @@ class PullRequestsViewModelTest { repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", LocalDateTime.parse("2023-03-07T09:21:45"), - ) - ), viewModel.state.pullRequests + ), + ), + viewModel.state.pullRequests, ) assertFalse(viewModel.state.isLoading) } - @Test fun `GIVEN logged in user WHEN fetching failed THEN display error`() = runTest { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } @@ -76,8 +76,9 @@ class PullRequestsViewModelTest { repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", LocalDateTime.parse("2023-03-07T09:21:45"), - ) - ), viewModel.state.pullRequests + ), + ), + viewModel.state.pullRequests, ) assertFalse(viewModel.state.isLoading) } From 715dfdc23a5f51900132ad8e181bcee0ad650c1f Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 15 Mar 2023 14:30:23 +0100 Subject: [PATCH 182/526] Add showing snackbar on the notify success. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 24 ++++++++++++++++--- .../ui/reviewers/ReviewersViewModel.kt | 6 +++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 95c6cd201..488209284 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -12,8 +12,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -34,12 +39,22 @@ fun ReviewersScreen( navigateBack: () -> Unit, ) { val state = viewModel.state + val snackbarHostState = remember { SnackbarHostState() } ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, onClickBackArrow = navigateBack, - onNotifyClick = viewModel::onAction + onNotifyClick = viewModel::onAction, + snackbarHostState = snackbarHostState ) + LaunchedEffect(state.showSuccessSnackbar) { + state.showSuccessSnackbar?.let { + val result = snackbarHostState.showSnackbar(message = "Hurray person is notified") + if (result == SnackbarResult.Dismissed) { + viewModel.onAction(ReviewersAction.OnSnackbarDismiss) + } + } + } } @OptIn(ExperimentalMaterial3Api::class) @@ -48,7 +63,8 @@ private fun ReviewersScreenStateless( pullRequestNumber: String, reviewers: List, onClickBackArrow: () -> Unit, - onNotifyClick: (ReviewersAction) -> Unit + onNotifyClick: (ReviewersAction) -> Unit, + snackbarHostState: SnackbarHostState, ) { Scaffold( topBar = { @@ -57,6 +73,7 @@ private fun ReviewersScreenStateless( title = stringResource(id = R.string.details_title, pullRequestNumber), ) }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, content = { padding -> ReviewersScreenContent(reviewers, modifier = Modifier.padding(padding), onNotifyClick) }, @@ -187,6 +204,7 @@ fun DetailsScreenPreview() { pullRequestNumber = "Pull request #1", reviewers = reviewers, {}, - {}) + {}, SnackbarHostState() + ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 4886f0258..5b5cfca55 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -19,11 +19,13 @@ import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() + object OnSnackbarDismiss : ReviewersAction() } data class ReviewersState( val reviewers: List = emptyList(), val pullRequestNumber: String = "", + val showSuccessSnackbar: Unit? = null, ) @HiltViewModel @@ -103,6 +105,7 @@ class ReviewersViewModel @Inject constructor( fun onAction(action: ReviewersAction) = when (action) { is ReviewersAction.Notify -> notifyUser(action.userLogin) + is ReviewersAction.OnSnackbarDismiss -> state = state.copy(showSuccessSnackbar = null) } private fun notifyUser(userLogin: String) { @@ -110,6 +113,9 @@ class ReviewersViewModel @Inject constructor( viewModelScope.launch { repository.notify(owner, repo, pullRequestNumber, "@$userLogin") + .onSuccess { + state = state.copy(showSuccessSnackbar = Unit) + } } } From 288c21248be0289e17cd132b027ce89934bfb72b Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 15 Mar 2023 14:30:34 +0100 Subject: [PATCH 183/526] Add tests for the ReviewersViewModelTest.kt. --- .../fakes/FakePullRequestRepository.kt | 5 +- .../ui/reviewers/ReviewersViewModelTest.kt | 161 ++++++++++-------- 2 files changed, 97 insertions(+), 69 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 3a0e3f8f6..d8f24a4ca 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -62,7 +62,8 @@ class FakePullRequestRepository : PullRequestRepository { repo: String, pullRequestNumber: String, message: String - ): Result { - TODO("Not yet implemented") + ): Result = when (pullRequestNumber) { + "correctPullRequestNumber" -> Result.success(Unit) + else -> Result.failure(WebException.NetworkError()) } } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 6c5da6750..c62ea842a 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,17 +9,18 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -44,87 +45,113 @@ class ReviewersViewModelTest { every { Clock.systemDefaultZone() } returns systemClockFixed } - @Test - fun `GIVEN no values in saved state WHEN init THEN throw IllegalStateException`() { - every { savedStateHandle.get(any()) } returns null + @Nested + inner class InitTest { + @Test + fun `GIVEN no values in saved state WHEN init THEN throw IllegalStateException`() { + every { savedStateHandle.get(any()) } returns null - assertThrows { - createViewModel() + assertThrows { + createViewModel() + } } - } - - @Test - fun `GIVEN correct initial values WHEN init THEN all initial values are read once`() { - viewModel = createViewModel() - verify(exactly = 1) { savedStateHandle.get("owner") } - verify(exactly = 1) { savedStateHandle.get("repo") } - verify(exactly = 1) { savedStateHandle.get("pull_request_number") } - verify(exactly = 1) { savedStateHandle.get("submission_date") } - } - - @Test - fun `GIVEN correct initial values WHEN init THEN pull request number is correct`() { - viewModel = createViewModel() + @Test + fun `GIVEN correct initial values WHEN init THEN all initial values are read once`() { + viewModel = createViewModel() - assertEquals("correctPullRequestNumber", viewModel.state.pullRequestNumber) - } + verify(exactly = 1) { savedStateHandle.get("owner") } + verify(exactly = 1) { savedStateHandle.get("repo") } + verify(exactly = 1) { savedStateHandle.get("pull_request_number") } + verify(exactly = 1) { savedStateHandle.get("submission_date") } + } - @Test - fun `GIVEN no reviewers WHEN init THEN state is correct with no reviewers`() { - every { savedStateHandle.get("pull_request_number") } returns "pullRequestWithNoReviewers" + @Test + fun `GIVEN correct initial values WHEN init THEN pull request number is correct`() { + viewModel = createViewModel() - viewModel = createViewModel() + assertEquals("correctPullRequestNumber", viewModel.state.pullRequestNumber) + } - assertEquals("pullRequestWithNoReviewers", viewModel.state.pullRequestNumber) - assertEquals(emptyList(), viewModel.state.reviewers) - } + @Test + fun `GIVEN no reviewers WHEN init THEN state is correct with no reviewers`() { + every { savedStateHandle.get("pull_request_number") } returns "pullRequestWithNoReviewers" - @Test - fun `GIVEN mixed reviewers WHEN init THEN list of reviewers is fetched`() = - runTest { viewModel = createViewModel() - val expected = listOf( - Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), - Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), - ) - val actual = viewModel.state.reviewers - - assertEquals(expected, actual) + assertEquals("pullRequestWithNoReviewers", viewModel.state.pullRequestNumber) + assertEquals(emptyList(), viewModel.state.reviewers) } - @Test - fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = - runTest { - every { savedStateHandle.get("pull_request_number") } returns "onlyRequestedReviewersPullNumber" + @Test + fun `GIVEN mixed reviewers WHEN init THEN list of reviewers is fetched`() = + runTest { + viewModel = createViewModel() + + val expected = listOf( + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), + ) + val actual = viewModel.state.reviewers + + assertEquals(expected, actual) + } + + @Test + fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = + runTest { + every { savedStateHandle.get("pull_request_number") } returns "onlyRequestedReviewersPullNumber" + + viewModel = createViewModel() + + val expected = listOf( + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), + ) + val actual = viewModel.state.reviewers + + assertEquals(expected, actual) + } + + @Test + fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = + runTest { + every { savedStateHandle.get("pull_request_number") } returns "onlyReviewsPullNumber" + + viewModel = createViewModel() + + val expected = listOf( + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), + ) + val actual = viewModel.state.reviewers + + assertEquals(expected, actual) + } + } + + @Nested + inner class OnActionTest { + @Test + fun `GIVEN user login WHEN Notify action THEN show snackbar`() = runTest { viewModel = createViewModel() - val expected = listOf( - Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), - ) - val actual = viewModel.state.reviewers + viewModel.onAction(ReviewersAction.Notify("ExampleUser")) - assertEquals(expected, actual) + assertEquals(Unit, viewModel.state.showSuccessSnackbar) } - @Test - fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = - runTest { - every { savedStateHandle.get("pull_request_number") } returns "onlyReviewsPullNumber" - - viewModel = createViewModel() + @Test + fun `GIVEN user login WHEN on snackbar dismiss action THEN show snackbar state is null`() = + runTest { + viewModel = createViewModel() - val expected = listOf( - Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), - ) - val actual = viewModel.state.reviewers + viewModel.onAction(ReviewersAction.OnSnackbarDismiss) - assertEquals(expected, actual) - } + assertEquals(null, viewModel.state.showSuccessSnackbar) + } + } } From 3c3458311f78c081f3646c211af0c55695cb1213 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Wed, 15 Mar 2023 13:51:47 +0000 Subject: [PATCH 184/526] [MegaLinter] Apply linters fixes --- .../domain/PullRequestRepositoryImpl.kt | 12 +++++------- .../PullRequestsNetworkDataSource.kt | 4 ++-- .../model/request/NotifyRequestBody.kt | 3 +-- .../network/services/PullRequestsService.kt | 3 +-- .../loudius/ui/reviewers/ReviewersScreen.kt | 11 ++++++----- .../ui/reviewers/ReviewersViewModel.kt | 2 +- .../domain/PullRequestRepositoryImpTest.kt | 19 ++++++++----------- .../fakes/FakePullRequestDataSource.kt | 6 +++--- .../fakes/FakePullRequestRepository.kt | 2 +- .../PullRequestsNetworkDataSourceTest.kt | 8 ++++---- .../ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 11 files changed, 36 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index 8d2b6d60e..da3812286 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -7,10 +7,10 @@ import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.User -import javax.inject.Inject -import kotlin.coroutines.coroutineContext import kotlinx.coroutines.async import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.coroutines.coroutineContext interface PullRequestRepository { suspend fun getReviews( @@ -31,7 +31,7 @@ interface PullRequestRepository { owner: String, repo: String, pullRequestNumber: String, - message: String + message: String, ): Result } @@ -40,7 +40,6 @@ class PullRequestRepositoryImpl @Inject constructor( private val userDataSource: UserDataSource, ) : PullRequestRepository { override suspend fun getCurrentUserPullRequests(): Result { - val currentUser = userDataSource.getUser() return currentUser.flatMap { pullRequestsNetworkDataSource.getPullRequestsForUser(it.login) } } @@ -62,9 +61,8 @@ class PullRequestRepositoryImpl @Inject constructor( } } - private fun List.excludeUserReviews( - user: User + user: User, ) = filter { review -> review.user.id != user.id } override suspend fun getRequestedReviewers( @@ -78,7 +76,7 @@ class PullRequestRepositoryImpl @Inject constructor( owner: String, repo: String, pullRequestNumber: String, - message: String + message: String, ): Result = pullRequestsNetworkDataSource.notify(owner, repo, pullRequestNumber, message) } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 672d6ec31..829cac644 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -28,7 +28,7 @@ interface PullRequestDataSource { owner: String, repository: String, pullRequestNumber: String, - message: String + message: String, ): Result } @@ -60,7 +60,7 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Pul owner: String, repository: String, pullRequestNumber: String, - message: String + message: String, ): Result = safeApiCall { service.notify(owner, repository, pullRequestNumber, NotifyRequestBody(message)) } diff --git a/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt index 6c3bc0daf..85a7e3384 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt @@ -1,6 +1,5 @@ package com.appunite.loudius.network.model.request - data class NotifyRequestBody( - val body: String + val body: String, ) diff --git a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt index 46bcb3204..ba8877a2e 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt @@ -37,7 +37,6 @@ interface PullRequestsService { @Path("owner") owner: String, @Path("repo") repo: String, @Path("issue_number") issueNumber: String, - @Body body: NotifyRequestBody + @Body body: NotifyRequestBody, ) - } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 488209284..f78b64c06 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -45,7 +45,7 @@ fun ReviewersScreen( reviewers = state.reviewers, onClickBackArrow = navigateBack, onNotifyClick = viewModel::onAction, - snackbarHostState = snackbarHostState + snackbarHostState = snackbarHostState, ) LaunchedEffect(state.showSuccessSnackbar) { state.showSuccessSnackbar?.let { @@ -85,7 +85,7 @@ private fun ReviewersScreenStateless( private fun ReviewersScreenContent( reviewers: List, modifier: Modifier, - onNotifyClick: (ReviewersAction) -> Unit + onNotifyClick: (ReviewersAction) -> Unit, ) { LazyColumn( modifier = modifier.fillMaxWidth(), @@ -108,7 +108,7 @@ private fun resolveReviewerBackgroundColor(index: Int) = private fun ReviewerItem( reviewer: Reviewer, backgroundColor: Color, - onNotifyClick: (ReviewersAction) -> Unit + onNotifyClick: (ReviewersAction) -> Unit, ) { Row( modifier = Modifier @@ -129,7 +129,7 @@ private fun ReviewerItem( } NotifyButton( { onNotifyClick(ReviewersAction.Notify(reviewer.login)) }, - Modifier.align(CenterVertically) + Modifier.align(CenterVertically), ) } } @@ -204,7 +204,8 @@ fun DetailsScreenPreview() { pullRequestNumber = "Pull request #1", reviewers = reviewers, {}, - {}, SnackbarHostState() + {}, + SnackbarHostState(), ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 5b5cfca55..e6dab5292 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,10 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject -import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index 4fa9b4376..939deef01 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -9,12 +9,12 @@ import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.User import io.mockk.coEvery import io.mockk.mockk -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @OptIn(ExperimentalCoroutinesApi::class) class PullRequestRepositoryImpTest { @@ -36,12 +36,11 @@ class PullRequestRepositoryImpTest { val date1 = LocalDateTime.parse("2022-01-29T10:00:00") val expected = Result.success( - listOf - ( + listOf( Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), Review("5", User(2, "user2"), ReviewState.COMMENTED, date1), Review("6", User(2, "user2"), ReviewState.APPROVED, date1), - ) + ), ) assertEquals(expected, actual) @@ -58,17 +57,16 @@ class PullRequestRepositoryImpTest { val actual = repository.getRequestedReviewers( "example", "example", - "correctPullRequestNumber" + "correctPullRequestNumber", ) val expected = Result.success( RequestedReviewersResponse( - listOf - ( + listOf( RequestedReviewer(3, "user3"), RequestedReviewer(4, "user4"), - ) - ) + ), + ), ) assertEquals(expected, actual) @@ -85,7 +83,7 @@ class PullRequestRepositoryImpTest { "exampleOwner", "exampleRepo", "correctPullRequestNumber", - "@ExampleUser" + "@ExampleUser", ) val expected = Result.success(Unit) @@ -94,5 +92,4 @@ class PullRequestRepositoryImpTest { } // TODO: Write tests with failure cases } - } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index 21361f7f0..6afd23251 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -17,7 +17,7 @@ class FakePullRequestDataSource : PullRequestDataSource { override suspend fun getReviews( owner: String, repository: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result> = when (pullRequestNumber) { "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( listOf( @@ -36,7 +36,7 @@ class FakePullRequestDataSource : PullRequestDataSource { override suspend fun getReviewers( owner: String, repository: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber", "onlyRequestedReviewersPullNumber" -> Result.success( RequestedReviewersResponse( @@ -58,7 +58,7 @@ class FakePullRequestDataSource : PullRequestDataSource { owner: String, repository: String, pullRequestNumber: String, - message: String + message: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index d8f24a4ca..6f3faf738 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -61,7 +61,7 @@ class FakePullRequestRepository : PullRequestRepository { owner: String, repo: String, pullRequestNumber: String, - message: String + message: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) else -> Result.failure(WebException.NetworkError()) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index d3e879e75..dc947dfb6 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,7 +10,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -441,7 +441,7 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser" + "@ExampleUser", ) Assertions.assertInstanceOf( WebException.NetworkError::class.java, @@ -494,7 +494,7 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser" + "@ExampleUser", ) assertEquals(Result.success(Unit), actual) @@ -521,7 +521,7 @@ class PullRequestsNetworkDataSourceTest { "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser" + "@ExampleUser", ) val expected = Result.failure( diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index c62ea842a..40ce31bfe 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,10 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -21,6 +17,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From dfdfa21af18b51399490d164795894001d00b636 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 15 Mar 2023 15:17:49 +0100 Subject: [PATCH 185/526] removal of given, when, then comments --- .../appunite/loudius/ui/loading/LoadingViewModelTest.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt index 0526ad990..ed0377bc1 100644 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -25,10 +25,8 @@ class LoadingViewModelTest { @Test fun `GIVEN valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigateToPullRequests`() { - // when viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - // then assertEquals(viewModel.state.code, EXAMPLE_CODE) assertEquals(viewModel.state.accessToken, EXAMPLE_ACCESS_TOKEN) assertEquals(viewModel.state.navigateToPullRequests, NavigateToPullRequests) @@ -36,28 +34,22 @@ class LoadingViewModelTest { @Test fun `GIVEN OnTryAgain action WHEN onAction THEN set showErrorScreen and get access token`() { - // given val action = LoadingAction.OnTryAgainClick viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - // when viewModel.onAction(action) - // then assertEquals(viewModel.state.showErrorScreen, false) assertEquals(viewModel.state.accessToken, EXAMPLE_ACCESS_TOKEN) } @Test fun `GIVEN OnNavigateToPullRequests action WHEN onAction THEN set navigateToPullRequests as null`() { - // given val action = LoadingAction.OnNavigateToPullRequests viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - // when viewModel.onAction(action) - // then assertEquals(viewModel.state.navigateToPullRequests, null) } } From c60a80372cd2a3cf768d6d09f1eacdb229004071 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 15 Mar 2023 15:00:07 +0100 Subject: [PATCH 186/526] Change message on notify success. --- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 3 ++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index f78b64c06..250b5e111 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -47,9 +47,10 @@ fun ReviewersScreen( onNotifyClick = viewModel::onAction, snackbarHostState = snackbarHostState, ) + val snackbarMessage = stringResource(id = R.string.reviewers_snackbar_message) LaunchedEffect(state.showSuccessSnackbar) { state.showSuccessSnackbar?.let { - val result = snackbarHostState.showSnackbar(message = "Hurray person is notified") + val result = snackbarHostState.showSnackbar(message = snackbarMessage) if (result == SnackbarResult.Dismissed) { viewModel.onAction(ReviewersAction.OnSnackbarDismiss) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d44dc5082..c72d57e9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,4 +14,5 @@ OK error image Try again + Awesome! Your collaborators have been pinged for some serious code review action! 🎉 From 1c88f39d611a2a5f3fb2f56ca4513a2d169a8b2b Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 16 Mar 2023 08:33:19 +0100 Subject: [PATCH 187/526] Extract snack bar launched effect into the function. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 250b5e111..abb3aecab 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -40,6 +40,9 @@ fun ReviewersScreen( ) { val state = viewModel.state val snackbarHostState = remember { SnackbarHostState() } + val snackbarMessage = stringResource(id = R.string.reviewers_snackbar_message) + + ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, @@ -47,7 +50,16 @@ fun ReviewersScreen( onNotifyClick = viewModel::onAction, snackbarHostState = snackbarHostState, ) - val snackbarMessage = stringResource(id = R.string.reviewers_snackbar_message) + SnackbarLaunchedEffect(state, snackbarHostState, snackbarMessage, viewModel) +} + +@Composable +private fun SnackbarLaunchedEffect( + state: ReviewersState, + snackbarHostState: SnackbarHostState, + snackbarMessage: String, + viewModel: ReviewersViewModel +) { LaunchedEffect(state.showSuccessSnackbar) { state.showSuccessSnackbar?.let { val result = snackbarHostState.showSnackbar(message = snackbarMessage) From be5f16ca0e94647a7abe182beca0332b7de0404e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 16 Mar 2023 09:04:30 +0100 Subject: [PATCH 188/526] Perform minor cleaning. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 32 +++++++++++-------- .../ui/reviewers/ReviewersViewModel.kt | 8 ++--- .../PullRequestsNetworkDataSourceTest.kt | 7 ++-- .../ui/reviewers/ReviewersViewModelTest.kt | 14 ++++---- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index abb3aecab..3d0530002 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -47,24 +47,29 @@ fun ReviewersScreen( pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, onClickBackArrow = navigateBack, + snackbarHostState = snackbarHostState, onNotifyClick = viewModel::onAction, + ) + SnackbarLaunchedEffect( + isSuccessSnackbarShown = state.isSuccessSnackbarShown, snackbarHostState = snackbarHostState, + snackbarMessage = snackbarMessage, + onSnackbarDismiss = viewModel::onAction ) - SnackbarLaunchedEffect(state, snackbarHostState, snackbarMessage, viewModel) } @Composable private fun SnackbarLaunchedEffect( - state: ReviewersState, + isSuccessSnackbarShown: Boolean, snackbarHostState: SnackbarHostState, snackbarMessage: String, - viewModel: ReviewersViewModel + onSnackbarDismiss: (ReviewersAction) -> Unit ) { - LaunchedEffect(state.showSuccessSnackbar) { - state.showSuccessSnackbar?.let { + LaunchedEffect(isSuccessSnackbarShown) { + if (isSuccessSnackbarShown) { val result = snackbarHostState.showSnackbar(message = snackbarMessage) if (result == SnackbarResult.Dismissed) { - viewModel.onAction(ReviewersAction.OnSnackbarDismiss) + onSnackbarDismiss(ReviewersAction.OnSnackbarDismiss) } } } @@ -76,8 +81,8 @@ private fun ReviewersScreenStateless( pullRequestNumber: String, reviewers: List, onClickBackArrow: () -> Unit, - onNotifyClick: (ReviewersAction) -> Unit, snackbarHostState: SnackbarHostState, + onNotifyClick: (ReviewersAction) -> Unit, ) { Scaffold( topBar = { @@ -140,10 +145,9 @@ private fun ReviewerItem( IsReviewedHeadlineText(reviewer) ReviewerName(reviewer) } - NotifyButton( - { onNotifyClick(ReviewersAction.Notify(reviewer.login)) }, - Modifier.align(CenterVertically), - ) + NotifyButton(Modifier.align(CenterVertically)) { + onNotifyClick(ReviewersAction.Notify(reviewer.login)) + } } } @@ -184,7 +188,7 @@ private fun ReviewerName(reviewer: Reviewer) { } @Composable -private fun NotifyButton(onNotifyClick: () -> Unit, modifier: Modifier = Modifier) { +private fun NotifyButton(modifier: Modifier = Modifier, onNotifyClick: () -> Unit) { OutlinedButton(onClick = onNotifyClick, modifier = modifier) { Text( text = stringResource(R.string.details_notify), @@ -214,11 +218,11 @@ fun DetailsScreenPreview() { val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { ReviewersScreenStateless( - pullRequestNumber = "Pull request #1", + pullRequestNumber = "1", reviewers = reviewers, {}, - {}, SnackbarHostState(), + {}, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index e6dab5292..175649879 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,10 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -25,7 +25,7 @@ sealed class ReviewersAction { data class ReviewersState( val reviewers: List = emptyList(), val pullRequestNumber: String = "", - val showSuccessSnackbar: Unit? = null, + val isSuccessSnackbarShown: Boolean = false, ) @HiltViewModel @@ -105,7 +105,7 @@ class ReviewersViewModel @Inject constructor( fun onAction(action: ReviewersAction) = when (action) { is ReviewersAction.Notify -> notifyUser(action.userLogin) - is ReviewersAction.OnSnackbarDismiss -> state = state.copy(showSuccessSnackbar = null) + is ReviewersAction.OnSnackbarDismiss -> state = state.copy(isSuccessSnackbarShown = false) } private fun notifyUser(userLogin: String) { @@ -114,7 +114,7 @@ class ReviewersViewModel @Inject constructor( viewModelScope.launch { repository.notify(owner, repo, pullRequestNumber, "@$userLogin") .onSuccess { - state = state.copy(showSuccessSnackbar = Unit) + state = state.copy(isSuccessSnackbarShown = true) } } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index dc947dfb6..b05b48633 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,6 +10,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -20,7 +21,6 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -430,7 +430,7 @@ class PullRequestsNetworkDataSourceTest { inner class NotifyRequestTest { @Test - fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = + fun `GIVEN connectivity problem WHEN request THEN return failure with Network error`() = runTest { mockWebServer.enqueue( MockResponse() @@ -485,7 +485,6 @@ class PullRequestsNetworkDataSourceTest { "author_association": "COLLABORATOR" } """.trimIndent() - mockWebServer.enqueue( MockResponse().setResponseCode(200).setBody(jsonResponse), ) @@ -510,7 +509,6 @@ class PullRequestsNetworkDataSourceTest { "documentation_url": "https://docs.github.com/rest" } """.trimIndent() - mockWebServer.enqueue( MockResponse() .setResponseCode(401) @@ -530,7 +528,6 @@ class PullRequestsNetworkDataSourceTest { "Bad credentials", ), ) - assertEquals(expected, actualResponse) } } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 40ce31bfe..78716bffe 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,6 +9,10 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -17,10 +21,6 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -141,17 +141,17 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("ExampleUser")) - assertEquals(Unit, viewModel.state.showSuccessSnackbar) + assertEquals(true, viewModel.state.isSuccessSnackbarShown) } @Test - fun `GIVEN user login WHEN on snackbar dismiss action THEN show snackbar state is null`() = + fun `GIVEN user login WHEN on snackbar dismiss action THEN snackbar is not shown`() = runTest { viewModel = createViewModel() viewModel.onAction(ReviewersAction.OnSnackbarDismiss) - assertEquals(null, viewModel.state.showSuccessSnackbar) + assertEquals(false, viewModel.state.isSuccessSnackbarShown) } } } From 9b1e00b4fb924e4c3a2ed94bb4fe549046b3e14a Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 16 Mar 2023 09:09:37 +0100 Subject: [PATCH 189/526] Remove plural form from the snackbar string. --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c72d57e9f..2dbcfc6b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,5 +14,5 @@ OK error image Try again - Awesome! Your collaborators have been pinged for some serious code review action! 🎉 + Awesome! Your collaborator have been pinged for some serious code review action! 🎉 From 12e2890b9bc6d04676b9ba2950ba04835884f0f1 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 16 Mar 2023 08:16:27 +0000 Subject: [PATCH 190/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 5 ++--- .../appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 2 +- .../datasource/PullRequestsNetworkDataSourceTest.kt | 2 +- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 3d0530002..360b699db 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -42,7 +42,6 @@ fun ReviewersScreen( val snackbarHostState = remember { SnackbarHostState() } val snackbarMessage = stringResource(id = R.string.reviewers_snackbar_message) - ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, @@ -54,7 +53,7 @@ fun ReviewersScreen( isSuccessSnackbarShown = state.isSuccessSnackbarShown, snackbarHostState = snackbarHostState, snackbarMessage = snackbarMessage, - onSnackbarDismiss = viewModel::onAction + onSnackbarDismiss = viewModel::onAction, ) } @@ -63,7 +62,7 @@ private fun SnackbarLaunchedEffect( isSuccessSnackbarShown: Boolean, snackbarHostState: SnackbarHostState, snackbarMessage: String, - onSnackbarDismiss: (ReviewersAction) -> Unit + onSnackbarDismiss: (ReviewersAction) -> Unit, ) { LaunchedEffect(isSuccessSnackbarShown) { if (isSuccessSnackbarShown) { diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 175649879..46335d2a9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,10 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject -import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index b05b48633..b46719a5b 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,7 +10,6 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 78716bffe..ef0665a71 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,10 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -21,6 +17,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From 54dca8efc8f1c5a1b36ce481b3ee634a80d21fd7 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 16 Mar 2023 10:35:34 +0100 Subject: [PATCH 191/526] add lottie animation --- app/build.gradle | 2 + .../ui/components/LoudiusLoadingIndicator.kt | 47 +++++++++++++++++++ app/src/main/res/raw/loading_indicator.json | 1 + 3 files changed, 50 insertions(+) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt create mode 100644 app/src/main/res/raw/loading_indicator.json diff --git a/app/build.gradle b/app/build.gradle index e57c36bb8..4959d10b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,8 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-test-manifest' androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + //Lottie - Compose + implementation ("com.airbnb.android:lottie-compose:5.2.0") //DI - Hilt implementation "com.google.dagger:hilt-android:2.45" diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt new file mode 100644 index 000000000..9d4dfee53 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -0,0 +1,47 @@ +package com.appunite.loudius.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.appunite.loudius.R +import com.appunite.loudius.ui.theme.LoudiusTheme + +@Composable +fun LoudiusLoaderIndicator() { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) + val progress by animateLottieCompositionAsState( + composition, + iterations = LottieConstants.IterateForever + ) + Box( + modifier = Modifier + .fillMaxSize() + ) { + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier + .align(Alignment.Center) + .size(96.dp) + ) + } +} + +@Preview +@Composable +fun LoudiusLoadingIndicatorPreview() { + LoudiusTheme { + LoudiusLoaderIndicator() + } +} diff --git a/app/src/main/res/raw/loading_indicator.json b/app/src/main/res/raw/loading_indicator.json new file mode 100644 index 000000000..37db72e87 --- /dev/null +++ b/app/src/main/res/raw/loading_indicator.json @@ -0,0 +1 @@ +{"v":"5.7.6","fr":30,"ip":0,"op":60,"w":24,"h":24,"nm":"Loader top","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 9957","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[7.224,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[7.224,9.133,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[7.224,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,1.228,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,1.91],[0.546,2.456],[1.091,1.91],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9957 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,1.91],[0.546,2.456],[1.091,1.91],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9957 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 9956","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[8.816,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[8.816,12.883,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[8.816,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,3.275,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.302,0],[0,0.301],[0,0]],"o":[[-0.302,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9956 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.302,0],[0,0.301],[0,0]],"o":[[-0.302,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9956 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Path 9955","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[10.408,12.377,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[10.408,11.127,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[10.408,12.377,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,5.731,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.302,0],[0,-0.325],[0,0],[-0.301,0],[0,0.325],[0,0]],"o":[[-0.301,0],[0,0],[0,0.325],[0.302,0],[0,0],[0,-0.325]],"v":[[0.546,0],[0,0.588],[0,10.875],[0.546,11.463],[1.091,10.875],[1.091,0.588]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9955 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.302,0],[0,-0.325],[0,0],[-0.301,0],[0,0.325],[0,0]],"o":[[-0.301,0],[0,0],[0,0.325],[0.302,0],[0,0],[0,-0.325]],"v":[[0.546,0],[0,0.588],[0,10.875],[0.546,11.463],[1.091,10.875],[1.091,0.588]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9955 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Path 9954","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,12.128,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[12,14.628,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[12,12.128,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,3.957,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.302]],"v":[[0.546,0],[0,0.546],[0,7.368],[0.546,7.914],[1.091,7.368],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9954 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.302]],"v":[[0.546,0],[0,0.546],[0,7.368],[0.546,7.914],[1.091,7.368],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9954 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Path 9953","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[13.592,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[13.592,10.383,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[13.592,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,2.592,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,4.639],[0.546,5.185],[1.092,4.639],[1.092,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9953 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,4.639],[0.546,5.185],[1.092,4.639],[1.092,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9953 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Path 9952","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[15.184,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[15.184,12.883,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[15.184,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,3.275,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9952 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9952 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Path 9951","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[16.776,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[16.776,10.383,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[16.776,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,1.91,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,3.274],[0.546,3.82],[1.091,3.274],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9951 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,3.274],[0.546,3.82],[1.091,3.274],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9951 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Ellipse 416","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":-0.75,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 416 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 416 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[100]},{"t":59,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":59,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-184]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[170]},{"t":59,"s":[507]}],"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Ellipse 415","sr":1,"ks":{"o":{"a":0,"k":25,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":-0.75,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 415 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 415 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file From 406d2e72ffdf995672b0903c10bf1463b462ab01 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 16 Mar 2023 09:40:30 +0000 Subject: [PATCH 192/526] [MegaLinter] Apply linters fixes --- .../ui/components/LoudiusLoadingIndicator.kt | 6 +- app/src/main/res/raw/loading_indicator.json | 1644 ++++++++++++++++- 2 files changed, 1646 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index 9d4dfee53..003a0be8a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -22,18 +22,18 @@ fun LoudiusLoaderIndicator() { val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) val progress by animateLottieCompositionAsState( composition, - iterations = LottieConstants.IterateForever + iterations = LottieConstants.IterateForever, ) Box( modifier = Modifier - .fillMaxSize() + .fillMaxSize(), ) { LottieAnimation( composition = composition, progress = { progress }, modifier = Modifier .align(Alignment.Center) - .size(96.dp) + .size(96.dp), ) } } diff --git a/app/src/main/res/raw/loading_indicator.json b/app/src/main/res/raw/loading_indicator.json index 37db72e87..757a7becb 100644 --- a/app/src/main/res/raw/loading_indicator.json +++ b/app/src/main/res/raw/loading_indicator.json @@ -1 +1,1643 @@ -{"v":"5.7.6","fr":30,"ip":0,"op":60,"w":24,"h":24,"nm":"Loader top","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 9957","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[7.224,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[7.224,9.133,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[7.224,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,1.228,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,1.91],[0.546,2.456],[1.091,1.91],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9957 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,1.91],[0.546,2.456],[1.091,1.91],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9957 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 9956","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[8.816,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[8.816,12.883,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[8.816,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,3.275,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.302,0],[0,0.301],[0,0]],"o":[[-0.302,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9956 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.302,0],[0,0.301],[0,0]],"o":[[-0.302,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9956 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Path 9955","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[10.408,12.377,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[10.408,11.127,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[10.408,12.377,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,5.731,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.302,0],[0,-0.325],[0,0],[-0.301,0],[0,0.325],[0,0]],"o":[[-0.301,0],[0,0],[0,0.325],[0.302,0],[0,0],[0,-0.325]],"v":[[0.546,0],[0,0.588],[0,10.875],[0.546,11.463],[1.091,10.875],[1.091,0.588]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9955 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.302,0],[0,-0.325],[0,0],[-0.301,0],[0,0.325],[0,0]],"o":[[-0.301,0],[0,0],[0,0.325],[0.302,0],[0,0],[0,-0.325]],"v":[[0.546,0],[0,0.588],[0,10.875],[0.546,11.463],[1.091,10.875],[1.091,0.588]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9955 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Path 9954","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[12,12.128,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[12,14.628,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[12,12.128,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,3.957,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.302]],"v":[[0.546,0],[0,0.546],[0,7.368],[0.546,7.914],[1.091,7.368],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9954 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.302]],"v":[[0.546,0],[0,0.546],[0,7.368],[0.546,7.914],[1.091,7.368],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9954 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Path 9953","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[13.592,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[13.592,10.383,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[13.592,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,2.592,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,4.639],[0.546,5.185],[1.092,4.639],[1.092,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9953 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,4.639],[0.546,5.185],[1.092,4.639],[1.092,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9953 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Path 9952","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[15.184,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[15.184,12.883,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[15.184,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,3.275,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9952 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.301],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,6.004],[0.546,6.55],[1.091,6.004],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9952 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Path 9951","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":180,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[16.776,11.633,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":28,"s":[16.776,10.383,0],"to":[0,0,0],"ti":[0,0,0]},{"t":59,"s":[16.776,11.633,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0.546,1.91,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,3.274],[0.546,3.82],[1.091,3.274],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":1.5,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9951 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.301,0],[0,-0.302],[0,0],[-0.301,0],[0,0.301],[0,0]],"o":[[-0.301,0],[0,0],[0,0.301],[0.301,0],[0,0],[0,-0.301]],"v":[[0.546,0],[0,0.546],[0,3.274],[0.546,3.82],[1.091,3.274],[1.091,0.546]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 9951 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Ellipse 416","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.458823529412,0.78431372549,0.682352941176,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":-0.75,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 416 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 416 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[100]},{"t":59,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":59,"s":[100]}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[-184]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[170]},{"t":59,"s":[507]}],"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":1800,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Ellipse 415","sr":1,"ks":{"o":{"a":0,"k":25,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"op","nm":"Offset Paths 1","a":{"a":0,"k":-0.75,"ix":1},"lj":1,"ml":{"a":0,"k":4,"ix":3},"ix":3,"mn":"ADBE Vector Filter - Offset","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 415 Stroke","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 415 Fill","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file +{ + "v": "5.7.6", + "fr": 30, + "ip": 0, + "op": 60, + "w": 24, + "h": 24, + "nm": "Loader top", + "ddd": 0, + "assets": [], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Path 9957", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 180, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [7.224, 11.633, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 28, + "s": [7.224, 9.133, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { "t": 59, "s": [7.224, 11.633, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0.546, 1.228, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 1.91], + [0.546, 2.456], + [1.091, 1.91], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": 1.5, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9957 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 1.91], + [0.546, 2.456], + [1.091, 1.91], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9957 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Path 9956", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 180, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [8.816, 11.633, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 28, + "s": [8.816, 12.883, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { "t": 59, "s": [8.816, 11.633, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0.546, 3.275, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.302, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.302, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 6.004], + [0.546, 6.55], + [1.091, 6.004], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": 1.5, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9956 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.302, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.302, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 6.004], + [0.546, 6.55], + [1.091, 6.004], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9956 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 3, + "ty": 4, + "nm": "Path 9955", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 180, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [10.408, 12.377, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 28, + "s": [10.408, 11.127, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { "t": 59, "s": [10.408, 12.377, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0.546, 5.731, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.302, 0], + [0, -0.325], + [0, 0], + [-0.301, 0], + [0, 0.325], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.325], + [0.302, 0], + [0, 0], + [0, -0.325] + ], + "v": [ + [0.546, 0], + [0, 0.588], + [0, 10.875], + [0.546, 11.463], + [1.091, 10.875], + [1.091, 0.588] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": 1.5, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9955 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.302, 0], + [0, -0.325], + [0, 0], + [-0.301, 0], + [0, 0.325], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.325], + [0.302, 0], + [0, 0], + [0, -0.325] + ], + "v": [ + [0.546, 0], + [0, 0.588], + [0, 10.875], + [0.546, 11.463], + [1.091, 10.875], + [1.091, 0.588] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9955 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 4, + "ty": 4, + "nm": "Path 9954", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 180, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [12, 12.128, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 28, + "s": [12, 14.628, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { "t": 59, "s": [12, 12.128, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0.546, 3.957, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.302], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.302] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 7.368], + [0.546, 7.914], + [1.091, 7.368], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": 1.5, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9954 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.302], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.302] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 7.368], + [0.546, 7.914], + [1.091, 7.368], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9954 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 5, + "ty": 4, + "nm": "Path 9953", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 180, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [13.592, 11.633, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 28, + "s": [13.592, 10.383, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { "t": 59, "s": [13.592, 11.633, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0.546, 2.592, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 4.639], + [0.546, 5.185], + [1.092, 4.639], + [1.092, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": 1.5, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9953 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 4.639], + [0.546, 5.185], + [1.092, 4.639], + [1.092, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9953 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 6, + "ty": 4, + "nm": "Path 9952", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 180, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [15.184, 11.633, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 28, + "s": [15.184, 12.883, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { "t": 59, "s": [15.184, 11.633, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0.546, 3.275, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 6.004], + [0.546, 6.55], + [1.091, 6.004], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": 1.5, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9952 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.301], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 6.004], + [0.546, 6.55], + [1.091, 6.004], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9952 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 7, + "ty": 4, + "nm": "Path 9951", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 180, "ix": 10 }, + "p": { + "a": 1, + "k": [ + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 0, + "s": [16.776, 11.633, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { + "i": { "x": 0.833, "y": 0.833 }, + "o": { "x": 0.167, "y": 0.167 }, + "t": 28, + "s": [16.776, 10.383, 0], + "to": [0, 0, 0], + "ti": [0, 0, 0] + }, + { "t": 59, "s": [16.776, 11.633, 0] } + ], + "ix": 2, + "l": 2 + }, + "a": { "a": 0, "k": [0.546, 1.91, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.302], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 3.274], + [0.546, 3.82], + [1.091, 3.274], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": 1.5, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9951 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [0.301, 0], + [0, -0.302], + [0, 0], + [-0.301, 0], + [0, 0.301], + [0, 0] + ], + "o": [ + [-0.301, 0], + [0, 0], + [0, 0.301], + [0.301, 0], + [0, 0], + [0, -0.301] + ], + "v": [ + [0.546, 0], + [0, 0.546], + [0, 3.274], + [0.546, 3.82], + [1.091, 3.274], + [1.091, 0.546] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 4 + }, + "o": { "a": 0, "k": 100, "ix": 5 }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Path 9951 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 8, + "ty": 4, + "nm": "Ellipse 416", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 100, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [12, 12, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [20, 20], "ix": 2 }, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "st", + "c": { + "a": 0, + "k": [0.458823529412, 0.78431372549, 0.682352941176, 1], + "ix": 3 + }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 1.5, "ix": 5 }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": -0.75, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 416 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [20, 20], "ix": 2 }, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 416 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "tm", + "s": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 0, + "s": [2] + }, + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 28, + "s": [100] + }, + { "t": 59, "s": [100] } + ], + "ix": 1 + }, + "e": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 0, + "s": [0] + }, + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 28, + "s": [0] + }, + { "t": 59, "s": [100] } + ], + "ix": 2 + }, + "o": { + "a": 1, + "k": [ + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 0, + "s": [-184] + }, + { + "i": { "x": [0.833], "y": [0.833] }, + "o": { "x": [0.167], "y": [0.167] }, + "t": 28, + "s": [170] + }, + { "t": 59, "s": [507] } + ], + "ix": 3 + }, + "m": 1, + "ix": 3, + "nm": "Trim Paths 1", + "mn": "ADBE Vector Filter - Trim", + "hd": false + } + ], + "ip": 0, + "op": 1800, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 9, + "ty": 4, + "nm": "Ellipse 415", + "sr": 1, + "ks": { + "o": { "a": 0, "k": 25, "ix": 11 }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "p": { "a": 0, "k": [12, 12, 0], "ix": 2, "l": 2 }, + "a": { "a": 0, "k": [0, 0, 0], "ix": 1, "l": 2 }, + "s": { "a": 0, "k": [100, 100, 100], "ix": 6, "l": 2 } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [20, 20], "ix": 2 }, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "st", + "c": { "a": 0, "k": [1, 1, 1, 1], "ix": 3 }, + "o": { "a": 0, "k": 100, "ix": 4 }, + "w": { "a": 0, "k": 1.5, "ix": 5 }, + "lc": 1, + "lj": 1, + "ml": 4, + "bm": 0, + "nm": "Stroke 1", + "mn": "ADBE Vector Graphic - Stroke", + "hd": false + }, + { + "ty": "op", + "nm": "Offset Paths 1", + "a": { "a": 0, "k": -0.75, "ix": 1 }, + "lj": 1, + "ml": { "a": 0, "k": 4, "ix": 3 }, + "ix": 3, + "mn": "ADBE Vector Filter - Offset", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 415 Stroke", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "d": 1, + "ty": "el", + "s": { "a": 0, "k": [20, 20], "ix": 2 }, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "nm": "Ellipse Path 1", + "mn": "ADBE Vector Shape - Ellipse", + "hd": false + }, + { + "ty": "tr", + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "o": { "a": 0, "k": 100, "ix": 7 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "nm": "Transform" + } + ], + "nm": "Ellipse 415 Fill", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} From 79b3fe4a3823c8f4057e5db2067215bfab0e6178 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 16 Mar 2023 12:11:57 +0100 Subject: [PATCH 193/526] Make requests on Reviewers screen to run in parallel. --- .../ui/reviewers/ReviewersViewModel.kt | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 46335d2a9..6b25a186a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,10 +12,12 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -26,6 +28,8 @@ data class ReviewersState( val reviewers: List = emptyList(), val pullRequestNumber: String = "", val isSuccessSnackbarShown: Boolean = false, + val isLoading: Boolean = false, + val isError: Boolean = false, ) @HiltViewModel @@ -39,13 +43,8 @@ class ReviewersViewModel @Inject constructor( init { val initialValues = getInitialValues(savedStateHandle) - - state = state.copy(pullRequestNumber = initialValues.pullRequestNumber) - - viewModelScope.launch { - fetchRequestedReviewers(initialValues) - fetchReviews(initialValues) - } + state = state.copy(pullRequestNumber = initialValues.pullRequestNumber, isLoading = true) + fetchData(initialValues) } private fun getInitialValues(savedStateHandle: SavedStateHandle) = InitialValues( @@ -55,14 +54,35 @@ class ReviewersViewModel @Inject constructor( checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]), ) - private suspend fun fetchRequestedReviewers(initialValues: InitialValues) { - val (owner, repo, pullRequestNumber, submissionTime) = initialValues + private fun fetchData(initialValues: InitialValues) { + viewModelScope.launch { + getMergedData(initialValues) + .onSuccess { state = state.copy(reviewers = it.orEmpty(), isLoading = false) } + .onFailure { state = state.copy(isError = true, isLoading = false) } + } + } - repository.getRequestedReviewers(owner, repo, pullRequestNumber) - .onSuccess { response -> - val reviewers = response.mapToReviewers(submissionTime) - state = state.copy(reviewers = state.reviewers + reviewers) + private suspend fun getMergedData(initialValues: InitialValues): Result?> = + coroutineScope { + val requestedReviewersDeferred = async { fetchRequestedReviewers(initialValues) } + val reviewersDeferred = async { fetchReviews(initialValues) } + + val requestedReviewerResult = requestedReviewersDeferred.await() + val reviewersResult = reviewersDeferred.await() + + requestedReviewerResult.map { requestedReviewers -> + reviewersResult.map { it + requestedReviewers } + .getOrNull() } + } + + + private suspend fun fetchRequestedReviewers(initialValues: InitialValues): Result> { + val (owner, repo, pullRequestNumber, submissionTime) = initialValues + + return repository.getRequestedReviewers(owner, repo, pullRequestNumber) + .map { it.mapToReviewers(submissionTime) } + } private fun RequestedReviewersResponse.mapToReviewers(submissionTime: String): List { @@ -73,13 +93,11 @@ class ReviewersViewModel @Inject constructor( } } - private suspend fun fetchReviews(initialValues: InitialValues) { + private suspend fun fetchReviews(initialValues: InitialValues): Result> { val (owner, repo, pullRequestNumber, submissionTime) = initialValues - repository.getReviews(owner, repo, pullRequestNumber).onSuccess { reviews -> - val reviewersAfterReview = reviews.mapToReviewers(submissionTime) - state = state.copy(reviewers = state.reviewers + reviewersAfterReview) - } + return repository.getReviews(owner, repo, pullRequestNumber) + .map { it.mapToReviewers(submissionTime) } } private fun List.mapToReviewers(submissionTime: String): List { From 0df4c00d4b3c358332b9291d7e7e48ee2da4559c Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 16 Mar 2023 12:21:05 +0100 Subject: [PATCH 194/526] code cleanup --- .../loudius/ui/components/LoudiusLoadingIndicator.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index 003a0be8a..4f9e87104 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -18,15 +18,14 @@ import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme @Composable -fun LoudiusLoaderIndicator() { +fun LoudiusLoadingIndicator() { val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) val progress by animateLottieCompositionAsState( - composition, + composition = composition, iterations = LottieConstants.IterateForever, ) Box( - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { LottieAnimation( composition = composition, @@ -42,6 +41,6 @@ fun LoudiusLoaderIndicator() { @Composable fun LoudiusLoadingIndicatorPreview() { LoudiusTheme { - LoudiusLoaderIndicator() + LoudiusLoadingIndicator() } } From 9918cb0f34e326e96a519419733ccaa73fae0c50 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 16 Mar 2023 12:25:17 +0100 Subject: [PATCH 195/526] Add error handling and loading to the ReviewersViewModel.kt during initial fetching data. --- .../ui/components/LoudiusErrorScreen.kt | 8 ++-- .../loudius/ui/reviewers/ReviewersScreen.kt | 40 +++++++++++++++---- .../ui/reviewers/ReviewersViewModel.kt | 4 +- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index 095a30337..ae51f66ab 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -19,12 +19,14 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusErrorScreen( - errorText: String, - buttonText: String, + errorText: String = stringResource(id = R.string.error_dialog_text), + buttonText: String = stringResource(id = R.string.try_again), onButtonClick: () -> Unit, ) { Column( - modifier = Modifier.padding(top = 142.dp).fillMaxSize(), + modifier = Modifier + .padding(top = 142.dp) + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(56.dp), ) { diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 360b699db..7862a3423 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -2,12 +2,15 @@ package com.appunite.loudius.ui.reviewers import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -19,6 +22,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -29,6 +33,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.domain.model.Reviewer +import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.ui.utils.bottomBorder @@ -45,9 +50,11 @@ fun ReviewersScreen( ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, reviewers = state.reviewers, + isLoading = state.isLoading, + isError = state.isError, onClickBackArrow = navigateBack, snackbarHostState = snackbarHostState, - onNotifyClick = viewModel::onAction, + onAction = viewModel::onAction, ) SnackbarLaunchedEffect( isSuccessSnackbarShown = state.isSuccessSnackbarShown, @@ -79,9 +86,11 @@ private fun SnackbarLaunchedEffect( private fun ReviewersScreenStateless( pullRequestNumber: String, reviewers: List, + isLoading: Boolean, + isError: Boolean, onClickBackArrow: () -> Unit, snackbarHostState: SnackbarHostState, - onNotifyClick: (ReviewersAction) -> Unit, + onAction: (ReviewersAction) -> Unit, ) { Scaffold( topBar = { @@ -91,13 +100,28 @@ private fun ReviewersScreenStateless( ) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + modifier = Modifier.background(MaterialTheme.colorScheme.surface), content = { padding -> - ReviewersScreenContent(reviewers, modifier = Modifier.padding(padding), onNotifyClick) + when { + isError -> LoudiusErrorScreen(onButtonClick = { onAction(ReviewersAction.OnTryAgain) }) + isLoading -> LoadingIndicator(Modifier.padding(padding)) + else -> ReviewersScreenContent( + reviewers = reviewers, + modifier = Modifier.padding(padding), + onNotifyClick = onAction + ) + } }, - modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) } +@Composable +private fun LoadingIndicator(modifier: Modifier) { + Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) { + CircularProgressIndicator() + } +} + @Composable private fun ReviewersScreenContent( reviewers: List, @@ -219,9 +243,11 @@ fun DetailsScreenPreview() { ReviewersScreenStateless( pullRequestNumber = "1", reviewers = reviewers, - {}, - SnackbarHostState(), - {}, + isError = false, + isLoading = false, + onClickBackArrow = {}, + snackbarHostState = SnackbarHostState(), + onAction = {}, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 6b25a186a..0836b2075 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() object OnSnackbarDismiss : ReviewersAction() + object OnTryAgain : ReviewersAction() } data class ReviewersState( @@ -37,12 +38,12 @@ class ReviewersViewModel @Inject constructor( private val repository: PullRequestRepository, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { + private val initialValues: InitialValues = getInitialValues(savedStateHandle) var state by mutableStateOf(ReviewersState()) private set init { - val initialValues = getInitialValues(savedStateHandle) state = state.copy(pullRequestNumber = initialValues.pullRequestNumber, isLoading = true) fetchData(initialValues) } @@ -124,6 +125,7 @@ class ReviewersViewModel @Inject constructor( fun onAction(action: ReviewersAction) = when (action) { is ReviewersAction.Notify -> notifyUser(action.userLogin) is ReviewersAction.OnSnackbarDismiss -> state = state.copy(isSuccessSnackbarShown = false) + is ReviewersAction.OnTryAgain -> fetchData(initialValues) } private fun notifyUser(userLogin: String) { From 0bde0ab5a3e14fdb8e3e2a133cc99eeda09efa95 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 16 Mar 2023 13:25:11 +0100 Subject: [PATCH 196/526] Use Loudius loading indicator at ReviewersScreen.kt --- .../loudius/ui/reviewers/ReviewersScreen.kt | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 7862a3423..8bd5b3689 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -2,15 +2,12 @@ package com.appunite.loudius.ui.reviewers import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -22,7 +19,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -34,6 +30,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.ui.utils.bottomBorder @@ -104,7 +101,7 @@ private fun ReviewersScreenStateless( content = { padding -> when { isError -> LoudiusErrorScreen(onButtonClick = { onAction(ReviewersAction.OnTryAgain) }) - isLoading -> LoadingIndicator(Modifier.padding(padding)) + isLoading -> LoudiusLoadingIndicator() else -> ReviewersScreenContent( reviewers = reviewers, modifier = Modifier.padding(padding), @@ -115,13 +112,6 @@ private fun ReviewersScreenStateless( ) } -@Composable -private fun LoadingIndicator(modifier: Modifier) { - Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) { - CircularProgressIndicator() - } -} - @Composable private fun ReviewersScreenContent( reviewers: List, From 3617479a927faaadbeec5c0a591f260704b9a21e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 17 Mar 2023 08:24:51 +0100 Subject: [PATCH 197/526] Add tests and rewrite merging fetched data. --- .../ui/reviewers/ReviewersViewModel.kt | 7 +- .../fakes/FakePullRequestRepository.kt | 12 ++-- .../ui/reviewers/ReviewersViewModelTest.kt | 66 +++++++++++++++++-- 3 files changed, 72 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 0836b2075..da37afae8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Screen +import com.appunite.loudius.common.flatMap import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse @@ -44,7 +45,7 @@ class ReviewersViewModel @Inject constructor( private set init { - state = state.copy(pullRequestNumber = initialValues.pullRequestNumber, isLoading = true) + state = state.copy(pullRequestNumber = initialValues.pullRequestNumber) fetchData(initialValues) } @@ -65,15 +66,15 @@ class ReviewersViewModel @Inject constructor( private suspend fun getMergedData(initialValues: InitialValues): Result?> = coroutineScope { + state = state.copy(isLoading = true) val requestedReviewersDeferred = async { fetchRequestedReviewers(initialValues) } val reviewersDeferred = async { fetchReviews(initialValues) } val requestedReviewerResult = requestedReviewersDeferred.await() val reviewersResult = reviewersDeferred.await() - requestedReviewerResult.map { requestedReviewers -> + requestedReviewerResult.flatMap { requestedReviewers -> reviewersResult.map { it + requestedReviewers } - .getOrNull() } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 6f3faf738..162ee570c 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -22,7 +22,7 @@ class FakePullRequestRepository : PullRequestRepository { repo: String, pullRequestNumber: String, ): Result> = when (pullRequestNumber) { - "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( + "failureOnlyOnRequestedReviewers", "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( listOf( Review("1", User(1, "user1"), CHANGES_REQUESTED, date1), Review("2", User(1, "user1"), COMMENTED, date2), @@ -32,7 +32,9 @@ class FakePullRequestRepository : PullRequestRepository { Review("6", User(2, "user2"), APPROVED, date3), ), ) - "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + "failureOnlyOnReviews", "notExistingPullRequestNumber" -> { + Result.failure(WebException.UnknownError(404, null)) + } else -> Result.success(emptyList()) } @@ -41,7 +43,7 @@ class FakePullRequestRepository : PullRequestRepository { repo: String, pullRequestNumber: String, ): Result = when (pullRequestNumber) { - "correctPullRequestNumber", "onlyRequestedReviewersPullNumber" -> Result.success( + "correctPullRequestNumber", "onlyRequestedReviewersPullNumber", "failureOnlyOnReviews" -> Result.success( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), @@ -49,7 +51,9 @@ class FakePullRequestRepository : PullRequestRepository { ), ), ) - "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + "failureOnlyOnRequestedReviewers", "notExistingPullRequestNumber" -> Result.failure( + WebException.UnknownError(404, null) + ) else -> Result.success(RequestedReviewersResponse(emptyList())) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index ef0665a71..42107fa2a 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,18 +9,22 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.yield import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -73,6 +77,17 @@ class ReviewersViewModelTest { assertEquals("correctPullRequestNumber", viewModel.state.pullRequestNumber) } + @Test + fun `GIVEN correct initial values WHEN init starts THEN state is loading`() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + + viewModel = createViewModel() + yield() + + assertEquals(true, viewModel.state.isLoading) + assertEquals(emptyList(), viewModel.state.reviewers) + } + @Test fun `GIVEN no reviewers WHEN init THEN state is correct with no reviewers`() { every { savedStateHandle.get("pull_request_number") } returns "pullRequestWithNoReviewers" @@ -81,6 +96,7 @@ class ReviewersViewModelTest { assertEquals("pullRequestWithNoReviewers", viewModel.state.pullRequestNumber) assertEquals(emptyList(), viewModel.state.reviewers) + assertEquals(false, viewModel.state.isLoading) } @Test @@ -89,14 +105,15 @@ class ReviewersViewModelTest { viewModel = createViewModel() val expected = listOf( - Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", true, 7, 5), + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), ) val actual = viewModel.state.reviewers assertEquals(expected, actual) + assertEquals(false, viewModel.state.isLoading) } @Test @@ -130,6 +147,43 @@ class ReviewersViewModelTest { assertEquals(expected, actual) } + + + @Test + fun `WHEN there is an error during fetching data on init THEN error is shown`() = + runTest { + every { savedStateHandle.get("pull_request_number") } returns "notExistingPullRequestNumber" + + viewModel = createViewModel() + + assertEquals(true, viewModel.state.isError) + assertEquals(emptyList(), viewModel.state.reviewers) + assertEquals(false, viewModel.state.isLoading) + } + + @Test + fun `WHEN there is an error during fetching data on init only from requested reviewers request THEN error is shown`() = + runTest { + every { savedStateHandle.get("pull_request_number") } returns "failureOnlyOnRequestedReviewers" + + viewModel = createViewModel() + + assertEquals(true, viewModel.state.isError) + assertEquals(emptyList(), viewModel.state.reviewers) + assertEquals(false, viewModel.state.isLoading) + } + + @Test + fun `WHEN there is an error during fetching data on init only from reviews request THEN error is shown`() = + runTest { + every { savedStateHandle.get("pull_request_number") } returns "failureOnlyOnReviews" + + viewModel = createViewModel() + + assertEquals(true, viewModel.state.isError) + assertEquals(emptyList(), viewModel.state.reviewers) + assertEquals(false, viewModel.state.isLoading) + } } @Nested From 603ae1adb8484edfe8544d8045aec31acb32ff66 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 17 Mar 2023 08:27:46 +0100 Subject: [PATCH 198/526] Use initial values from a variable. --- .../com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index da37afae8..21e79393d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -37,7 +37,7 @@ data class ReviewersState( @HiltViewModel class ReviewersViewModel @Inject constructor( private val repository: PullRequestRepository, - private val savedStateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, ) : ViewModel() { private val initialValues: InitialValues = getInitialValues(savedStateHandle) @@ -130,7 +130,7 @@ class ReviewersViewModel @Inject constructor( } private fun notifyUser(userLogin: String) { - val (owner, repo, pullRequestNumber) = getInitialValues(savedStateHandle) + val (owner, repo, pullRequestNumber) = initialValues viewModelScope.launch { repository.notify(owner, repo, pullRequestNumber, "@$userLogin") From 72b05d0d29aad71fea6042fb5f41d260e35b3bc6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 17 Mar 2023 08:36:33 +0100 Subject: [PATCH 199/526] Correct test name at the ReviewersViewModelTest.kt. --- .../com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 42107fa2a..55dc84a42 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -150,7 +150,7 @@ class ReviewersViewModelTest { @Test - fun `WHEN there is an error during fetching data on init THEN error is shown`() = + fun `WHEN there is an error during fetching data from 2 requests on init THEN error is shown`() = runTest { every { savedStateHandle.get("pull_request_number") } returns "notExistingPullRequestNumber" From 56997ebb01369ca3c3322a27b741474e90dedafb Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 17 Mar 2023 09:10:47 +0100 Subject: [PATCH 200/526] Perform minor code cleaning. --- .../appunite/loudius/ui/reviewers/ReviewersScreen.kt | 12 ++++++++---- .../loudius/ui/reviewers/ReviewersViewModel.kt | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 360b699db..1663ee281 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -92,7 +92,11 @@ private fun ReviewersScreenStateless( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, content = { padding -> - ReviewersScreenContent(reviewers, modifier = Modifier.padding(padding), onNotifyClick) + ReviewersScreenContent( + reviewers = reviewers, + modifier = Modifier.padding(padding), + onNotifyClick = onNotifyClick + ) }, modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) @@ -219,9 +223,9 @@ fun DetailsScreenPreview() { ReviewersScreenStateless( pullRequestNumber = "1", reviewers = reviewers, - {}, - SnackbarHostState(), - {}, + onNotifyClick = {}, + snackbarHostState = SnackbarHostState(), + onClickBackArrow = {}, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 46335d2a9..9a328f0e9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -105,7 +105,7 @@ class ReviewersViewModel @Inject constructor( fun onAction(action: ReviewersAction) = when (action) { is ReviewersAction.Notify -> notifyUser(action.userLogin) - is ReviewersAction.OnSnackbarDismiss -> state = state.copy(isSuccessSnackbarShown = false) + is ReviewersAction.OnSnackbarDismiss -> dismissSnackbar() } private fun notifyUser(userLogin: String) { @@ -119,6 +119,10 @@ class ReviewersViewModel @Inject constructor( } } + private fun dismissSnackbar() { + state = state.copy(isSuccessSnackbarShown = false) + } + private data class InitialValues( val owner: String, val repo: String, From 08b51476836e67b8033df4f0ae5abe9ce4a2467e Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Fri, 17 Mar 2023 10:27:49 +0000 Subject: [PATCH 201/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 1663ee281..b7b82256d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -95,7 +95,7 @@ private fun ReviewersScreenStateless( ReviewersScreenContent( reviewers = reviewers, modifier = Modifier.padding(padding), - onNotifyClick = onNotifyClick + onNotifyClick = onNotifyClick, ) }, modifier = Modifier.background(MaterialTheme.colorScheme.surface), From b7c5aebed83e4e2ea7d8e78c1ef9a689d8828d15 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 11:49:20 +0100 Subject: [PATCH 202/526] Rename tests testing initial state --- .../loudius/ui/pullrequests/PullRequestsViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index e897dcf75..1803d8fc8 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -22,7 +22,7 @@ class PullRequestsViewModelTest { private fun getViewModel() = PullRequestsViewModel(pullRequestRepository) @Test - fun `GIVEN logged in user WHEN init THEN display loading`() = runTest { + fun `WHEN init THEN display loading`() = runTest { pullRequestRepository.setCurrentUserPullRequests { neverCompletingSuspension() } val viewModel = getViewModel() @@ -30,7 +30,7 @@ class PullRequestsViewModelTest { } @Test - fun `GIVEN logged in user WHEN init THEN display pull requests list`() { + fun `WHEN init THEN display pull requests list`() { val viewModel = getViewModel() assertEquals( @@ -50,7 +50,7 @@ class PullRequestsViewModelTest { } @Test - fun `GIVEN logged in user WHEN fetching failed THEN display error`() = runTest { + fun `WHEN fetching data failed on init THEN display error`() = runTest { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } val viewModel = getViewModel() @@ -58,7 +58,7 @@ class PullRequestsViewModelTest { } @Test - fun `GIVEN logged in user WHEN retry THEN fetch pull requests list again`() { + fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } val viewModel = getViewModel() assertTrue(viewModel.state.isError) From b19ad9f9382fcbf8129d97aa50252462134edf8a Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 12:08:35 +0100 Subject: [PATCH 203/526] Extract default pull request --- .../fakes/FakePullRequestRepository.kt | 10 ++-------- .../PullRequestsNetworkDataSourceTest.kt | 10 ++-------- .../pullrequests/PullRequestsViewModelTest.kt | 20 ++++--------------- .../com/appunite/loudius/util/Defaults.kt | 15 ++++++++++++++ 4 files changed, 23 insertions(+), 32 deletions(-) create mode 100644 app/src/test/java/com/appunite/loudius/util/Defaults.kt diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index fd4ef2a8c..649557e30 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -11,6 +11,7 @@ import com.appunite.loudius.network.model.ReviewState.CHANGES_REQUESTED import com.appunite.loudius.network.model.ReviewState.COMMENTED import com.appunite.loudius.network.model.User import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.util.Defaults import java.time.LocalDateTime class FakePullRequestRepository : PullRequestRepository { @@ -59,14 +60,7 @@ class FakePullRequestRepository : PullRequestRepository { incompleteResults = false, totalCount = 1, items = listOf( - PullRequest( - id = 1, - draft = false, - number = 1, - repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", - title = "example title", - LocalDateTime.parse("2023-03-07T09:21:45"), - ), + Defaults.pullRequest(), ), ), ) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 2cb186abb..22d8b8127 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -10,6 +10,7 @@ import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.util.Defaults import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -149,14 +150,7 @@ class PullRequestsNetworkDataSourceTest { incompleteResults = false, totalCount = 1, items = listOf( - PullRequest( - id = 1, - draft = false, - number = 1, - repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", - title = "example title", - LocalDateTime.parse("2023-03-07T09:21:45"), - ), + Defaults.pullRequest(), ), ), ) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 1803d8fc8..29f769633 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -5,6 +5,7 @@ package com.appunite.loudius.ui.pullrequests import com.appunite.loudius.fakes.FakePullRequestRepository import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.util.Defaults import com.appunite.loudius.util.MainDispatcherExtension import com.appunite.loudius.utils.neverCompletingSuspension import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -35,14 +36,7 @@ class PullRequestsViewModelTest { assertEquals( listOf( - PullRequest( - id = 1, - draft = false, - number = 1, - repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", - title = "example title", - LocalDateTime.parse("2023-03-07T09:21:45"), - ), + Defaults.pullRequest(), ), viewModel.state.pullRequests, ) @@ -69,17 +63,11 @@ class PullRequestsViewModelTest { assertEquals( listOf( - PullRequest( - id = 1, - draft = false, - number = 1, - repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", - title = "example title", - LocalDateTime.parse("2023-03-07T09:21:45"), - ), + Defaults.pullRequest(), ), viewModel.state.pullRequests, ) assertFalse(viewModel.state.isLoading) } + } diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt new file mode 100644 index 000000000..4e5a77cb8 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -0,0 +1,15 @@ +package com.appunite.loudius.util + +import com.appunite.loudius.network.model.PullRequest +import java.time.LocalDateTime + +object Defaults { + fun pullRequest(id: Int = 1) = PullRequest( + id = id, + draft = false, + number = id, + repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", + title = "example title", + LocalDateTime.parse("2023-03-07T08:21:45").plusHours(id.toLong()), + ) +} From 2757bcdf7c5edbf02c60dbe81742f88710a6325f Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 12:12:16 +0100 Subject: [PATCH 204/526] Merge util and utils packages --- .../com/appunite/loudius/{utils => util}/CoroutinesHelpers.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/test/java/com/appunite/loudius/{utils => util}/CoroutinesHelpers.kt (100%) diff --git a/app/src/test/java/com/appunite/loudius/utils/CoroutinesHelpers.kt b/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt similarity index 100% rename from app/src/test/java/com/appunite/loudius/utils/CoroutinesHelpers.kt rename to app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt From 86ba341e2e34d9541aa52fecf09f003b3d1709a5 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 12:29:59 +0100 Subject: [PATCH 205/526] Add navigations tests --- .../pullrequests/PullRequestsViewModelTest.kt | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 29f769633..aadd4146e 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -12,10 +12,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import java.time.LocalDateTime @ExtendWith(MainDispatcherExtension::class) class PullRequestsViewModelTest { @@ -70,4 +70,31 @@ class PullRequestsViewModelTest { assertFalse(viewModel.state.isLoading) } + @Test + fun `GIVEN item id WHEN item click THEN redirect user`() { + val viewModel = getViewModel() + assertNull(viewModel.state.navigateToReviewers) + val pullRequest = Defaults.pullRequest() + + viewModel.onAction(PulLRequestsAction.ItemClick(pullRequest.id)) + + val expected = NavigationPayload( + pullRequest.owner, + pullRequest.shortRepositoryName, + pullRequest.number.toString(), + pullRequest.createdAt.toString() + ) + assertEquals(expected, viewModel.state.navigateToReviewers) + } + + @Test + fun `GIVEN navigation payload WHEN navigating to reviewers THEN reset payload`() { + val viewModel = getViewModel() + val pullRequest = Defaults.pullRequest() + viewModel.onAction(PulLRequestsAction.ItemClick(pullRequest.id)) + + viewModel.onAction(PulLRequestsAction.OnNavigateToReviewers) + + assertNull(viewModel.state.navigateToReviewers) + } } From 3eaada999ca46b5963f172099c925a83f87d8083 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 12:36:00 +0100 Subject: [PATCH 206/526] Rearrange tests --- .../pullrequests/PullRequestsViewModelTest.kt | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index aadd4146e..c3b15a63e 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -34,12 +34,7 @@ class PullRequestsViewModelTest { fun `WHEN init THEN display pull requests list`() { val viewModel = getViewModel() - assertEquals( - listOf( - Defaults.pullRequest(), - ), - viewModel.state.pullRequests, - ) + assertEquals(listOf(Defaults.pullRequest()), viewModel.state.pullRequests) assertFalse(viewModel.state.isLoading) } @@ -48,6 +43,7 @@ class PullRequestsViewModelTest { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } val viewModel = getViewModel() + assertEquals(emptyList(), viewModel.state.pullRequests) assertTrue(viewModel.state.isError) } @@ -55,18 +51,11 @@ class PullRequestsViewModelTest { fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } val viewModel = getViewModel() - assertTrue(viewModel.state.isError) - assertEquals(emptyList(), viewModel.state.pullRequests) pullRequestRepository.resetCurrentUserPullRequestAnswer() viewModel.onAction(PulLRequestsAction.RetryClick) - assertEquals( - listOf( - Defaults.pullRequest(), - ), - viewModel.state.pullRequests, - ) + assertEquals(listOf(Defaults.pullRequest()), viewModel.state.pullRequests) assertFalse(viewModel.state.isLoading) } From c8be6fd7900f4147bee925ebe8ad6ebda8b2222d Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 12:36:32 +0100 Subject: [PATCH 207/526] Use runTest --- .../loudius/ui/pullrequests/PullRequestsViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index c3b15a63e..9245ef0ac 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -31,7 +31,7 @@ class PullRequestsViewModelTest { } @Test - fun `WHEN init THEN display pull requests list`() { + fun `WHEN init THEN display pull requests list`() = runTest { val viewModel = getViewModel() assertEquals(listOf(Defaults.pullRequest()), viewModel.state.pullRequests) @@ -48,7 +48,7 @@ class PullRequestsViewModelTest { } @Test - fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() { + fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() = runTest { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } val viewModel = getViewModel() @@ -60,7 +60,7 @@ class PullRequestsViewModelTest { } @Test - fun `GIVEN item id WHEN item click THEN redirect user`() { + fun `GIVEN item id WHEN item click THEN redirect user`() = runTest { val viewModel = getViewModel() assertNull(viewModel.state.navigateToReviewers) val pullRequest = Defaults.pullRequest() @@ -77,7 +77,7 @@ class PullRequestsViewModelTest { } @Test - fun `GIVEN navigation payload WHEN navigating to reviewers THEN reset payload`() { + fun `GIVEN navigation payload WHEN navigating to reviewers THEN reset payload`() = runTest { val viewModel = getViewModel() val pullRequest = Defaults.pullRequest() viewModel.onAction(PulLRequestsAction.ItemClick(pullRequest.id)) From 9950622c976e5a9313a183b179187830116b921d Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 17 Mar 2023 12:45:14 +0100 Subject: [PATCH 208/526] add loading indicator to login screen --- .../loudius/ui/loading/LoadingScreen.kt | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 310a45590..355372d1b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -1,15 +1,16 @@ package com.appunite.loudius.ui.loading import android.content.Intent -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusLoadingIndicator +import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoadingScreen( @@ -37,7 +38,7 @@ fun LoadingScreen( viewModel.onAction(LoadingAction.OnTryAgainClick) } } else { - ShowLoadingIndicator(code = code) + ShowLoadingIndicator() } } @@ -54,9 +55,22 @@ private fun ShowLoudiusErrorScreen( } @Composable -private fun ShowLoadingIndicator(code: String?) { - // TODO add loading indicator - Column { - Text(text = code ?: "code is already consumed") +private fun ShowLoadingIndicator() { + LoudiusLoadingIndicator() +} + +@Preview(showSystemUi = true) +@Composable +fun ShowLoudiusErrorScreenPreview() { + LoudiusTheme { + ShowLoudiusErrorScreen {} + } +} + +@Preview(showSystemUi = true) +@Composable +fun ShowLoudiusLoadingIndicatorPreview() { + LoudiusTheme { + ShowLoadingIndicator() } } From e17d80d24bc5b5bc5d0d9617f9c26d3ba6fdc9e3 Mon Sep 17 00:00:00 2001 From: kezc Date: Fri, 17 Mar 2023 11:49:03 +0000 Subject: [PATCH 209/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/fakes/FakePullRequestRepository.kt | 1 - .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 1 - .../loudius/ui/pullrequests/PullRequestsViewModelTest.kt | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 25c74e986..c22c0796f 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -1,7 +1,6 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.PullRequestRepository -import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 2c6faef0e..430812208 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -1,6 +1,5 @@ package com.appunite.loudius.network.datasource -import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 9245ef0ac..83737db81 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -71,7 +71,7 @@ class PullRequestsViewModelTest { pullRequest.owner, pullRequest.shortRepositoryName, pullRequest.number.toString(), - pullRequest.createdAt.toString() + pullRequest.createdAt.toString(), ) assertEquals(expected, viewModel.state.navigateToReviewers) } From 32fa5e8ebc6b2399a25570b907c9134b86c51f17 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 12:56:56 +0100 Subject: [PATCH 210/526] Update resource text --- .../com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 00ee4a9d8..7f21b3d99 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -72,7 +72,7 @@ private fun PullRequestsScreenStateless( when { isError -> LoudiusErrorScreen( errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again_text), + buttonText = stringResource(R.string.try_again), onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) isLoading -> LoadingIndicator(modifier = Modifier.padding(padding)) From 9b6ae17ccb9eedd04a2558f25948356863519542 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 13:32:39 +0100 Subject: [PATCH 211/526] Rename function creating view model --- .../ui/pullrequests/PullRequestsViewModelTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 83737db81..22bc8c9ad 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -20,19 +20,19 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MainDispatcherExtension::class) class PullRequestsViewModelTest { private val pullRequestRepository = FakePullRequestRepository() - private fun getViewModel() = PullRequestsViewModel(pullRequestRepository) + private fun createViewModel() = PullRequestsViewModel(pullRequestRepository) @Test fun `WHEN init THEN display loading`() = runTest { pullRequestRepository.setCurrentUserPullRequests { neverCompletingSuspension() } - val viewModel = getViewModel() + val viewModel = createViewModel() assertTrue(viewModel.state.isLoading) } @Test fun `WHEN init THEN display pull requests list`() = runTest { - val viewModel = getViewModel() + val viewModel = createViewModel() assertEquals(listOf(Defaults.pullRequest()), viewModel.state.pullRequests) assertFalse(viewModel.state.isLoading) @@ -41,7 +41,7 @@ class PullRequestsViewModelTest { @Test fun `WHEN fetching data failed on init THEN display error`() = runTest { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } - val viewModel = getViewModel() + val viewModel = createViewModel() assertEquals(emptyList(), viewModel.state.pullRequests) assertTrue(viewModel.state.isError) @@ -50,7 +50,7 @@ class PullRequestsViewModelTest { @Test fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() = runTest { pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } - val viewModel = getViewModel() + val viewModel = createViewModel() pullRequestRepository.resetCurrentUserPullRequestAnswer() viewModel.onAction(PulLRequestsAction.RetryClick) @@ -61,7 +61,7 @@ class PullRequestsViewModelTest { @Test fun `GIVEN item id WHEN item click THEN redirect user`() = runTest { - val viewModel = getViewModel() + val viewModel = createViewModel() assertNull(viewModel.state.navigateToReviewers) val pullRequest = Defaults.pullRequest() @@ -78,7 +78,7 @@ class PullRequestsViewModelTest { @Test fun `GIVEN navigation payload WHEN navigating to reviewers THEN reset payload`() = runTest { - val viewModel = getViewModel() + val viewModel = createViewModel() val pullRequest = Defaults.pullRequest() viewModel.onAction(PulLRequestsAction.ItemClick(pullRequest.id)) From b31c70d483b322bc289ec6f5217bd73420e50b7f Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 17 Mar 2023 13:33:24 +0100 Subject: [PATCH 212/526] Rename test checking navigation --- .../loudius/ui/pullrequests/PullRequestsViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 22bc8c9ad..dde2eab22 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -60,7 +60,7 @@ class PullRequestsViewModelTest { } @Test - fun `GIVEN item id WHEN item click THEN redirect user`() = runTest { + fun `GIVEN item id WHEN item click THEN navigate the user to reviewers`() = runTest { val viewModel = createViewModel() assertNull(viewModel.state.navigateToReviewers) val pullRequest = Defaults.pullRequest() From 5354e5a958c15abb8c5cecdc2e11be0964407394 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 17 Mar 2023 13:56:48 +0100 Subject: [PATCH 213/526] code cleanup --- .../loudius/ui/loading/LoadingScreen.kt | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 355372d1b..798606c1c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -32,13 +32,22 @@ fun LoadingScreen( viewModel.onAction(LoadingAction.OnNavigateToPullRequests) } } + ResolveScreenToShow(showErrorScreen = state.showErrorScreen) { + viewModel.onAction(LoadingAction.OnTryAgainClick) + } +} - if (state.showErrorScreen) { +@Composable +fun ResolveScreenToShow( + showErrorScreen: Boolean, + onTryAgainClick: () -> Unit +) { + if (showErrorScreen) { ShowLoudiusErrorScreen { - viewModel.onAction(LoadingAction.OnTryAgainClick) + onTryAgainClick() } } else { - ShowLoadingIndicator() + LoudiusLoadingIndicator() } } @@ -54,23 +63,18 @@ private fun ShowLoudiusErrorScreen( } } -@Composable -private fun ShowLoadingIndicator() { - LoudiusLoadingIndicator() -} - @Preview(showSystemUi = true) @Composable fun ShowLoudiusErrorScreenPreview() { LoudiusTheme { - ShowLoudiusErrorScreen {} + ResolveScreenToShow(showErrorScreen = true) {} } } @Preview(showSystemUi = true) @Composable -fun ShowLoudiusLoadingIndicatorPreview() { +fun ShowLoadingIndicatorScreenPreview() { LoudiusTheme { - ShowLoadingIndicator() + ResolveScreenToShow(showErrorScreen = false) {} } } From ff81fc6e27cd88b25f162d661f4faf501da576ed Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 17 Mar 2023 12:59:46 +0000 Subject: [PATCH 214/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 798606c1c..dbd581be4 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -40,7 +40,7 @@ fun LoadingScreen( @Composable fun ResolveScreenToShow( showErrorScreen: Boolean, - onTryAgainClick: () -> Unit + onTryAgainClick: () -> Unit, ) { if (showErrorScreen) { ShowLoudiusErrorScreen { From 5535032f8084393a63fa0c85462598fadfa2df49 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 17 Mar 2023 14:50:51 +0100 Subject: [PATCH 215/526] rename function --- .../java/com/appunite/loudius/ui/loading/LoadingScreen.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index dbd581be4..054b663fc 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -32,13 +32,13 @@ fun LoadingScreen( viewModel.onAction(LoadingAction.OnNavigateToPullRequests) } } - ResolveScreenToShow(showErrorScreen = state.showErrorScreen) { + LoadingScreenStateless(showErrorScreen = state.showErrorScreen) { viewModel.onAction(LoadingAction.OnTryAgainClick) } } @Composable -fun ResolveScreenToShow( +fun LoadingScreenStateless( showErrorScreen: Boolean, onTryAgainClick: () -> Unit, ) { @@ -67,7 +67,7 @@ private fun ShowLoudiusErrorScreen( @Composable fun ShowLoudiusErrorScreenPreview() { LoudiusTheme { - ResolveScreenToShow(showErrorScreen = true) {} + LoadingScreenStateless(showErrorScreen = true) {} } } @@ -75,6 +75,6 @@ fun ShowLoudiusErrorScreenPreview() { @Composable fun ShowLoadingIndicatorScreenPreview() { LoudiusTheme { - ResolveScreenToShow(showErrorScreen = false) {} + LoadingScreenStateless(showErrorScreen = false) {} } } From 4ddedbc05db577974ec1953f203280d4fcb1b9e2 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Mon, 20 Mar 2023 09:02:53 +0100 Subject: [PATCH 216/526] Add loudius loading indicator to Pull Requests Screen --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 7f21b3d99..9399faa6a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -33,6 +33,7 @@ import com.appunite.loudius.R import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import java.time.LocalDateTime @@ -75,7 +76,7 @@ private fun PullRequestsScreenStateless( buttonText = stringResource(R.string.try_again), onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) - isLoading -> LoadingIndicator(modifier = Modifier.padding(padding)) + isLoading -> LoudiusLoadingIndicator() else -> PullRequestsList( pullRequests = pullRequests, modifier = Modifier.padding(padding), @@ -85,13 +86,6 @@ private fun PullRequestsScreenStateless( }) } -@Composable -private fun LoadingIndicator(modifier: Modifier) { - Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) { - CircularProgressIndicator() - } -} - @Composable private fun PullRequestsList( pullRequests: List, From 1ec2ea7c85e3113b8fe1cdb6280080948287d2cd Mon Sep 17 00:00:00 2001 From: kezc Date: Mon, 20 Mar 2023 08:09:17 +0000 Subject: [PATCH 217/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 9399faa6a..76467bb02 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -5,7 +5,6 @@ package com.appunite.loudius.ui.pullrequests import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -14,7 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -22,7 +20,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource From 6036b76cb679d99c210706e42c538a81401e6fb3 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 20 Mar 2023 13:28:33 +0000 Subject: [PATCH 218/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 2 +- .../appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 8 +++----- .../appunite/loudius/fakes/FakePullRequestRepository.kt | 2 +- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 9 ++++----- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 8bd5b3689..eb94bd6d5 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -105,7 +105,7 @@ private fun ReviewersScreenStateless( else -> ReviewersScreenContent( reviewers = reviewers, modifier = Modifier.padding(padding), - onNotifyClick = onAction + onNotifyClick = onAction, ) } }, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 03b7b8d30..21de851f1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -13,12 +13,12 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -78,13 +78,11 @@ class ReviewersViewModel @Inject constructor( } } - private suspend fun fetchRequestedReviewers(initialValues: InitialValues): Result> { val (owner, repo, pullRequestNumber, submissionTime) = initialValues return repository.getRequestedReviewers(owner, repo, pullRequestNumber) .map { it.mapToReviewers(submissionTime) } - } private fun RequestedReviewersResponse.mapToReviewers(submissionTime: String): List { diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 9997aa32f..5a8ea3972 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -53,7 +53,7 @@ class FakePullRequestRepository : PullRequestRepository { ), ) "failureOnlyOnRequestedReviewers", "notExistingPullRequestNumber" -> Result.failure( - WebException.UnknownError(404, null) + WebException.UnknownError(404, null), ) else -> Result.success(RequestedReviewersResponse(emptyList())) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 55dc84a42..812142e17 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -9,10 +9,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -25,6 +21,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -148,7 +148,6 @@ class ReviewersViewModelTest { assertEquals(expected, actual) } - @Test fun `WHEN there is an error during fetching data from 2 requests on init THEN error is shown`() = runTest { From 11d7a67aff7d3e8da9ca2d16dd5a52e311329f32 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 20 Mar 2023 14:28:31 +0100 Subject: [PATCH 219/526] Remove default parameters from the LoudiusErrorScreen call places. --- .../com/appunite/loudius/ui/loading/LoadingScreen.kt | 9 ++------- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 2 -- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 054b663fc..cbc06f716 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -4,10 +4,8 @@ import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import com.appunite.loudius.R import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.theme.LoudiusTheme @@ -56,11 +54,8 @@ private fun ShowLoudiusErrorScreen( onTryAgainClick: () -> Unit, ) { LoudiusErrorScreen( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(id = R.string.try_again), - ) { - onTryAgainClick() - } + onButtonClick = { onTryAgainClick() } + ) } @Preview(showSystemUi = true) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 76467bb02..7bac14a23 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -69,8 +69,6 @@ private fun PullRequestsScreenStateless( }, content = { padding -> when { isError -> LoudiusErrorScreen( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) isLoading -> LoudiusLoadingIndicator() From 6e55d2161e52ee334f2d55c64081a2c3d8fac5fb Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 20 Mar 2023 13:37:12 +0000 Subject: [PATCH 220/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index cbc06f716..d42603a5e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -54,7 +54,7 @@ private fun ShowLoudiusErrorScreen( onTryAgainClick: () -> Unit, ) { LoudiusErrorScreen( - onButtonClick = { onTryAgainClick() } + onButtonClick = { onTryAgainClick() }, ) } From a4f97c70d10444c24e1c13e49004e55a721f0ba9 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 21 Mar 2023 09:15:41 +0100 Subject: [PATCH 221/526] change pull request icon --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 2 +- app/src/main/res/drawable/ic_pull_request.xml | 9 +++++++++ app/src/main/res/drawable/ic_share.xml | 5 ----- 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/ic_pull_request.xml delete mode 100644 app/src/main/res/drawable/ic_share.xml diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 76467bb02..478ab455d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -129,7 +129,7 @@ private fun PullRequestItem( @Composable private fun PullRequestIcon() { Image( - painter = painterResource(id = R.drawable.ic_share), + painter = painterResource(id = R.drawable.ic_pull_request), contentDescription = null, modifier = Modifier .padding(start = 18.dp, top = 10.dp) diff --git a/app/src/main/res/drawable/ic_pull_request.xml b/app/src/main/res/drawable/ic_pull_request.xml new file mode 100644 index 000000000..63b4d2072 --- /dev/null +++ b/app/src/main/res/drawable/ic_pull_request.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml deleted file mode 100644 index 43178a392..000000000 --- a/app/src/main/res/drawable/ic_share.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - From 7503722ba46b43008ee2209a3998a277a1548170 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 21 Mar 2023 09:59:28 +0100 Subject: [PATCH 222/526] Rebuild ReviewersViewModelTests with new implementation of fake repository. --- .../ui/reviewers/ReviewersViewModel.kt | 8 +- .../fakes/FakePullRequestRepository.kt | 85 ++++++++----------- .../ui/reviewers/ReviewersViewModelTest.kt | 85 +++++++++++++------ .../com/appunite/loudius/util/Defaults.kt | 24 ++++++ 4 files changed, 124 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 21de851f1..0bcc2ebe8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -13,12 +13,12 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -66,7 +66,7 @@ class ReviewersViewModel @Inject constructor( private suspend fun getMergedData(initialValues: InitialValues): Result?> = coroutineScope { - state = state.copy(isLoading = true) + state = state.copy(isLoading = true, isError = false) val requestedReviewersDeferred = async { fetchRequestedReviewers(initialValues) } val reviewersDeferred = async { fetchReviews(initialValues) } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 5a8ea3972..680666e26 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -2,62 +2,17 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.network.model.PullRequestsResponse -import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.ReviewState.APPROVED -import com.appunite.loudius.network.model.ReviewState.CHANGES_REQUESTED -import com.appunite.loudius.network.model.ReviewState.COMMENTED -import com.appunite.loudius.network.model.User import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults -import java.time.LocalDateTime class FakePullRequestRepository : PullRequestRepository { - private val date1 = LocalDateTime.parse("2022-01-29T10:00:00") - private val date2 = LocalDateTime.parse("2022-01-29T11:00:00") - private val date3 = LocalDateTime.parse("2022-01-29T12:00:00") - - override suspend fun getReviews( - owner: String, - repo: String, - pullRequestNumber: String, - ): Result> = when (pullRequestNumber) { - "failureOnlyOnRequestedReviewers", "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( - listOf( - Review("1", User(1, "user1"), CHANGES_REQUESTED, date1), - Review("2", User(1, "user1"), COMMENTED, date2), - Review("3", User(1, "user1"), APPROVED, date3), - Review("4", User(2, "user2"), COMMENTED, date1), - Review("5", User(2, "user2"), COMMENTED, date2), - Review("6", User(2, "user2"), APPROVED, date3), - ), - ) - "failureOnlyOnReviews", "notExistingPullRequestNumber" -> { - Result.failure(WebException.UnknownError(404, null)) - } - else -> Result.success(emptyList()) - } - - override suspend fun getRequestedReviewers( - owner: String, - repo: String, - pullRequestNumber: String, - ): Result = when (pullRequestNumber) { - "correctPullRequestNumber", "onlyRequestedReviewersPullNumber", "failureOnlyOnReviews" -> Result.success( - RequestedReviewersResponse( - listOf( - RequestedReviewer(3, "user3"), - RequestedReviewer(4, "user4"), - ), - ), - ) - "failureOnlyOnRequestedReviewers", "notExistingPullRequestNumber" -> Result.failure( - WebException.UnknownError(404, null), - ) - else -> Result.success(RequestedReviewersResponse(emptyList())) - } + private val initialReviewsAnswer = Result.success(Defaults.reviews()) + private val initialRequestedReviewersAnswer = Result.success( + RequestedReviewersResponse(Defaults.requestedReviewers()) + ) private val initialPullRequestAnswer = Result.success( PullRequestsResponse( incompleteResults = false, @@ -68,9 +23,41 @@ class FakePullRequestRepository : PullRequestRepository { ), ) + private var lazyReviewsAnswer: suspend () -> Result> = { initialReviewsAnswer } + private var lazyRequestedReviewersAnswer: suspend () -> Result = + { initialRequestedReviewersAnswer } private var lazyCurrentUserPullRequests: suspend () -> Result = { initialPullRequestAnswer } + override suspend fun getReviews( + owner: String, + repo: String, + pullRequestNumber: String, + ): Result> = lazyReviewsAnswer() + + + fun setReviewsAnswer(result: suspend () -> Result>) { + lazyReviewsAnswer = result + } + + fun resetReviewsAnswer() { + lazyReviewsAnswer = { initialReviewsAnswer } + } + + override suspend fun getRequestedReviewers( + owner: String, + repo: String, + pullRequestNumber: String, + ): Result = lazyRequestedReviewersAnswer() + + fun setRequestedReviewersAnswer(result: suspend () -> Result) { + lazyRequestedReviewersAnswer = result + } + + fun resetRequestedReviewersAnswer() { + lazyRequestedReviewersAnswer = { initialRequestedReviewersAnswer } + } + fun setCurrentUserPullRequests(result: suspend () -> Result) { lazyCurrentUserPullRequests = result } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 812142e17..e5db4a064 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -1,30 +1,28 @@ package com.appunite.loudius.ui.reviewers import androidx.lifecycle.SavedStateHandle -import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.fakes.FakePullRequestRepository +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.MainDispatcherExtension +import com.appunite.loudius.utils.neverCompletingSuspension import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import kotlinx.coroutines.Dispatchers +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import kotlinx.coroutines.yield import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -33,7 +31,8 @@ class ReviewersViewModelTest { private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") private val systemClockFixed = Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) - private val repository: PullRequestRepository = FakePullRequestRepository() + + private val repository: FakePullRequestRepository = FakePullRequestRepository() private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) { every { get(any()) } returns "example" every { get("submission_date") } returns "2022-01-29T08:00:00" @@ -79,10 +78,8 @@ class ReviewersViewModelTest { @Test fun `GIVEN correct initial values WHEN init starts THEN state is loading`() = runTest { - Dispatchers.setMain(StandardTestDispatcher(testScheduler)) - + repository.setReviewsAnswer { neverCompletingSuspension() } viewModel = createViewModel() - yield() assertEquals(true, viewModel.state.isLoading) assertEquals(emptyList(), viewModel.state.reviewers) @@ -90,11 +87,13 @@ class ReviewersViewModelTest { @Test fun `GIVEN no reviewers WHEN init THEN state is correct with no reviewers`() { - every { savedStateHandle.get("pull_request_number") } returns "pullRequestWithNoReviewers" + repository.setReviewsAnswer { Result.success(emptyList()) } + repository.setRequestedReviewersAnswer { + Result.success(RequestedReviewersResponse(emptyList())) + } viewModel = createViewModel() - assertEquals("pullRequestWithNoReviewers", viewModel.state.pullRequestNumber) assertEquals(emptyList(), viewModel.state.reviewers) assertEquals(false, viewModel.state.isLoading) } @@ -119,8 +118,7 @@ class ReviewersViewModelTest { @Test fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = runTest { - every { savedStateHandle.get("pull_request_number") } returns "onlyRequestedReviewersPullNumber" - + repository.setReviewsAnswer { Result.success(emptyList()) } viewModel = createViewModel() val expected = listOf( @@ -135,8 +133,9 @@ class ReviewersViewModelTest { @Test fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = runTest { - every { savedStateHandle.get("pull_request_number") } returns "onlyReviewsPullNumber" - + repository.setRequestedReviewersAnswer { + Result.success(RequestedReviewersResponse(emptyList())) + } viewModel = createViewModel() val expected = listOf( @@ -151,8 +150,8 @@ class ReviewersViewModelTest { @Test fun `WHEN there is an error during fetching data from 2 requests on init THEN error is shown`() = runTest { - every { savedStateHandle.get("pull_request_number") } returns "notExistingPullRequestNumber" - + repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } + repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } viewModel = createViewModel() assertEquals(true, viewModel.state.isError) @@ -163,8 +162,7 @@ class ReviewersViewModelTest { @Test fun `WHEN there is an error during fetching data on init only from requested reviewers request THEN error is shown`() = runTest { - every { savedStateHandle.get("pull_request_number") } returns "failureOnlyOnRequestedReviewers" - + repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } viewModel = createViewModel() assertEquals(true, viewModel.state.isError) @@ -175,8 +173,7 @@ class ReviewersViewModelTest { @Test fun `WHEN there is an error during fetching data on init only from reviews request THEN error is shown`() = runTest { - every { savedStateHandle.get("pull_request_number") } returns "failureOnlyOnReviews" - + repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } viewModel = createViewModel() assertEquals(true, viewModel.state.isError) @@ -202,9 +199,47 @@ class ReviewersViewModelTest { runTest { viewModel = createViewModel() + viewModel.onAction(ReviewersAction.Notify("ExampleUser")) viewModel.onAction(ReviewersAction.OnSnackbarDismiss) assertEquals(false, viewModel.state.isSuccessSnackbarShown) } + + @Test + fun `GIVEN error state WHEN on try again action with success result THEN state has reviewers`() = + runTest { + repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } + repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } + viewModel = createViewModel() + + repository.resetReviewsAnswer() + repository.resetRequestedReviewersAnswer() + viewModel.onAction(ReviewersAction.OnTryAgain) + + val expected = listOf( + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), + ) + val actual = viewModel.state.reviewers + + assertEquals(expected, actual) + assertEquals(false, viewModel.state.isError) + assertEquals(false, viewModel.state.isLoading) + } + + @Test + fun `GIVEN error state WHEN on try again action with failure result THEN error is shown`() = + runTest { + repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } + repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } + viewModel = createViewModel() + + viewModel.onAction(ReviewersAction.OnTryAgain) + + assertEquals(true, viewModel.state.isError) + assertEquals(false, viewModel.state.isLoading) + } } } diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index 4e5a77cb8..4bea0caf0 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -1,9 +1,17 @@ package com.appunite.loudius.util import com.appunite.loudius.network.model.PullRequest +import com.appunite.loudius.network.model.RequestedReviewer +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.ReviewState +import com.appunite.loudius.network.model.User import java.time.LocalDateTime object Defaults { + private val date1 = LocalDateTime.parse("2022-01-29T10:00:00") + private val date2 = LocalDateTime.parse("2022-01-29T11:00:00") + private val date3 = LocalDateTime.parse("2022-01-29T12:00:00") + fun pullRequest(id: Int = 1) = PullRequest( id = id, draft = false, @@ -12,4 +20,20 @@ object Defaults { title = "example title", LocalDateTime.parse("2023-03-07T08:21:45").plusHours(id.toLong()), ) + + fun reviews() = listOf( + Review("1", User(1, "user1"), ReviewState.CHANGES_REQUESTED, date1), + Review("2", User(1, "user1"), ReviewState.COMMENTED, date2), + Review("3", User(1, "user1"), ReviewState.APPROVED, date3), + Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), + Review("5", User(2, "user2"), ReviewState.COMMENTED, date2), + Review("6", User(2, "user2"), ReviewState.APPROVED, date3), + ) + + fun requestedReviewers() = listOf( + RequestedReviewer(3, "user3"), + RequestedReviewer(4, "user4"), + ) + + } From 2be9bfdbc5e5183d36d9e7053cdd5b07815985cc Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Tue, 21 Mar 2023 09:05:38 +0000 Subject: [PATCH 223/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 6 +++--- .../appunite/loudius/fakes/FakePullRequestRepository.kt | 3 +-- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- app/src/test/java/com/appunite/loudius/util/Defaults.kt | 2 -- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 0bcc2ebe8..3fdbc1325 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -13,12 +13,12 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 680666e26..77c9e774d 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -11,7 +11,7 @@ class FakePullRequestRepository : PullRequestRepository { private val initialReviewsAnswer = Result.success(Defaults.reviews()) private val initialRequestedReviewersAnswer = Result.success( - RequestedReviewersResponse(Defaults.requestedReviewers()) + RequestedReviewersResponse(Defaults.requestedReviewers()), ) private val initialPullRequestAnswer = Result.success( PullRequestsResponse( @@ -35,7 +35,6 @@ class FakePullRequestRepository : PullRequestRepository { pullRequestNumber: String, ): Result> = lazyReviewsAnswer() - fun setReviewsAnswer(result: suspend () -> Result>) { lazyReviewsAnswer = result } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index e5db4a064..f0998b317 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -11,10 +11,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -23,6 +19,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index 4bea0caf0..b24d1ffdd 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -34,6 +34,4 @@ object Defaults { RequestedReviewer(3, "user3"), RequestedReviewer(4, "user4"), ) - - } From b9986352872e138e52ce2dc880506ae48b231546 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 21 Mar 2023 10:08:54 +0100 Subject: [PATCH 224/526] Perform minor changes. --- .../com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index f0998b317..1e7a076eb 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -32,7 +32,7 @@ class ReviewersViewModelTest { private val systemClockFixed = Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) - private val repository: FakePullRequestRepository = FakePullRequestRepository() + private val repository = FakePullRequestRepository() private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) { every { get(any()) } returns "example" every { get("submission_date") } returns "2022-01-29T08:00:00" From f836144163e9a3a56084ab5183f9c9ac237948a1 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 20 Mar 2023 15:16:22 +0100 Subject: [PATCH 225/526] Add showing snackbar on notifying failure. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 40 ++++++++++++------- .../ui/reviewers/ReviewersViewModel.kt | 21 ++++++---- app/src/main/res/values/strings.xml | 3 +- .../ui/reviewers/ReviewersViewModelTest.kt | 4 +- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index eb94bd6d5..ce335194a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -42,7 +42,6 @@ fun ReviewersScreen( ) { val state = viewModel.state val snackbarHostState = remember { SnackbarHostState() } - val snackbarMessage = stringResource(id = R.string.reviewers_snackbar_message) ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, @@ -53,31 +52,42 @@ fun ReviewersScreen( snackbarHostState = snackbarHostState, onAction = viewModel::onAction, ) - SnackbarLaunchedEffect( - isSuccessSnackbarShown = state.isSuccessSnackbarShown, - snackbarHostState = snackbarHostState, - snackbarMessage = snackbarMessage, - onSnackbarDismiss = viewModel::onAction, - ) + if (state.snackbarTypeShown != null) { + SnackbarLaunchedEffect( + snackbarTypeShown = state.snackbarTypeShown, + snackbarHostState = snackbarHostState, + onSnackbarDismiss = viewModel::onAction, + ) + } } @Composable private fun SnackbarLaunchedEffect( - isSuccessSnackbarShown: Boolean, + snackbarTypeShown: ReviewersSnackbarType, snackbarHostState: SnackbarHostState, - snackbarMessage: String, onSnackbarDismiss: (ReviewersAction) -> Unit, ) { - LaunchedEffect(isSuccessSnackbarShown) { - if (isSuccessSnackbarShown) { - val result = snackbarHostState.showSnackbar(message = snackbarMessage) - if (result == SnackbarResult.Dismissed) { - onSnackbarDismiss(ReviewersAction.OnSnackbarDismiss) - } + val snackbarMessage = resolveSnackbarMessage(snackbarTypeShown) + + LaunchedEffect(snackbarTypeShown) { + val result = when (snackbarTypeShown) { + ReviewersSnackbarType.FAILURE -> snackbarHostState.showSnackbar(message = snackbarMessage) + ReviewersSnackbarType.SUCCESS -> snackbarHostState.showSnackbar(message = snackbarMessage) + } + if (result == SnackbarResult.Dismissed) { + onSnackbarDismiss(ReviewersAction.OnSnackbarDismiss) } } } +@Composable +private fun resolveSnackbarMessage(snackbarTypeShown: ReviewersSnackbarType) = + if (snackbarTypeShown == ReviewersSnackbarType.SUCCESS) { + stringResource(id = R.string.reviewers_snackbar_success) + } else { + stringResource(id = R.string.reviewers_snackbar_failure) + } + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReviewersScreenStateless( diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 3fdbc1325..272a57e4e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -12,13 +12,15 @@ import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review +import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE +import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -29,11 +31,15 @@ sealed class ReviewersAction { data class ReviewersState( val reviewers: List = emptyList(), val pullRequestNumber: String = "", - val isSuccessSnackbarShown: Boolean = false, + val snackbarTypeShown: ReviewersSnackbarType? = null, val isLoading: Boolean = false, val isError: Boolean = false, ) +enum class ReviewersSnackbarType { + SUCCESS, FAILURE +} + @HiltViewModel class ReviewersViewModel @Inject constructor( private val repository: PullRequestRepository, @@ -132,14 +138,13 @@ class ReviewersViewModel @Inject constructor( viewModelScope.launch { repository.notify(owner, repo, pullRequestNumber, "@$userLogin") - .onSuccess { - state = state.copy(isSuccessSnackbarShown = true) - } + .onSuccess { state = state.copy(snackbarTypeShown = SUCCESS) } + .onFailure { state = state.copy(snackbarTypeShown = FAILURE) } } } private fun dismissSnackbar() { - state = state.copy(isSuccessSnackbarShown = false) + state = state.copy(snackbarTypeShown = null) } private data class InitialValues( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ff8dd20c..743415dcf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,5 +14,6 @@ OK error image Try again - Awesome! Your collaborator have been pinged for some serious code review action! 🎉 + Awesome! Your collaborator have been pinged for some serious code review action! 🎉 + Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 1e7a076eb..502a8964e 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -191,7 +191,7 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("ExampleUser")) - assertEquals(true, viewModel.state.isSuccessSnackbarShown) + assertEquals(true, viewModel.state.snackbarTypeShown) } @Test @@ -202,7 +202,7 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("ExampleUser")) viewModel.onAction(ReviewersAction.OnSnackbarDismiss) - assertEquals(false, viewModel.state.isSuccessSnackbarShown) + assertEquals(false, viewModel.state.snackbarTypeShown) } @Test From 4c404d0a707211a2e8eb97e282f4e49254f43b54 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 20 Mar 2023 15:23:18 +0100 Subject: [PATCH 226/526] Add additional test for showing correct type of the snackbar. --- .../loudius/fakes/FakePullRequestRepository.kt | 1 + .../ui/reviewers/ReviewersViewModelTest.kt | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 77c9e774d..82e084053 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -76,6 +76,7 @@ class FakePullRequestRepository : PullRequestRepository { message: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) + "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) else -> Result.failure(WebException.NetworkError()) } } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 502a8964e..2a0f73120 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -186,12 +186,22 @@ class ReviewersViewModelTest { inner class OnActionTest { @Test - fun `GIVEN user login WHEN Notify action THEN show snackbar`() = runTest { + fun `WHEN successful notify action THEN show success snackbar`() = runTest { viewModel = createViewModel() viewModel.onAction(ReviewersAction.Notify("ExampleUser")) - assertEquals(true, viewModel.state.snackbarTypeShown) + assertEquals(ReviewersSnackbarType.SUCCESS, viewModel.state.snackbarTypeShown) + } + + @Test + fun `WHEN failed notify action THEN show failure snackbar`() = runTest { + every { savedStateHandle.get("pull_request_number") } returns "nonExistingPullRequestNumber" + viewModel = createViewModel() + + viewModel.onAction(ReviewersAction.Notify("ExampleUser")) + + assertEquals(ReviewersSnackbarType.FAILURE, viewModel.state.snackbarTypeShown) } @Test @@ -202,7 +212,7 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("ExampleUser")) viewModel.onAction(ReviewersAction.OnSnackbarDismiss) - assertEquals(false, viewModel.state.snackbarTypeShown) + assertEquals(null, viewModel.state.snackbarTypeShown) } @Test From 59f2d8ebd9c70519f9c238d9748e6c737da5c8bc Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Tue, 21 Mar 2023 10:28:19 +0000 Subject: [PATCH 227/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 272a57e4e..4d7da2fc1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -15,12 +15,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() From 16a642e1454600cdee1c7ed1302925f9717aa18c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 21 Mar 2023 12:10:32 +0100 Subject: [PATCH 228/526] Simplify showing snackbar at ReviewersScreen.kt. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index ce335194a..5c3b065a1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -32,6 +32,7 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusTopAppBar +import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.* import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.ui.utils.bottomBorder @@ -70,10 +71,7 @@ private fun SnackbarLaunchedEffect( val snackbarMessage = resolveSnackbarMessage(snackbarTypeShown) LaunchedEffect(snackbarTypeShown) { - val result = when (snackbarTypeShown) { - ReviewersSnackbarType.FAILURE -> snackbarHostState.showSnackbar(message = snackbarMessage) - ReviewersSnackbarType.SUCCESS -> snackbarHostState.showSnackbar(message = snackbarMessage) - } + val result = snackbarHostState.showSnackbar(message = snackbarMessage) if (result == SnackbarResult.Dismissed) { onSnackbarDismiss(ReviewersAction.OnSnackbarDismiss) } @@ -82,10 +80,9 @@ private fun SnackbarLaunchedEffect( @Composable private fun resolveSnackbarMessage(snackbarTypeShown: ReviewersSnackbarType) = - if (snackbarTypeShown == ReviewersSnackbarType.SUCCESS) { - stringResource(id = R.string.reviewers_snackbar_success) - } else { - stringResource(id = R.string.reviewers_snackbar_failure) + when (snackbarTypeShown) { + SUCCESS -> stringResource(id = R.string.reviewers_snackbar_success) + FAILURE -> stringResource(id = R.string.reviewers_snackbar_failure) } @OptIn(ExperimentalMaterial3Api::class) From e1380a2c3971cb8aa440adfcb7a8d0d709cddaf2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 22 Mar 2023 09:14:09 +0100 Subject: [PATCH 229/526] Remove initialValues variable from functions parameters. --- .../ui/reviewers/ReviewersViewModel.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 3fdbc1325..e8dc38c31 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -13,12 +13,12 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -46,7 +46,7 @@ class ReviewersViewModel @Inject constructor( init { state = state.copy(pullRequestNumber = initialValues.pullRequestNumber) - fetchData(initialValues) + fetchData() } private fun getInitialValues(savedStateHandle: SavedStateHandle) = InitialValues( @@ -56,19 +56,19 @@ class ReviewersViewModel @Inject constructor( checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]), ) - private fun fetchData(initialValues: InitialValues) { + private fun fetchData() { viewModelScope.launch { - getMergedData(initialValues) + getMergedData() .onSuccess { state = state.copy(reviewers = it.orEmpty(), isLoading = false) } .onFailure { state = state.copy(isError = true, isLoading = false) } } } - private suspend fun getMergedData(initialValues: InitialValues): Result?> = + private suspend fun getMergedData(): Result?> = coroutineScope { state = state.copy(isLoading = true, isError = false) - val requestedReviewersDeferred = async { fetchRequestedReviewers(initialValues) } - val reviewersDeferred = async { fetchReviews(initialValues) } + val requestedReviewersDeferred = async { fetchRequestedReviewers() } + val reviewersDeferred = async { fetchReviews() } val requestedReviewerResult = requestedReviewersDeferred.await() val reviewersResult = reviewersDeferred.await() @@ -78,7 +78,7 @@ class ReviewersViewModel @Inject constructor( } } - private suspend fun fetchRequestedReviewers(initialValues: InitialValues): Result> { + private suspend fun fetchRequestedReviewers(): Result> { val (owner, repo, pullRequestNumber, submissionTime) = initialValues return repository.getRequestedReviewers(owner, repo, pullRequestNumber) @@ -93,7 +93,7 @@ class ReviewersViewModel @Inject constructor( } } - private suspend fun fetchReviews(initialValues: InitialValues): Result> { + private suspend fun fetchReviews(): Result> { val (owner, repo, pullRequestNumber, submissionTime) = initialValues return repository.getReviews(owner, repo, pullRequestNumber) @@ -124,7 +124,7 @@ class ReviewersViewModel @Inject constructor( fun onAction(action: ReviewersAction) = when (action) { is ReviewersAction.Notify -> notifyUser(action.userLogin) is ReviewersAction.OnSnackbarDismiss -> dismissSnackbar() - is ReviewersAction.OnTryAgain -> fetchData(initialValues) + is ReviewersAction.OnTryAgain -> fetchData() } private fun notifyUser(userLogin: String) { From ed40195232f6a994a015807b89690ae3883c46fe Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 22 Mar 2023 09:14:32 +0100 Subject: [PATCH 230/526] Change withContext use to coroutineScope. --- .../appunite/loudius/domain/PullRequestRepositoryImpl.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt index da3812286..fda1c352c 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt @@ -7,10 +7,9 @@ import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.User -import kotlinx.coroutines.async -import kotlinx.coroutines.withContext import javax.inject.Inject -import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope interface PullRequestRepository { suspend fun getReviews( @@ -48,7 +47,7 @@ class PullRequestRepositoryImpl @Inject constructor( owner: String, repo: String, pullRequestNumber: String, - ): Result> = withContext(coroutineContext) { + ): Result> = coroutineScope { val currentUserDeferred = async { userDataSource.getUser() } val reviewsDeferred = async { pullRequestsNetworkDataSource.getReviews(owner, repo, pullRequestNumber) @@ -56,7 +55,7 @@ class PullRequestRepositoryImpl @Inject constructor( val currentUser = currentUserDeferred.await() val reviews = reviewsDeferred.await() - return@withContext currentUser.flatMap { user -> + currentUser.flatMap { user -> reviews.map { it.excludeUserReviews(user) } } } From a9e3412440f3c4206d6a525751a1b3beddd92cdf Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 22 Mar 2023 09:19:14 +0100 Subject: [PATCH 231/526] Remove wildcard import from ReviewersScreen.kt . --- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 5c3b065a1..0e4737077 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -32,7 +32,8 @@ import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusTopAppBar -import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.* +import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE +import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.ui.utils.bottomBorder From dcc7e87cb34f4802adba03cac18dbe86f586efbf Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Wed, 22 Mar 2023 09:09:49 +0000 Subject: [PATCH 232/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 62e4e0aeb..52ec4d02c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -15,12 +15,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() From c04f24ccfaeb0eea0f732c41bd01bde90ff74261 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 21 Mar 2023 11:26:06 +0100 Subject: [PATCH 233/526] Add log in error screen --- .../java/com/appunite/loudius/MainActivity.kt | 12 +++- .../appunite/loudius/domain/AuthRepository.kt | 6 +- .../loudius/domain/AuthRepositoryImpl.kt | 11 ++-- .../loudius/domain/UserLocalDataSource.kt | 5 +- .../network/datasource/AuthDataSource.kt | 18 +++++- .../network/model/AccessTokenResponse.kt | 4 +- .../ui/components/LoudiusErrorScreen.kt | 3 + .../loudius/ui/loading/LoadingScreen.kt | 58 +++++++++++++----- .../loudius/ui/loading/LoadingViewModel.kt | 56 ++++++++++------- app/src/main/res/values/strings.xml | 2 + .../loudius/domain/AuthRepositoryImplTest.kt | 8 +-- .../loudius/fakes/FakeAuthRepository.kt | 11 +++- .../datasource/AuthNetworkDataSourceTest.kt | 61 +++++++++++++++++++ .../ui/loading/LoadingViewModelTest.kt | 55 +++++++++++------ 14 files changed, 231 insertions(+), 79 deletions(-) create mode 100644 app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 21452da38..5e0bfa6e2 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -47,9 +47,15 @@ class MainActivity : ComponentActivity() { }, ), ) { - LoadingScreen(intent = intent) { - navController.navigate(Screen.PullRequests.route) - } + LoadingScreen( + intent = intent, + onNavigateToPullRequest = { + navController.navigate(Screen.PullRequests.route) + }, + onNavigateToLogin = { + navController.navigate(Screen.Login.route) + }, + ) } composable(route = Screen.PullRequests.route) { PullRequestsScreen { owner, repo, pullRequestNumber, submissionTime -> diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt index 526cc4090..68041d9c8 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.domain -import com.appunite.loudius.network.model.AccessTokenResponse +import com.appunite.loudius.network.model.AccessToken interface AuthRepository { @@ -8,7 +8,7 @@ interface AuthRepository { clientId: String, clientSecret: String, code: String, - ): Result + ): Result - fun getAccessToken(): String + fun getAccessToken(): AccessToken } diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt index ea626aeac..abbe94b9e 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.datasource.AuthDataSource +import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.model.AccessTokenResponse import javax.inject.Inject import javax.inject.Singleton @@ -15,15 +16,13 @@ class AuthRepositoryImpl @Inject constructor( clientId: String, clientSecret: String, code: String, - ): Result { + ): Result { val result = authDataSource.getAccessToken(clientId, clientSecret, code) - result.onSuccess { response -> - response.accessToken?.let { - userLocalDataSource.saveAccessToken(it) - } + result.onSuccess { + userLocalDataSource.saveAccessToken(it) } return result } - override fun getAccessToken(): String = userLocalDataSource.getAccessToken() + override fun getAccessToken(): AccessToken = userLocalDataSource.getAccessToken() } diff --git a/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt index 6254e8de5..c43a2edea 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt @@ -2,6 +2,7 @@ package com.appunite.loudius.domain import android.content.Context import android.content.SharedPreferences +import com.appunite.loudius.network.model.AccessToken import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -17,9 +18,9 @@ class UserLocalDataSource @Inject constructor(@ApplicationContext context: Conte private val sharedPreferences: SharedPreferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) - fun saveAccessToken(accessToken: String) { + fun saveAccessToken(accessToken: AccessToken) { sharedPreferences.edit().putString(KEY_ACCESS_TOKEN, accessToken).apply() } - fun getAccessToken(): String = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) ?: "" + fun getAccessToken(): AccessToken = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) ?: "" } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index d382a01eb..211e6689c 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -1,5 +1,7 @@ package com.appunite.loudius.network.datasource +import com.appunite.loudius.common.flatMap +import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.model.AccessTokenResponse import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.utils.safeApiCall @@ -12,7 +14,7 @@ interface AuthDataSource { clientId: String, clientSecret: String, code: String, - ): Result + ): Result } @Singleton @@ -24,6 +26,18 @@ class AuthNetworkDataSource @Inject constructor( clientId: String, clientSecret: String, code: String, - ): Result = + ): Result = safeApiCall { authService.getAccessToken(clientId, clientSecret, code) } + .flatMap { response -> + response.accessToken?.let { token -> Result.success(token) } + ?: Result.failure(response.mapErrorToException()) + } + + private fun AccessTokenResponse.mapErrorToException() = when (error) { + "bad_verification_code" -> BadVerificationCodeException + else -> UnknownGithubException + } } + +object BadVerificationCodeException : Exception() +object UnknownGithubException : Exception() diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt index 86f35aba1..aededaefd 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt @@ -1,6 +1,8 @@ package com.appunite.loudius.network.model +typealias AccessToken = String + data class AccessTokenResponse( - val accessToken: String?, + val accessToken: AccessToken?, val error: String? = null, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index ae51f66ab..aa69752a3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R @@ -53,6 +54,8 @@ private fun ErrorText(text: String) { text = text, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 16.dp) ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index d42603a5e..5a54678d1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -4,8 +4,10 @@ import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel +import com.appunite.loudius.R import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.theme.LoudiusTheme @@ -15,6 +17,7 @@ fun LoadingScreen( intent: Intent, viewModel: LoadingViewModel = hiltViewModel(), onNavigateToPullRequest: () -> Unit, + onNavigateToLogin: () -> Unit, ) { val state = viewModel.state val code = intent.data?.getQueryParameter("code") @@ -24,28 +27,45 @@ fun LoadingScreen( viewModel.setCodeAndGetAccessToken(it) } } - LaunchedEffect(key1 = state.navigateToPullRequests) { - state.navigateToPullRequests?.let { - onNavigateToPullRequest() - viewModel.onAction(LoadingAction.OnNavigateToPullRequests) + LaunchedEffect(key1 = state.navigateTo) { + when (state.navigateTo) { + LoadingScreenNavigation.NavigateToLogin -> { + onNavigateToLogin() + viewModel.onAction(LoadingAction.OnNavigate) + } + LoadingScreenNavigation.NavigateToPullRequests -> { + onNavigateToPullRequest() + viewModel.onAction(LoadingAction.OnNavigate) + } + null -> {} } } - LoadingScreenStateless(showErrorScreen = state.showErrorScreen) { + LoadingScreenStateless(errorScreenType = state.errorScreenType) { viewModel.onAction(LoadingAction.OnTryAgainClick) } } @Composable fun LoadingScreenStateless( - showErrorScreen: Boolean, + errorScreenType: LoadingErrorType?, onTryAgainClick: () -> Unit, ) { - if (showErrorScreen) { - ShowLoudiusErrorScreen { - onTryAgainClick() - } - } else { - LoudiusLoadingIndicator() + when (errorScreenType) { + LoadingErrorType.GENERIC_ERROR -> ShowLoudiusErrorScreen(onTryAgainClick) + LoadingErrorType.LOGIN_ERROR -> ShowLoudiusLoginErrorScreen(onTryAgainClick) + else -> LoudiusLoadingIndicator() + } +} + +@Composable +private fun ShowLoudiusLoginErrorScreen( + onTryAgainClick: () -> Unit, +) { + LoudiusErrorScreen( + errorText = stringResource(id = R.string.error_login_text), + buttonText = stringResource(id = R.string.go_to_login), + ) { + onTryAgainClick() } } @@ -60,9 +80,17 @@ private fun ShowLoudiusErrorScreen( @Preview(showSystemUi = true) @Composable -fun ShowLoudiusErrorScreenPreview() { +fun ShowLoudiusGenericErrorScreenPreview() { + LoudiusTheme { + LoadingScreenStateless(errorScreenType = LoadingErrorType.GENERIC_ERROR) {} + } +} + +@Preview(showSystemUi = true) +@Composable +fun ShowLoudiusLoginErrorScreenPreview() { LoudiusTheme { - LoadingScreenStateless(showErrorScreen = true) {} + LoadingScreenStateless(errorScreenType = LoadingErrorType.LOGIN_ERROR) {} } } @@ -70,6 +98,6 @@ fun ShowLoudiusErrorScreenPreview() { @Composable fun ShowLoadingIndicatorScreenPreview() { LoudiusTheme { - LoadingScreenStateless(showErrorScreen = false) {} + LoadingScreenStateless(errorScreenType = null) {} } } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index 6089808f3..e0a837ee3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -8,26 +8,34 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.AuthRepository -import com.appunite.loudius.network.model.AccessTokenResponse +import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class LoadingAction { - object OnNavigateToPullRequests : LoadingAction() + object OnNavigate : LoadingAction() object OnTryAgainClick : LoadingAction() } +enum class LoadingErrorType { + LOGIN_ERROR, + GENERIC_ERROR, +} + data class LoadingState( val accessToken: String? = null, val code: String? = null, - val navigateToPullRequests: NavigateToPullRequests? = null, - val showErrorScreen: Boolean = false, + val navigateTo: LoadingScreenNavigation? = null, + val errorScreenType: LoadingErrorType? = null, ) -object NavigateToPullRequests +sealed class LoadingScreenNavigation { + object NavigateToPullRequests : LoadingScreenNavigation() + object NavigateToLogin : LoadingScreenNavigation() +} @HiltViewModel class LoadingViewModel @Inject constructor( @@ -44,18 +52,22 @@ class LoadingViewModel @Inject constructor( fun onAction(action: LoadingAction) = when (action) { is LoadingAction.OnTryAgainClick -> onTryAgain() - is LoadingAction.OnNavigateToPullRequests -> onNavigateToPullRequests() + is LoadingAction.OnNavigate -> onNavigateToPullRequests() } private fun onTryAgain() { - state = state.copy(showErrorScreen = false) - state.code?.let { - getAccessToken(it) + if (state.errorScreenType == LoadingErrorType.LOGIN_ERROR) { + state = state.copy(navigateTo = LoadingScreenNavigation.NavigateToLogin) + } else { + state = state.copy(errorScreenType = null) + state.code?.let { + getAccessToken(it) + } } } private fun onNavigateToPullRequests() { - state = state.copy(navigateToPullRequests = null) + state = state.copy(navigateTo = null) } private fun getAccessToken(code: String) { @@ -65,20 +77,18 @@ class LoadingViewModel @Inject constructor( clientSecret = BuildConfig.CLIENT_SECRET, code = code, ).onSuccess { token -> - state = handleGetAccessTokenSuccess(token) + state = state.copy( + accessToken = token, + navigateTo = LoadingScreenNavigation.NavigateToPullRequests + ) }.onFailure { - state = state.copy(showErrorScreen = true) + state = state.copy( + errorScreenType = when (it) { + is BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR + else -> LoadingErrorType.GENERIC_ERROR + } + ) } } } - - private fun handleGetAccessTokenSuccess(token: AccessTokenResponse) = - if (token.accessToken != null) { - state.copy( - accessToken = token.accessToken, - navigateToPullRequests = NavigateToPullRequests, - ) - } else { - state.copy(showErrorScreen = true) - } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ff8dd20c..c80ce27e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,5 +14,7 @@ OK error image Try again + Something went wrong…\nYou need to log in again. + Take me to login Awesome! Your collaborator have been pinged for some serious code review action! 🎉 diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index b77e9f65e..e3a9bd2b7 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -16,7 +16,7 @@ class AuthRepositoryImplTest { private val networkDataSource: AuthDataSource = mockk { coEvery { getAccessToken(any(), any(), any()) - } returns Result.success(AccessTokenResponse("validAccessToken")) + } returns Result.success("validAccessToken") } private val localDataSource: UserLocalDataSource = mockk { every { getAccessToken() } returns "validAccessToken" @@ -27,11 +27,11 @@ class AuthRepositoryImplTest { @Test fun `GIVEN fetch access token function WHEN processing THEN return success with new valid token`() = runTest { - val result = repository.fetchAccessToken("clientId", "clientSecret", "code") + val result = repository.fetchAccessToken("clientId", "clientSecret", "validCode") coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( - Result.success(AccessTokenResponse("validAccessToken")), + Result.success("validAccessToken"), result, ) { "Expected success result with valid access token" } } @@ -39,7 +39,7 @@ class AuthRepositoryImplTest { @Test fun `GIVEN fetch access token WHEN processing THEN new token should be saved`() = runTest { - repository.fetchAccessToken("clientId", "clientSecret", "code") + repository.fetchAccessToken("clientId", "clientSecret", "validCode") coVerify(exactly = 1) { localDataSource.saveAccessToken("validAccessToken") } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 0cc2de388..4d6ead152 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -1,17 +1,22 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.AuthRepository -import com.appunite.loudius.network.model.AccessTokenResponse +import com.appunite.loudius.network.datasource.BadVerificationCodeException +import com.appunite.loudius.network.datasource.UnknownGithubException +import com.appunite.loudius.network.model.AccessToken class FakeAuthRepository : AuthRepository { override suspend fun fetchAccessToken( clientId: String, clientSecret: String, code: String, - ): Result { - return Result.success(AccessTokenResponse("validToken")) + ): Result = when (code) { + "validCode" -> Result.success("validToken") + "invalidCode" -> Result.failure(BadVerificationCodeException) + else -> Result.failure(UnknownGithubException) } + override fun getAccessToken(): String { return "validToken" } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt new file mode 100644 index 000000000..ecc55a691 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -0,0 +1,61 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.appunite.loudius.network.datasource + +import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.model.AccessTokenResponse +import com.appunite.loudius.network.services.AuthService +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class AuthNetworkDataSourceTest { + private val authService = FakeAuthService() + private val authNetworkDataSource = AuthNetworkDataSource(authService) + + @Test + fun `GIVEN correct data WHEN processing THEN return success with new valid token`() = + runTest { + val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "correct_code") + + Assertions.assertEquals( + Result.success("validAccessToken"), + result, + ) + } + + @Test + fun `GIVEN incorrect access code WHEN accessing token THEN return failure with BadVerificationCodeException`() = + runTest { + val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrect_code") + + Assertions.assertEquals( + Result.failure(BadVerificationCodeException), + result, + ) + } + + @Test + fun `GIVEN incorrect data WHEN processing THEN return failure with UnknownGithubException`() = + runTest { + val result = authNetworkDataSource.getAccessToken("", "", "") + + Assertions.assertEquals( + Result.failure(UnknownGithubException), + result, + ) + } +} + +class FakeAuthService : AuthService { + override suspend fun getAccessToken( + clientId: String, + clientSecret: String, + code: String + ): AccessTokenResponse = when (code) { + "correct_code" -> AccessTokenResponse("validAccessToken") + "incorrect_code" -> AccessTokenResponse(null, "bad_verification_code") + else -> AccessTokenResponse(null, "error") + } +} diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt index ed0377bc1..c298b020e 100644 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -3,7 +3,7 @@ package com.appunite.loudius.ui.loading import com.appunite.loudius.fakes.FakeAuthRepository import com.appunite.loudius.util.MainDispatcherExtension import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -11,25 +11,21 @@ import org.junit.jupiter.api.extension.ExtendWith class LoadingViewModelTest { companion object { - private const val EXAMPLE_CODE = "code" + private const val EXAMPLE_CODE = "validCode" + private const val EXAMPLE_INVALID_CODE = "invalidCode" private const val EXAMPLE_ACCESS_TOKEN = "validToken" } private val repository: FakeAuthRepository = FakeAuthRepository() - private lateinit var viewModel: LoadingViewModel - - @BeforeEach - fun setup() { - viewModel = LoadingViewModel(repository) - } + private val viewModel = LoadingViewModel(repository) @Test - fun `GIVEN valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigateToPullRequests`() { + fun `GIVEN valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigate to pull requests`() { viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - assertEquals(viewModel.state.code, EXAMPLE_CODE) - assertEquals(viewModel.state.accessToken, EXAMPLE_ACCESS_TOKEN) - assertEquals(viewModel.state.navigateToPullRequests, NavigateToPullRequests) + assertEquals(EXAMPLE_CODE, viewModel.state.code) + assertEquals(EXAMPLE_ACCESS_TOKEN, viewModel.state.accessToken) + assertEquals(LoadingScreenNavigation.NavigateToPullRequests, viewModel.state.navigateTo) } @Test @@ -39,17 +35,42 @@ class LoadingViewModelTest { viewModel.onAction(action) - assertEquals(viewModel.state.showErrorScreen, false) - assertEquals(viewModel.state.accessToken, EXAMPLE_ACCESS_TOKEN) + assertNull(viewModel.state.errorScreenType) + assertEquals(EXAMPLE_ACCESS_TOKEN, viewModel.state.accessToken) } @Test - fun `GIVEN OnNavigateToPullRequests action WHEN onAction THEN set navigateToPullRequests as null`() { - val action = LoadingAction.OnNavigateToPullRequests + fun `GIVEN OnNavigateToPullRequests action WHEN onAction THEN set navigateToPullRequests to null`() { + val action = LoadingAction.OnNavigate viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) viewModel.onAction(action) - assertEquals(viewModel.state.navigateToPullRequests, null) + assertNull(viewModel.state.navigateTo) + } + + @Test + fun `GIVEN invalid code WHEN setCodeAndGetAccessToken THEN show login error screen`() { + viewModel.setCodeAndGetAccessToken(EXAMPLE_INVALID_CODE) + + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + assertNull(viewModel.state.navigateTo) + } + + @Test + fun `GIVEN unexpected Github behavior WHEN setCodeAndGetAccessToken THEN show generic error screen`() { + viewModel.setCodeAndGetAccessToken("code_leading_to_unexpected_error") + + assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) + assertNull(viewModel.state.navigateTo) + } + + @Test + fun `GIVEN retry click WHEN logging in error THEN redirect to login screen`() { + viewModel.setCodeAndGetAccessToken(EXAMPLE_INVALID_CODE) + + viewModel.onAction(LoadingAction.OnTryAgainClick) + + assertEquals(LoadingScreenNavigation.NavigateToLogin, viewModel.state.navigateTo) } } From 6996c842ace07edcbea92cd30e13316bee685eb8 Mon Sep 17 00:00:00 2001 From: kezc Date: Thu, 23 Mar 2023 07:35:19 +0000 Subject: [PATCH 234/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/domain/AuthRepositoryImpl.kt | 1 - .../appunite/loudius/ui/components/LoudiusErrorScreen.kt | 2 +- .../com/appunite/loudius/ui/loading/LoadingViewModel.kt | 6 +++--- .../com/appunite/loudius/domain/AuthRepositoryImplTest.kt | 1 - .../java/com/appunite/loudius/fakes/FakeAuthRepository.kt | 1 - .../loudius/network/datasource/AuthNetworkDataSourceTest.kt | 2 +- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt index abbe94b9e..069e9bad7 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt @@ -2,7 +2,6 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.model.AccessToken -import com.appunite.loudius.network.model.AccessTokenResponse import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index aa69752a3..2651cf06b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -55,7 +55,7 @@ private fun ErrorText(text: String) { color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.padding(horizontal = 16.dp), ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index e0a837ee3..b1b725a8a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -10,8 +10,8 @@ import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class LoadingAction { @@ -79,14 +79,14 @@ class LoadingViewModel @Inject constructor( ).onSuccess { token -> state = state.copy( accessToken = token, - navigateTo = LoadingScreenNavigation.NavigateToPullRequests + navigateTo = LoadingScreenNavigation.NavigateToPullRequests, ) }.onFailure { state = state.copy( errorScreenType = when (it) { is BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR else -> LoadingErrorType.GENERIC_ERROR - } + }, ) } } diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index e3a9bd2b7..dcd43bcde 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -1,7 +1,6 @@ package com.appunite.loudius.domain import com.appunite.loudius.network.datasource.AuthDataSource -import com.appunite.loudius.network.model.AccessTokenResponse import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 4d6ead152..8d5984cab 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -16,7 +16,6 @@ class FakeAuthRepository : AuthRepository { else -> Result.failure(UnknownGithubException) } - override fun getAccessToken(): String { return "validToken" } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index ecc55a691..3affc46cc 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -52,7 +52,7 @@ class FakeAuthService : AuthService { override suspend fun getAccessToken( clientId: String, clientSecret: String, - code: String + code: String, ): AccessTokenResponse = when (code) { "correct_code" -> AccessTokenResponse("validAccessToken") "incorrect_code" -> AccessTokenResponse(null, "bad_verification_code") From 11b1b8e2ee380c5e1f95673f1020d7986f5f755f Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 23 Mar 2023 09:18:45 +0100 Subject: [PATCH 235/526] Change assertion to assertNull instead of assertEquals in ReviewersViewModelTest.kt. --- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 2a0f73120..00b3d1c77 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -11,18 +11,19 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -212,7 +213,7 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("ExampleUser")) viewModel.onAction(ReviewersAction.OnSnackbarDismiss) - assertEquals(null, viewModel.state.snackbarTypeShown) + assertNull(viewModel.state.snackbarTypeShown) } @Test From 0f664da8e5867458f28189c3f44868e2568c60d8 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 23 Mar 2023 08:21:48 +0000 Subject: [PATCH 236/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 00b3d1c77..5dbf95517 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -11,10 +11,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -24,6 +20,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From 2f69790248c0bd6925d5e0b9aeaedf30e2b4ba5e Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 09:44:09 +0100 Subject: [PATCH 237/526] Pop navigation back stack after logging in and going back to login --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 5e0bfa6e2..76524e990 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -50,10 +50,14 @@ class MainActivity : ComponentActivity() { LoadingScreen( intent = intent, onNavigateToPullRequest = { - navController.navigate(Screen.PullRequests.route) + navController.navigate(Screen.PullRequests.route) { + popUpTo(Screen.Login.route) { inclusive = true } + } }, onNavigateToLogin = { - navController.navigate(Screen.Login.route) + navController.navigate(Screen.Login.route) { + popUpTo(Screen.Login.route) { inclusive = true } + } }, ) } From 4af669d39555fabb9056b9c214e6d2f8ebeae0e2 Mon Sep 17 00:00:00 2001 From: wojtek krystyniak Date: Thu, 23 Mar 2023 10:13:06 +0100 Subject: [PATCH 238/526] Create LICENSE file --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From af3a978188eb9feea7d7435684c89947a4342691 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 11:34:02 +0100 Subject: [PATCH 239/526] Extract error string to companion object --- .../loudius/network/datasource/AuthDataSource.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index 211e6689c..530649cdc 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -33,9 +33,15 @@ class AuthNetworkDataSource @Inject constructor( ?: Result.failure(response.mapErrorToException()) } - private fun AccessTokenResponse.mapErrorToException() = when (error) { - "bad_verification_code" -> BadVerificationCodeException - else -> UnknownGithubException + private fun AccessTokenResponse.mapErrorToException(): java.lang.Exception { + return when (error) { + BAD_VERIFICATION_CODE_ERROR -> BadVerificationCodeException + else -> UnknownGithubException + } + } + + companion object { + private const val BAD_VERIFICATION_CODE_ERROR = "bad_verification_code" } } From 30284b778ef254ea3cc97d1b485c29da97841041 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 11:41:30 +0100 Subject: [PATCH 240/526] Use if instead of elvis operator --- .../appunite/loudius/network/datasource/AuthDataSource.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index 530649cdc..685e05796 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -29,8 +29,11 @@ class AuthNetworkDataSource @Inject constructor( ): Result = safeApiCall { authService.getAccessToken(clientId, clientSecret, code) } .flatMap { response -> - response.accessToken?.let { token -> Result.success(token) } - ?: Result.failure(response.mapErrorToException()) + if (response.accessToken != null) { + Result.success(response.accessToken) + } else { + Result.failure(response.mapErrorToException()) + } } private fun AccessTokenResponse.mapErrorToException(): java.lang.Exception { From e50a31fe9c91c13d362201666aeb8bba80cf3fc9 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 11:45:45 +0100 Subject: [PATCH 241/526] Use method reference --- .../appunite/loudius/ui/loading/LoadingScreen.kt | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index 5a54678d1..b3cc99389 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -51,7 +51,7 @@ fun LoadingScreenStateless( onTryAgainClick: () -> Unit, ) { when (errorScreenType) { - LoadingErrorType.GENERIC_ERROR -> ShowLoudiusErrorScreen(onTryAgainClick) + LoadingErrorType.GENERIC_ERROR -> ShowLoudiusGenericErrorScreen(onTryAgainClick) LoadingErrorType.LOGIN_ERROR -> ShowLoudiusLoginErrorScreen(onTryAgainClick) else -> LoudiusLoadingIndicator() } @@ -64,18 +64,15 @@ private fun ShowLoudiusLoginErrorScreen( LoudiusErrorScreen( errorText = stringResource(id = R.string.error_login_text), buttonText = stringResource(id = R.string.go_to_login), - ) { - onTryAgainClick() - } + onButtonClick = onTryAgainClick, + ) } @Composable -private fun ShowLoudiusErrorScreen( +private fun ShowLoudiusGenericErrorScreen( onTryAgainClick: () -> Unit, ) { - LoudiusErrorScreen( - onButtonClick = { onTryAgainClick() }, - ) + LoudiusErrorScreen(onButtonClick = onTryAgainClick) } @Preview(showSystemUi = true) From e64b73c53fb92e1b2da976ab76412244534bef68 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 11:48:08 +0100 Subject: [PATCH 242/526] Rename onNavigateToPullRequests to onNavigate --- .../java/com/appunite/loudius/ui/loading/LoadingViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index b1b725a8a..b90de96bf 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -52,7 +52,7 @@ class LoadingViewModel @Inject constructor( fun onAction(action: LoadingAction) = when (action) { is LoadingAction.OnTryAgainClick -> onTryAgain() - is LoadingAction.OnNavigate -> onNavigateToPullRequests() + is LoadingAction.OnNavigate -> onNavigate() } private fun onTryAgain() { @@ -66,7 +66,7 @@ class LoadingViewModel @Inject constructor( } } - private fun onNavigateToPullRequests() { + private fun onNavigate() { state = state.copy(navigateTo = null) } From 27d4493be29200419b6faa3e133028627bc575f4 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 11:50:17 +0100 Subject: [PATCH 243/526] Resolve error type in different function --- .../loudius/ui/loading/LoadingViewModel.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index b90de96bf..7ebfa83e6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -10,8 +10,8 @@ import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class LoadingAction { @@ -82,13 +82,13 @@ class LoadingViewModel @Inject constructor( navigateTo = LoadingScreenNavigation.NavigateToPullRequests, ) }.onFailure { - state = state.copy( - errorScreenType = when (it) { - is BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR - else -> LoadingErrorType.GENERIC_ERROR - }, - ) + state = state.copy(errorScreenType = resolveErrorType(it)) } } } + + private fun resolveErrorType(it: Throwable) = when (it) { + is BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR + else -> LoadingErrorType.GENERIC_ERROR + } } From 387c8ade8963d9b5314dbf7234fdf96efaccd623 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 11:54:16 +0100 Subject: [PATCH 244/526] Use camelCase in tests' strings --- .../network/datasource/AuthNetworkDataSourceTest.kt | 8 ++++---- .../appunite/loudius/ui/loading/LoadingViewModelTest.kt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 3affc46cc..df552f9e9 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -17,7 +17,7 @@ class AuthNetworkDataSourceTest { @Test fun `GIVEN correct data WHEN processing THEN return success with new valid token`() = runTest { - val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "correct_code") + val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "correctCode") Assertions.assertEquals( Result.success("validAccessToken"), @@ -28,7 +28,7 @@ class AuthNetworkDataSourceTest { @Test fun `GIVEN incorrect access code WHEN accessing token THEN return failure with BadVerificationCodeException`() = runTest { - val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrect_code") + val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") Assertions.assertEquals( Result.failure(BadVerificationCodeException), @@ -54,8 +54,8 @@ class FakeAuthService : AuthService { clientSecret: String, code: String, ): AccessTokenResponse = when (code) { - "correct_code" -> AccessTokenResponse("validAccessToken") - "incorrect_code" -> AccessTokenResponse(null, "bad_verification_code") + "correctCode" -> AccessTokenResponse("validAccessToken") + "incorrectCode" -> AccessTokenResponse(null, "bad_verification_code") else -> AccessTokenResponse(null, "error") } } diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt index c298b020e..7a8c013ff 100644 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -59,7 +59,7 @@ class LoadingViewModelTest { @Test fun `GIVEN unexpected Github behavior WHEN setCodeAndGetAccessToken THEN show generic error screen`() { - viewModel.setCodeAndGetAccessToken("code_leading_to_unexpected_error") + viewModel.setCodeAndGetAccessToken("codeLeadingToUnexpectedError") assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) assertNull(viewModel.state.navigateTo) From 7aa9c0a00d4983f7246ece8a6be5a6b458fe8fdc Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 12:02:16 +0100 Subject: [PATCH 245/526] Remove UnknownGithubException --- .../appunite/loudius/network/datasource/AuthDataSource.kt | 6 +++--- .../java/com/appunite/loudius/network/utils/WebException.kt | 2 +- .../java/com/appunite/loudius/fakes/FakeAuthRepository.kt | 4 ++-- .../loudius/network/datasource/AuthNetworkDataSourceTest.kt | 5 +++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index 685e05796..ab94c9ab1 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -4,6 +4,7 @@ import com.appunite.loudius.common.flatMap import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.model.AccessTokenResponse import com.appunite.loudius.network.services.AuthService +import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject import javax.inject.Singleton @@ -36,10 +37,10 @@ class AuthNetworkDataSource @Inject constructor( } } - private fun AccessTokenResponse.mapErrorToException(): java.lang.Exception { + private fun AccessTokenResponse.mapErrorToException(): Exception { return when (error) { BAD_VERIFICATION_CODE_ERROR -> BadVerificationCodeException - else -> UnknownGithubException + else -> WebException.UnknownError(null, error) } } @@ -49,4 +50,3 @@ class AuthNetworkDataSource @Inject constructor( } object BadVerificationCodeException : Exception() -object UnknownGithubException : Exception() diff --git a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt index 100771b25..ebd06cc9f 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt @@ -7,7 +7,7 @@ sealed class WebException : Exception() { /** * Represents exception which comes from backend. */ - data class UnknownError(val code: Int, override val message: String?) : WebException() + data class UnknownError(val code: Int?, override val message: String?) : WebException() /** * Represents web exception which can be thrown during network communication. diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 8d5984cab..104f504b8 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -2,8 +2,8 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException -import com.appunite.loudius.network.datasource.UnknownGithubException import com.appunite.loudius.network.model.AccessToken +import com.appunite.loudius.network.utils.WebException class FakeAuthRepository : AuthRepository { override suspend fun fetchAccessToken( @@ -13,7 +13,7 @@ class FakeAuthRepository : AuthRepository { ): Result = when (code) { "validCode" -> Result.success("validToken") "invalidCode" -> Result.failure(BadVerificationCodeException) - else -> Result.failure(UnknownGithubException) + else -> Result.failure(WebException.UnknownError(null, null)) } override fun getAccessToken(): String { diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index df552f9e9..b7d8668bc 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -5,6 +5,7 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.model.AccessTokenResponse import com.appunite.loudius.network.services.AuthService +import com.appunite.loudius.network.utils.WebException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions @@ -37,12 +38,12 @@ class AuthNetworkDataSourceTest { } @Test - fun `GIVEN incorrect data WHEN processing THEN return failure with UnknownGithubException`() = + fun `GIVEN incorrect data WHEN processing THEN return failure with UnknownError`() = runTest { val result = authNetworkDataSource.getAccessToken("", "", "") Assertions.assertEquals( - Result.failure(UnknownGithubException), + Result.failure(WebException.UnknownError(null, "error")), result, ) } From 1fb6e5cb177ed9e78d5fa3674c41bf033d944266 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 23 Mar 2023 12:33:26 +0100 Subject: [PATCH 246/526] Extract string codeLeadingToUnexpectedError to companion object --- .../datasource/AuthNetworkDataSourceTest.kt | 67 ++++++++++++++----- .../ui/loading/LoadingViewModelTest.kt | 3 +- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index b7d8668bc..5e57806a2 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -2,23 +2,50 @@ package com.appunite.loudius.network.datasource +import com.appunite.loudius.fakes.FakeAuthRepository import com.appunite.loudius.network.model.AccessToken -import com.appunite.loudius.network.model.AccessTokenResponse +import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.AuthService +import com.appunite.loudius.network.testOkHttpClient import com.appunite.loudius.network.utils.WebException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test class AuthNetworkDataSourceTest { - private val authService = FakeAuthService() + private val fakeUserRepository = FakeAuthRepository() + private val testOkHttpClient = testOkHttpClient(fakeUserRepository) + private val mockWebServer: MockWebServer = MockWebServer() + private val authService = retrofitTestDouble( + mockWebServer = mockWebServer, + client = testOkHttpClient, + ).create(AuthService::class.java) + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + } + private val authNetworkDataSource = AuthNetworkDataSource(authService) @Test - fun `GIVEN correct data WHEN processing THEN return success with new valid token`() = + fun `GIVEN correct data WHEN accessing token THEN return success with new valid token`() = runTest { - val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "correctCode") + //language=JSON + val jsonResponse = """ + { "access_token": "validAccessToken" } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(200).setBody(jsonResponse), + ) + + val result = + authNetworkDataSource.getAccessToken("clientId", "clientSecret", "correctCode") Assertions.assertEquals( Result.success("validAccessToken"), @@ -29,6 +56,15 @@ class AuthNetworkDataSourceTest { @Test fun `GIVEN incorrect access code WHEN accessing token THEN return failure with BadVerificationCodeException`() = runTest { + //language=JSON + val jsonResponse = """ + { "error": "bad_verification_code" } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(200).setBody(jsonResponse), + ) + val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") Assertions.assertEquals( @@ -38,8 +74,17 @@ class AuthNetworkDataSourceTest { } @Test - fun `GIVEN incorrect data WHEN processing THEN return failure with UnknownError`() = + fun `GIVEN incorrect data WHEN accessing token THEN return failure with UnknownError`() = runTest { + //language=JSON + val jsonResponse = """ + { "error": "error" } + """.trimIndent() + + mockWebServer.enqueue( + MockResponse().setResponseCode(200).setBody(jsonResponse), + ) + val result = authNetworkDataSource.getAccessToken("", "", "") Assertions.assertEquals( @@ -48,15 +93,3 @@ class AuthNetworkDataSourceTest { ) } } - -class FakeAuthService : AuthService { - override suspend fun getAccessToken( - clientId: String, - clientSecret: String, - code: String, - ): AccessTokenResponse = when (code) { - "correctCode" -> AccessTokenResponse("validAccessToken") - "incorrectCode" -> AccessTokenResponse(null, "bad_verification_code") - else -> AccessTokenResponse(null, "error") - } -} diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt index 7a8c013ff..8a6444048 100644 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -13,6 +13,7 @@ class LoadingViewModelTest { companion object { private const val EXAMPLE_CODE = "validCode" private const val EXAMPLE_INVALID_CODE = "invalidCode" + private const val EXAMPLE_CODE_LEADING_TO_UNEXPECTED_ERROR = "codeLeadingToUnexpectedError" private const val EXAMPLE_ACCESS_TOKEN = "validToken" } @@ -59,7 +60,7 @@ class LoadingViewModelTest { @Test fun `GIVEN unexpected Github behavior WHEN setCodeAndGetAccessToken THEN show generic error screen`() { - viewModel.setCodeAndGetAccessToken("codeLeadingToUnexpectedError") + viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE_LEADING_TO_UNEXPECTED_ERROR) assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) assertNull(viewModel.state.navigateTo) From 4361765cbb707a760e5494be1f6a1b71e968ed4e Mon Sep 17 00:00:00 2001 From: kezc Date: Thu, 23 Mar 2023 11:43:18 +0000 Subject: [PATCH 247/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/loading/LoadingViewModel.kt | 2 +- .../loudius/network/datasource/AuthNetworkDataSourceTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index 7ebfa83e6..e5df6b397 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -10,8 +10,8 @@ import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class LoadingAction { diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 5e57806a2..94dd6f1c3 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -38,7 +38,7 @@ class AuthNetworkDataSourceTest { //language=JSON val jsonResponse = """ { "access_token": "validAccessToken" } - """.trimIndent() + """.trimIndent() mockWebServer.enqueue( MockResponse().setResponseCode(200).setBody(jsonResponse), @@ -59,7 +59,7 @@ class AuthNetworkDataSourceTest { //language=JSON val jsonResponse = """ { "error": "bad_verification_code" } - """.trimIndent() + """.trimIndent() mockWebServer.enqueue( MockResponse().setResponseCode(200).setBody(jsonResponse), @@ -79,7 +79,7 @@ class AuthNetworkDataSourceTest { //language=JSON val jsonResponse = """ { "error": "error" } - """.trimIndent() + """.trimIndent() mockWebServer.enqueue( MockResponse().setResponseCode(200).setBody(jsonResponse), From 346d2aa91119977d86e80ccf37d331d152d22f87 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk <33498031+Krzysiudan@users.noreply.github.com> Date: Thu, 23 Mar 2023 16:41:10 +0100 Subject: [PATCH 248/526] Update README.md --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7eadb06b7..8af6c3ef5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,50 @@ -# Loudius +# Loudius - Android experimental playground +This project serves as an example Android project and a playground for experimenting with new architectures, solutions, and libraries. Functionalities of the app are simple. It uses GithubAPI and let's user ping his collaborators for a faster code review. + +## Contributing +We believe that there is no ideal code and that every code can be improved. Therefore, we welcome every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn from each other. + +## Experiments +We have a dedicated folder called experiments Every experiment should be on a separate branch, and the branch should be named after the experiment. We ask that experimental branches not be merged into the develop branch. Instead, they serve as an experiment, and if someone wants to see how it looks, they should checkout the experimental branch. + +### Rules for Experiments +We have a few rules for experiments to keep everything organized and consistent: + +- Every experiment should be on a separate branch in the experiment folder. +- The branch should be named after the experiment. +- The experiment should not be merged into the develop branch. +- The experiment should have a clear purpose and goal. +- The experiment should not interfere with the stability of the existing codebase. + + +### Example Experiment +Here's an example of an experiment that meets our rules: + +Branch Name: experiment/user-profile-ui +Purpose: To experiment with different UI components for the user profile screen. +Goal: To improve the user experience and make the screen more visually appealing. +Method: Test different UI components and layouts on the screen and collect user feedback. ### How to set environmental variable on mac? 1. Launch zsh (command `zsh`) 2. `$ echo 'export CLIENT_SECRET=you know what' >> ~/.zshenv` 3. `$ echo $CLIENT_SECRET` 4. Restart your computer. + + +### License + + Copyright (C) 2023 AppUnite + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + From 10a3a6d3446dde9da2ce12bca0168ce81cf2dc0f Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Fri, 24 Mar 2023 08:41:56 +0000 Subject: [PATCH 249/526] [MegaLinter] Apply linters fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8af6c3ef5..5f39bca0d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Loudius - Android experimental playground -This project serves as an example Android project and a playground for experimenting with new architectures, solutions, and libraries. Functionalities of the app are simple. It uses GithubAPI and let's user ping his collaborators for a faster code review. +This project serves as an example Android project and a playground for experimenting with new architectures, solutions, and libraries. Functionalities of the app are simple. It uses GithubAPI and let's user ping his collaborators for a faster code review. ## Contributing We believe that there is no ideal code and that every code can be improved. Therefore, we welcome every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn from each other. @@ -14,7 +14,7 @@ We have a few rules for experiments to keep everything organized and consistent: - The branch should be named after the experiment. - The experiment should not be merged into the develop branch. - The experiment should have a clear purpose and goal. -- The experiment should not interfere with the stability of the existing codebase. +- The experiment should not interfere with the stability of the existing codebase. ### Example Experiment From 186c6418092262ae843876e28bd982fdc65979dc Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 24 Mar 2023 14:21:32 +0100 Subject: [PATCH 250/526] Add loading spinner upon clicking notify --- .../appunite/loudius/domain/model/Reviewer.kt | 1 + .../ui/pullrequests/PullRequestsScreen.kt | 2 +- .../loudius/ui/reviewers/ReviewersScreen.kt | 25 ++++++++++-- .../ui/reviewers/ReviewersViewModel.kt | 24 +++++++++--- .../fakes/FakePullRequestRepository.kt | 28 ++++++++++--- .../ui/reviewers/ReviewersViewModelTest.kt | 39 +++++++++++++++---- 6 files changed, 98 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt index b76c0d1db..08f5267c8 100644 --- a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt @@ -6,4 +6,5 @@ data class Reviewer( val isReviewDone: Boolean, val hoursFromPRStart: Long, val hoursFromReviewDone: Long?, + val isLoading: Boolean = false, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 8eb6f8d48..ea1803564 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -49,7 +49,7 @@ fun PullRequestsScreen( viewModel.onAction(PulLRequestsAction.OnNavigateToReviewers) } } - PullRequestsScreenStateless( + PullRequestsScreenStateless( pullRequests = state.pullRequests, onAction = viewModel::onAction, isLoading = state.isLoading, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 0e4737077..6a1e01fba 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -2,12 +2,15 @@ package com.appunite.loudius.ui.reviewers import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton @@ -19,8 +22,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -166,8 +171,22 @@ private fun ReviewerItem( IsReviewedHeadlineText(reviewer) ReviewerName(reviewer) } - NotifyButton(Modifier.align(CenterVertically)) { - onNotifyClick(ReviewersAction.Notify(reviewer.login)) + NotifyButtonOrLoadingIndicator(reviewer = reviewer, onNotifyClick = onNotifyClick) + } +} + +@Composable +private fun NotifyButtonOrLoadingIndicator( + reviewer: Reviewer, + onNotifyClick: (ReviewersAction) -> Unit +) { + Box(contentAlignment = Center) { + NotifyButton( + modifier = Modifier.alpha(if (reviewer.isLoading) 0f else 1f) + ) { onNotifyClick(ReviewersAction.Notify(reviewer.login)) } + + if (reviewer.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) } } } @@ -234,7 +253,7 @@ private fun ReviewerViewPreview() { fun DetailsScreenPreview() { val reviewer1 = Reviewer(1, "Kezc", true, 24, 12) val reviewer2 = Reviewer(2, "Krzysiudan", false, 24, 0) - val reviewer3 = Reviewer(3, "Weronika", false, 24, 0) + val reviewer3 = Reviewer(3, "Weronika", false, 24, 0, true) val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 52ec4d02c..fa6721bba 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -15,12 +15,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -137,12 +137,26 @@ class ReviewersViewModel @Inject constructor( val (owner, repo, pullRequestNumber) = initialValues viewModelScope.launch { + state = state.copy(reviewers = updateReviewerLoadingState(userLogin, true)) repository.notify(owner, repo, pullRequestNumber, "@$userLogin") - .onSuccess { state = state.copy(snackbarTypeShown = SUCCESS) } - .onFailure { state = state.copy(snackbarTypeShown = FAILURE) } + .onSuccess { + state = state.copy( + snackbarTypeShown = SUCCESS, + reviewers = updateReviewerLoadingState(userLogin, false) + ) + } + .onFailure { + state = state.copy( + snackbarTypeShown = FAILURE, + reviewers = updateReviewerLoadingState(userLogin, false) + ) + } } } + private fun updateReviewerLoadingState(userLogin: String, isLoading: Boolean) = + state.reviewers.map { if (it.login == userLogin) it.copy(isLoading = isLoading) else it } + private fun dismissSnackbar() { state = state.copy(snackbarTypeShown = null) } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 82e084053..448d31953 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -23,11 +23,25 @@ class FakePullRequestRepository : PullRequestRepository { ), ) + private val initialNotifyAnswer: suspend (pullRequestNumber: String) -> Result = + { pullRequestNumber: String -> + when (pullRequestNumber) { + "correctPullRequestNumber" -> Result.success(Unit) + "notExistingPullRequestNumber" -> Result.failure( + WebException.UnknownError(404, null) + ) + else -> Result.failure(WebException.NetworkError()) + } + } + + private var lazyReviewsAnswer: suspend () -> Result> = { initialReviewsAnswer } private var lazyRequestedReviewersAnswer: suspend () -> Result = { initialRequestedReviewersAnswer } private var lazyCurrentUserPullRequests: suspend () -> Result = { initialPullRequestAnswer } + private var lazyNotifyAnswer: suspend (pullRequestNumber: String) -> Result = + initialNotifyAnswer override suspend fun getReviews( owner: String, @@ -69,14 +83,18 @@ class FakePullRequestRepository : PullRequestRepository { return lazyCurrentUserPullRequests() } + fun setNotifyResponse(result: suspend (pullRequestNumber: String) -> Result) { + lazyNotifyAnswer = result + } + + fun resetNotifyResponse() { + lazyNotifyAnswer = initialNotifyAnswer + } + override suspend fun notify( owner: String, repo: String, pullRequestNumber: String, message: String, - ): Result = when (pullRequestNumber) { - "correctPullRequestNumber" -> Result.success(Unit) - "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) - else -> Result.failure(WebException.NetworkError()) - } + ): Result = lazyNotifyAnswer(pullRequestNumber) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 5dbf95517..aac19b12c 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -11,19 +11,20 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -190,27 +191,51 @@ class ReviewersViewModelTest { fun `WHEN successful notify action THEN show success snackbar`() = runTest { viewModel = createViewModel() - viewModel.onAction(ReviewersAction.Notify("ExampleUser")) + viewModel.onAction(ReviewersAction.Notify("user1")) assertEquals(ReviewersSnackbarType.SUCCESS, viewModel.state.snackbarTypeShown) } + @Test + fun `WHEN notify action THEN show success snackbar`() = runTest { + viewModel = createViewModel() + repository.setNotifyResponse { neverCompletingSuspension() } + + viewModel.onAction(ReviewersAction.Notify("user1")) + + assertTrue( + viewModel.state.reviewers.first { it.login == "user1" }.isLoading + ) { "Clicked item should have loading indicator" } + assertTrue( + viewModel.state.reviewers.filterNot { it.login == "user1" }.none { it.isLoading } + ) { "Only clicked item should have loading indicator" } + } + @Test fun `WHEN failed notify action THEN show failure snackbar`() = runTest { every { savedStateHandle.get("pull_request_number") } returns "nonExistingPullRequestNumber" viewModel = createViewModel() - viewModel.onAction(ReviewersAction.Notify("ExampleUser")) + viewModel.onAction(ReviewersAction.Notify("user1")) assertEquals(ReviewersSnackbarType.FAILURE, viewModel.state.snackbarTypeShown) } + @Test + fun `WHEN successful notify action THEN show loading`() = runTest { + viewModel = createViewModel() + + viewModel.onAction(ReviewersAction.Notify("user1")) + + assertEquals(ReviewersSnackbarType.SUCCESS, viewModel.state.snackbarTypeShown) + } + @Test fun `GIVEN user login WHEN on snackbar dismiss action THEN snackbar is not shown`() = runTest { viewModel = createViewModel() - viewModel.onAction(ReviewersAction.Notify("ExampleUser")) + viewModel.onAction(ReviewersAction.Notify("user1")) viewModel.onAction(ReviewersAction.OnSnackbarDismiss) assertNull(viewModel.state.snackbarTypeShown) From b7f7ba19ca8d4edd7b7a112b6e8a9ea6d61ef6cb Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Mar 2023 10:59:01 +0200 Subject: [PATCH 251/526] Update experiment example. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5f39bca0d..f3c06d48c 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ We have a few rules for experiments to keep everything organized and consistent: ### Example Experiment Here's an example of an experiment that meets our rules: -Branch Name: experiment/user-profile-ui -Purpose: To experiment with different UI components for the user profile screen. -Goal: To improve the user experience and make the screen more visually appealing. -Method: Test different UI components and layouts on the screen and collect user feedback. +Branch Name: experiment/navigation-by-voyager-library +Purpose: Check compose navigation with voyager library. Compare that with standard way. +Goal: To resolve which navigation is better for compose. What are the pros and cons of each way. +Method: Implement navigation with voyager library. ### How to set environmental variable on mac? 1. Launch zsh (command `zsh`) From 15ac20b3148a9c32f10dec0f4c619f686c8488a6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Mar 2023 11:59:22 +0200 Subject: [PATCH 252/526] Update experiments section. --- README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f3c06d48c..384cc5ab6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,35 @@ # Loudius - Android experimental playground -This project serves as an example Android project and a playground for experimenting with new architectures, solutions, and libraries. Functionalities of the app are simple. It uses GithubAPI and let's user ping his collaborators for a faster code review. + +This project serves as an example Android project and a playground for experimenting with new +architectures, solutions, and libraries. Functionalities of the app are simple. It uses GithubAPI +and let's user ping his collaborators for a faster code review. ## Contributing -We believe that there is no ideal code and that every code can be improved. Therefore, we welcome every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn from each other. + +We believe that there is no ideal code and that every code can be improved. Therefore, we welcome +every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn +from each other. ## Experiments -We have a dedicated folder called experiments Every experiment should be on a separate branch, and the branch should be named after the experiment. We ask that experimental branches not be merged into the develop branch. Instead, they serve as an experiment, and if someone wants to see how it looks, they should checkout the experimental branch. + +Our project is designed for those who want to experiment with different solutions, architectures, +and libraries in the Android development world. We believe that experimenting is the key to +improving your skills and finding the best solutions for your needs. + +We encourage everyone to join us and create their own experiments. You can experiment with anything +related to Android development - UI, performance, architecture, libraries, and more. + +To create your own experiment, simply download our repository, create a new branch, and start +experimenting. Once you're done, create a pull request, and our community will review it. We believe +that by sharing our experiments with each other, we can all learn and improve. + +We welcome all levels of experience in our community, whether you're a beginner or an expert. We're +all here to learn and grow together. + +So come and join us in Loudius - Android experimental playground, and let's experiment together! ### Rules for Experiments + We have a few rules for experiments to keep everything organized and consistent: - Every experiment should be on a separate branch in the experiment folder. @@ -16,7 +38,6 @@ We have a few rules for experiments to keep everything organized and consistent: - The experiment should have a clear purpose and goal. - The experiment should not interfere with the stability of the existing codebase. - ### Example Experiment Here's an example of an experiment that meets our rules: From 28ffeffac605e91653e61c135ade43a5d7aa78a3 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Mar 2023 12:53:58 +0200 Subject: [PATCH 253/526] Refactor dependency injections modules. --- .../{GithubModule.kt => DataSourceModule.kt} | 37 +++++++------------ ...llRequestModule.kt => RepositoryModule.kt} | 20 ++++++---- .../com/appunite/loudius/di/ServiceModule.kt | 31 ++++++++++++++++ .../com/appunite/loudius/di/UserModule.kt | 21 ----------- 4 files changed, 56 insertions(+), 53 deletions(-) rename app/src/main/java/com/appunite/loudius/di/{GithubModule.kt => DataSourceModule.kt} (55%) rename app/src/main/java/com/appunite/loudius/di/{PullRequestModule.kt => RepositoryModule.kt} (62%) create mode 100644 app/src/main/java/com/appunite/loudius/di/ServiceModule.kt delete mode 100644 app/src/main/java/com/appunite/loudius/di/UserModule.kt diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt similarity index 55% rename from app/src/main/java/com/appunite/loudius/di/GithubModule.kt rename to app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index fcb968e44..3fff62e27 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -1,11 +1,13 @@ package com.appunite.loudius.di import android.content.Context -import com.appunite.loudius.domain.AuthRepository -import com.appunite.loudius.domain.AuthRepositoryImpl import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource +import com.appunite.loudius.network.datasource.PullRequestDataSource +import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource +import com.appunite.loudius.network.datasource.UserDataSource +import com.appunite.loudius.network.datasource.UserDataSourceImpl import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.services.UserService @@ -14,43 +16,30 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -object GithubModule { +object DataSourceModule { - @Singleton @Provides - fun provideAuthService(@AuthAPI retrofit: Retrofit): AuthService = - retrofit.create(AuthService::class.java) - @Singleton - @Provides - fun provideUserService(@BaseAPI retrofit: Retrofit): UserService = - retrofit.create(UserService::class.java) + fun providePullRequestNetworkDataSource(service: PullRequestsService): PullRequestDataSource = + PullRequestsNetworkDataSource(service) - @Singleton @Provides - fun provideReposService(@BaseAPI retrofit: Retrofit): PullRequestsService = - retrofit.create(PullRequestsService::class.java) + @Singleton + fun provideUserDataSource(userService: UserService): UserDataSource = + UserDataSourceImpl(userService) @Singleton @Provides - fun provideAuthRepository( - authDataSource: AuthDataSource, - userLocalDataSource: UserLocalDataSource, - ): AuthRepository = AuthRepositoryImpl(authDataSource, userLocalDataSource) + fun provideUserLocalDataSource(@ApplicationContext context: Context): UserLocalDataSource = + UserLocalDataSource(context) @Singleton @Provides - fun provideAuthServiceDataSource( + fun provideAuthNetworkDataSource( service: AuthService, ): AuthDataSource = AuthNetworkDataSource(service) - - @Singleton - @Provides - fun provideUserLocalDataSource(@ApplicationContext context: Context): UserLocalDataSource = - UserLocalDataSource(context) } diff --git a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt similarity index 62% rename from app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt rename to app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt index 9c6d9a659..8e9a8b056 100644 --- a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt @@ -1,11 +1,13 @@ package com.appunite.loudius.di +import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.domain.AuthRepositoryImpl import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.PullRequestRepositoryImpl +import com.appunite.loudius.domain.UserLocalDataSource +import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.PullRequestDataSource -import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource import com.appunite.loudius.network.datasource.UserDataSource -import com.appunite.loudius.network.services.PullRequestsService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,12 +16,7 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -object PullRequestModule { - - @Provides - @Singleton - fun providePullRequestNetworkDataSource(service: PullRequestsService): PullRequestDataSource = - PullRequestsNetworkDataSource(service) +object RepositoryModule { @Provides @Singleton @@ -27,4 +24,11 @@ object PullRequestModule { dataSource: PullRequestDataSource, userDataSource: UserDataSource, ): PullRequestRepository = PullRequestRepositoryImpl(dataSource, userDataSource) + + @Singleton + @Provides + fun provideAuthRepository( + authDataSource: AuthDataSource, + userLocalDataSource: UserLocalDataSource, + ): AuthRepository = AuthRepositoryImpl(authDataSource, userLocalDataSource) } diff --git a/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt new file mode 100644 index 000000000..288fb7146 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt @@ -0,0 +1,31 @@ +package com.appunite.loudius.di + +import com.appunite.loudius.network.services.AuthService +import com.appunite.loudius.network.services.PullRequestsService +import com.appunite.loudius.network.services.UserService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import retrofit2.Retrofit + +@InstallIn(SingletonComponent::class) +@Module +object ServiceModule { + + @Singleton + @Provides + fun provideAuthService(@AuthAPI retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java) + + @Singleton + @Provides + fun provideUserService(@BaseAPI retrofit: Retrofit): UserService = + retrofit.create(UserService::class.java) + + @Singleton + @Provides + fun provideReposService(@BaseAPI retrofit: Retrofit): PullRequestsService = + retrofit.create(PullRequestsService::class.java) +} diff --git a/app/src/main/java/com/appunite/loudius/di/UserModule.kt b/app/src/main/java/com/appunite/loudius/di/UserModule.kt deleted file mode 100644 index 7c6931c16..000000000 --- a/app/src/main/java/com/appunite/loudius/di/UserModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.appunite.loudius.di - -import com.appunite.loudius.network.datasource.UserDataSource -import com.appunite.loudius.network.datasource.UserDataSourceImpl -import com.appunite.loudius.network.services.UserService -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@InstallIn(SingletonComponent::class) -@Module -object UserModule { - - @Provides - @Singleton - fun provideUserDataSource(userService: UserService): UserDataSource = - UserDataSourceImpl(userService) - -} From 0c387c8a43966989cd22e3416881eb85522a8bc1 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 27 Mar 2023 11:00:38 +0000 Subject: [PATCH 254/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/di/ServiceModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt index 288fb7146..200689fc6 100644 --- a/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt @@ -7,8 +7,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import retrofit2.Retrofit +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module From 1a3c368d563d6dd30fae8c6ebf5378b549c7e29e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Mar 2023 13:04:33 +0200 Subject: [PATCH 255/526] Move AuthRepositoryImpl.kt into the same file as AuthRepository.kt interface. --- .../appunite/loudius/domain/AuthRepository.kt | 24 +++++++++++++++++ .../loudius/domain/AuthRepositoryImpl.kt | 27 ------------------- 2 files changed, 24 insertions(+), 27 deletions(-) delete mode 100644 app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt index 68041d9c8..dc618678a 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt @@ -1,6 +1,9 @@ package com.appunite.loudius.domain +import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.model.AccessToken +import javax.inject.Inject +import javax.inject.Singleton interface AuthRepository { @@ -12,3 +15,24 @@ interface AuthRepository { fun getAccessToken(): AccessToken } + +@Singleton +class AuthRepositoryImpl @Inject constructor( + private val authDataSource: AuthDataSource, + private val userLocalDataSource: UserLocalDataSource, +) : AuthRepository { + + override suspend fun fetchAccessToken( + clientId: String, + clientSecret: String, + code: String, + ): Result { + val result = authDataSource.getAccessToken(clientId, clientSecret, code) + result.onSuccess { + userLocalDataSource.saveAccessToken(it) + } + return result + } + + override fun getAccessToken(): AccessToken = userLocalDataSource.getAccessToken() +} diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt deleted file mode 100644 index 069e9bad7..000000000 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepositoryImpl.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.appunite.loudius.domain - -import com.appunite.loudius.network.datasource.AuthDataSource -import com.appunite.loudius.network.model.AccessToken -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AuthRepositoryImpl @Inject constructor( - private val authDataSource: AuthDataSource, - private val userLocalDataSource: UserLocalDataSource, -) : AuthRepository { - - override suspend fun fetchAccessToken( - clientId: String, - clientSecret: String, - code: String, - ): Result { - val result = authDataSource.getAccessToken(clientId, clientSecret, code) - result.onSuccess { - userLocalDataSource.saveAccessToken(it) - } - return result - } - - override fun getAccessToken(): AccessToken = userLocalDataSource.getAccessToken() -} From 31d02ddf92cabe676f587e2e07d4f6c22a331f8f Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Mar 2023 13:04:56 +0200 Subject: [PATCH 256/526] Rename PullRequestRepositoryImpl.kt file into PullRequestRepository.kt --- .../{PullRequestRepositoryImpl.kt => PullRequestRepository.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{PullRequestRepositoryImpl.kt => PullRequestRepository.kt} (100%) diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt b/app/src/main/java/com/appunite/loudius/domain/PullRequestRepository.kt similarity index 100% rename from app/src/main/java/com/appunite/loudius/domain/PullRequestRepositoryImpl.kt rename to app/src/main/java/com/appunite/loudius/domain/PullRequestRepository.kt From 9ef2ce7f6f18e02ae38eccf630ae49940d425b64 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Mar 2023 13:07:11 +0200 Subject: [PATCH 257/526] Move AuthRepository.kt into repository package. --- app/src/main/java/com/appunite/loudius/di/GithubModule.kt | 6 +++--- .../loudius/domain/{ => repository}/AuthRepository.kt | 3 ++- .../com/appunite/loudius/network/utils/AuthInterceptor.kt | 4 ++-- .../com/appunite/loudius/ui/loading/LoadingViewModel.kt | 4 ++-- .../com/appunite/loudius/domain/AuthRepositoryImplTest.kt | 1 + .../java/com/appunite/loudius/fakes/FakeAuthRepository.kt | 2 +- .../java/com/appunite/loudius/network/NetworkTestDoubles.kt | 6 +++--- 7 files changed, 14 insertions(+), 12 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{ => repository}/AuthRepository.kt (90%) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index fcb968e44..87f456ce7 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.di import android.content.Context -import com.appunite.loudius.domain.AuthRepository -import com.appunite.loudius.domain.AuthRepositoryImpl import com.appunite.loudius.domain.UserLocalDataSource +import com.appunite.loudius.domain.repository.AuthRepository +import com.appunite.loudius.domain.repository.AuthRepositoryImpl import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource import com.appunite.loudius.network.services.AuthService @@ -14,8 +14,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit import javax.inject.Singleton +import retrofit2.Retrofit @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt similarity index 90% rename from app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt rename to app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt index dc618678a..b64a16100 100644 --- a/app/src/main/java/com/appunite/loudius/domain/AuthRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt @@ -1,5 +1,6 @@ -package com.appunite.loudius.domain +package com.appunite.loudius.domain.repository +import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt index 3a904863f..946fe4a85 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.network.utils -import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.domain.repository.AuthRepository +import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject class AuthInterceptor @Inject constructor( private val authRepository: AuthRepository, diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index e5df6b397..b704e7697 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -7,11 +7,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID -import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class LoadingAction { diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index dcd43bcde..670a59b33 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -1,5 +1,6 @@ package com.appunite.loudius.domain +import com.appunite.loudius.domain.repository.AuthRepositoryImpl import com.appunite.loudius.network.datasource.AuthDataSource import io.mockk.coEvery import io.mockk.coVerify diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 104f504b8..b74b4e30b 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.fakes -import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.utils.WebException diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index b91fac13f..0550b0716 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -1,18 +1,18 @@ package com.appunite.loudius.network -import com.appunite.loudius.domain.AuthRepository +import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.fakes.FakeAuthRepository import com.appunite.loudius.network.utils.AuthInterceptor import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit fun testOkHttpClient(authRepository: AuthRepository = FakeAuthRepository()) = OkHttpClient.Builder() From ee301f658efd353e326ad105d6ebb5f53b9deec2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 27 Mar 2023 13:08:46 +0200 Subject: [PATCH 258/526] Move PullRequestRepository.kt into repository package. --- .../java/com/appunite/loudius/di/PullRequestModule.kt | 4 ++-- .../domain/{ => repository}/PullRequestRepository.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 4 ++-- .../appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 8 ++++---- .../loudius/domain/PullRequestRepositoryImpTest.kt | 3 ++- .../appunite/loudius/fakes/FakePullRequestRepository.kt | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{ => repository}/PullRequestRepository.kt (98%) diff --git a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt index 9c6d9a659..5a918a3a9 100644 --- a/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/PullRequestModule.kt @@ -1,7 +1,7 @@ package com.appunite.loudius.di -import com.appunite.loudius.domain.PullRequestRepository -import com.appunite.loudius.domain.PullRequestRepositoryImpl +import com.appunite.loudius.domain.repository.PullRequestRepository +import com.appunite.loudius.domain.repository.PullRequestRepositoryImpl import com.appunite.loudius.network.datasource.PullRequestDataSource import com.appunite.loudius.network.datasource.PullRequestsNetworkDataSource import com.appunite.loudius.network.datasource.UserDataSource diff --git a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt similarity index 98% rename from app/src/main/java/com/appunite/loudius/domain/PullRequestRepository.kt rename to app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt index fda1c352c..03f8498cb 100644 --- a/app/src/main/java/com/appunite/loudius/domain/PullRequestRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.domain +package com.appunite.loudius.domain.repository import com.appunite.loudius.common.flatMap import com.appunite.loudius.network.datasource.PullRequestDataSource diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index d053feaba..719597b9c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -5,11 +5,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.appunite.loudius.domain.PullRequestRepository +import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 52ec4d02c..a647a4e8e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -8,19 +8,19 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Screen import com.appunite.loudius.common.flatMap -import com.appunite.loudius.domain.PullRequestRepository import com.appunite.loudius.domain.model.Reviewer +import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index 939deef01..dbbddcdcd 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -1,5 +1,6 @@ package com.appunite.loudius.domain +import com.appunite.loudius.domain.repository.PullRequestRepositoryImpl import com.appunite.loudius.fakes.FakePullRequestDataSource import com.appunite.loudius.network.datasource.UserDataSource import com.appunite.loudius.network.model.RequestedReviewer @@ -9,12 +10,12 @@ import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.User import io.mockk.coEvery import io.mockk.mockk +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @OptIn(ExperimentalCoroutinesApi::class) class PullRequestRepositoryImpTest { diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 82e084053..8b3aee2ff 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.fakes -import com.appunite.loudius.domain.PullRequestRepository +import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review From d375d1bd7191532229929e0dcfeb9b8ace1e24e7 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 27 Mar 2023 11:14:52 +0000 Subject: [PATCH 259/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/di/GithubModule.kt | 2 +- .../loudius/domain/repository/PullRequestRepository.kt | 2 +- .../com/appunite/loudius/network/utils/AuthInterceptor.kt | 2 +- .../com/appunite/loudius/ui/loading/LoadingViewModel.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 2 +- .../com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 6 +++--- .../appunite/loudius/domain/PullRequestRepositoryImpTest.kt | 2 +- .../java/com/appunite/loudius/network/NetworkTestDoubles.kt | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt index 87f456ce7..833a6c475 100644 --- a/app/src/main/java/com/appunite/loudius/di/GithubModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/GithubModule.kt @@ -14,8 +14,8 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import retrofit2.Retrofit +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt index 03f8498cb..93d4663e6 100644 --- a/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt @@ -7,9 +7,9 @@ import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.User -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import javax.inject.Inject interface PullRequestRepository { suspend fun getReviews( diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt index 946fe4a85..2a6f588ab 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.network.utils import com.appunite.loudius.domain.repository.AuthRepository -import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Response +import javax.inject.Inject class AuthInterceptor @Inject constructor( private val authRepository: AuthRepository, diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index b704e7697..8d55d9590 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -10,8 +10,8 @@ import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class LoadingAction { diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 719597b9c..b8f1bffc8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -8,8 +8,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index a647a4e8e..fb6b09f6b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -15,12 +15,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index dbbddcdcd..3e37c9bce 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -10,12 +10,12 @@ import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.User import io.mockk.coEvery import io.mockk.mockk -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @OptIn(ExperimentalCoroutinesApi::class) class PullRequestRepositoryImpTest { diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 0550b0716..7709d3f9a 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -7,12 +7,12 @@ import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit fun testOkHttpClient(authRepository: AuthRepository = FakeAuthRepository()) = OkHttpClient.Builder() From 7b9fb9866bd2ec4b228e91cc0b111b891660ae49 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Mar 2023 08:34:13 +0200 Subject: [PATCH 260/526] Move reviewer to ui.reviewers package --- .../appunite/loudius/{domain/model => ui/reviewers}/Reviewer.kt | 2 +- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 1 - .../com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 1 - .../com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) rename app/src/main/java/com/appunite/loudius/{domain/model => ui/reviewers}/Reviewer.kt (82%) diff --git a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/Reviewer.kt similarity index 82% rename from app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt rename to app/src/main/java/com/appunite/loudius/ui/reviewers/Reviewer.kt index 08f5267c8..65ca9363a 100644 --- a/app/src/main/java/com/appunite/loudius/domain/model/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/Reviewer.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.domain.model +package com.appunite.loudius.ui.reviewers data class Reviewer( val id: Int, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 6a1e01fba..8a6f53367 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusTopAppBar diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index fa6721bba..e6cf65218 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -9,7 +9,6 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.common.Screen import com.appunite.loudius.common.flatMap import com.appunite.loudius.domain.PullRequestRepository -import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index aac19b12c..8f2337bd2 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -1,7 +1,6 @@ package com.appunite.loudius.ui.reviewers import androidx.lifecycle.SavedStateHandle -import com.appunite.loudius.domain.model.Reviewer import com.appunite.loudius.fakes.FakePullRequestRepository import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.utils.WebException From bd8d2e3f5a4196dd8cccd09b97c28ed7d6a7fb33 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Mar 2023 08:44:10 +0200 Subject: [PATCH 261/526] Extract button alpha to variable --- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 8a6f53367..d7bbc4039 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -179,9 +179,10 @@ private fun NotifyButtonOrLoadingIndicator( reviewer: Reviewer, onNotifyClick: (ReviewersAction) -> Unit ) { + val buttonAlpha = if (reviewer.isLoading) 0f else 1f Box(contentAlignment = Center) { NotifyButton( - modifier = Modifier.alpha(if (reviewer.isLoading) 0f else 1f) + modifier = Modifier.alpha(buttonAlpha) ) { onNotifyClick(ReviewersAction.Notify(reviewer.login)) } if (reviewer.isLoading) { From 5956ce7bae8cfc8958f4628312d7c592739308e1 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Mar 2023 08:51:19 +0200 Subject: [PATCH 262/526] Rename test checking showing the loading --- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 8f2337bd2..458fae8a2 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -196,7 +196,7 @@ class ReviewersViewModelTest { } @Test - fun `WHEN notify action THEN show success snackbar`() = runTest { + fun `WHEN successful notify action THEN show loading indicator`() = runTest { viewModel = createViewModel() repository.setNotifyResponse { neverCompletingSuspension() } @@ -220,15 +220,6 @@ class ReviewersViewModelTest { assertEquals(ReviewersSnackbarType.FAILURE, viewModel.state.snackbarTypeShown) } - @Test - fun `WHEN successful notify action THEN show loading`() = runTest { - viewModel = createViewModel() - - viewModel.onAction(ReviewersAction.Notify("user1")) - - assertEquals(ReviewersSnackbarType.SUCCESS, viewModel.state.snackbarTypeShown) - } - @Test fun `GIVEN user login WHEN on snackbar dismiss action THEN snackbar is not shown`() = runTest { From 444cc5c886eb8ed3b99a7bbef1e187b523de39c6 Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 28 Mar 2023 06:55:44 +0000 Subject: [PATCH 263/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 2 +- .../appunite/loudius/ui/reviewers/ReviewersScreen.kt | 4 ++-- .../loudius/ui/reviewers/ReviewersViewModel.kt | 10 +++++----- .../loudius/fakes/FakePullRequestRepository.kt | 3 +-- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 12 ++++++------ 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index ea1803564..8eb6f8d48 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -49,7 +49,7 @@ fun PullRequestsScreen( viewModel.onAction(PulLRequestsAction.OnNavigateToReviewers) } } - PullRequestsScreenStateless( + PullRequestsScreenStateless( pullRequests = state.pullRequests, onAction = viewModel::onAction, isLoading = state.isLoading, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index d7bbc4039..a67afe36c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -177,12 +177,12 @@ private fun ReviewerItem( @Composable private fun NotifyButtonOrLoadingIndicator( reviewer: Reviewer, - onNotifyClick: (ReviewersAction) -> Unit + onNotifyClick: (ReviewersAction) -> Unit, ) { val buttonAlpha = if (reviewer.isLoading) 0f else 1f Box(contentAlignment = Center) { NotifyButton( - modifier = Modifier.alpha(buttonAlpha) + modifier = Modifier.alpha(buttonAlpha), ) { onNotifyClick(ReviewersAction.Notify(reviewer.login)) } if (reviewer.isLoading) { diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index e6cf65218..d828db107 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -14,12 +14,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -141,13 +141,13 @@ class ReviewersViewModel @Inject constructor( .onSuccess { state = state.copy( snackbarTypeShown = SUCCESS, - reviewers = updateReviewerLoadingState(userLogin, false) + reviewers = updateReviewerLoadingState(userLogin, false), ) } .onFailure { state = state.copy( snackbarTypeShown = FAILURE, - reviewers = updateReviewerLoadingState(userLogin, false) + reviewers = updateReviewerLoadingState(userLogin, false), ) } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 448d31953..41551917e 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -28,13 +28,12 @@ class FakePullRequestRepository : PullRequestRepository { when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) "notExistingPullRequestNumber" -> Result.failure( - WebException.UnknownError(404, null) + WebException.UnknownError(404, null), ) else -> Result.failure(WebException.NetworkError()) } } - private var lazyReviewsAnswer: suspend () -> Result> = { initialReviewsAnswer } private var lazyRequestedReviewersAnswer: suspend () -> Result = { initialRequestedReviewersAnswer } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 458fae8a2..98ce03992 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -10,10 +10,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -24,6 +20,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -203,10 +203,10 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("user1")) assertTrue( - viewModel.state.reviewers.first { it.login == "user1" }.isLoading + viewModel.state.reviewers.first { it.login == "user1" }.isLoading, ) { "Clicked item should have loading indicator" } assertTrue( - viewModel.state.reviewers.filterNot { it.login == "user1" }.none { it.isLoading } + viewModel.state.reviewers.filterNot { it.login == "user1" }.none { it.isLoading }, ) { "Only clicked item should have loading indicator" } } From 5c74adce95f21d3e84679c725840203a879d1422 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Mar 2023 09:12:40 +0200 Subject: [PATCH 264/526] Resolve conflicts with develop changes. --- .../main/java/com/appunite/loudius/di/DataSourceModule.kt | 2 -- .../main/java/com/appunite/loudius/di/RepositoryModule.kt | 8 +++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 773c8f03e..3fff62e27 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -2,8 +2,6 @@ package com.appunite.loudius.di import android.content.Context import com.appunite.loudius.domain.UserLocalDataSource -import com.appunite.loudius.domain.repository.AuthRepository -import com.appunite.loudius.domain.repository.AuthRepositoryImpl import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource import com.appunite.loudius.network.datasource.PullRequestDataSource diff --git a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt index fcaf7e603..7c68e1458 100644 --- a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt @@ -1,12 +1,10 @@ package com.appunite.loudius.di +import com.appunite.loudius.domain.UserLocalDataSource +import com.appunite.loudius.domain.repository.AuthRepository +import com.appunite.loudius.domain.repository.AuthRepositoryImpl import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.domain.repository.PullRequestRepositoryImpl -import com.appunite.loudius.domain.AuthRepository -import com.appunite.loudius.domain.AuthRepositoryImpl -import com.appunite.loudius.domain.PullRequestRepository -import com.appunite.loudius.domain.PullRequestRepositoryImpl -import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.PullRequestDataSource import com.appunite.loudius.network.datasource.UserDataSource From 973e7902e753d00ccaab01eff36e85ab82cb1fe6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Mar 2023 09:21:45 +0200 Subject: [PATCH 265/526] Move UserLocalDataSource.kt to the new package - store. --- app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt | 2 +- app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt | 2 +- .../com/appunite/loudius/domain/repository/AuthRepository.kt | 2 +- .../appunite/loudius/domain/{ => store}/UserLocalDataSource.kt | 2 +- .../java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt | 1 + .../java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt | 1 + 6 files changed, 6 insertions(+), 4 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/{ => store}/UserLocalDataSource.kt (95%) diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 3fff62e27..3a821088a 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -1,7 +1,7 @@ package com.appunite.loudius.di import android.content.Context -import com.appunite.loudius.domain.UserLocalDataSource +import com.appunite.loudius.domain.store.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource import com.appunite.loudius.network.datasource.PullRequestDataSource diff --git a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt index 7c68e1458..b1975846a 100644 --- a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt @@ -1,10 +1,10 @@ package com.appunite.loudius.di -import com.appunite.loudius.domain.UserLocalDataSource import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.domain.repository.AuthRepositoryImpl import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.domain.repository.PullRequestRepositoryImpl +import com.appunite.loudius.domain.store.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.PullRequestDataSource import com.appunite.loudius.network.datasource.UserDataSource diff --git a/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt index b64a16100..76a45188b 100644 --- a/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt @@ -1,6 +1,6 @@ package com.appunite.loudius.domain.repository -import com.appunite.loudius.domain.UserLocalDataSource +import com.appunite.loudius.domain.store.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.model.AccessToken import javax.inject.Inject diff --git a/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt similarity index 95% rename from app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt rename to app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt index c43a2edea..2f351afe3 100644 --- a/app/src/main/java/com/appunite/loudius/domain/UserLocalDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.domain +package com.appunite.loudius.domain.store import android.content.Context import android.content.SharedPreferences diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index 670a59b33..7bcf3bf34 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -1,6 +1,7 @@ package com.appunite.loudius.domain import com.appunite.loudius.domain.repository.AuthRepositoryImpl +import com.appunite.loudius.domain.store.UserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import io.mockk.coEvery import io.mockk.coVerify diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index 0d3db01b8..965ff160f 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -2,6 +2,7 @@ package com.appunite.loudius.domain import android.content.Context import android.content.SharedPreferences +import com.appunite.loudius.domain.store.UserLocalDataSource import io.mockk.every import io.mockk.mockk import io.mockk.verify From a72eba0d119fdb9e8679c2caab68a1a6377c9945 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Tue, 28 Mar 2023 07:30:05 +0000 Subject: [PATCH 266/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 713b8be5a..f657ef423 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -14,12 +14,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() From bbf421234e2b62631fd14e12c330192303fe65fb Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Mar 2023 08:40:05 +0200 Subject: [PATCH 267/526] Move companion object to the beginning of the class at the AuthDataSource.kt. --- .../appunite/loudius/network/datasource/AuthDataSource.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index ab94c9ab1..66cbb8588 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -23,6 +23,10 @@ class AuthNetworkDataSource @Inject constructor( private val authService: AuthService, ) : AuthDataSource { + companion object { + private const val BAD_VERIFICATION_CODE_ERROR = "bad_verification_code" + } + override suspend fun getAccessToken( clientId: String, clientSecret: String, @@ -43,10 +47,6 @@ class AuthNetworkDataSource @Inject constructor( else -> WebException.UnknownError(null, error) } } - - companion object { - private const val BAD_VERIFICATION_CODE_ERROR = "bad_verification_code" - } } object BadVerificationCodeException : Exception() From 0159f2245c4a9364cc1f53f2d0dd85381cfd794a Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Mar 2023 08:41:15 +0200 Subject: [PATCH 268/526] Rename UserDataSourceImpl.kt into UserDataSource.kt. --- .../datasource/{UserDataSourceImpl.kt => UserDataSource.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/appunite/loudius/network/datasource/{UserDataSourceImpl.kt => UserDataSource.kt} (100%) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSourceImpl.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt similarity index 100% rename from app/src/main/java/com/appunite/loudius/network/datasource/UserDataSourceImpl.kt rename to app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt From 7dca39624ae987b2e939c80d436b7e6efb580689 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Mar 2023 08:46:00 +0200 Subject: [PATCH 269/526] Move DefaultErrorResponse.kt into model.error package. --- .../network/{utils => model/error}/DefaultErrorResponse.kt | 2 +- .../java/com/appunite/loudius/network/utils/ApiCallUtil.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename app/src/main/java/com/appunite/loudius/network/{utils => model/error}/DefaultErrorResponse.kt (65%) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/DefaultErrorResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt similarity index 65% rename from app/src/main/java/com/appunite/loudius/network/utils/DefaultErrorResponse.kt rename to app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt index cdbc1bd27..94c3f95cc 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/DefaultErrorResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.network.utils +package com.appunite.loudius.network.model.error data class DefaultErrorResponse( val message: String, diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index 8f6580100..ee8b124f6 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,9 +1,10 @@ package com.appunite.loudius.network.utils +import com.appunite.loudius.network.model.error.DefaultErrorResponse import com.google.gson.Gson +import java.io.IOException import org.json.JSONException import retrofit2.HttpException -import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, From c2791ebf451bc3c993cd536701b1983171d3350d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Mar 2023 08:52:06 +0200 Subject: [PATCH 270/526] Move BadVerificationCodeException to the WebException.kt. --- .../appunite/loudius/network/datasource/AuthDataSource.kt | 4 +--- .../java/com/appunite/loudius/network/utils/WebException.kt | 5 +++++ .../java/com/appunite/loudius/ui/loading/LoadingViewModel.kt | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index 66cbb8588..01f9beb29 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -43,10 +43,8 @@ class AuthNetworkDataSource @Inject constructor( private fun AccessTokenResponse.mapErrorToException(): Exception { return when (error) { - BAD_VERIFICATION_CODE_ERROR -> BadVerificationCodeException + BAD_VERIFICATION_CODE_ERROR -> WebException.BadVerificationCodeException else -> WebException.UnknownError(null, error) } } } - -object BadVerificationCodeException : Exception() diff --git a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt index ebd06cc9f..a4068f9be 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt @@ -14,4 +14,9 @@ sealed class WebException : Exception() { * For example [IOException]. */ data class NetworkError(override val cause: Throwable? = null) : WebException() + + /** + * Thrown during authorization with incorrect verification code. + */ + object BadVerificationCodeException : WebException() } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index 8d55d9590..0352f0140 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.domain.repository.AuthRepository -import com.appunite.loudius.network.datasource.BadVerificationCodeException +import com.appunite.loudius.network.utils.WebException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -88,7 +88,7 @@ class LoadingViewModel @Inject constructor( } private fun resolveErrorType(it: Throwable) = when (it) { - is BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR + is WebException.BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR else -> LoadingErrorType.GENERIC_ERROR } } From 5ce89e2a26ea77dc039bc92516bd98e2fc67d416 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Tue, 28 Mar 2023 08:02:45 +0000 Subject: [PATCH 271/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index ee8b124f6..d2ddba2de 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -2,9 +2,9 @@ package com.appunite.loudius.network.utils import com.appunite.loudius.network.model.error.DefaultErrorResponse import com.google.gson.Gson -import java.io.IOException import org.json.JSONException import retrofit2.HttpException +import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, From 40fcbe01eca4d0a4e5d667c66596e8a4f4fd5a22 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 28 Mar 2023 10:22:35 +0200 Subject: [PATCH 272/526] Correct imports at tests. --- .../test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt | 3 +-- .../loudius/network/datasource/AuthNetworkDataSourceTest.kt | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index b74b4e30b..87cf9670d 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -1,7 +1,6 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.repository.AuthRepository -import com.appunite.loudius.network.datasource.BadVerificationCodeException import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.utils.WebException @@ -12,7 +11,7 @@ class FakeAuthRepository : AuthRepository { code: String, ): Result = when (code) { "validCode" -> Result.success("validToken") - "invalidCode" -> Result.failure(BadVerificationCodeException) + "invalidCode" -> Result.failure(WebException.BadVerificationCodeException) else -> Result.failure(WebException.UnknownError(null, null)) } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 94dd6f1c3..5bbfae321 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -68,7 +68,7 @@ class AuthNetworkDataSourceTest { val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") Assertions.assertEquals( - Result.failure(BadVerificationCodeException), + Result.failure(WebException.BadVerificationCodeException), result, ) } From 1a379556d97948b2b0185c890ab5eb2eca8509e5 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 28 Mar 2023 10:58:59 +0200 Subject: [PATCH 273/526] Add placeholder text for empty pull requests list --- .../ui/components/LoudiusPlaceholderText.kt | 43 +++++++++++++++++++ .../ui/pullrequests/PullRequestsScreen.kt | 11 +++++ .../loudius/ui/reviewers/ReviewersScreen.kt | 26 +++++++++++ app/src/main/res/values/strings.xml | 2 + 4 files changed, 82 insertions(+) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt new file mode 100644 index 000000000..8895b30ca --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt @@ -0,0 +1,43 @@ +package com.appunite.loudius.ui.components + +import androidx.annotation.StringRes +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.appunite.loudius.R +import com.appunite.loudius.ui.theme.LoudiusTheme + +@Composable +fun LoudiusPlaceholderText(@StringRes textId: Int, padding: PaddingValues) { + Box( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(id = textId), + textAlign = TextAlign.Center + ) + } +} + +@Preview(showSystemUi = true) +@Composable +fun PreviewLoudiusPlaceholderText() { + LoudiusTheme { + LoudiusPlaceholderText(R.string.you_dont_have_any_pull_request, PaddingValues(0.dp)) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 8eb6f8d48..fa8a5dbfe 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -31,6 +32,7 @@ import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusLoadingIndicator +import com.appunite.loudius.ui.components.LoudiusPlaceholderText import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import java.time.LocalDateTime @@ -72,6 +74,7 @@ private fun PullRequestsScreenStateless( onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) isLoading -> LoudiusLoadingIndicator() + pullRequests.isEmpty() -> EmptyListPlaceholder(padding) else -> PullRequestsList( pullRequests = pullRequests, modifier = Modifier.padding(padding), @@ -147,6 +150,14 @@ private fun RepoDetails(pullRequestTitle: String, repositoryName: String) { } } +@Composable +private fun EmptyListPlaceholder(padding: PaddingValues) { + LoudiusPlaceholderText( + textId = R.string.you_dont_have_any_pull_request, + padding = padding, + ) +} + @Preview("Pull requests - filled list") @Composable fun PullRequestsScreenPreview() { diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index a67afe36c..09391b2a1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -35,6 +36,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.ui.components.LoudiusErrorScreen import com.appunite.loudius.ui.components.LoudiusLoadingIndicator +import com.appunite.loudius.ui.components.LoudiusPlaceholderText import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS @@ -114,6 +116,7 @@ private fun ReviewersScreenStateless( when { isError -> LoudiusErrorScreen(onButtonClick = { onAction(ReviewersAction.OnTryAgain) }) isLoading -> LoudiusLoadingIndicator() + reviewers.isEmpty() -> EmptyListPlaceholder(padding) else -> ReviewersScreenContent( reviewers = reviewers, modifier = Modifier.padding(padding), @@ -237,6 +240,14 @@ private fun NotifyButton(modifier: Modifier = Modifier, onNotifyClick: () -> Uni } } +@Composable +private fun EmptyListPlaceholder(padding: PaddingValues) { + LoudiusPlaceholderText( + textId = R.string.you_dont_have_any_reviewers, + padding = padding, + ) +} + @Preview @Composable private fun ReviewerViewPreview() { @@ -268,3 +279,18 @@ fun DetailsScreenPreview() { ) } } +@Preview +@Composable +fun DetailsScreenNoReviewsPreview() { + LoudiusTheme { + ReviewersScreenStateless( + pullRequestNumber = "1", + reviewers = emptyList(), + isError = false, + isLoading = false, + onClickBackArrow = {}, + snackbarHostState = SnackbarHostState(), + onAction = {}, + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06773d8c5..86c19d940 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,6 @@ Awesome! Your collaborator have been pinged for some serious code review action! 🎉 Awesome! Your collaborator have been pinged for some serious code review action! 🎉 Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! + Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 + Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 From a42374ac41e2d7cc0c013734092def9f15713a60 Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 28 Mar 2023 10:58:44 +0000 Subject: [PATCH 274/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/components/LoudiusPlaceholderText.kt | 6 ++---- .../com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt index 8895b30ca..3335c4b3e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt @@ -1,7 +1,6 @@ package com.appunite.loudius.ui.components import androidx.annotation.StringRes -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -10,7 +9,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -25,11 +23,11 @@ fun LoudiusPlaceholderText(@StringRes textId: Int, padding: PaddingValues) { .padding(padding) .fillMaxSize() .padding(16.dp), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, ) { Text( text = stringResource(id = textId), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 09391b2a1..ceeda6d96 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -279,6 +279,7 @@ fun DetailsScreenPreview() { ) } } + @Preview @Composable fun DetailsScreenNoReviewsPreview() { From e03fbc7d3fb3b0083c543c0d1709d450fac22152 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 28 Mar 2023 14:17:01 +0200 Subject: [PATCH 275/526] Add new app icon --- app/src/main/AndroidManifest.xml | 2 +- app/src/main/ic_app-playstore.png | Bin 0 -> 22068 bytes .../drawable-v24/ic_launcher_foreground.xml | 30 ---- .../res/drawable/ic_launcher_background.xml | 170 ------------------ app/src/main/res/mipmap-anydpi-v26/ic_app.xml | 5 + .../res/mipmap-anydpi-v26/ic_app_round.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - .../res/mipmap-anydpi-v33/ic_launcher.xml | 6 - app/src/main/res/mipmap-hdpi/ic_app.png | Bin 0 -> 2044 bytes .../res/mipmap-hdpi/ic_app_foreground.png | Bin 0 -> 3401 bytes app/src/main/res/mipmap-hdpi/ic_app_round.png | Bin 0 -> 4148 bytes app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_app.png | Bin 0 -> 1374 bytes .../res/mipmap-mdpi/ic_app_foreground.png | Bin 0 -> 2149 bytes app/src/main/res/mipmap-mdpi/ic_app_round.png | Bin 0 -> 2570 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 0 bytes app/src/main/res/mipmap-xhdpi/ic_app.png | Bin 0 -> 2909 bytes .../res/mipmap-xhdpi/ic_app_foreground.png | Bin 0 -> 4622 bytes .../main/res/mipmap-xhdpi/ic_app_round.png | Bin 0 -> 5931 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 0 bytes app/src/main/res/mipmap-xxhdpi/ic_app.png | Bin 0 -> 4497 bytes .../res/mipmap-xxhdpi/ic_app_foreground.png | Bin 0 -> 7493 bytes .../main/res/mipmap-xxhdpi/ic_app_round.png | Bin 0 -> 9391 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 0 bytes app/src/main/res/mipmap-xxxhdpi/ic_app.png | Bin 0 -> 6365 bytes .../res/mipmap-xxxhdpi/ic_app_foreground.png | Bin 0 -> 10638 bytes .../main/res/mipmap-xxxhdpi/ic_app_round.png | Bin 0 -> 13716 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 0 bytes app/src/main/res/values/ic_app_background.xml | 4 + 35 files changed, 15 insertions(+), 217 deletions(-) create mode 100644 app/src/main/ic_app-playstore.png delete mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_app.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_app.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_app_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_app_round.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_app.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_app_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_app_round.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_app.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_app_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_app_round.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_app.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_app_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_app_round.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_app.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_app_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_app_round.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/ic_app_background.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5b8f929e8..8d4efb848 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,7 @@ android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" - android:icon="@mipmap/ic_launcher" + android:icon="@mipmap/ic_app" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.Loudius" diff --git a/app/src/main/ic_app-playstore.png b/app/src/main/ic_app-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..b8e8a7b0539714a9ee1eb263a68fd3b6ceff667f GIT binary patch literal 22068 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 7706ab9e6..000000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9cb..000000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_app.xml b/app/src/main/res/mipmap-anydpi-v26/ic_app.xml new file mode 100644 index 000000000..73954bb82 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_app.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml new file mode 100644 index 000000000..73954bb82 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6b78462d6..000000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6b78462d6..000000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml deleted file mode 100644 index b3e26b4c6..000000000 --- a/app/src/main/res/mipmap-anydpi-v33/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/mipmap-hdpi/ic_app.png b/app/src/main/res/mipmap-hdpi/ic_app.png new file mode 100644 index 0000000000000000000000000000000000000000..a51b2c59391cb80b715120c0bf2bec8adeb5bf9a GIT binary patch literal 2044 zcmV=??-I>|h;nj!J|Nm!qyE|tV*k!MiJ;|5ZopbigeBb=H zb6{d(qAl8@E!v_jvKwPDQ&?*1G(!P6X?gn8V6v&&b4F>C=H@zq#gy=+XiC^AYa3v4hOp!&L_Ab32@NBe@iSeyPPn=H zCrs9e6TZwzcx5CYr{S!h%|MaF@hT=^5X0lM1FODrh@*W9kq+SX>e>EP%)B)&)|K;w zntkw;$64`vj4F^NXYH%!*=tzqwx!v~)fa)L4(?rPXhJt`KbUWGqgZkmXsh@p9JdLy`m5&GqOO9DN9O` zt1$#J`LPZ&l0d?--m%LXLmuAVX_+ON`dNT0I^~Z{~MRzDno6|YCgp*2NU^z2O?D

(TiE#jb45M`*H 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 Kmw_-CKmY()leWbG diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2898 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 diff --git a/app/src/main/res/mipmap-mdpi/ic_app.png b/app/src/main/res/mipmap-mdpi/ic_app.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf4f04c5c23036842975e1e2a7d53c55aec7523 GIT binary patch literal 1374 zcmV-k1)=(hP)3$g6h=`4lK4jxHPI##G)VQAA>oHVFdB^}#waN#!^5(vGm+!mx zoO|w@w_?SzrLaL=#!7HF?nx?;gPGTYh7Io~nC~ac7R5(j(eKWBP;%`|6ALpRPX=gg z;t-BoSko#@mD=tX+}TB}z&KO4j6&BiuWU*I(bVn8_ZH7KL!z%Si0k`IDbK=JmY7!SyUCy{ub zr4Lq?3+&f@2^K(s+EU2<+e}&y+4boOZM1kj8S3^R^CFE+akiE1&RJfIABgF1D?w90dC8WRDuPpfyX zf!x=CXf%?*?5T{Lv5<=d6i(iU56hH zW;vR18&QepMr4gK>Su91SCm=QJSY$L+$)d zCrb&>F20f+3xTB~wgSw+C08f~in`DWGSecS4$U70`)NeEq&0d4Fl*N4!(_w9P*wmQf$yagYw+_>(M z^izAbHjVDzE_t0cb^HgLLf1q>8`FavPVUY)@!Z;n$$jX%_51?Jf)!RHo_JwH+KbyZ zrN6#))A~7VTo+&H#-gz|Kb`gxaUo6%lGltXW~8Uzhdy3hCz-GHQ gnV2_Vmn}u?KU5?k6g=k;NB{r;07*qoM6N<$f*Q1n4gdfE literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_app_foreground.png b/app/src/main/res/mipmap-mdpi/ic_app_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c27ee9ce718c24f1f03ac7eb2b0544d4979e633f GIT binary patch literal 2149 zcmd5;`8(TL7uQZHQEhEqEb&f_wA0i!r3R(7i>1^OBrRGMTVm-OAt*((6YJ25X65X#jD1g*J zQc_Y1Fh^UDZzCFLvNGTLJe1FXl$7i_n5~spy#KRyM%vsvPa^?Kq$0?1OOmheqg<`yA9x6oS`qN~;pkF}H zarQb}=v3kH2QYl{>fKq+z4}u6e4yNb3f|y564NFB;J;&nNR8eVL%?$;8GM>+d~~r7 zHx$5EtNOl#d!urSpF2G$UC$T{lsQ-C2R$m-9KL7leMt9GSQ z4Scsg$1C>d7uCD%t|+NCTPx3 zO*eAqy;vJfb4q`>-7}Egy2h8E*Tj=-lRZ2axr3!J)7~`P17u!TvN>w^F z{q?(q?u@sGe61T$I)a@+m9O||X;0Pgt4K?uo}JXsN5hmbomRryc3?+kCp;RPoToYG zx>>e;a5#L1ns8Jl0UJ}_gGu<9QkT7FWc~{dLn^?iHsy& z{CU3c6A19rSf0nN@4fe>(LFw96hC;%fYiLwCT!A|ZpAU1y9T~Yhy)sXc-(+RmndAK zMsjLWTe0l@j|>nsBT&qT(63!Soc9FxdspC=lW<2@1>id3%;QOu zWsawRg&tuRT_q>JGq35^{*HG@Mn@8rGab|mqjq>z`sIQb&^<3Mb(-J!TN9Lu1mCS2VDcLaN&%DLC`;abMzgzyc&?+C&&noJHT?P@zfR>nF?UNCNAn zuZ54jL<E0T$=^ymsYq?PD?d)IJ5X>QBs~N^JI`yN`O;kozSw+PG`$+(bvs zL9S_I+R5hxRBSukdJcC-C$yOeXZd9{%RgO04Y$lpi0%GsBy7=MYhdD16SxTy*Q9Uo z%`03D3KtJlG2IilGO@e1@Fvoo@QBTy@mTKep_6w3i^f51#8*#jL_hwOFavkd2|eT5 zXCHs9V0}}OHf%eezQ`Uo6zhmAvkU+h;Lykwb5FyK2gpqAc08mLd%uX{u%-Xp1%%MX za#n|Uf1kJ>vfSX94p?sV_>GYdw_dr$)XWmDB^F*d{;YQUQ870L)rdkaA`sh^r7afnhsEpVd8_lUU+#0?zdspI+Kjb$w=d!RE$zt~_wnFuf^x7+El z^VfBI6Li;90h7BGS{=6*)2${HIZDFYs5EJ|=}zw@ya*UlCZTn8prU5 zG`Lo|JZI7Dp8rke>a&(6RrA(_>J#h?z1@pe00^`dI|800^7T72xMYQVJ-p!Il*jMl z(8x34M)?&Hnf$_i??y?J-+1Tu9B=+qd&2WcYb%7?Pq`Fk+=k(n9oW==Xbq&3TJS?If+PB*!HB zWr&WOOR9rx8V1_?^@B|8gXV$T>Za-nl9~mgd%MvPPH(7)lK>9SX&^7o+kyW*lmEZv c-p+FY*@~=mWycTfZ{G(gnB5gys&!!6KPHX{?f?J) literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_app_round.png b/app/src/main/res/mipmap-mdpi/ic_app_round.png new file mode 100644 index 0000000000000000000000000000000000000000..ece789c79405cb2e0e3348c60cb9f5cca472b5a4 GIT binary patch literal 2570 zcmV+l3ib7gP)oYF8hrk7bvU&@5}BA~3)VD1tUtYQ#rD z9ub)V1dW9lKm~0SF%g$9d>{(Z_&|9SR5U#1&XfE}R)y33oqNx4=iV8dfvIP$Z_V}I zbN2c6K6~%8&%v;;F*e4+#<8l%FR~KDBly*R*EV%ujMaXBXpN<(e}OPw`yS0&Xq+gO zBu{o(5}yX}jCb08XpjL}+V}sr=8L#gVET^Tk{D$#FcnMY#8cJ+V+WyMvLreIJmVd{ zp%40Ej9SJ+Q3Ojbo?%g2f$0saC9zJjm_8+<%xWjq5tAnYvBN(8{1E^F^8|Y zN7d8<$aj;;Z|{31w;09qn4!qxieL`OY)baUA*iMvNCJz5)fKC0T_6;I3qm&kci&c1 zu`ezGHFY*1m#I+53;)$l98xHA@OgT+##a&rIW7WKLLEH)`NnM^uNFFH6p%5tCDU(` z{@XA-#=4UmR}vLoQt zT~qsgU%s&sLexPuW)LAq{cdKju`qYnMdt3g&Xv*iBS;nNnd9_sp#gfZMHD?ttK|$yv>{+A7zd+Rcyd-(3aP@g(x+n+1m z4Z)Rq^Ns9kW>lh@tC~F5sHembP8zpiU`XDDZ|N0)8#U}_E^;Nfk+boACcV3McqKF|A|N=mWkeqf6R`m9VuQGmz2w<`ULo=U zqgnxpvWnyAJLEDqbfndAwVh**0QaqCW7y_n56>gZ#e0Gz2$lT)V)?6##d!0=IJwJJ2cUypJqXoyA<87i5cS$@+og zL^*S{ob-AJ16YfebD7ePRg^|oc)1j~5!mJRi^_P`=W^sm2@#SRB$bv4}G zm$@6M#|NSS+`K(2x^wlMUp?tj;CNc(H+#35`W~nN?Tog)ncOFxmp&Xdc@Byd`;qOk z>nZOXXRGD>PXUaYNh)72E2OxQP@pR(u0Lz!%bdBo=hj``~b2dV8cjqIXB#?o$ z#MKHA*y*&tmJ{bkICrI#7)LJ?m5wMShLAE{*G|Zj*gAPuaNRuZHMyJo7PY>s<2=tI zyarMMK@Jz>z%vXWK!J;r0be{?QT_ym%?GHG%CDh$H2-O^Z8zG-=XQHD|RF(CAAwE9S4kT?_)oI?^iaTE}k| zJW3$ll2W`J_R)U~@L2yn!|Zk7s5{i7sWh70lCiWIgVnL1LqQV1`tVAjloFkyAk+3A zB|N_W*6Wvu+fyJ|73t@iQPFHdM#Qh@b?3%^j%6Fkm9W{ldxmcuuy3Hy3zY(WmW2B} z^JK+-p7!S#Bf+iN;OAmXG)0LgsuSk^6FC>l=c$i;02zv>Rm$@yEkYH{O4=N{yCt5L z#m>qCC!emPZAeX#kzrV(DRrwNDsToRL4I$hk;m;wv88E#Zxn)sLVgeiJdiwZ54eC+ zK$p8{N<3hxo<9EOUuu+<;7h8VEYuKggE^0+zFJEho&guXe($d-JM;v?wAz%2%6ob7 zTnPjf^0Ic~u>vw78*{*I4lL)EpAHUu#~n>m211FcWro>VojI|eVKOByLZXO@IG5Ft z9rM}FnAzEw6LXK6yl8Nz88gBjrN=4BGk>z+{Mv=hSPf(-k0Nq>1fz`jizX)yS&-R4 zvXfr^@j}dnIfHf^e+|Jy!k`3h0-oEkbk>GT8y2@?sX++K4p_}{u#W`v@${#$kO5iS zqh@X-+26+;m@D9b4W=nM7%nyGYvJL~n_vI^np3M6)VFVralwMv!9hw{9WN-ie!LHM z)sC!KSN)m=^^gHskO|oj%=2Syu#7UvV}x%IzB~VgXXX?x`s3QeDf9Q7|Hr~c)GRkM zm-cpSj4{qmjO$5Bg#Zjm6CZWIP6$>FM&N?L%pZ4P|42GPtt`k*S2|CO;nj z!Y`&j^_y8goc=O^XS~BVr4RaI3_n%r3bTjVup-D5E$NX6I>d=s9sK`?uPDGX-XWhq gijx$^gu>7N2e4ndv@ZI(v;Y7A07*qoM6N<$f-dO(5dZ)H literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i diff --git a/app/src/main/res/mipmap-xhdpi/ic_app.png b/app/src/main/res/mipmap-xhdpi/ic_app.png new file mode 100644 index 0000000000000000000000000000000000000000..66c970f06ee9ce3c137acfa72644c5c24bcc0db9 GIT binary patch literal 2909 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_app_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_app_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..aa9446e2a4994d95a72eb9a0b1bd6fb435b08fb4 GIT binary patch literal 4622 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_app_round.png b/app/src/main/res/mipmap-xhdpi/ic_app_round.png new file mode 100644 index 0000000000000000000000000000000000000000..93951dc0da88e211c011e31bed54215e018fbc14 GIT binary patch literal 5931 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 N002ovPDHLkV1kzfa!LRI literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070fe34c611c42c0d3ad3013a0dce358be0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_app_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_app_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..974c7678a295b0f4e13b978af6a777bff1b10042 GIT binary patch literal 10638 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_app_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_app_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8efb77eab73fbb6b1a395a50308e75bb5aa4b62b GIT binary patch literal 13716 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e6fa1074b79ccd52ef67ac15c5637e85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3844 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s diff --git a/app/src/main/res/values/ic_app_background.xml b/app/src/main/res/values/ic_app_background.xml new file mode 100644 index 000000000..18b34e3c8 --- /dev/null +++ b/app/src/main/res/values/ic_app_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file From 4ca2d576ddb4c90b781319668985ecb879eb7f2e Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 29 Mar 2023 09:18:09 +0200 Subject: [PATCH 276/526] Add new line at the end of files --- app/src/main/res/mipmap-anydpi-v26/ic_app.xml | 2 +- app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml | 2 +- app/src/main/res/values/ic_app_background.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_app.xml b/app/src/main/res/mipmap-anydpi-v26/ic_app.xml index 73954bb82..3cdee3cb1 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_app.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_app.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml index 73954bb82..3cdee3cb1 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_app_round.xml @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values/ic_app_background.xml b/app/src/main/res/values/ic_app_background.xml index 18b34e3c8..24ad8730d 100644 --- a/app/src/main/res/values/ic_app_background.xml +++ b/app/src/main/res/values/ic_app_background.xml @@ -1,4 +1,4 @@ #FFFFFF - \ No newline at end of file + From 146692935bc4b0dd3e995fa2f7ecb833cf39a601 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 29 Mar 2023 12:28:31 +0200 Subject: [PATCH 277/526] Add license header to kotlin files --- .../appunite/loudius/ExampleInstrumentedTest.kt | 16 ++++++++++++++++ .../com/appunite/loudius/LoudiusApplication.kt | 16 ++++++++++++++++ .../java/com/appunite/loudius/MainActivity.kt | 16 ++++++++++++++++ .../com/appunite/loudius/common/Constants.kt | 16 ++++++++++++++++ .../appunite/loudius/common/ResultExtension.kt | 16 ++++++++++++++++ .../java/com/appunite/loudius/common/Screen.kt | 16 ++++++++++++++++ .../com/appunite/loudius/di/APIQualifiers.kt | 16 ++++++++++++++++ .../com/appunite/loudius/di/DataSourceModule.kt | 16 ++++++++++++++++ .../com/appunite/loudius/di/NetworkModule.kt | 16 ++++++++++++++++ .../com/appunite/loudius/di/RepositoryModule.kt | 16 ++++++++++++++++ .../com/appunite/loudius/di/ServiceModule.kt | 16 ++++++++++++++++ .../loudius/domain/repository/AuthRepository.kt | 16 ++++++++++++++++ .../domain/repository/PullRequestRepository.kt | 16 ++++++++++++++++ .../loudius/domain/store/UserLocalDataSource.kt | 16 ++++++++++++++++ .../loudius/network/datasource/AuthDataSource.kt | 16 ++++++++++++++++ .../datasource/PullRequestsNetworkDataSource.kt | 16 ++++++++++++++++ .../loudius/network/datasource/UserDataSource.kt | 16 ++++++++++++++++ .../loudius/network/model/AccessTokenResponse.kt | 16 ++++++++++++++++ .../loudius/network/model/PullRequest.kt | 16 ++++++++++++++++ .../network/model/PullRequestsResponse.kt | 16 ++++++++++++++++ .../loudius/network/model/RequestedReviewer.kt | 16 ++++++++++++++++ .../network/model/RequestedReviewersResponse.kt | 16 ++++++++++++++++ .../com/appunite/loudius/network/model/Review.kt | 16 ++++++++++++++++ .../loudius/network/model/ReviewState.kt | 16 ++++++++++++++++ .../com/appunite/loudius/network/model/User.kt | 16 ++++++++++++++++ .../network/model/error/DefaultErrorResponse.kt | 16 ++++++++++++++++ .../network/model/request/NotifyRequestBody.kt | 16 ++++++++++++++++ .../loudius/network/services/AuthService.kt | 16 ++++++++++++++++ .../network/services/PullRequestsService.kt | 16 ++++++++++++++++ .../loudius/network/services/UserService.kt | 16 ++++++++++++++++ .../loudius/network/utils/ApiCallUtil.kt | 16 ++++++++++++++++ .../loudius/network/utils/AuthInterceptor.kt | 16 ++++++++++++++++ .../network/utils/LocalDateTimeDeserializer.kt | 16 ++++++++++++++++ .../loudius/network/utils/RequestErrorParser.kt | 16 ++++++++++++++++ .../loudius/network/utils/WebException.kt | 16 ++++++++++++++++ .../loudius/ui/components/LoudiusErrorDialog.kt | 16 ++++++++++++++++ .../loudius/ui/components/LoudiusErrorScreen.kt | 16 ++++++++++++++++ .../ui/components/LoudiusLoadingIndicator.kt | 16 ++++++++++++++++ .../ui/components/LoudiusOutlinedButton.kt | 16 ++++++++++++++++ .../ui/components/LoudiusPlaceholderText.kt | 16 ++++++++++++++++ .../loudius/ui/components/LoudiusTopAppBar.kt | 16 ++++++++++++++++ .../appunite/loudius/ui/loading/LoadingScreen.kt | 16 ++++++++++++++++ .../loudius/ui/loading/LoadingViewModel.kt | 16 ++++++++++++++++ .../com/appunite/loudius/ui/login/LoginScreen.kt | 16 ++++++++++++++++ .../ui/pullrequests/PullRequestsScreen.kt | 16 ++++++++++++++++ .../ui/pullrequests/PullRequestsViewModel.kt | 16 ++++++++++++++++ .../appunite/loudius/ui/reviewers/Reviewer.kt | 16 ++++++++++++++++ .../loudius/ui/reviewers/ReviewersScreen.kt | 16 ++++++++++++++++ .../loudius/ui/reviewers/ReviewersViewModel.kt | 16 ++++++++++++++++ .../java/com/appunite/loudius/ui/theme/Color.kt | 16 ++++++++++++++++ .../java/com/appunite/loudius/ui/theme/Theme.kt | 16 ++++++++++++++++ .../java/com/appunite/loudius/ui/theme/Type.kt | 16 ++++++++++++++++ .../loudius/ui/utils/BottomBorderModifier.kt | 16 ++++++++++++++++ .../loudius/domain/AuthRepositoryImplTest.kt | 16 ++++++++++++++++ .../domain/PullRequestRepositoryImpTest.kt | 16 ++++++++++++++++ .../loudius/domain/UserLocalDataSourceTest.kt | 16 ++++++++++++++++ .../appunite/loudius/fakes/FakeAuthRepository.kt | 16 ++++++++++++++++ .../loudius/fakes/FakePullRequestDataSource.kt | 16 ++++++++++++++++ .../loudius/fakes/FakePullRequestRepository.kt | 16 ++++++++++++++++ .../loudius/network/AuthInterceptorTest.kt | 16 ++++++++++++++++ .../loudius/network/NetworkTestDoubles.kt | 16 ++++++++++++++++ .../datasource/AuthNetworkDataSourceTest.kt | 16 ++++++++++++++++ .../PullRequestsNetworkDataSourceTest.kt | 16 ++++++++++++++++ .../network/datasource/UserDataSourceTest.kt | 16 ++++++++++++++++ .../loudius/ui/loading/LoadingViewModelTest.kt | 16 ++++++++++++++++ .../ui/pullrequests/PullRequestsViewModelTest.kt | 16 ++++++++++++++++ .../ui/reviewers/ReviewersViewModelTest.kt | 16 ++++++++++++++++ .../appunite/loudius/util/CoroutinesHelpers.kt | 16 ++++++++++++++++ .../java/com/appunite/loudius/util/Defaults.kt | 16 ++++++++++++++++ .../loudius/util/MainDispatcherExtension.kt | 16 ++++++++++++++++ 70 files changed, 1120 insertions(+) diff --git a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt index f57da3ce0..5decb6a8a 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius import androidx.test.ext.junit.runners.AndroidJUnit4 diff --git a/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt b/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt index 737f76879..bf5769404 100644 --- a/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt +++ b/app/src/main/java/com/appunite/loudius/LoudiusApplication.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius import android.app.Application diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 76524e990..e9c4a36cd 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius import android.os.Bundle diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index 60e2ea798..3fed85a92 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.common object Constants { diff --git a/app/src/main/java/com/appunite/loudius/common/ResultExtension.kt b/app/src/main/java/com/appunite/loudius/common/ResultExtension.kt index 05ab0c91c..be849d9a8 100644 --- a/app/src/main/java/com/appunite/loudius/common/ResultExtension.kt +++ b/app/src/main/java/com/appunite/loudius/common/ResultExtension.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.common inline fun Result.flatMap(mapper: (value: T) -> Result): Result = diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index 87fd303b9..b2a68b0e3 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.common import androidx.navigation.NamedNavArgument diff --git a/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt b/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt index 7ec2c60d4..a67b01536 100644 --- a/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt +++ b/app/src/main/java/com/appunite/loudius/di/APIQualifiers.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.di import javax.inject.Qualifier diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 3a821088a..b2aa474c4 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.di import android.content.Context diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index e20a73a55..ead0c075c 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.di import com.appunite.loudius.common.Constants diff --git a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt index b1975846a..47b489fd5 100644 --- a/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/RepositoryModule.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.di import com.appunite.loudius.domain.repository.AuthRepository diff --git a/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt index 200689fc6..2745bc719 100644 --- a/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/ServiceModule.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.di import com.appunite.loudius.network.services.AuthService diff --git a/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt index 76a45188b..afe36052e 100644 --- a/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/AuthRepository.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.domain.repository import com.appunite.loudius.domain.store.UserLocalDataSource diff --git a/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt index 93d4663e6..ba5793eb0 100644 --- a/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt +++ b/app/src/main/java/com/appunite/loudius/domain/repository/PullRequestRepository.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.domain.repository import com.appunite.loudius.common.flatMap diff --git a/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt index 2f351afe3..ae46c7024 100644 --- a/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.domain.store import android.content.Context diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index 01f9beb29..e66398772 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.datasource import com.appunite.loudius.common.flatMap diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 829cac644..17efb604e 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt index a5eaa1b93..4bfacf3c6 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.User diff --git a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt index aededaefd..cb55b63a9 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/AccessTokenResponse.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model typealias AccessToken = String diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt index 50180c904..0be70ac02 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model import com.appunite.loudius.common.Constants diff --git a/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt index 03fa49905..1670a8a72 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/PullRequestsResponse.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model data class PullRequestsResponse( diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt index 3e5533f8f..b05c0985e 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewer.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model data class RequestedReviewer( diff --git a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt index 908ed5c36..f88a6da98 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/RequestedReviewersResponse.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model data class RequestedReviewersResponse( diff --git a/app/src/main/java/com/appunite/loudius/network/model/Review.kt b/app/src/main/java/com/appunite/loudius/network/model/Review.kt index 020a6c903..17b2c5ec1 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/Review.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/Review.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model import java.time.LocalDateTime diff --git a/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt b/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt index 1ea1bb9f7..c194401e1 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/ReviewState.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model enum class ReviewState { diff --git a/app/src/main/java/com/appunite/loudius/network/model/User.kt b/app/src/main/java/com/appunite/loudius/network/model/User.kt index d5f1e6b38..dbc12a21d 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/User.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/User.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model data class User(val id: Int, val login: String) diff --git a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt index 94c3f95cc..8d1764fb2 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model.error data class DefaultErrorResponse( diff --git a/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt index 85a7e3384..d7544f817 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/request/NotifyRequestBody.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.model.request data class NotifyRequestBody( diff --git a/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt b/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt index 2ca324698..4b0fede65 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/AuthService.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.services import com.appunite.loudius.network.model.AccessTokenResponse diff --git a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt index ba8877a2e..9ccd0eb34 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/PullRequestsService.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.services import com.appunite.loudius.network.model.PullRequestsResponse diff --git a/app/src/main/java/com/appunite/loudius/network/services/UserService.kt b/app/src/main/java/com/appunite/loudius/network/services/UserService.kt index 6d9f62933..58ffeb43a 100644 --- a/app/src/main/java/com/appunite/loudius/network/services/UserService.kt +++ b/app/src/main/java/com/appunite/loudius/network/services/UserService.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.services import com.appunite.loudius.network.model.User diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index d2ddba2de..065ee7fea 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.utils import com.appunite.loudius.network.model.error.DefaultErrorResponse diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt index 2a6f588ab..eb7bc1f46 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.utils import com.appunite.loudius.domain.repository.AuthRepository diff --git a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt index 500fa25f2..9701839fe 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.utils import com.google.gson.JsonDeserializationContext diff --git a/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt b/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt index 388cc4b5b..ae8e76198 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/RequestErrorParser.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.utils interface RequestErrorParser { diff --git a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt index a4068f9be..d2521e9f5 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.utils import java.io.IOException diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index a9cb7c500..4b7806270 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.material3.AlertDialog diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index 2651cf06b..d48204926 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.foundation.Image diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index 4f9e87104..aea8523de 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.foundation.layout.Box diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt index ddfff695b..c84d42d3f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.foundation.layout.fillMaxWidth diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt index 3335c4b3e..9215c7235 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.annotation.StringRes diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index e2ead0f34..0fd86d6cb 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.material3.ExperimentalMaterial3Api diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt index b3cc99389..f3fc3068d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.loading import android.content.Intent diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt index 0352f0140..12e406a3b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.loading import androidx.compose.runtime.getValue diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 74bfcbee2..c34150a26 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.login import android.content.Context diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index fa8a5dbfe..c1650e5a2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + @file:OptIn(ExperimentalMaterial3Api::class) package com.appunite.loudius.ui.pullrequests diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index b8f1bffc8..7da619eb6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.pullrequests import androidx.compose.runtime.getValue diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/Reviewer.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/Reviewer.kt index 65ca9363a..024e527a3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/Reviewer.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/Reviewer.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.reviewers data class Reviewer( diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index ceeda6d96..cfae289ab 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.reviewers import androidx.compose.foundation.Image diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index f657ef423..2d31f4141 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.reviewers import androidx.compose.runtime.getValue diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt index dd0bade0b..a9deacbf3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt index 37e2714bc..d29d43031 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.theme import android.app.Activity diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt index f08a44f91..05075b5e9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt +++ b/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.theme import androidx.compose.material3.Typography diff --git a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt index b0f5ebd5b..6df173163 100644 --- a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt +++ b/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.utils import androidx.compose.ui.Modifier diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index 7bcf3bf34..ee9f86d17 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.domain import com.appunite.loudius.domain.repository.AuthRepositoryImpl diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index 3e37c9bce..ae9439231 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.domain import com.appunite.loudius.domain.repository.PullRequestRepositoryImpl diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index 965ff160f..aef16995e 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.domain import android.content.Context diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 87cf9670d..026dc67ed 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.fakes import com.appunite.loudius.domain.repository.AuthRepository diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index 6afd23251..e27cb73c5 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.fakes import com.appunite.loudius.network.datasource.PullRequestDataSource diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 5fc9f5a49..c2eb525cc 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.fakes import com.appunite.loudius.domain.repository.PullRequestRepository diff --git a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt index cc72e854a..654eabeac 100644 --- a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + @file:OptIn(ExperimentalCoroutinesApi::class) package com.appunite.loudius.network diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 7709d3f9a..801be37cc 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network import com.appunite.loudius.domain.repository.AuthRepository diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 5bbfae321..141972b37 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + @file:OptIn(ExperimentalCoroutinesApi::class) package com.appunite.loudius.network.datasource diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 430812208..5f3d2a18d 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.PullRequestsResponse diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index a41a25b75..a5f8e8ae2 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.RequestedReviewersResponse diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt index 8a6444048..433c7ce6f 100644 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.loading import com.appunite.loudius.fakes.FakeAuthRepository diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index dde2eab22..ebbf4aacb 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + @file:OptIn(ExperimentalCoroutinesApi::class) package com.appunite.loudius.ui.pullrequests diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 98ce03992..af7f639ed 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.reviewers import androidx.lifecycle.SavedStateHandle diff --git a/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt b/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt index 7a7d6155b..a8e7ff817 100644 --- a/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt +++ b/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.utils import kotlinx.coroutines.suspendCancellableCoroutine diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index b24d1ffdd..810e21f4c 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.util import com.appunite.loudius.network.model.PullRequest diff --git a/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt index e429203e4..b4f48d1f4 100644 --- a/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt +++ b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.util import kotlinx.coroutines.Dispatchers From 28145a08253c281aae4dba15149100d2087e4a60 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 30 Mar 2023 08:56:40 +0200 Subject: [PATCH 278/526] Implement handling not authorized user. Implement handling not authorized user. --- .../java/com/appunite/loudius/MainActivity.kt | 30 +++++++++++++ .../appunite/loudius/di/DataSourceModule.kt | 14 ++++++- .../com/appunite/loudius/di/NetworkModule.kt | 10 +++-- .../intercept/AuthFailureInterceptor.kt | 42 +++++++++++++++++++ .../network/intercept/AuthInterceptor.kt | 33 +++++++++++++++ .../loudius/network/utils/ApiCallUtil.kt | 2 +- .../network/utils/AuthFailureHandler.kt | 23 ++++++++++ .../loudius/network/utils/AuthInterceptor.kt | 17 -------- .../com/appunite/loudius/ui/MainViewModel.kt | 35 ++++++++++++++++ app/src/main/res/values/strings.xml | 2 +- 10 files changed, 184 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt create mode 100644 app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt delete mode 100644 app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 76524e990..9058af0b2 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -1,18 +1,23 @@ package com.appunite.loudius import android.os.Bundle +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navDeepLink import com.appunite.loudius.common.Constants.REDIRECT_URL import com.appunite.loudius.common.Screen +import com.appunite.loudius.ui.MainViewModel import com.appunite.loudius.ui.loading.LoadingScreen import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.pullrequests.PullRequestsScreen @@ -22,6 +27,8 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { + + private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -32,6 +39,11 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background, ) { val navController = rememberNavController() + + LaunchedEffect(viewModel.state.authFailureEvent) { + navigateToLoginOnAuthFailure(navController) + } + NavHost( navController = navController, startDestination = Screen.Login.route, @@ -83,4 +95,22 @@ class MainActivity : ComponentActivity() { } } } + + private fun navigateToLoginOnAuthFailure(navController: NavHostController) { + if (viewModel.state.authFailureEvent != null) { + showAuthFailureToast() + navController.navigate(Screen.Login.route) { + popUpTo(navController.graph.id) { inclusive = true } + } + viewModel.onAuthFailureHandled() + } + } + + private fun showAuthFailureToast() { + Toast.makeText( + this@MainActivity, + getString(R.string.user_unauthorized_message), + Toast.LENGTH_LONG + ).show() + } } diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 3a821088a..10e1bb019 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -11,6 +11,8 @@ import com.appunite.loudius.network.datasource.UserDataSourceImpl import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.services.UserService +import com.appunite.loudius.network.utils.AuthFailureHandler +import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,12 +26,16 @@ object DataSourceModule { @Provides @Singleton - fun providePullRequestNetworkDataSource(service: PullRequestsService): PullRequestDataSource = + fun providePullRequestNetworkDataSource( + service: PullRequestsService, + ): PullRequestDataSource = PullRequestsNetworkDataSource(service) @Provides @Singleton - fun provideUserDataSource(userService: UserService): UserDataSource = + fun provideUserDataSource( + userService: UserService, + ): UserDataSource = UserDataSourceImpl(userService) @Singleton @@ -42,4 +48,8 @@ object DataSourceModule { fun provideAuthNetworkDataSource( service: AuthService, ): AuthDataSource = AuthNetworkDataSource(service) + + @Singleton + @Provides + fun provideAuthManager(): AuthFailureHandler = AuthFailureHandlerImpl() } diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index e20a73a55..e2c96c457 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -1,7 +1,9 @@ package com.appunite.loudius.di import com.appunite.loudius.common.Constants -import com.appunite.loudius.network.utils.AuthInterceptor +import com.appunite.loudius.network.intercept.AuthFailureInterceptor +import com.appunite.loudius.network.intercept.AuthInterceptor +import com.appunite.loudius.network.utils.AuthFailureHandler import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson @@ -10,12 +12,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import java.time.LocalDateTime +import javax.inject.Singleton import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -61,9 +63,11 @@ object NetworkModule { @BaseAPI baseAPIUrl: String, loggingInterceptor: HttpLoggingInterceptor, authInterceptor: AuthInterceptor, + authFailureHandler: AuthFailureHandler, ): Retrofit { val okHttpClient = OkHttpClient.Builder() .addInterceptor(authInterceptor) + .addInterceptor(AuthFailureInterceptor(authFailureHandler)) .addInterceptor(loggingInterceptor) .build() diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt new file mode 100644 index 000000000..3b4d469bd --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.network.intercept + +import com.appunite.loudius.network.utils.AuthFailureHandler +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import okhttp3.Interceptor +import okhttp3.Response + +class AuthFailureInterceptor @Inject constructor( + private val authFailureHandler: AuthFailureHandler, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + if (response.code == 401) { + CoroutineScope(Dispatchers.IO).launch { + authFailureHandler.emitAuthFailure() + } + } + + return response + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt new file mode 100644 index 000000000..5e4b1d101 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.network.intercept + +import com.appunite.loudius.domain.repository.AuthRepository +import javax.inject.Inject +import okhttp3.Interceptor +import okhttp3.Response + +class AuthInterceptor @Inject constructor( + private val authRepository: AuthRepository, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val authenticatedRequest = chain.request().newBuilder() + .addHeader("Authorization", "Bearer ${authRepository.getAccessToken()}") + .build() + return chain.proceed(authenticatedRequest) + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index 8f6580100..8cffbb064 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.network.utils import com.google.gson.Gson +import java.io.IOException import org.json.JSONException import retrofit2.HttpException -import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt new file mode 100644 index 000000000..096304c4c --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -0,0 +1,23 @@ +package com.appunite.loudius.network.utils + +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow + +interface AuthFailureHandler { + suspend fun emitAuthFailure() + + val authFailureFlow: SharedFlow +} + +@Singleton +class AuthFailureHandlerImpl @Inject constructor() : AuthFailureHandler { + + private val _logoutFlow = MutableSharedFlow() + override val authFailureFlow: SharedFlow = _logoutFlow + + override suspend fun emitAuthFailure() { + _logoutFlow.emit(Unit) + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt deleted file mode 100644 index 2a6f588ab..000000000 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthInterceptor.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.appunite.loudius.network.utils - -import com.appunite.loudius.domain.repository.AuthRepository -import okhttp3.Interceptor -import okhttp3.Response -import javax.inject.Inject - -class AuthInterceptor @Inject constructor( - private val authRepository: AuthRepository, -) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val authenticatedRequest = chain.request().newBuilder() - .addHeader("Authorization", "Bearer ${authRepository.getAccessToken()}") - .build() - return chain.proceed(authenticatedRequest) - } -} diff --git a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt new file mode 100644 index 000000000..dd1e99116 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt @@ -0,0 +1,35 @@ +package com.appunite.loudius.ui + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.appunite.loudius.network.utils.AuthFailureHandler +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +data class MainState( + val authFailureEvent: Unit? = null +) + +@HiltViewModel +class MainViewModel @Inject constructor(private val authFailureHandler: AuthFailureHandler) : + ViewModel() { + + var state by mutableStateOf(MainState()) + private set + + init { + viewModelScope.launch { + authFailureHandler.authFailureFlow.collect { + state = MainState(Unit) + } + } + } + + fun onAuthFailureHandled() { + state = state.copy(authFailureEvent = null) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06773d8c5..9e9decf7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,7 @@ Try again Something went wrong…\nYou need to log in again. Take me to login - Awesome! Your collaborator have been pinged for some serious code review action! 🎉 Awesome! Your collaborator have been pinged for some serious code review action! 🎉 Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! + "Unauthorized collaborator detected! Please login again." From 9c62047919593a2f5c85e75f0080ad4388029064 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 30 Mar 2023 12:18:32 +0200 Subject: [PATCH 279/526] Add tests for handling the auth failure. --- .../com/appunite/loudius/ui/MainViewModel.kt | 2 +- .../loudius/fakes/FakeAuthFailureHandler.kt | 30 +++++++ .../loudius/network/NetworkTestDoubles.kt | 26 +++--- .../intercept/AuthFailureInterceptorTest.kt | 86 +++++++++++++++++++ .../{ => intercept}/AuthInterceptorTest.kt | 20 ++++- .../appunite/loudius/ui/MainViewModelTest.kt | 60 +++++++++++++ 6 files changed, 212 insertions(+), 12 deletions(-) create mode 100644 app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt create mode 100644 app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt rename app/src/test/java/com/appunite/loudius/network/{ => intercept}/AuthInterceptorTest.kt (67%) create mode 100644 app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt diff --git a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt index dd1e99116..e5584b8e8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt @@ -30,6 +30,6 @@ class MainViewModel @Inject constructor(private val authFailureHandler: AuthFail } fun onAuthFailureHandled() { - state = state.copy(authFailureEvent = null) + state = MainState(null) } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt new file mode 100644 index 000000000..12a04e937 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.fakes + +import com.appunite.loudius.network.utils.AuthFailureHandler +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow + +class FakeAuthFailureHandler : AuthFailureHandler { + private val _authFailureFlow = MutableSharedFlow() + override val authFailureFlow: SharedFlow = _authFailureFlow + + override suspend fun emitAuthFailure() { + _authFailureFlow.emit(Unit) + } +} diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 7709d3f9a..508c8a744 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -2,25 +2,31 @@ package com.appunite.loudius.network import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.fakes.FakeAuthRepository -import com.appunite.loudius.network.utils.AuthInterceptor +import com.appunite.loudius.network.intercept.AuthFailureInterceptor +import com.appunite.loudius.network.intercept.AuthInterceptor +import com.appunite.loudius.network.utils.AuthFailureHandler +import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit -fun testOkHttpClient(authRepository: AuthRepository = FakeAuthRepository()) = - OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.SECONDS) - .readTimeout(1, TimeUnit.SECONDS) - .writeTimeout(1, TimeUnit.SECONDS) - .addInterceptor(AuthInterceptor(authRepository)) - .build() +fun testOkHttpClient( + authRepository: AuthRepository = FakeAuthRepository(), + authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl() +) = OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.SECONDS) + .readTimeout(1, TimeUnit.SECONDS) + .writeTimeout(1, TimeUnit.SECONDS) + .addInterceptor(AuthInterceptor(authRepository)) + .addInterceptor(AuthFailureInterceptor(authFailureHandler)) + .build() private fun testGson() = GsonBuilder() diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt new file mode 100644 index 000000000..520853881 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.appunite.loudius.network.intercept + +import com.appunite.loudius.fakes.FakeAuthFailureHandler +import com.appunite.loudius.network.retrofitTestDouble +import com.appunite.loudius.network.testOkHttpClient +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import retrofit2.HttpException +import retrofit2.http.GET + +class AuthFailureInterceptorTest { + private val fakeAuthFailureHandler: FakeAuthFailureHandler = mockk(relaxed = true) + private val testOkHttpClient = testOkHttpClient(authFailureHandler = fakeAuthFailureHandler) + private val mockWebServer: MockWebServer = MockWebServer() + private val service = retrofitTestDouble( + mockWebServer = mockWebServer, + client = testOkHttpClient, + ).create(TestApi::class.java) + + @AfterEach + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun `GIVEN not authorized user WHEN making an api call THEN auth failure should be handled`() { + runTest { + val testDataJson = "{\"message\":\"AuthFailureResponse\"}" + val failureResponse = + MockResponse().setResponseCode(401).setBody(testDataJson) + mockWebServer.enqueue(failureResponse) + + assertThrows { service.makeARequest() } + coVerify(exactly = 1) { fakeAuthFailureHandler.emitAuthFailure() } + } + } + + @Test + fun `GIVEN authorized user WHEN making an api call THEN auth failure is not emitted`() { + runTest { + val testDataJson = "{\"message\":\"successResponse\"}" + val successResponse = + MockResponse().setResponseCode(200).setBody(testDataJson) + mockWebServer.enqueue(successResponse) + + assertDoesNotThrow("Should not throw Http exception") { + service.makeARequest() + } + coVerify(exactly = 0) { fakeAuthFailureHandler.emitAuthFailure() } + } + } + + private interface TestApi { + + @GET("/test") + suspend fun makeARequest(): TestData + } + + private data class TestData(val message: String) +} diff --git a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt similarity index 67% rename from app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt rename to app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt index cc72e854a..4454863fc 100644 --- a/app/src/test/java/com/appunite/loudius/network/AuthInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt @@ -1,8 +1,26 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + @file:OptIn(ExperimentalCoroutinesApi::class) -package com.appunite.loudius.network +package com.appunite.loudius.network.intercept import com.appunite.loudius.fakes.FakeAuthRepository +import com.appunite.loudius.network.retrofitTestDouble +import com.appunite.loudius.network.testOkHttpClient import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse diff --git a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt new file mode 100644 index 000000000..cc5333734 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.ui + +import com.appunite.loudius.network.utils.AuthFailureHandler +import com.appunite.loudius.network.utils.AuthFailureHandlerImpl +import com.appunite.loudius.util.MainDispatcherExtension +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@OptIn(ExperimentalCoroutinesApi::class) +@ExtendWith(MainDispatcherExtension::class) +class MainViewModelTest { + + private val authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl() + private lateinit var viewModel: MainViewModel + + + @Test + fun `WHEN init THEN auth failure event is null`() = runTest { + viewModel = MainViewModel(authFailureHandler) + assertEquals(null, viewModel.state.authFailureEvent) + } + + @Test + fun `WHEN auth failure emitted THEN set state with auth failure event`() = runTest { + viewModel = MainViewModel(authFailureHandler) + authFailureHandler.emitAuthFailure() + + assertEquals(Unit, viewModel.state.authFailureEvent) + } + + @Test + fun `WHEN auth failure handled THEN set auth failure event is null`() = runTest { + viewModel = MainViewModel(authFailureHandler) + authFailureHandler.emitAuthFailure() + + viewModel.onAuthFailureHandled() + + assertEquals(null, viewModel.state.authFailureEvent) + } + +} From 8bb9b0a5b6da43ddc8dc6696b08fa4ea364d0830 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 30 Mar 2023 12:46:18 +0200 Subject: [PATCH 280/526] Perform minor code cleaning. --- .../appunite/loudius/di/DataSourceModule.kt | 8 ++--- .../network/utils/AuthFailureHandler.kt | 6 ++-- .../loudius/fakes/FakeAuthFailureHandler.kt | 30 ------------------- .../intercept/AuthFailureInterceptorTest.kt | 12 ++++---- .../appunite/loudius/ui/MainViewModelTest.kt | 3 -- 5 files changed, 11 insertions(+), 48 deletions(-) delete mode 100644 app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index a0ca2eb0b..6efb5625a 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -42,16 +42,12 @@ object DataSourceModule { @Provides @Singleton - fun providePullRequestNetworkDataSource( - service: PullRequestsService, - ): PullRequestDataSource = + fun providePullRequestNetworkDataSource(service: PullRequestsService): PullRequestDataSource = PullRequestsNetworkDataSource(service) @Provides @Singleton - fun provideUserDataSource( - userService: UserService, - ): UserDataSource = + fun provideUserDataSource(userService: UserService): UserDataSource = UserDataSourceImpl(userService) @Singleton diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt index 096304c4c..22ea7907e 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -14,10 +14,10 @@ interface AuthFailureHandler { @Singleton class AuthFailureHandlerImpl @Inject constructor() : AuthFailureHandler { - private val _logoutFlow = MutableSharedFlow() - override val authFailureFlow: SharedFlow = _logoutFlow + private val _authFailureFlow = MutableSharedFlow() + override val authFailureFlow: SharedFlow = _authFailureFlow override suspend fun emitAuthFailure() { - _logoutFlow.emit(Unit) + _authFailureFlow.emit(Unit) } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt deleted file mode 100644 index 12a04e937..000000000 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthFailureHandler.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.fakes - -import com.appunite.loudius.network.utils.AuthFailureHandler -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow - -class FakeAuthFailureHandler : AuthFailureHandler { - private val _authFailureFlow = MutableSharedFlow() - override val authFailureFlow: SharedFlow = _authFailureFlow - - override suspend fun emitAuthFailure() { - _authFailureFlow.emit(Unit) - } -} diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt index 520853881..5c5d72acf 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt @@ -18,9 +18,9 @@ package com.appunite.loudius.network.intercept -import com.appunite.loudius.fakes.FakeAuthFailureHandler import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.testOkHttpClient +import com.appunite.loudius.network.utils.AuthFailureHandler import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -35,7 +35,7 @@ import retrofit2.HttpException import retrofit2.http.GET class AuthFailureInterceptorTest { - private val fakeAuthFailureHandler: FakeAuthFailureHandler = mockk(relaxed = true) + private val fakeAuthFailureHandler: AuthFailureHandler = mockk(relaxed = true) private val testOkHttpClient = testOkHttpClient(authFailureHandler = fakeAuthFailureHandler) private val mockWebServer: MockWebServer = MockWebServer() private val service = retrofitTestDouble( @@ -49,7 +49,7 @@ class AuthFailureInterceptorTest { } @Test - fun `GIVEN not authorized user WHEN making an api call THEN auth failure should be handled`() { + fun `GIVEN not authorized user WHEN making an api call THEN auth failure should be handled`() = runTest { val testDataJson = "{\"message\":\"AuthFailureResponse\"}" val failureResponse = @@ -59,10 +59,10 @@ class AuthFailureInterceptorTest { assertThrows { service.makeARequest() } coVerify(exactly = 1) { fakeAuthFailureHandler.emitAuthFailure() } } - } + @Test - fun `GIVEN authorized user WHEN making an api call THEN auth failure is not emitted`() { + fun `GIVEN authorized user WHEN making an api call THEN auth failure is not emitted`() = runTest { val testDataJson = "{\"message\":\"successResponse\"}" val successResponse = @@ -74,7 +74,7 @@ class AuthFailureInterceptorTest { } coVerify(exactly = 0) { fakeAuthFailureHandler.emitAuthFailure() } } - } + private interface TestApi { diff --git a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt index cc5333734..b5a047033 100644 --- a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt @@ -28,11 +28,9 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) class MainViewModelTest { - private val authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl() private lateinit var viewModel: MainViewModel - @Test fun `WHEN init THEN auth failure event is null`() = runTest { viewModel = MainViewModel(authFailureHandler) @@ -56,5 +54,4 @@ class MainViewModelTest { assertEquals(null, viewModel.state.authFailureEvent) } - } From 9daba6595dc443a6ee1cfef91220fde01d274b0e Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 30 Mar 2023 10:50:04 +0000 Subject: [PATCH 281/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 2 +- app/src/main/java/com/appunite/loudius/di/NetworkModule.kt | 4 ++-- .../loudius/network/intercept/AuthFailureInterceptor.kt | 2 +- .../appunite/loudius/network/intercept/AuthInterceptor.kt | 2 +- .../java/com/appunite/loudius/network/utils/ApiCallUtil.kt | 2 +- .../appunite/loudius/network/utils/AuthFailureHandler.kt | 4 ++-- app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt | 4 ++-- .../java/com/appunite/loudius/network/NetworkTestDoubles.kt | 6 +++--- .../loudius/network/intercept/AuthFailureInterceptorTest.kt | 2 -- 9 files changed, 13 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 5d1f02e6f..3b08f726e 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -126,7 +126,7 @@ class MainActivity : ComponentActivity() { Toast.makeText( this@MainActivity, getString(R.string.user_unauthorized_message), - Toast.LENGTH_LONG + Toast.LENGTH_LONG, ).show() } } diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 809f99bdf..c0fca4070 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -28,12 +28,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import java.time.LocalDateTime -import javax.inject.Singleton import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt index 3b4d469bd..a9d437129 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt @@ -17,12 +17,12 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.network.utils.AuthFailureHandler -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.Interceptor import okhttp3.Response +import javax.inject.Inject class AuthFailureInterceptor @Inject constructor( private val authFailureHandler: AuthFailureHandler, diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt index 5e4b1d101..36ab5115c 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthInterceptor.kt @@ -17,9 +17,9 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.domain.repository.AuthRepository -import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Response +import javax.inject.Inject class AuthInterceptor @Inject constructor( private val authRepository: AuthRepository, diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index d41d592a8..065ee7fea 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -18,9 +18,9 @@ package com.appunite.loudius.network.utils import com.appunite.loudius.network.model.error.DefaultErrorResponse import com.google.gson.Gson -import java.io.IOException import org.json.JSONException import retrofit2.HttpException +import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt index 22ea7907e..82902a555 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -1,9 +1,9 @@ package com.appunite.loudius.network.utils -import javax.inject.Inject -import javax.inject.Singleton import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import javax.inject.Inject +import javax.inject.Singleton interface AuthFailureHandler { suspend fun emitAuthFailure() diff --git a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt index e5584b8e8..c02a5262e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt @@ -7,11 +7,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.network.utils.AuthFailureHandler import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject data class MainState( - val authFailureEvent: Unit? = null + val authFailureEvent: Unit? = null, ) @HiltViewModel diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 32f155706..ba9f71cd4 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -26,16 +26,16 @@ import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), - authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl() + authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl(), ) = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt index 5c5d72acf..81b8fc228 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt @@ -60,7 +60,6 @@ class AuthFailureInterceptorTest { coVerify(exactly = 1) { fakeAuthFailureHandler.emitAuthFailure() } } - @Test fun `GIVEN authorized user WHEN making an api call THEN auth failure is not emitted`() = runTest { @@ -75,7 +74,6 @@ class AuthFailureInterceptorTest { coVerify(exactly = 0) { fakeAuthFailureHandler.emitAuthFailure() } } - private interface TestApi { @GET("/test") From 5ab822c416283d921356ad3828f29e5854b73f90 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 30 Mar 2023 14:13:58 +0200 Subject: [PATCH 282/526] Perform minor code fixes. --- .../network/utils/AuthFailureHandler.kt | 19 +++++++++++++++++-- .../com/appunite/loudius/ui/MainViewModel.kt | 18 +++++++++++++++++- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt index 82902a555..f7011e844 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -1,8 +1,23 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.network.utils import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import javax.inject.Inject import javax.inject.Singleton interface AuthFailureHandler { @@ -12,7 +27,7 @@ interface AuthFailureHandler { } @Singleton -class AuthFailureHandlerImpl @Inject constructor() : AuthFailureHandler { +class AuthFailureHandlerImpl : AuthFailureHandler { private val _authFailureFlow = MutableSharedFlow() override val authFailureFlow: SharedFlow = _authFailureFlow diff --git a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt index c02a5262e..38f431554 100644 --- a/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/MainViewModel.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui import androidx.compose.runtime.getValue @@ -30,6 +46,6 @@ class MainViewModel @Inject constructor(private val authFailureHandler: AuthFail } fun onAuthFailureHandled() { - state = MainState(null) + state = MainState() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 262a4b066..7589f3449 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,7 +18,7 @@ Take me to login Awesome! Your collaborator have been pinged for some serious code review action! 🎉 Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! - "Unauthorized collaborator detected! Please login again." + Unauthorized collaborator detected! Please login again. Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 From d865b7b1c06ad2815db05b2fc50a9294483b7625 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 30 Mar 2023 18:53:58 +0200 Subject: [PATCH 283/526] readme update --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 384cc5ab6..e5130e209 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ # Loudius - Android experimental playground -This project serves as an example Android project and a playground for experimenting with new -architectures, solutions, and libraries. Functionalities of the app are simple. It uses GithubAPI -and let's user ping his collaborators for a faster code review. +Our app is a sample Android application designed to showcase some solutions to the architecture, +networking layer, and Jetpack Compose. It provides a basic user interface and functionality that can +be expanded upon to suit specific needs. + +The app is open-source and intended for use by developers who are interested in expanding their +knowledge about Android development. The main functionalities are: + +- login through GitHub OAuth, +- list user’s pull requests, +- ping collaborators for a faster code review. + +The small range of functionalities leads to the ease of understanding the code and conducting +experiments with different development libraries and tools. ## Contributing @@ -10,7 +20,7 @@ We believe that there is no ideal code and that every code can be improved. Ther every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn from each other. -## Experiments +## Experiments, the purpose of the project Our project is designed for those who want to experiment with different solutions, architectures, and libraries in the Android development world. We believe that experimenting is the key to @@ -20,13 +30,13 @@ We encourage everyone to join us and create their own experiments. You can exper related to Android development - UI, performance, architecture, libraries, and more. To create your own experiment, simply download our repository, create a new branch, and start -experimenting. Once you're done, create a pull request, and our community will review it. We believe -that by sharing our experiments with each other, we can all learn and improve. +exploring. Once you're done, create a pull request, and our community will review it. We believe +that by sharing our ideas with each other, we can all learn and improve. We welcome all levels of experience in our community, whether you're a beginner or an expert. We're all here to learn and grow together. -So come and join us in Loudius - Android experimental playground, and let's experiment together! +So come, join us in Loudius - Android experimental playground, and let's experiment together! ### Rules for Experiments @@ -39,6 +49,7 @@ We have a few rules for experiments to keep everything organized and consistent: - The experiment should not interfere with the stability of the existing codebase. ### Example Experiment + Here's an example of an experiment that meets our rules: Branch Name: experiment/navigation-by-voyager-library @@ -47,12 +58,12 @@ Goal: To resolve which navigation is better for compose. What are the pros and c Method: Implement navigation with voyager library. ### How to set environmental variable on mac? + 1. Launch zsh (command `zsh`) 2. `$ echo 'export CLIENT_SECRET=you know what' >> ~/.zshenv` 3. `$ echo $CLIENT_SECRET` 4. Restart your computer. - ### License Copyright (C) 2023 AppUnite From 0ef0dad2480375391c9a6e71fc851a2090c20637 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 30 Mar 2023 19:12:59 +0200 Subject: [PATCH 284/526] add used tech and frameworks --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index e5130e209..0aa80dfb5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ knowledge about Android development. The main functionalities are: The small range of functionalities leads to the ease of understanding the code and conducting experiments with different development libraries and tools. +## Tech/framework used + +- Jetpack Compose +- Hilt +- Kotlin Coroutines +- Retrofit +- OkHttp3 +- Gson + ## Contributing We believe that there is no ideal code and that every code can be improved. Therefore, we welcome From 84ec4dd11d1dc26a5d2bb5292131293a38f8ff7b Mon Sep 17 00:00:00 2001 From: nowakweronika <72873966+nowakweronika@users.noreply.github.com> Date: Thu, 30 Mar 2023 19:25:12 +0200 Subject: [PATCH 285/526] add video, text and emojis --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0aa80dfb5..8be24c76f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Loudius - Android experimental playground +## 📢 Project overview + Our app is a sample Android application designed to showcase some solutions to the architecture, networking layer, and Jetpack Compose. It provides a basic user interface and functionality that can be expanded upon to suit specific needs. @@ -14,7 +16,9 @@ knowledge about Android development. The main functionalities are: The small range of functionalities leads to the ease of understanding the code and conducting experiments with different development libraries and tools. -## Tech/framework used +https://user-images.githubusercontent.com/72873966/228913610-6cce166f-37dd-4443-a8c3-83f6bf5fc489.mov + +## ⚙️ Tech/framework used - Jetpack Compose - Hilt @@ -23,13 +27,13 @@ experiments with different development libraries and tools. - OkHttp3 - Gson -## Contributing +## 🧑🏻‍🎓 Contributing We believe that there is no ideal code and that every code can be improved. Therefore, we welcome every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn from each other. -## Experiments, the purpose of the project +## 🔬 Experiments, the purpose of the project Our project is designed for those who want to experiment with different solutions, architectures, and libraries in the Android development world. We believe that experimenting is the key to @@ -66,6 +70,10 @@ Purpose: Check compose navigation with voyager library. Compare that with standa Goal: To resolve which navigation is better for compose. What are the pros and cons of each way. Method: Implement navigation with voyager library. +## 🚀 Project setup + +In order to properly start the application and use it, the CLIENT_SECRET environment variable must be set on your computer. + ### How to set environmental variable on mac? 1. Launch zsh (command `zsh`) @@ -73,7 +81,7 @@ Method: Implement navigation with voyager library. 3. `$ echo $CLIENT_SECRET` 4. Restart your computer. -### License +## ⭐️ License Copyright (C) 2023 AppUnite From ff77ce0e007edd118a3f27d9872f8003ecef1df9 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 30 Mar 2023 17:28:50 +0000 Subject: [PATCH 286/526] [MegaLinter] Apply linters fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8be24c76f..59526f0c6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ knowledge about Android development. The main functionalities are: The small range of functionalities leads to the ease of understanding the code and conducting experiments with different development libraries and tools. -https://user-images.githubusercontent.com/72873966/228913610-6cce166f-37dd-4443-a8c3-83f6bf5fc489.mov + ## ⚙️ Tech/framework used From bfdd2a285806ddaddf8f003bb2f83893d7bb9d7d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 31 Mar 2023 08:28:40 +0200 Subject: [PATCH 287/526] Change the dispatcher to Default and inject it into AuthFailureInterceptor.kt. --- .../loudius/network/intercept/AuthFailureInterceptor.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt index a9d437129..879a556d5 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt @@ -17,22 +17,24 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.network.utils.AuthFailureHandler +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject class AuthFailureInterceptor @Inject constructor( private val authFailureHandler: AuthFailureHandler, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) if (response.code == 401) { - CoroutineScope(Dispatchers.IO).launch { + CoroutineScope(dispatcher).launch { authFailureHandler.emitAuthFailure() } } From 747fce4f3a08835702ba5697b1a288df8a2a0b02 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Fri, 31 Mar 2023 06:32:37 +0000 Subject: [PATCH 288/526] [MegaLinter] Apply linters fixes --- .../loudius/network/intercept/AuthFailureInterceptor.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt index 879a556d5..ca2645932 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt @@ -17,17 +17,17 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.network.utils.AuthFailureHandler -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import okhttp3.Interceptor import okhttp3.Response +import javax.inject.Inject class AuthFailureInterceptor @Inject constructor( private val authFailureHandler: AuthFailureHandler, - private val dispatcher: CoroutineDispatcher = Dispatchers.Default + private val dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() From e90c86cecbde4735126ebed01aa584df2556317d Mon Sep 17 00:00:00 2001 From: nowakweronika <72873966+nowakweronika@users.noreply.github.com> Date: Fri, 31 Mar 2023 11:11:10 +0200 Subject: [PATCH 289/526] new video, text improvements --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 59526f0c6..33e7ebade 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ knowledge about Android development. The main functionalities are: The small range of functionalities leads to the ease of understanding the code and conducting experiments with different development libraries and tools. - +https://user-images.githubusercontent.com/72873966/229077972-0e22227f-e90c-43e2-a604-b23410da7da2.mov ## ⚙️ Tech/framework used @@ -27,12 +27,6 @@ experiments with different development libraries and tools. - OkHttp3 - Gson -## 🧑🏻‍🎓 Contributing - -We believe that there is no ideal code and that every code can be improved. Therefore, we welcome -every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn -from each other. - ## 🔬 Experiments, the purpose of the project Our project is designed for those who want to experiment with different solutions, architectures, @@ -65,14 +59,14 @@ We have a few rules for experiments to keep everything organized and consistent: Here's an example of an experiment that meets our rules: -Branch Name: experiment/navigation-by-voyager-library -Purpose: Check compose navigation with voyager library. Compare that with standard way. -Goal: To resolve which navigation is better for compose. What are the pros and cons of each way. -Method: Implement navigation with voyager library. +**Branch Name:** experiment/navigation-by-voyager-library\ +**Purpose:** Check compose navigation with voyager library. Compare that with standard way.\ +**Goal:** To resolve which navigation is better for compose. What are the pros and cons of each way.\ +**Method:** Implement navigation with voyager library. ## 🚀 Project setup -In order to properly start the application and use it, the CLIENT_SECRET environment variable must be set on your computer. +In order to properly start the application and use it, the CLIENT_SECRET environment variable must be set on your computer. CLIENT_SECRET is a GitHub client secret key provided from ``Settings -> Developer Settings -> OAuth Apps -> my application``. ### How to set environmental variable on mac? @@ -81,6 +75,12 @@ In order to properly start the application and use it, the CLIENT_SECRET environ 3. `$ echo $CLIENT_SECRET` 4. Restart your computer. +## 🧑🏻‍🎓 Contributing + +We believe that there is no ideal code and that every code can be improved. Therefore, we welcome +every issue and new idea. We encourage you to open a new issue or pull request, as we can all learn +from each other. + ## ⭐️ License Copyright (C) 2023 AppUnite From 2251e9008dbf82173e61ef14821bddbba8194740 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 31 Mar 2023 09:14:00 +0000 Subject: [PATCH 290/526] [MegaLinter] Apply linters fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33e7ebade..e3f3d6de5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ knowledge about Android development. The main functionalities are: The small range of functionalities leads to the ease of understanding the code and conducting experiments with different development libraries and tools. -https://user-images.githubusercontent.com/72873966/229077972-0e22227f-e90c-43e2-a604-b23410da7da2.mov + ## ⚙️ Tech/framework used From d925ae7a7286925a272c258da78daefc3857f28d Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Tue, 4 Apr 2023 11:09:48 +0200 Subject: [PATCH 291/526] chore: cleanup authenticating --- .../java/com/appunite/loudius/MainActivity.kt | 13 +- .../com/appunite/loudius/common/Screen.kt | 19 ++- .../AuthenticatingScreen.kt} | 34 ++-- .../AuthenticatingViewModel.kt} | 77 ++++----- .../AuthenticatingViewModelTest.kt | 152 ++++++++++++++++++ .../ui/loading/LoadingViewModelTest.kt | 93 ----------- 6 files changed, 227 insertions(+), 161 deletions(-) rename app/src/main/java/com/appunite/loudius/ui/{loading/LoadingScreen.kt => authenticating/AuthenticatingScreen.kt} (72%) rename app/src/main/java/com/appunite/loudius/ui/{loading/LoadingViewModel.kt => authenticating/AuthenticatingViewModel.kt} (50%) create mode 100644 app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt delete mode 100644 app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 3b08f726e..34646947f 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -34,7 +34,7 @@ import androidx.navigation.navDeepLink import com.appunite.loudius.common.Constants.REDIRECT_URL import com.appunite.loudius.common.Screen import com.appunite.loudius.ui.MainViewModel -import com.appunite.loudius.ui.loading.LoadingScreen +import com.appunite.loudius.ui.authenticating.AuthenticatingScreen import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.reviewers.ReviewersScreen @@ -68,15 +68,10 @@ class MainActivity : ComponentActivity() { LoginScreen() } composable( - route = Screen.Repos.route, - deepLinks = listOf( - navDeepLink { - uriPattern = REDIRECT_URL - }, - ), + route = Screen.Authenticating.route, + deepLinks = Screen.Authenticating.deepLinks, ) { - LoadingScreen( - intent = intent, + AuthenticatingScreen( onNavigateToPullRequest = { navController.navigate(Screen.PullRequests.route) { popUpTo(Screen.Login.route) { inclusive = true } diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index b2a68b0e3..68934b30d 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -16,16 +16,33 @@ package com.appunite.loudius.common +import android.content.Intent +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NamedNavArgument +import androidx.navigation.NavController +import androidx.navigation.NavDeepLink import androidx.navigation.NavType import androidx.navigation.navArgument +import androidx.navigation.navDeepLink sealed class Screen(val route: String) { open val arguments: List = emptyList() object Login : Screen("login_screen") - object Repos : Screen("repos_screen") + object Authenticating : Screen("repos_screen") { + fun getCode(savedStateHandle: SavedStateHandle): Result { + val intent: Intent? = savedStateHandle[NavController.KEY_DEEP_LINK_INTENT] + val code = intent?.data?.getQueryParameter("code") + return code?.let { Result.success(it) } ?: Result.failure(Exception("No error code")) + } + + val deepLinks: List get() = listOf( + navDeepLink { + uriPattern = Constants.REDIRECT_URL + }, + ) + } object PullRequests : Screen("pull_requests_screen") diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt similarity index 72% rename from app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt rename to app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index f3fc3068d..4014ff6c2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.loading +package com.appunite.loudius.ui.authenticating import android.content.Intent import androidx.compose.runtime.Composable @@ -29,40 +29,32 @@ import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.theme.LoudiusTheme @Composable -fun LoadingScreen( - intent: Intent, - viewModel: LoadingViewModel = hiltViewModel(), +fun AuthenticatingScreen( + viewModel: AuthenticatingViewModel = hiltViewModel(), onNavigateToPullRequest: () -> Unit, onNavigateToLogin: () -> Unit, ) { val state = viewModel.state - val code = intent.data?.getQueryParameter("code") - val rememberedCode = rememberUpdatedState(newValue = code) - LaunchedEffect(key1 = rememberedCode) { - rememberedCode.value?.let { - viewModel.setCodeAndGetAccessToken(it) - } - } LaunchedEffect(key1 = state.navigateTo) { when (state.navigateTo) { - LoadingScreenNavigation.NavigateToLogin -> { + AuthenticatingScreenNavigation.NavigateToLogin -> { onNavigateToLogin() - viewModel.onAction(LoadingAction.OnNavigate) + viewModel.onAction(AuthenticatingAction.OnNavigate) } - LoadingScreenNavigation.NavigateToPullRequests -> { + AuthenticatingScreenNavigation.NavigateToPullRequests -> { onNavigateToPullRequest() - viewModel.onAction(LoadingAction.OnNavigate) + viewModel.onAction(AuthenticatingAction.OnNavigate) } null -> {} } } - LoadingScreenStateless(errorScreenType = state.errorScreenType) { - viewModel.onAction(LoadingAction.OnTryAgainClick) + AuthenticatingScreenStateless(errorScreenType = state.errorScreenType) { + viewModel.onAction(AuthenticatingAction.OnTryAgainClick) } } @Composable -fun LoadingScreenStateless( +fun AuthenticatingScreenStateless( errorScreenType: LoadingErrorType?, onTryAgainClick: () -> Unit, ) { @@ -95,7 +87,7 @@ private fun ShowLoudiusGenericErrorScreen( @Composable fun ShowLoudiusGenericErrorScreenPreview() { LoudiusTheme { - LoadingScreenStateless(errorScreenType = LoadingErrorType.GENERIC_ERROR) {} + AuthenticatingScreenStateless(errorScreenType = LoadingErrorType.GENERIC_ERROR) {} } } @@ -103,7 +95,7 @@ fun ShowLoudiusGenericErrorScreenPreview() { @Composable fun ShowLoudiusLoginErrorScreenPreview() { LoudiusTheme { - LoadingScreenStateless(errorScreenType = LoadingErrorType.LOGIN_ERROR) {} + AuthenticatingScreenStateless(errorScreenType = LoadingErrorType.LOGIN_ERROR) {} } } @@ -111,6 +103,6 @@ fun ShowLoudiusLoginErrorScreenPreview() { @Composable fun ShowLoadingIndicatorScreenPreview() { LoudiusTheme { - LoadingScreenStateless(errorScreenType = null) {} + AuthenticatingScreenStateless(errorScreenType = null) {} } } diff --git a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt similarity index 50% rename from app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt rename to app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt index 12e406a3b..20c1138bb 100644 --- a/app/src/main/java/com/appunite/loudius/ui/loading/LoadingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt @@ -14,26 +14,28 @@ * limitations under the License. */ -package com.appunite.loudius.ui.loading +package com.appunite.loudius.ui.authenticating import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID +import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.utils.WebException import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject -sealed class LoadingAction { +sealed class AuthenticatingAction { - object OnNavigate : LoadingAction() + object OnNavigate : AuthenticatingAction() - object OnTryAgainClick : LoadingAction() + object OnTryAgainClick : AuthenticatingAction() } enum class LoadingErrorType { @@ -41,44 +43,42 @@ enum class LoadingErrorType { GENERIC_ERROR, } -data class LoadingState( - val accessToken: String? = null, - val code: String? = null, - val navigateTo: LoadingScreenNavigation? = null, +data class AuthenticatingState( + val navigateTo: AuthenticatingScreenNavigation? = null, val errorScreenType: LoadingErrorType? = null, ) -sealed class LoadingScreenNavigation { - object NavigateToPullRequests : LoadingScreenNavigation() - object NavigateToLogin : LoadingScreenNavigation() +sealed class AuthenticatingScreenNavigation { + object NavigateToPullRequests : AuthenticatingScreenNavigation() + object NavigateToLogin : AuthenticatingScreenNavigation() } @HiltViewModel -class LoadingViewModel @Inject constructor( +class AuthenticatingViewModel @Inject constructor( private val authRepository: AuthRepository, + private val savedStateHandle: SavedStateHandle, ) : ViewModel() { - var state by mutableStateOf(LoadingState()) + private val code = Screen.Authenticating.getCode(savedStateHandle) + + var state by mutableStateOf(AuthenticatingState()) private set - fun setCodeAndGetAccessToken(code: String) { - state = state.copy(code = code) - getAccessToken(code) + init { + getAccessToken() } - fun onAction(action: LoadingAction) = when (action) { - is LoadingAction.OnTryAgainClick -> onTryAgain() - is LoadingAction.OnNavigate -> onNavigate() + fun onAction(action: AuthenticatingAction) = when (action) { + is AuthenticatingAction.OnTryAgainClick -> onTryAgain() + is AuthenticatingAction.OnNavigate -> onNavigate() } private fun onTryAgain() { if (state.errorScreenType == LoadingErrorType.LOGIN_ERROR) { - state = state.copy(navigateTo = LoadingScreenNavigation.NavigateToLogin) + state = state.copy(navigateTo = AuthenticatingScreenNavigation.NavigateToLogin) } else { state = state.copy(errorScreenType = null) - state.code?.let { - getAccessToken(it) - } + getAccessToken() } } @@ -86,21 +86,24 @@ class LoadingViewModel @Inject constructor( state = state.copy(navigateTo = null) } - private fun getAccessToken(code: String) { - viewModelScope.launch { - authRepository.fetchAccessToken( - clientId = CLIENT_ID, - clientSecret = BuildConfig.CLIENT_SECRET, - code = code, - ).onSuccess { token -> - state = state.copy( - accessToken = token, - navigateTo = LoadingScreenNavigation.NavigateToPullRequests, - ) - }.onFailure { - state = state.copy(errorScreenType = resolveErrorType(it)) + private fun getAccessToken() { + code.fold(onSuccess = { code -> + viewModelScope.launch { + authRepository.fetchAccessToken( + clientId = CLIENT_ID, + clientSecret = BuildConfig.CLIENT_SECRET, + code = code, + ).onSuccess { + state = state.copy( + navigateTo = AuthenticatingScreenNavigation.NavigateToPullRequests, + ) + }.onFailure { + state = state.copy(errorScreenType = resolveErrorType(it)) + } } - } + }, onFailure = { + state = state.copy(errorScreenType = LoadingErrorType.LOGIN_ERROR) + }) } private fun resolveErrorType(it: Throwable) = when (it) { diff --git a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt new file mode 100644 index 000000000..d5371956b --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.ui.authenticating + +import android.content.Intent +import android.net.Uri +import androidx.lifecycle.SavedStateHandle +import androidx.navigation.NavController +import com.appunite.loudius.fakes.FakeAuthRepository +import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.util.MainDispatcherExtension +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MainDispatcherExtension::class) +class AuthenticatingViewModelTest { + + private val repository: FakeAuthRepository = spyk(FakeAuthRepository()) + private val savedStateHandle = SavedStateHandle() + + private fun create() = AuthenticatingViewModel(repository, savedStateHandle) + + @Test + fun `GIVEN valid code WHEN authenticated THEN navigate to pull requests screen`() { + setupIntent("validCode") + val viewModel = create() + + assertNull(viewModel.state.errorScreenType) + assertEquals(AuthenticatingScreenNavigation.NavigateToPullRequests, viewModel.state.navigateTo) + + viewModel.onAction(AuthenticatingAction.OnNavigate) + assertNull(viewModel.state.navigateTo) + } + + @Test + fun `GIVEN invalid code WHEN authenticating screen is opened THEN show login error screen`() { + setupIntent("invalidCode") + val viewModel = create() + + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + assertNull(viewModel.state.navigateTo) + } + + @Test + fun `GIVEN unexpected Github behavior WHEN authenticating screen is opened THEN show generic error screen`() { + coEvery { repository.fetchAccessToken(any(), any(), any())} returns Result.failure( + WebException.UnknownError(null, null)) + setupIntent("validCode") + val viewModel = create() + + assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) + assertNull(viewModel.state.navigateTo) + } + + @Test + fun `GIVEN unexpected error is presented WHEN try again success THEN navigate to pull requests`() { + // simulate unknown error response + coEvery { repository.fetchAccessToken(any(), any(), any())} returns Result.failure( + WebException.UnknownError(null, null)) + setupIntent("validCode") + val viewModel = create() + + // ensure error screen is displayed + assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) + assertNull(viewModel.state.navigateTo) + + // simulate success response and click try again + clearMocks(repository) + viewModel.onAction(AuthenticatingAction.OnTryAgainClick) + + // ensure user is navigated to the pull requests screen + assertEquals(AuthenticatingScreenNavigation.NavigateToPullRequests, viewModel.state.navigateTo) + assertNull(viewModel.state.errorScreenType) + + // clear the navigation state + viewModel.onAction(AuthenticatingAction.OnNavigate) + assertNull(viewModel.state.navigateTo) + assertNull(viewModel.state.errorScreenType) + } + + @Test + fun `GIVEN invalid screen and error is presented WHEN retry click THEN navigate to the login screen`() { + setupIntent("invalidCode") + val viewModel = create() + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + + viewModel.onAction(AuthenticatingAction.OnTryAgainClick) + + assertEquals(AuthenticatingScreenNavigation.NavigateToLogin, viewModel.state.navigateTo) + viewModel.onAction(AuthenticatingAction.OnNavigate) + assertNull(viewModel.state.navigateTo) + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + } + + @Test + fun `GIVEN no intent is provided WHEN screen is presented THEN show login error`() { + savedStateHandle[NavController.KEY_DEEP_LINK_INTENT] = null + val viewModel = create() + + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + assertNull(viewModel.state.navigateTo) + } + + @Test + fun `GIVEN no intent is provided and login error is presented WHEN retry is clicked THEN navigate to the login screen`() { + savedStateHandle[NavController.KEY_DEEP_LINK_INTENT] = null + val viewModel = create() + assertNull(viewModel.state.navigateTo) + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + + viewModel.onAction(AuthenticatingAction.OnTryAgainClick) + + assertEquals(AuthenticatingScreenNavigation.NavigateToLogin, viewModel.state.navigateTo) + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + + viewModel.onAction(AuthenticatingAction.OnNavigate) + + assertNull(viewModel.state.navigateTo) + assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + } + + private fun setupIntent(intentCode: String) { + val uri = mockk { + every { getQueryParameter("code") } returns intentCode + } + val intent = mockk { + every { data } returns uri + } + savedStateHandle[NavController.KEY_DEEP_LINK_INTENT] = intent + } +} diff --git a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt deleted file mode 100644 index 433c7ce6f..000000000 --- a/app/src/test/java/com/appunite/loudius/ui/loading/LoadingViewModelTest.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.ui.loading - -import com.appunite.loudius.fakes.FakeAuthRepository -import com.appunite.loudius.util.MainDispatcherExtension -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(MainDispatcherExtension::class) -class LoadingViewModelTest { - - companion object { - private const val EXAMPLE_CODE = "validCode" - private const val EXAMPLE_INVALID_CODE = "invalidCode" - private const val EXAMPLE_CODE_LEADING_TO_UNEXPECTED_ERROR = "codeLeadingToUnexpectedError" - private const val EXAMPLE_ACCESS_TOKEN = "validToken" - } - - private val repository: FakeAuthRepository = FakeAuthRepository() - private val viewModel = LoadingViewModel(repository) - - @Test - fun `GIVEN valid code WHEN setCodeAndGetAccessToken THEN set code, access token and navigate to pull requests`() { - viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - - assertEquals(EXAMPLE_CODE, viewModel.state.code) - assertEquals(EXAMPLE_ACCESS_TOKEN, viewModel.state.accessToken) - assertEquals(LoadingScreenNavigation.NavigateToPullRequests, viewModel.state.navigateTo) - } - - @Test - fun `GIVEN OnTryAgain action WHEN onAction THEN set showErrorScreen and get access token`() { - val action = LoadingAction.OnTryAgainClick - viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - - viewModel.onAction(action) - - assertNull(viewModel.state.errorScreenType) - assertEquals(EXAMPLE_ACCESS_TOKEN, viewModel.state.accessToken) - } - - @Test - fun `GIVEN OnNavigateToPullRequests action WHEN onAction THEN set navigateToPullRequests to null`() { - val action = LoadingAction.OnNavigate - viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE) - - viewModel.onAction(action) - - assertNull(viewModel.state.navigateTo) - } - - @Test - fun `GIVEN invalid code WHEN setCodeAndGetAccessToken THEN show login error screen`() { - viewModel.setCodeAndGetAccessToken(EXAMPLE_INVALID_CODE) - - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) - assertNull(viewModel.state.navigateTo) - } - - @Test - fun `GIVEN unexpected Github behavior WHEN setCodeAndGetAccessToken THEN show generic error screen`() { - viewModel.setCodeAndGetAccessToken(EXAMPLE_CODE_LEADING_TO_UNEXPECTED_ERROR) - - assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) - assertNull(viewModel.state.navigateTo) - } - - @Test - fun `GIVEN retry click WHEN logging in error THEN redirect to login screen`() { - viewModel.setCodeAndGetAccessToken(EXAMPLE_INVALID_CODE) - - viewModel.onAction(LoadingAction.OnTryAgainClick) - - assertEquals(LoadingScreenNavigation.NavigateToLogin, viewModel.state.navigateTo) - } -} From 2ba462fa76f4fafae491051d1e5df9cd44be8156 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 4 Apr 2023 11:05:12 +0200 Subject: [PATCH 292/526] Refactor FakePullRequestRepository to not use setters --- .../fakes/FakePullRequestRepository.kt | 90 ++++--------------- .../pullrequests/PullRequestsViewModelTest.kt | 14 +-- .../ui/reviewers/ReviewersViewModelTest.kt | 60 +++++++------ 3 files changed, 58 insertions(+), 106 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index c2eb525cc..501688050 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -24,92 +24,36 @@ import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults class FakePullRequestRepository : PullRequestRepository { - - private val initialReviewsAnswer = Result.success(Defaults.reviews()) - private val initialRequestedReviewersAnswer = Result.success( - RequestedReviewersResponse(Defaults.requestedReviewers()), - ) - private val initialPullRequestAnswer = Result.success( - PullRequestsResponse( - incompleteResults = false, - totalCount = 1, - items = listOf( - Defaults.pullRequest(), - ), - ), - ) - - private val initialNotifyAnswer: suspend (pullRequestNumber: String) -> Result = - { pullRequestNumber: String -> - when (pullRequestNumber) { - "correctPullRequestNumber" -> Result.success(Unit) - "notExistingPullRequestNumber" -> Result.failure( - WebException.UnknownError(404, null), - ) - else -> Result.failure(WebException.NetworkError()) - } - } - - private var lazyReviewsAnswer: suspend () -> Result> = { initialReviewsAnswer } - private var lazyRequestedReviewersAnswer: suspend () -> Result = - { initialRequestedReviewersAnswer } - private var lazyCurrentUserPullRequests: suspend () -> Result = - { initialPullRequestAnswer } - private var lazyNotifyAnswer: suspend (pullRequestNumber: String) -> Result = - initialNotifyAnswer - override suspend fun getReviews( owner: String, repo: String, pullRequestNumber: String, - ): Result> = lazyReviewsAnswer() - - fun setReviewsAnswer(result: suspend () -> Result>) { - lazyReviewsAnswer = result - } - - fun resetReviewsAnswer() { - lazyReviewsAnswer = { initialReviewsAnswer } - } + ): Result> = Result.success(Defaults.reviews()) override suspend fun getRequestedReviewers( owner: String, repo: String, pullRequestNumber: String, - ): Result = lazyRequestedReviewersAnswer() - - fun setRequestedReviewersAnswer(result: suspend () -> Result) { - lazyRequestedReviewersAnswer = result - } - - fun resetRequestedReviewersAnswer() { - lazyRequestedReviewersAnswer = { initialRequestedReviewersAnswer } - } - - fun setCurrentUserPullRequests(result: suspend () -> Result) { - lazyCurrentUserPullRequests = result - } - - fun resetCurrentUserPullRequestAnswer() { - lazyCurrentUserPullRequests = { initialPullRequestAnswer } - } - - override suspend fun getCurrentUserPullRequests(): Result { - return lazyCurrentUserPullRequests() - } - - fun setNotifyResponse(result: suspend (pullRequestNumber: String) -> Result) { - lazyNotifyAnswer = result - } - - fun resetNotifyResponse() { - lazyNotifyAnswer = initialNotifyAnswer - } + ): Result = + Result.success(RequestedReviewersResponse(Defaults.requestedReviewers())) + + override suspend fun getCurrentUserPullRequests(): Result = + Result.success( + PullRequestsResponse( + incompleteResults = false, + totalCount = 1, + items = listOf(Defaults.pullRequest()) + ) + ) override suspend fun notify( owner: String, repo: String, pullRequestNumber: String, message: String, - ): Result = lazyNotifyAnswer(pullRequestNumber) + ): Result = when (pullRequestNumber) { + "correctPullRequestNumber" -> Result.success(Unit) + "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) + else -> Result.failure(WebException.NetworkError()) + } } diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index ebbf4aacb..b13f53a70 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -24,6 +24,9 @@ import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults import com.appunite.loudius.util.MainDispatcherExtension import com.appunite.loudius.utils.neverCompletingSuspension +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.spyk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -35,12 +38,13 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(MainDispatcherExtension::class) class PullRequestsViewModelTest { - private val pullRequestRepository = FakePullRequestRepository() + private val pullRequestRepository = spyk(FakePullRequestRepository()) private fun createViewModel() = PullRequestsViewModel(pullRequestRepository) @Test fun `WHEN init THEN display loading`() = runTest { - pullRequestRepository.setCurrentUserPullRequests { neverCompletingSuspension() } + coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { neverCompletingSuspension() } + val viewModel = createViewModel() assertTrue(viewModel.state.isLoading) @@ -56,7 +60,7 @@ class PullRequestsViewModelTest { @Test fun `WHEN fetching data failed on init THEN display error`() = runTest { - pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } + coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { Result.failure(WebException.NetworkError()) } val viewModel = createViewModel() assertEquals(emptyList(), viewModel.state.pullRequests) @@ -65,10 +69,10 @@ class PullRequestsViewModelTest { @Test fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() = runTest { - pullRequestRepository.setCurrentUserPullRequests { Result.failure(WebException.NetworkError()) } + coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { Result.failure(WebException.NetworkError()) } val viewModel = createViewModel() - pullRequestRepository.resetCurrentUserPullRequestAnswer() + clearMocks(pullRequestRepository) viewModel.onAction(PulLRequestsAction.RetryClick) assertEquals(listOf(Defaults.pullRequest()), viewModel.state.pullRequests) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index af7f639ed..0cf00b3d6 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -22,10 +22,17 @@ import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.MainDispatcherExtension import com.appunite.loudius.utils.neverCompletingSuspension +import io.mockk.clearMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.spyk import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -36,10 +43,6 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -49,7 +52,7 @@ class ReviewersViewModelTest { private val systemClockFixed = Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) - private val repository = FakePullRequestRepository() + private val repository = spyk(FakePullRequestRepository()) private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) { every { get(any()) } returns "example" every { get("submission_date") } returns "2022-01-29T08:00:00" @@ -71,9 +74,7 @@ class ReviewersViewModelTest { fun `GIVEN no values in saved state WHEN init THEN throw IllegalStateException`() { every { savedStateHandle.get(any()) } returns null - assertThrows { - createViewModel() - } + assertThrows { createViewModel() } } @Test @@ -95,7 +96,7 @@ class ReviewersViewModelTest { @Test fun `GIVEN correct initial values WHEN init starts THEN state is loading`() = runTest { - repository.setReviewsAnswer { neverCompletingSuspension() } + coEvery { repository.getReviews(any(), any(), any()) } coAnswers { neverCompletingSuspension() } viewModel = createViewModel() assertEquals(true, viewModel.state.isLoading) @@ -104,10 +105,8 @@ class ReviewersViewModelTest { @Test fun `GIVEN no reviewers WHEN init THEN state is correct with no reviewers`() { - repository.setReviewsAnswer { Result.success(emptyList()) } - repository.setRequestedReviewersAnswer { - Result.success(RequestedReviewersResponse(emptyList())) - } + coEvery { repository.getReviews(any(), any(), any()) } returns Result.success(emptyList()) + coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() @@ -135,7 +134,7 @@ class ReviewersViewModelTest { @Test fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = runTest { - repository.setReviewsAnswer { Result.success(emptyList()) } + coEvery { repository.getReviews(any(), any(), any()) } returns Result.success(emptyList()) viewModel = createViewModel() val expected = listOf( @@ -150,9 +149,7 @@ class ReviewersViewModelTest { @Test fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = runTest { - repository.setRequestedReviewersAnswer { - Result.success(RequestedReviewersResponse(emptyList())) - } + coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() val expected = listOf( @@ -167,8 +164,8 @@ class ReviewersViewModelTest { @Test fun `WHEN there is an error during fetching data from 2 requests on init THEN error is shown`() = runTest { - repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } - repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } + coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) + coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() assertEquals(true, viewModel.state.isError) @@ -179,7 +176,7 @@ class ReviewersViewModelTest { @Test fun `WHEN there is an error during fetching data on init only from requested reviewers request THEN error is shown`() = runTest { - repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } + coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() assertEquals(true, viewModel.state.isError) @@ -190,7 +187,7 @@ class ReviewersViewModelTest { @Test fun `WHEN there is an error during fetching data on init only from reviews request THEN error is shown`() = runTest { - repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } + coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() assertEquals(true, viewModel.state.isError) @@ -214,7 +211,7 @@ class ReviewersViewModelTest { @Test fun `WHEN successful notify action THEN show loading indicator`() = runTest { viewModel = createViewModel() - repository.setNotifyResponse { neverCompletingSuspension() } + coEvery { repository.notify(any(), any(), any(), any()) } coAnswers { neverCompletingSuspension() } viewModel.onAction(ReviewersAction.Notify("user1")) @@ -250,12 +247,15 @@ class ReviewersViewModelTest { @Test fun `GIVEN error state WHEN on try again action with success result THEN state has reviewers`() = runTest { - repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } - repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } + coEvery { + repository.getReviews(any(), any(), any()) + } returns Result.failure(WebException.NetworkError()) + coEvery { + repository.getRequestedReviewers(any(), any(), any()) + } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() - repository.resetReviewsAnswer() - repository.resetRequestedReviewersAnswer() + clearMocks(repository) viewModel.onAction(ReviewersAction.OnTryAgain) val expected = listOf( @@ -274,8 +274,12 @@ class ReviewersViewModelTest { @Test fun `GIVEN error state WHEN on try again action with failure result THEN error is shown`() = runTest { - repository.setReviewsAnswer { Result.failure(WebException.NetworkError()) } - repository.setRequestedReviewersAnswer { Result.failure(WebException.NetworkError()) } + coEvery { + repository.getReviews(any(), any(), any()) + } returns Result.failure(WebException.NetworkError()) + coEvery { + repository.getRequestedReviewers(any(), any(), any()) + } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() viewModel.onAction(ReviewersAction.OnTryAgain) From fc97bfd685495ab9e848442950cf6868195ed609 Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Tue, 4 Apr 2023 09:21:47 +0000 Subject: [PATCH 293/526] [MegaLinter] Apply linters fixes --- app/src/main/java/com/appunite/loudius/MainActivity.kt | 2 -- .../loudius/ui/authenticating/AuthenticatingScreen.kt | 2 -- .../ui/authenticating/AuthenticatingViewModelTest.kt | 10 ++++++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 34646947f..83a0c46c4 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -30,8 +30,6 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navDeepLink -import com.appunite.loudius.common.Constants.REDIRECT_URL import com.appunite.loudius.common.Screen import com.appunite.loudius.ui.MainViewModel import com.appunite.loudius.ui.authenticating.AuthenticatingScreen diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 4014ff6c2..10cdb77e1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -16,10 +16,8 @@ package com.appunite.loudius.ui.authenticating -import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel diff --git a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt index d5371956b..6ff7a85b9 100644 --- a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt @@ -64,8 +64,9 @@ class AuthenticatingViewModelTest { @Test fun `GIVEN unexpected Github behavior WHEN authenticating screen is opened THEN show generic error screen`() { - coEvery { repository.fetchAccessToken(any(), any(), any())} returns Result.failure( - WebException.UnknownError(null, null)) + coEvery { repository.fetchAccessToken(any(), any(), any()) } returns Result.failure( + WebException.UnknownError(null, null), + ) setupIntent("validCode") val viewModel = create() @@ -76,8 +77,9 @@ class AuthenticatingViewModelTest { @Test fun `GIVEN unexpected error is presented WHEN try again success THEN navigate to pull requests`() { // simulate unknown error response - coEvery { repository.fetchAccessToken(any(), any(), any())} returns Result.failure( - WebException.UnknownError(null, null)) + coEvery { repository.fetchAccessToken(any(), any(), any()) } returns Result.failure( + WebException.UnknownError(null, null), + ) setupIntent("validCode") val viewModel = create() From ececed7a1dd691957c50248bdd5fbfb169f2da6a Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 4 Apr 2023 09:25:49 +0000 Subject: [PATCH 294/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/fakes/FakePullRequestRepository.kt | 4 ++-- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 501688050..8b2ccc28c 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -42,8 +42,8 @@ class FakePullRequestRepository : PullRequestRepository { PullRequestsResponse( incompleteResults = false, totalCount = 1, - items = listOf(Defaults.pullRequest()) - ) + items = listOf(Defaults.pullRequest()), + ), ) override suspend fun notify( diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 0cf00b3d6..d76d6d2bd 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -29,10 +29,6 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -43,6 +39,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From d77d4126e5b1ca9e54fda02f5432476ff3025f72 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 4 Apr 2023 12:01:00 +0200 Subject: [PATCH 295/526] Remove NetworkError from FakePullRequestRepository --- .../com/appunite/loudius/fakes/FakePullRequestRepository.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 8b2ccc28c..b5d115e25 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -53,7 +53,6 @@ class FakePullRequestRepository : PullRequestRepository { message: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) - "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) - else -> Result.failure(WebException.NetworkError()) + else -> Result.failure(WebException.UnknownError(404, null)) } } From 6cd00cd9d429834112bc3055b8c7466d4d5f7f06 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 4 Apr 2023 12:51:39 +0200 Subject: [PATCH 296/526] Create FakeUserLocalDataSource --- .../appunite/loudius/di/DataSourceModule.kt | 3 ++- ...taSource.kt => UserLocalDataSourceImpl.kt} | 13 ++++++--- .../loudius/domain/AuthRepositoryImplTest.kt | 27 ++++++++++--------- .../loudius/domain/UserLocalDataSourceTest.kt | 4 +-- .../loudius/fakes/FakeUserLocalDataSource.kt | 13 +++++++++ 5 files changed, 42 insertions(+), 18 deletions(-) rename app/src/main/java/com/appunite/loudius/domain/store/{UserLocalDataSource.kt => UserLocalDataSourceImpl.kt} (75%) create mode 100644 app/src/test/java/com/appunite/loudius/fakes/FakeUserLocalDataSource.kt diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 6efb5625a..050a4be3f 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -18,6 +18,7 @@ package com.appunite.loudius.di import android.content.Context import com.appunite.loudius.domain.store.UserLocalDataSource +import com.appunite.loudius.domain.store.UserLocalDataSourceImpl import com.appunite.loudius.network.datasource.AuthDataSource import com.appunite.loudius.network.datasource.AuthNetworkDataSource import com.appunite.loudius.network.datasource.PullRequestDataSource @@ -53,7 +54,7 @@ object DataSourceModule { @Singleton @Provides fun provideUserLocalDataSource(@ApplicationContext context: Context): UserLocalDataSource = - UserLocalDataSource(context) + UserLocalDataSourceImpl(context) @Singleton @Provides diff --git a/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSourceImpl.kt similarity index 75% rename from app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt rename to app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSourceImpl.kt index ae46c7024..e47de8910 100644 --- a/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/domain/store/UserLocalDataSourceImpl.kt @@ -23,8 +23,14 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton +interface UserLocalDataSource { + fun saveAccessToken(accessToken: AccessToken) + fun getAccessToken(): AccessToken +} + @Singleton -class UserLocalDataSource @Inject constructor(@ApplicationContext context: Context) { +class UserLocalDataSourceImpl @Inject constructor(@ApplicationContext context: Context) : + UserLocalDataSource { companion object { private const val FILE_NAME = "com.appunite.loudius.sharedPreferences" @@ -34,9 +40,10 @@ class UserLocalDataSource @Inject constructor(@ApplicationContext context: Conte private val sharedPreferences: SharedPreferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) - fun saveAccessToken(accessToken: AccessToken) { + override fun saveAccessToken(accessToken: AccessToken) { sharedPreferences.edit().putString(KEY_ACCESS_TOKEN, accessToken).apply() } - fun getAccessToken(): AccessToken = sharedPreferences.getString(KEY_ACCESS_TOKEN, null) ?: "" + override fun getAccessToken(): AccessToken = + sharedPreferences.getString(KEY_ACCESS_TOKEN, null) ?: "" } diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index ee9f86d17..8d8a0d27d 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -18,10 +18,10 @@ package com.appunite.loudius.domain import com.appunite.loudius.domain.repository.AuthRepositoryImpl import com.appunite.loudius.domain.store.UserLocalDataSource +import com.appunite.loudius.fakes.FakeUserLocalDataSource import com.appunite.loudius.network.datasource.AuthDataSource import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -35,16 +35,16 @@ class AuthRepositoryImplTest { getAccessToken(any(), any(), any()) } returns Result.success("validAccessToken") } - private val localDataSource: UserLocalDataSource = mockk { - every { getAccessToken() } returns "validAccessToken" - every { saveAccessToken(any()) } returns Unit - } + + private val localDataSource: UserLocalDataSource = FakeUserLocalDataSource() private val repository = AuthRepositoryImpl(networkDataSource, localDataSource) @Test - fun `GIVEN fetch access token function WHEN processing THEN return success with new valid token`() = + fun `GIVEN valid code WHEN fetching access token THEN return success with new valid token`() = runTest { - val result = repository.fetchAccessToken("clientId", "clientSecret", "validCode") + val code = "validCode" + + val result = repository.fetchAccessToken("clientId", "clientSecret", code) coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } assertEquals( @@ -54,15 +54,20 @@ class AuthRepositoryImplTest { } @Test - fun `GIVEN fetch access token WHEN processing THEN new token should be saved`() = + fun `GIVEN valid code WHEN fetching access token THEN THEN new token should be stored`() = runTest { - repository.fetchAccessToken("clientId", "clientSecret", "validCode") + val code = "validCode" + + repository.fetchAccessToken("clientId", "clientSecret", code) - coVerify(exactly = 1) { localDataSource.saveAccessToken("validAccessToken") } + val result = repository.getAccessToken() + assertEquals("validAccessToken", result) { "Expected valid access token" } } @Test fun `GIVEN token stored WHEN getting access token THEN return stored access token`() = runTest { + localDataSource.saveAccessToken("validAccessToken") + val result = repository.getAccessToken() assertEquals("validAccessToken", result) { "Expected valid access token" } @@ -71,8 +76,6 @@ class AuthRepositoryImplTest { @Test fun `GIVEN not stored access token WHEN getting access token THEN return empty string`() = runTest { - every { repository.getAccessToken() } returns "" - val result = repository.getAccessToken() assertEquals("", result) { "Expected empty string" } diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index aef16995e..70c3f436c 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -18,7 +18,7 @@ package com.appunite.loudius.domain import android.content.Context import android.content.SharedPreferences -import com.appunite.loudius.domain.store.UserLocalDataSource +import com.appunite.loudius.domain.store.UserLocalDataSourceImpl import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -32,7 +32,7 @@ class UserLocalDataSourceTest { private val context = mockk { every { getSharedPreferences(any(), any()) } returns sharedPreferences } - private val userLocalDataSource = UserLocalDataSource(context) + private val userLocalDataSource = UserLocalDataSourceImpl(context) @Test fun `GIVEN filled data source WHEN getting access token THEN return access token`() { diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeUserLocalDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeUserLocalDataSource.kt new file mode 100644 index 000000000..67a5a8240 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeUserLocalDataSource.kt @@ -0,0 +1,13 @@ +package com.appunite.loudius.fakes + +import com.appunite.loudius.domain.store.UserLocalDataSource +import com.appunite.loudius.network.model.AccessToken + +class FakeUserLocalDataSource : UserLocalDataSource { + private var token = "" + override fun saveAccessToken(accessToken: AccessToken) { + token = accessToken + } + + override fun getAccessToken(): AccessToken = token +} From 973168e9a6076e5992f35b4255db60c8c300c112 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 4 Apr 2023 13:24:13 +0200 Subject: [PATCH 297/526] Add missing tests in PullRequestRepositoryImpTest --- .../domain/PullRequestRepositoryImpTest.kt | 119 +++++++++++++++--- .../fakes/FakePullRequestDataSource.kt | 15 ++- .../fakes/FakePullRequestRepository.kt | 8 +- .../PullRequestsNetworkDataSourceTest.kt | 18 +-- .../com/appunite/loudius/util/Defaults.kt | 7 ++ 5 files changed, 123 insertions(+), 44 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index ae9439231..9fa4ed4e9 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -19,24 +19,28 @@ package com.appunite.loudius.domain import com.appunite.loudius.domain.repository.PullRequestRepositoryImpl import com.appunite.loudius.fakes.FakePullRequestDataSource import com.appunite.loudius.network.datasource.UserDataSource +import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.User +import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.util.Defaults import io.mockk.coEvery import io.mockk.mockk +import io.mockk.spyk +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @OptIn(ExperimentalCoroutinesApi::class) class PullRequestRepositoryImpTest { - private val pullRequestDataSource = FakePullRequestDataSource() + private val pullRequestDataSource = spyk(FakePullRequestDataSource()) private val userDataSource: UserDataSource = mockk { coEvery { getUser() } returns Result.success(User(1, "user1")) } @@ -46,12 +50,13 @@ class PullRequestRepositoryImpTest { inner class GetReviewsFunctionTest { @Test - fun `GIVEN correct values WHEN get reviews THEN return result with reviews excluding ones from current user`() = + fun `GIVEN correct pull request data WHEN getting reviews THEN return result with reviews excluding ones from current user`() = runTest { - val actual = repository.getReviews("example", "example", "correctPullRequestNumber") + val pullRequestNumber = "correctPullRequestNumber" - val date1 = LocalDateTime.parse("2022-01-29T10:00:00") + val actual = repository.getReviews("example", "example", pullRequestNumber) + val date1 = LocalDateTime.parse("2022-01-29T10:00:00") val expected = Result.success( listOf( Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), @@ -59,22 +64,34 @@ class PullRequestRepositoryImpTest { Review("6", User(2, "user2"), ReviewState.APPROVED, date1), ), ) - assertEquals(expected, actual) } - // TODO: Write tests with failure cases + @Test + fun `GIVEN incorrect pull request number WHEN getting reviews THEN return Unknown Error with 404 code`() = + runTest { + val pullRequestNumber = "incorrectPullRequestNumber" + + val actual = repository.getReviews("example", "example", pullRequestNumber) + + assertEquals( + Result.failure>(WebException.UnknownError(404, null)), + actual + ) + } } @Nested inner class GetRequestedReviewersTest { @Test - fun `GIVEN correct values WHEN get requested reviewers THEN return result with requested reviewers`() = + fun `GIVEN correct pull request number WHEN get requesting reviewers THEN return result with requested reviewers`() = runTest { + val pullRequestNumber = "correctPullRequestNumber" + val actual = repository.getRequestedReviewers( "example", "example", - "correctPullRequestNumber", + pullRequestNumber, ) val expected = Result.success( @@ -85,28 +102,98 @@ class PullRequestRepositoryImpTest { ), ), ) - assertEquals(expected, actual) } - // TODO: Write tests with failure cases + + @Test + fun `GIVEN incorrect pull request number WHEN get requested reviewers THEN return Unknown Error with 404 code`() = + runTest { + val pullRequestNumber = "incorrectPullRequestNumber" + + val actual = repository.getRequestedReviewers( + "example", + "example", + pullRequestNumber, + ) + + assertEquals( + Result.failure>( + WebException.UnknownError(404, null) + ), + actual + ) + } } @Nested inner class NotifyTest { @Test - fun `GIVEN correct values WHEN notify THEN return success result`() = runTest { + fun `GIVEN correct pull request number WHEN notifying THEN return success result`() = runTest { + val pullRequestNumber = "correctPullRequestNumber" + val actual = repository.notify( "exampleOwner", "exampleRepo", - "correctPullRequestNumber", + pullRequestNumber, "@ExampleUser", ) - val expected = Result.success(Unit) + assertEquals(Result.success(Unit), actual) + } - assertEquals(expected, actual) + @Test + fun `GIVEN incorrect pull request number WHEN notifying THEN return success result`() = runTest { + coEvery { + repository.notify(any(), any(), any(), any()) + } returns Result.failure(WebException.UnknownError(404, null)) + val pullRequestNumber = "incorrectPullRequestNumber" + + val actual = repository.notify( + "exampleOwner", + "exampleRepo", + pullRequestNumber, + "@ExampleUser", + ) + + assertEquals( + Result.failure(WebException.UnknownError(404, null)), + actual + ) } - // TODO: Write tests with failure cases } + + @Nested + inner class GetPullRequestsForUserTest { + @Test + fun `GIVEN logged in user WHEN getting user's pull requests THEN return pull requests`() = + runTest { + coEvery { userDataSource.getUser() } returns Result.success( + User( + 0, + "correctAuthor" + ) + ) + + val actual = repository.getCurrentUserPullRequests() + + assertEquals(Result.success(Defaults.pullRequestsResponse()), actual) + } + + @Test + fun `WHEN Network Error is returned during fetching user's pull request THEN return Network Error`() = + runTest { + coEvery { + pullRequestDataSource.getPullRequestsForUser(any()) + } returns Result.failure(WebException.NetworkError()) + + val actual = repository.getCurrentUserPullRequests() + + assertEquals( + Result.failure(WebException.NetworkError()), + actual + ) + } + } + } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index e27cb73c5..c6b01ce2e 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -24,6 +24,7 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.User import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.util.Defaults import java.time.LocalDateTime class FakePullRequestDataSource : PullRequestDataSource { @@ -45,8 +46,7 @@ class FakePullRequestDataSource : PullRequestDataSource { Review("6", User(2, "user2"), ReviewState.APPROVED, date1), ), ) - "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) - else -> Result.success(emptyList()) + else -> Result.failure(WebException.UnknownError(404, null)) } override suspend fun getReviewers( @@ -62,12 +62,12 @@ class FakePullRequestDataSource : PullRequestDataSource { ), ), ) - "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) - else -> Result.success(RequestedReviewersResponse(emptyList())) + else -> Result.failure(WebException.UnknownError(404, null)) } - override suspend fun getPullRequestsForUser(author: String): Result { - TODO("Not yet implemented") + override suspend fun getPullRequestsForUser(author: String): Result = when (author) { + "correctAuthor" -> Result.success(Defaults.pullRequestsResponse()) + else -> Result.failure(WebException.UnknownError(422, null)) } override suspend fun notify( @@ -77,7 +77,6 @@ class FakePullRequestDataSource : PullRequestDataSource { message: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) - "notExistingPullRequestNumber" -> Result.failure(WebException.UnknownError(404, null)) - else -> Result.failure(WebException.NetworkError()) + else -> Result.failure(WebException.UnknownError(404, null)) } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index b5d115e25..ecff39438 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -38,13 +38,7 @@ class FakePullRequestRepository : PullRequestRepository { Result.success(RequestedReviewersResponse(Defaults.requestedReviewers())) override suspend fun getCurrentUserPullRequests(): Result = - Result.success( - PullRequestsResponse( - incompleteResults = false, - totalCount = 1, - items = listOf(Defaults.pullRequest()), - ), - ) + Result.success(Defaults.pullRequestsResponse()) override suspend fun notify( owner: String, diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 5f3d2a18d..5f5551960 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -16,7 +16,6 @@ package com.appunite.loudius.network.datasource -import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review @@ -26,6 +25,7 @@ import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -36,7 +36,6 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -160,17 +159,10 @@ class PullRequestsNetworkDataSourceTest { val actualResponse = pullRequestDataSource.getPullRequestsForUser("exampleUser") - val expected = Result.success( - PullRequestsResponse( - incompleteResults = false, - totalCount = 1, - items = listOf( - Defaults.pullRequest(), - ), - ), - ) - - assertEquals(expected, actualResponse) { "Data should be valid" } + assertEquals( + Result.success(Defaults.pullRequestsResponse()), + actualResponse + ) { "Data should be valid" } } @Test diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index 810e21f4c..e03bda3da 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -17,6 +17,7 @@ package com.appunite.loudius.util import com.appunite.loudius.network.model.PullRequest +import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.ReviewState @@ -50,4 +51,10 @@ object Defaults { RequestedReviewer(3, "user3"), RequestedReviewer(4, "user4"), ) + + fun pullRequestsResponse() = PullRequestsResponse( + incompleteResults = false, + totalCount = 1, + items = listOf(pullRequest()), + ) } From efb7c17cb428c81e8515d94f69cc2727a67df1b3 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Tue, 4 Apr 2023 14:49:26 +0200 Subject: [PATCH 298/526] chore: improve styles --- .../ui/components/LoudiusErrorScreen.kt | 9 +- .../loudius/ui/components/LoudiusListItem.kt | 192 ++++++++++++++++++ .../ui/components/LoudiusOutlinedButton.kt | 131 ++++++++++-- .../ui/components/LoudiusPlaceholderText.kt | 6 +- .../loudius/ui/components/LoudiusText.kt | 62 ++++++ .../loudius/ui/components/LoudiusTopAppBar.kt | 16 +- .../utils/BottomBorderModifier.kt | 2 +- .../appunite/loudius/ui/login/LoginScreen.kt | 14 +- .../ui/pullrequests/PullRequestsScreen.kt | 105 +++++----- .../loudius/ui/reviewers/ReviewersScreen.kt | 89 +++----- app/src/main/res/drawable/ic_pull_request.xml | 17 +- .../main/res/drawable/person_outline_24px.xml | 2 +- app/src/main/res/values/strings.xml | 1 + 13 files changed, 489 insertions(+), 157 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt rename app/src/main/java/com/appunite/loudius/ui/{ => components}/utils/BottomBorderModifier.kt (96%) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt index d48204926..18ab8825c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt @@ -21,14 +21,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R @@ -66,12 +63,10 @@ private fun ErrorImage() { @Composable private fun ErrorText(text: String) { - Text( + LoudiusText( text = text, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.titleLarge, - textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 16.dp), + style = LoudiusTextStyle.ScreenContent, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt new file mode 100644 index 000000000..d02e733c8 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt @@ -0,0 +1,192 @@ +package com.appunite.loudius.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.appunite.loudius.R +import com.appunite.loudius.ui.components.utils.bottomBorder + + +@Composable +fun resolveListItemBackgroundColor(index: Int): Color = + if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface + + +@Composable +fun LoudiusListItem( + index: Int, + modifier: Modifier = Modifier, + icon: @Composable (Modifier) -> Unit = {}, + content: @Composable (Modifier) -> Unit, + action: @Composable () -> Unit = {}, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(resolveListItemBackgroundColor(index)) + .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) + .padding(16.dp), + ) { + icon(Modifier.align(Alignment.CenterVertically)) + content( + Modifier + .weight(1f) + .padding(start = 16.dp) + .align(Alignment.CenterVertically) + ) + action() + } +} + +@Composable +fun LoudiusListIcon(modifier: Modifier, painter: Painter, contentDescription: String) { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = modifier, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface), + ) +} + +@Composable +@Preview(showBackground = true) +fun LoudiusListItemManyWithAllItemsPreview() { + Column { + LoudiusListItemExample(0) + LoudiusListItemExample(1) + LoudiusListItemExample(2) + } +} + +@Composable +@Preview(showBackground = true) +fun LoudiusListItemJustContentPreview() { + LoudiusListItem( + index = 0, + content = { modifier -> + LoudiusText( + text = "Text", + modifier = modifier, + style = LoudiusTextStyle.ListItem, + ) + }, + ) +} + +@Composable +@Preview(showBackground = true) +fun LoudiusListItemMultiplePreview() { + LoudiusListItem( + index = 0, + content = { modifier -> + Column(modifier = modifier) { + LoudiusText( + text = "Title", + style = LoudiusTextStyle.ListItem, + ) + LoudiusText( + text = "Caption", + style = LoudiusTextStyle.ListCaption, + ) + } + }, + ) +} + +@Composable +@Preview(showBackground = true) +fun LoudiusListItemWithHeaderPreview() { + LoudiusListItem( + index = 0, + content = { modifier -> + Column(modifier = modifier) { + LoudiusText( + text = "Header text", + style = LoudiusTextStyle.ListHeader, + ) + LoudiusText( + text = "Title", + style = LoudiusTextStyle.ListItem, + ) + + } + }, + ) +} + +@Composable +@Preview(showBackground = true) +fun LoudiusListItemContentAndActionPreview() { + LoudiusListItem( + index = 0, + content = { modifier -> + LoudiusText( + text = "Text", + modifier = modifier, + style = LoudiusTextStyle.ListItem, + ) + }, + action = { + LoudiusOutlinedButton(text = "Button") {} + }, + ) +} + +@Composable +@Preview(showBackground = true) +fun LoudiusListItemContentAndIconPreview() { + LoudiusListItem( + index = 0, + icon = { modifier -> + LoudiusListIcon( + painter = painterResource(id = R.drawable.person_outline_24px), + contentDescription = "Test", + modifier = modifier, + ) + }, + content = { modifier -> + LoudiusText( + text = "Text", + modifier = modifier, + style = LoudiusTextStyle.ListItem, + ) + }, + ) +} + +@Composable +private fun LoudiusListItemExample(index: Int) { + LoudiusListItem( + index = index, + icon = { modifier -> + LoudiusListIcon( + modifier = modifier, + painter = painterResource(id = R.drawable.person_outline_24px), + contentDescription = "Test" + ) + }, + content = { modifier -> + LoudiusText( + text = "Text", + modifier = modifier, + style = LoudiusTextStyle.ListItem, + ) + }, + action = { + LoudiusOutlinedButton(text = "Button", onClick = {}) + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt index c84d42d3f..8a6384b4d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt @@ -16,42 +16,137 @@ package com.appunite.loudius.ui.components -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.appunite.loudius.R +import com.appunite.loudius.ui.theme.LoudiusTheme + +enum class LoudiusOutlinedButtonStyle { + Large, + Regular, +} @Composable fun LoudiusOutlinedButton( text: String, - iconPainter: Painter? = null, - iconDescription: String? = null, + modifier: Modifier = Modifier, + enabled: Boolean = true, + icon: @Composable (() -> Unit)? = null, + style: LoudiusOutlinedButtonStyle = LoudiusOutlinedButtonStyle.Regular, onClick: () -> Unit, ) { OutlinedButton( + enabled = enabled, onClick = onClick, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 46.dp), + modifier = modifier + .applyIf(style == LoudiusOutlinedButtonStyle.Large) { padding(horizontal = 46.dp) }, ) { - if (iconPainter != null) { - Icon( - painter = iconPainter, - contentDescription = iconDescription, - tint = Color.Black, - ) - } - Text( - modifier = Modifier.padding(start = 8.dp, top = 8.dp, bottom = 8.dp), + icon?.invoke() + + LoudiusText( text = text, - color = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .applyIf(style == LoudiusOutlinedButtonStyle.Large) { padding(8.dp) } + .applyIf(style == LoudiusOutlinedButtonStyle.Regular && icon != null) { padding(start = 8.dp) }, + style = LoudiusTextStyle.Button, + ) + } +} + +inline fun T.applyIf(predicate: Boolean, block: T.() -> T): T { + return if (predicate) block() else this +} + +@Composable +fun LoudiusOutlinedButtonIcon( + painter: Painter, + contentDescription: String +) { + Icon( + painter = painter, + contentDescription = contentDescription, + tint = Color.Black, + ) +} + +@Composable +@Preview(showBackground = true) +fun LoudiusOutlinedButtonPreview() { + LoudiusTheme { + LoudiusOutlinedButton( + onClick = { }, + text = "Some button", ) } } +@Composable +@Preview(showBackground = true) +fun LoudiusOutlinedButtonLargePreview() { + LoudiusTheme { + LoudiusOutlinedButton( + onClick = { }, + text = "Some button", + style = LoudiusOutlinedButtonStyle.Large, + ) + } +} + +@Composable +@Preview(showBackground = true) +fun LoudiusOutlinedButtonWithIconPreview() { + LoudiusTheme { + LoudiusOutlinedButton( + onClick = { }, + text = "Log In", + icon = { + LoudiusOutlinedButtonIcon( + painter = painterResource(id = R.drawable.ic_github), + "Github Icon" + ) + } + ) + } +} +@Composable +@Preview(showBackground = true) +fun LoudiusOutlinedButtonDisabledPreview() { + LoudiusTheme { + LoudiusOutlinedButton( + onClick = { }, + text = "Disabled button", + enabled = false, + icon = { + LoudiusOutlinedButtonIcon( + painter = painterResource(id = R.drawable.ic_github), + "Github Icon" + ) + } + ) + } +} + +@Composable +@Preview(showBackground = true) +fun LoudiusOutlinedButtonWithIconLargePreview() { + LoudiusTheme { + LoudiusOutlinedButton( + onClick = { }, + text = "Log In", + style = LoudiusOutlinedButtonStyle.Large, + icon = { + LoudiusOutlinedButtonIcon( + painter = painterResource(id = R.drawable.ic_github), + "Github Icon" + ) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt index 9215c7235..f7bb67e96 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt @@ -21,12 +21,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R @@ -41,9 +39,9 @@ fun LoudiusPlaceholderText(@StringRes textId: Int, padding: PaddingValues) { .padding(16.dp), contentAlignment = Alignment.Center, ) { - Text( + LoudiusText( text = stringResource(id = textId), - textAlign = TextAlign.Center, + style = LoudiusTextStyle.ScreenContent, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt new file mode 100644 index 000000000..168d40e60 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt @@ -0,0 +1,62 @@ +package com.appunite.loudius.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.appunite.loudius.ui.theme.LoudiusTheme + +enum class LoudiusTextStyle { + ListHeader, + ListHeaderWarning, + ListItem, + ListCaption, + Button, + TitleLarge, + ScreenContent, +} +@Composable +fun LoudiusText( + text: String, + modifier: Modifier = Modifier, + style: LoudiusTextStyle = LoudiusTextStyle.ListCaption, + ) { + Text( + modifier = modifier, + text = text, + textAlign = when (style) { + LoudiusTextStyle.ScreenContent -> TextAlign.Center + else -> null + }, + style = when (style) { + LoudiusTextStyle.ListHeader -> MaterialTheme.typography.labelMedium + LoudiusTextStyle.ListHeaderWarning -> MaterialTheme.typography.labelMedium + LoudiusTextStyle.ListItem -> MaterialTheme.typography.bodyLarge + LoudiusTextStyle.ListCaption -> MaterialTheme.typography.bodyMedium + LoudiusTextStyle.Button -> MaterialTheme.typography.labelLarge + LoudiusTextStyle.TitleLarge -> MaterialTheme.typography.titleLarge + LoudiusTextStyle.ScreenContent -> MaterialTheme.typography.bodyLarge + + }, + color = when (style) { + LoudiusTextStyle.ListHeaderWarning -> MaterialTheme.colorScheme.error + LoudiusTextStyle.ListCaption -> MaterialTheme.colorScheme.onSurfaceVariant + else -> MaterialTheme.colorScheme.onSurface + }, + ) +} + +@Preview(showBackground = true) +@Composable +fun LoudiusTextStyles() { + LoudiusTheme { + Column { + LoudiusTextStyle.values().forEach { + LoudiusText(text = "$it Text", style = it) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 0fd86d6cb..279224df7 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -38,10 +37,9 @@ fun LoudiusTopAppBar( ) { TopAppBar( title = { - Text( + LoudiusText( text = title, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.titleLarge, + style = LoudiusTextStyle.TitleLarge, ) }, navigationIcon = { @@ -70,3 +68,13 @@ fun LoudiusTopAppBar() { ) } } + +@Preview +@Composable +fun LoudiusTopAppBarWithoutBackButton() { + LoudiusTheme { + LoudiusTopAppBar( + title = "Loudius", + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt b/app/src/main/java/com/appunite/loudius/ui/components/utils/BottomBorderModifier.kt similarity index 96% rename from app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt rename to app/src/main/java/com/appunite/loudius/ui/components/utils/BottomBorderModifier.kt index 6df173163..b2e6bcd3a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/utils/BottomBorderModifier.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/utils/BottomBorderModifier.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.utils +package com.appunite.loudius.ui.components.utils import androidx.compose.ui.Modifier import androidx.compose.ui.composed diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index c34150a26..1a610e1a9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -38,6 +39,8 @@ import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID import com.appunite.loudius.common.Constants.SCOPE_PARAM import com.appunite.loudius.ui.components.LoudiusOutlinedButton +import com.appunite.loudius.ui.components.LoudiusOutlinedButtonIcon +import com.appunite.loudius.ui.components.LoudiusOutlinedButtonStyle @Composable fun LoginScreen() { @@ -49,10 +52,17 @@ fun LoginScreen() { ) { LoginImage() LoudiusOutlinedButton( + modifier = Modifier.fillMaxWidth(), onClick = { startAuthorizing(context) }, text = stringResource(id = R.string.login), - iconPainter = painterResource(id = R.drawable.ic_github), - iconDescription = stringResource(R.string.github_icon), + style = LoudiusOutlinedButtonStyle.Large, + icon = { + LoudiusOutlinedButtonIcon( + painter = painterResource(id = R.drawable.ic_github), + contentDescription = stringResource(R.string.github_icon), + ) + } + ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index c1650e5a2..4c6d7727a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -18,37 +18,32 @@ package com.appunite.loudius.ui.pullrequests -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusListIcon +import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusPlaceholderText +import com.appunite.loudius.ui.components.LoudiusText +import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.theme.LoudiusTheme import java.time.LocalDateTime @@ -82,22 +77,26 @@ private fun PullRequestsScreenStateless( isLoading: Boolean, isError: Boolean, ) { - Scaffold(topBar = { - LoudiusTopAppBar(title = stringResource(R.string.app_name)) - }, content = { padding -> - when { - isError -> LoudiusErrorScreen( - onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, - ) - isLoading -> LoudiusLoadingIndicator() - pullRequests.isEmpty() -> EmptyListPlaceholder(padding) - else -> PullRequestsList( - pullRequests = pullRequests, - modifier = Modifier.padding(padding), - onItemClick = onAction, - ) - } - }) + Scaffold( + topBar = { + LoudiusTopAppBar(title = stringResource(R.string.app_name)) + }, + content = { padding -> + when { + isError -> LoudiusErrorScreen( + onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, + ) + + isLoading -> LoudiusLoadingIndicator() + pullRequests.isEmpty() -> EmptyListPlaceholder(padding) + else -> PullRequestsList( + pullRequests = pullRequests, + modifier = Modifier.padding(padding), + onItemClick = onAction, + ) + } + }, + ) } @Composable @@ -110,10 +109,9 @@ private fun PullRequestsList( modifier = modifier.fillMaxSize(), ) { itemsIndexed(pullRequests) { index, item -> - val isIndexEven = index % 2 == 0 PullRequestItem( + index = index, data = item, - darkBackground = isIndexEven, onClick = onItemClick, ) } @@ -122,46 +120,43 @@ private fun PullRequestsList( @Composable private fun PullRequestItem( + index: Int, data: PullRequest, - darkBackground: Boolean, onClick: (PulLRequestsAction) -> Unit, ) { - val backgroundColor = if (darkBackground) { - MaterialTheme.colorScheme.onSurface.copy(0.08f) - } else { - MaterialTheme.colorScheme.surface - } - Row( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor) - .clickable { onClick(PulLRequestsAction.ItemClick(data.id)) }, - ) { - PullRequestIcon() - RepoDetails(pullRequestTitle = data.title, repositoryName = data.fullRepositoryName) - } - Divider(color = MaterialTheme.colorScheme.outlineVariant) + LoudiusListItem( + index = index, + modifier = Modifier.clickable { onClick(PulLRequestsAction.ItemClick(data.id)) }, + icon = { modifier -> PullRequestIcon(modifier) }, + content = { modifier -> + RepoDetails( + modifier = modifier, + pullRequestTitle = data.title, + repositoryName = data.fullRepositoryName + ) + } + ) } @Composable -private fun PullRequestIcon() { - Image( +private fun PullRequestIcon(modifier: Modifier) { + LoudiusListIcon( + modifier = modifier, painter = painterResource(id = R.drawable.ic_pull_request), - contentDescription = null, - modifier = Modifier - .padding(start = 18.dp, top = 10.dp) - .size(width = 18.dp, height = 20.dp), + contentDescription = stringResource(id = R.string.pull_requests_screen_pull_request_content_description), ) } @Composable -private fun RepoDetails(pullRequestTitle: String, repositoryName: String) { - Column(Modifier.padding(start = 18.dp, top = 8.dp, bottom = 8.dp)) { - Text(text = pullRequestTitle, style = MaterialTheme.typography.bodyLarge) - Text( +private fun RepoDetails(modifier: Modifier, pullRequestTitle: String, repositoryName: String) { + Column(modifier = modifier) { + LoudiusText( + text = pullRequestTitle, + style = LoudiusTextStyle.ListItem, + ) + LoudiusText( text = repositoryName, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + style = LoudiusTextStyle.ListCaption, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index cfae289ab..131f5206a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -16,12 +16,9 @@ package com.appunite.loudius.ui.reviewers -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -29,21 +26,16 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.Center -import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -51,13 +43,17 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusListIcon +import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator +import com.appunite.loudius.ui.components.LoudiusOutlinedButton import com.appunite.loudius.ui.components.LoudiusPlaceholderText +import com.appunite.loudius.ui.components.LoudiusText +import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import com.appunite.loudius.ui.theme.LoudiusTheme -import com.appunite.loudius.ui.utils.bottomBorder @Composable fun ReviewersScreen( @@ -127,7 +123,6 @@ private fun ReviewersScreenStateless( ) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - modifier = Modifier.background(MaterialTheme.colorScheme.surface), content = { padding -> when { isError -> LoudiusErrorScreen(onButtonClick = { onAction(ReviewersAction.OnTryAgain) }) @@ -155,42 +150,32 @@ private fun ReviewersScreenContent( itemsIndexed(reviewers) { index, reviewer -> ReviewerItem( reviewer = reviewer, - backgroundColor = resolveReviewerBackgroundColor(index), + index = index, onNotifyClick = onNotifyClick, ) } } } -@Composable -private fun resolveReviewerBackgroundColor(index: Int) = - if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface - @Composable private fun ReviewerItem( reviewer: Reviewer, - backgroundColor: Color, + index: Int, onNotifyClick: (ReviewersAction) -> Unit, ) { - Row( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor) - .bottomBorder(1.dp, MaterialTheme.colorScheme.outlineVariant) - .padding(16.dp), - ) { - ReviewerAvatarView(Modifier.align(CenterVertically)) - Column( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp) - .align(CenterVertically), - ) { - IsReviewedHeadlineText(reviewer) - ReviewerName(reviewer) + LoudiusListItem( + index = index, + icon = { ReviewerAvatarView(it) }, + content = { + Column(modifier = it) { + IsReviewedHeadlineText(reviewer) + ReviewerName(reviewer) + } + }, + action = { + NotifyButtonOrLoadingIndicator(reviewer = reviewer, onNotifyClick = onNotifyClick) } - NotifyButtonOrLoadingIndicator(reviewer = reviewer, onNotifyClick = onNotifyClick) - } + ) } @Composable @@ -198,12 +183,12 @@ private fun NotifyButtonOrLoadingIndicator( reviewer: Reviewer, onNotifyClick: (ReviewersAction) -> Unit, ) { - val buttonAlpha = if (reviewer.isLoading) 0f else 1f Box(contentAlignment = Center) { - NotifyButton( - modifier = Modifier.alpha(buttonAlpha), - ) { onNotifyClick(ReviewersAction.Notify(reviewer.login)) } - + LoudiusOutlinedButton( + text = stringResource(R.string.details_notify), + onClick = { onNotifyClick(ReviewersAction.Notify(reviewer.login)) }, + modifier = Modifier.alpha(if (reviewer.isLoading) 0f else 1f), + ) if (reviewer.isLoading) { CircularProgressIndicator(modifier = Modifier.size(24.dp)) } @@ -212,7 +197,7 @@ private fun NotifyButtonOrLoadingIndicator( @Composable private fun ReviewerAvatarView(modifier: Modifier = Modifier) { - Image( + LoudiusListIcon( painter = painterResource(id = R.drawable.person_outline_24px), contentDescription = stringResource( R.string.details_screen_user_image_description, @@ -223,10 +208,9 @@ private fun ReviewerAvatarView(modifier: Modifier = Modifier) { @Composable private fun IsReviewedHeadlineText(reviewer: Reviewer) { - Text( + LoudiusText( text = resolveIsReviewedText(reviewer), - style = MaterialTheme.typography.labelMedium, - color = if (reviewer.isReviewDone) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.error, + style = if (reviewer.isReviewDone) LoudiusTextStyle.ListHeader else LoudiusTextStyle.ListHeaderWarning ) } @@ -239,23 +223,12 @@ private fun resolveIsReviewedText(reviewer: Reviewer) = if (reviewer.isReviewDon @Composable private fun ReviewerName(reviewer: Reviewer) { - Text( + LoudiusText( text = reviewer.login, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, + style = LoudiusTextStyle.ListItem, ) } -@Composable -private fun NotifyButton(modifier: Modifier = Modifier, onNotifyClick: () -> Unit) { - OutlinedButton(onClick = onNotifyClick, modifier = modifier) { - Text( - text = stringResource(R.string.details_notify), - color = MaterialTheme.colorScheme.onSurface, - ) - } -} - @Composable private fun EmptyListPlaceholder(padding: PaddingValues) { LoudiusPlaceholderText( @@ -264,13 +237,13 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { ) } -@Preview +@Preview(showBackground = true) @Composable private fun ReviewerViewPreview() { LoudiusTheme { ReviewerItem( + index = 0, reviewer = Reviewer(1, "Kezc", true, 12, 12), - backgroundColor = MaterialTheme.colorScheme.surface, ) {} } } diff --git a/app/src/main/res/drawable/ic_pull_request.xml b/app/src/main/res/drawable/ic_pull_request.xml index 63b4d2072..62b21fac5 100644 --- a/app/src/main/res/drawable/ic_pull_request.xml +++ b/app/src/main/res/drawable/ic_pull_request.xml @@ -1,9 +1,12 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="15.36" + android:viewportHeight="15.36"> - + android:pathData="m6.193,4.147a1.487,1.487 0,1 0,-2.231 1.287l0,4.862a1.487,1.487 0,1 0,1.487 0L5.449,5.434A1.487,1.487 0,0 0,6.193 4.147ZM4.705,3.404A0.744,0.744 0,1 1,3.962 4.147,0.744 0.744,0 0,1 4.705,3.404ZM4.705,12.327a0.744,0.744 0,1 1,0.744 -0.744,0.744 0.744,0 0,1 -0.744,0.744z" + android:fillColor="#000000"/> + + \ No newline at end of file diff --git a/app/src/main/res/drawable/person_outline_24px.xml b/app/src/main/res/drawable/person_outline_24px.xml index beb6560c2..d21313259 100644 --- a/app/src/main/res/drawable/person_outline_24px.xml +++ b/app/src/main/res/drawable/person_outline_24px.xml @@ -4,7 +4,7 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7589f3449..b33dd53e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Loudius Back button Log in + Pull request User image Notify Reviewed %d h ago. From a900cd1a2ad3a2c175c8bb03decf0dd07a8607ce Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Tue, 4 Apr 2023 12:56:43 +0000 Subject: [PATCH 299/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/components/LoudiusListItem.kt | 9 +++------ .../ui/components/LoudiusOutlinedButton.kt | 18 ++++++++++-------- .../loudius/ui/components/LoudiusText.kt | 6 +++--- .../appunite/loudius/ui/login/LoginScreen.kt | 2 +- .../ui/pullrequests/PullRequestsScreen.kt | 4 ++-- .../loudius/ui/reviewers/ReviewersScreen.kt | 4 ++-- 6 files changed, 21 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt index d02e733c8..131f5b264 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt @@ -19,12 +19,10 @@ import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.ui.components.utils.bottomBorder - @Composable fun resolveListItemBackgroundColor(index: Int): Color = if (index % 2 == 0) MaterialTheme.colorScheme.onSurface.copy(0.08f) else MaterialTheme.colorScheme.surface - @Composable fun LoudiusListItem( index: Int, @@ -45,7 +43,7 @@ fun LoudiusListItem( Modifier .weight(1f) .padding(start = 16.dp) - .align(Alignment.CenterVertically) + .align(Alignment.CenterVertically), ) action() } @@ -121,7 +119,6 @@ fun LoudiusListItemWithHeaderPreview() { text = "Title", style = LoudiusTextStyle.ListItem, ) - } }, ) @@ -175,7 +172,7 @@ private fun LoudiusListItemExample(index: Int) { LoudiusListIcon( modifier = modifier, painter = painterResource(id = R.drawable.person_outline_24px), - contentDescription = "Test" + contentDescription = "Test", ) }, content = { modifier -> @@ -189,4 +186,4 @@ private fun LoudiusListItemExample(index: Int) { LoudiusOutlinedButton(text = "Button", onClick = {}) }, ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt index 8a6384b4d..8ae80bacf 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt @@ -68,7 +68,7 @@ inline fun T.applyIf(predicate: Boolean, block: T.() -> T): T { @Composable fun LoudiusOutlinedButtonIcon( painter: Painter, - contentDescription: String + contentDescription: String, ) { Icon( painter = painter, @@ -87,6 +87,7 @@ fun LoudiusOutlinedButtonPreview() { ) } } + @Composable @Preview(showBackground = true) fun LoudiusOutlinedButtonLargePreview() { @@ -109,12 +110,13 @@ fun LoudiusOutlinedButtonWithIconPreview() { icon = { LoudiusOutlinedButtonIcon( painter = painterResource(id = R.drawable.ic_github), - "Github Icon" + "Github Icon", ) - } + }, ) } } + @Composable @Preview(showBackground = true) fun LoudiusOutlinedButtonDisabledPreview() { @@ -126,9 +128,9 @@ fun LoudiusOutlinedButtonDisabledPreview() { icon = { LoudiusOutlinedButtonIcon( painter = painterResource(id = R.drawable.ic_github), - "Github Icon" + "Github Icon", ) - } + }, ) } } @@ -144,9 +146,9 @@ fun LoudiusOutlinedButtonWithIconLargePreview() { icon = { LoudiusOutlinedButtonIcon( painter = painterResource(id = R.drawable.ic_github), - "Github Icon" + "Github Icon", ) - } + }, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt index 168d40e60..e261e9d52 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt @@ -18,12 +18,13 @@ enum class LoudiusTextStyle { TitleLarge, ScreenContent, } + @Composable fun LoudiusText( text: String, modifier: Modifier = Modifier, style: LoudiusTextStyle = LoudiusTextStyle.ListCaption, - ) { +) { Text( modifier = modifier, text = text, @@ -39,7 +40,6 @@ fun LoudiusText( LoudiusTextStyle.Button -> MaterialTheme.typography.labelLarge LoudiusTextStyle.TitleLarge -> MaterialTheme.typography.titleLarge LoudiusTextStyle.ScreenContent -> MaterialTheme.typography.bodyLarge - }, color = when (style) { LoudiusTextStyle.ListHeaderWarning -> MaterialTheme.colorScheme.error @@ -59,4 +59,4 @@ fun LoudiusTextStyles() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 1a610e1a9..0c3f278f5 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -61,7 +61,7 @@ fun LoginScreen() { painter = painterResource(id = R.drawable.ic_github), contentDescription = stringResource(R.string.github_icon), ) - } + }, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 4c6d7727a..0e3d0b06a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -132,9 +132,9 @@ private fun PullRequestItem( RepoDetails( modifier = modifier, pullRequestTitle = data.title, - repositoryName = data.fullRepositoryName + repositoryName = data.fullRepositoryName, ) - } + }, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 131f5206a..13c4fc811 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -174,7 +174,7 @@ private fun ReviewerItem( }, action = { NotifyButtonOrLoadingIndicator(reviewer = reviewer, onNotifyClick = onNotifyClick) - } + }, ) } @@ -210,7 +210,7 @@ private fun ReviewerAvatarView(modifier: Modifier = Modifier) { private fun IsReviewedHeadlineText(reviewer: Reviewer) { LoudiusText( text = resolveIsReviewedText(reviewer), - style = if (reviewer.isReviewDone) LoudiusTextStyle.ListHeader else LoudiusTextStyle.ListHeaderWarning + style = if (reviewer.isReviewDone) LoudiusTextStyle.ListHeader else LoudiusTextStyle.ListHeaderWarning, ) } From 07edeec0e279d482270ec8435b4fcb24218e7354 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Tue, 4 Apr 2023 14:57:20 +0200 Subject: [PATCH 300/526] chore: add missing copyrights --- .../loudius/ui/components/LoudiusListItem.kt | 16 ++++++++++++++++ .../loudius/ui/components/LoudiusText.kt | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt index 131f5b264..0bfabf07f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.foundation.Image diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt index e261e9d52..4bfa12c8f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.foundation.layout.Column From 6a4a50f5ff7834b7540210b1bca02e2d87e36354 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 5 Apr 2023 12:37:23 +0200 Subject: [PATCH 301/526] chore: add skirt library and migrate code to use it --- app/build.gradle | 5 + .../loudius/ExampleInstrumentedTest.kt | 7 +- .../loudius/domain/AuthRepositoryImplTest.kt | 27 +- .../domain/PullRequestRepositoryImpTest.kt | 56 +++-- .../loudius/domain/UserLocalDataSourceTest.kt | 9 +- .../datasource/AuthNetworkDataSourceTest.kt | 28 +-- .../PullRequestsNetworkDataSourceTest.kt | 155 ++++++------ .../network/datasource/UserDataSourceTest.kt | 41 ++-- .../intercept/AuthFailureInterceptorTest.kt | 16 +- .../network/intercept/AuthInterceptorTest.kt | 8 +- .../appunite/loudius/ui/MainViewModelTest.kt | 17 +- .../AuthenticatingViewModelTest.kt | 90 +++++-- .../pullrequests/PullRequestsViewModelTest.kt | 64 +++-- .../ui/reviewers/ReviewersViewModelTest.kt | 230 ++++++++++++------ 14 files changed, 467 insertions(+), 286 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4959d10b6..9421f385b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,6 +101,11 @@ dependencies { //gson implementation 'com.google.code.gson:gson:2.10.1' + // assertion library + testImplementation 'io.strikt:strikt-core:0.34.0' + testImplementation 'io.strikt:strikt-mockk:0.34.0' + androidTestImplementation 'io.strikt:strikt-core:0.34.0' + //testing testImplementation "io.mockk:mockk:1.13.3" testImplementation("com.squareup.okhttp3:mockwebserver:4.10.0") diff --git a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt index 5decb6a8a..b8c78bdba 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt @@ -18,9 +18,10 @@ package com.appunite.loudius import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import junit.framework.TestCase.assertEquals import org.junit.Test import org.junit.runner.RunWith +import strikt.api.expectThat +import strikt.assertions.isEqualTo /** * Instrumented test, which will execute on an Android device. @@ -33,6 +34,8 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.appunite.loudius", appContext.packageName) + + expectThat(appContext.packageName) + .isEqualTo("com.appunite.loudius") } } diff --git a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt index 8d8a0d27d..47be8a024 100644 --- a/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/AuthRepositoryImplTest.kt @@ -25,8 +25,11 @@ import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.isEmpty +import strikt.assertions.isEqualTo +import strikt.assertions.isSuccess @OptIn(ExperimentalCoroutinesApi::class) class AuthRepositoryImplTest { @@ -44,13 +47,12 @@ class AuthRepositoryImplTest { runTest { val code = "validCode" - val result = repository.fetchAccessToken("clientId", "clientSecret", code) + val accessToken = repository.fetchAccessToken("clientId", "clientSecret", code) coVerify(exactly = 1) { networkDataSource.getAccessToken(any(), any(), any()) } - assertEquals( - Result.success("validAccessToken"), - result, - ) { "Expected success result with valid access token" } + expectThat(accessToken) + .isSuccess() + .isEqualTo("validAccessToken") } @Test @@ -60,24 +62,25 @@ class AuthRepositoryImplTest { repository.fetchAccessToken("clientId", "clientSecret", code) - val result = repository.getAccessToken() - assertEquals("validAccessToken", result) { "Expected valid access token" } + val accessToken = repository.getAccessToken() + + expectThat(accessToken).isEqualTo("validAccessToken") } @Test fun `GIVEN token stored WHEN getting access token THEN return stored access token`() = runTest { localDataSource.saveAccessToken("validAccessToken") - val result = repository.getAccessToken() + val accessToken = repository.getAccessToken() - assertEquals("validAccessToken", result) { "Expected valid access token" } + expectThat(accessToken).isEqualTo("validAccessToken") } @Test fun `GIVEN not stored access token WHEN getting access token THEN return empty string`() = runTest { - val result = repository.getAccessToken() + val accessToken = repository.getAccessToken() - assertEquals("", result) { "Expected empty string" } + expectThat(accessToken).isEmpty() } } diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index ae9439231..6e02ff151 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -28,9 +28,12 @@ import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.containsExactly +import strikt.assertions.isEqualTo +import strikt.assertions.isSuccess import java.time.LocalDateTime @OptIn(ExperimentalCoroutinesApi::class) @@ -48,19 +51,30 @@ class PullRequestRepositoryImpTest { @Test fun `GIVEN correct values WHEN get reviews THEN return result with reviews excluding ones from current user`() = runTest { - val actual = repository.getReviews("example", "example", "correctPullRequestNumber") - - val date1 = LocalDateTime.parse("2022-01-29T10:00:00") - - val expected = Result.success( - listOf( - Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), - Review("5", User(2, "user2"), ReviewState.COMMENTED, date1), - Review("6", User(2, "user2"), ReviewState.APPROVED, date1), - ), - ) - - assertEquals(expected, actual) + val result = repository.getReviews("example", "example", "correctPullRequestNumber") + + expectThat(result) + .isSuccess() + .containsExactly( + Review( + "4", + User(2, "user2"), + ReviewState.COMMENTED, + LocalDateTime.parse("2022-01-29T10:00:00") + ), + Review( + "5", + User(2, "user2"), + ReviewState.COMMENTED, + LocalDateTime.parse("2022-01-29T10:00:00") + ), + Review( + "6", + User(2, "user2"), + ReviewState.APPROVED, + LocalDateTime.parse("2022-01-29T10:00:00") + ), + ) } // TODO: Write tests with failure cases @@ -71,22 +85,20 @@ class PullRequestRepositoryImpTest { @Test fun `GIVEN correct values WHEN get requested reviewers THEN return result with requested reviewers`() = runTest { - val actual = repository.getRequestedReviewers( + val result = repository.getRequestedReviewers( "example", "example", "correctPullRequestNumber", ) - val expected = Result.success( + expectThat(result).isSuccess().isEqualTo( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), RequestedReviewer(4, "user4"), ), - ), + ) ) - - assertEquals(expected, actual) } // TODO: Write tests with failure cases } @@ -96,16 +108,14 @@ class PullRequestRepositoryImpTest { @Test fun `GIVEN correct values WHEN notify THEN return success result`() = runTest { - val actual = repository.notify( + val result = repository.notify( "exampleOwner", "exampleRepo", "correctPullRequestNumber", "@ExampleUser", ) - val expected = Result.success(Unit) - - assertEquals(expected, actual) + expectThat(result).isSuccess() } // TODO: Write tests with failure cases } diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index 70c3f436c..6770beec7 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -22,8 +22,10 @@ import com.appunite.loudius.domain.store.UserLocalDataSourceImpl import io.mockk.every import io.mockk.mockk import io.mockk.verify -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.isEmpty +import strikt.assertions.isEqualTo class UserLocalDataSourceTest { private val sharedPreferences = mockk(relaxed = true) { @@ -37,7 +39,8 @@ class UserLocalDataSourceTest { @Test fun `GIVEN filled data source WHEN getting access token THEN return access token`() { val result = userLocalDataSource.getAccessToken() - assertEquals("exampleAccessToken", result) { "Access token should be correct" } + + expectThat(result).isEqualTo("exampleAccessToken") } @Test @@ -45,7 +48,7 @@ class UserLocalDataSourceTest { every { sharedPreferences.getString("access_token", null) } returns null val result = userLocalDataSource.getAccessToken() - assertEquals("", result) + expectThat(result).isEmpty() } @Test diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 141972b37..1cd420a54 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -19,7 +19,6 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.fakes.FakeAuthRepository -import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.testOkHttpClient @@ -29,9 +28,13 @@ import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.isEqualTo +import strikt.assertions.isFailure +import strikt.assertions.isSuccess +@OptIn(ExperimentalCoroutinesApi::class) class AuthNetworkDataSourceTest { private val fakeUserRepository = FakeAuthRepository() private val testOkHttpClient = testOkHttpClient(fakeUserRepository) @@ -63,10 +66,9 @@ class AuthNetworkDataSourceTest { val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "correctCode") - Assertions.assertEquals( - Result.success("validAccessToken"), - result, - ) + expectThat(result) + .isSuccess() + .isEqualTo("validAccessToken") } @Test @@ -83,10 +85,9 @@ class AuthNetworkDataSourceTest { val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") - Assertions.assertEquals( - Result.failure(WebException.BadVerificationCodeException), - result, - ) + expectThat(result) + .isFailure() + .isEqualTo(WebException.BadVerificationCodeException) } @Test @@ -103,9 +104,8 @@ class AuthNetworkDataSourceTest { val result = authNetworkDataSource.getAccessToken("", "", "") - Assertions.assertEquals( - Result.failure(WebException.UnknownError(null, "error")), - result, - ) + expectThat(result) + .isFailure() + .isEqualTo(WebException.UnknownError(null, "error")) } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 5f3d2a18d..8de927046 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -32,10 +32,14 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.isA +import strikt.assertions.isEqualTo +import strikt.assertions.isFailure +import strikt.assertions.isSuccess +import strikt.assertions.single import java.time.LocalDateTime @ExperimentalCoroutinesApi @@ -60,13 +64,13 @@ class PullRequestsNetworkDataSourceTest { MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) - val actualResponse = pullRequestDataSource.getPullRequestsForUser( + val response = pullRequestDataSource.getPullRequestsForUser( "exampleUser", ) - Assertions.assertInstanceOf( - WebException.NetworkError::class.java, - actualResponse.exceptionOrNull(), - ) { "Exception thrown should be NetworkError type" } + + expectThat(response) + .isFailure() + .isA() } @Test @@ -158,19 +162,16 @@ class PullRequestsNetworkDataSourceTest { MockResponse().setResponseCode(200).setBody(jsonResponse), ) - val actualResponse = pullRequestDataSource.getPullRequestsForUser("exampleUser") + val response = pullRequestDataSource.getPullRequestsForUser("exampleUser") - val expected = Result.success( - PullRequestsResponse( + expectThat(response).isSuccess() + .isEqualTo(PullRequestsResponse( incompleteResults = false, totalCount = 1, items = listOf( Defaults.pullRequest(), ), - ), - ) - - assertEquals(expected, actualResponse) { "Data should be valid" } + )) } @Test @@ -188,16 +189,14 @@ class PullRequestsNetworkDataSourceTest { MockResponse().setResponseCode(401).setBody(jsonResponse), ) - val actualResponse = pullRequestDataSource.getPullRequestsForUser("exampleUser") + val response = pullRequestDataSource.getPullRequestsForUser("exampleUser") - val expected = Result.failure( - WebException.UnknownError( + expectThat(response) + .isFailure() + .isEqualTo(WebException.UnknownError( 401, "Bad credentials", - ), - ) - - assertEquals(expected, actualResponse) { "Data should be valid" } + )) } } @@ -211,15 +210,15 @@ class PullRequestsNetworkDataSourceTest { MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) - val actualResponse = pullRequestDataSource.getReviewers( + val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", "exampleNumber", ) - Assertions.assertInstanceOf( - WebException.NetworkError::class.java, - actualResponse.exceptionOrNull(), - ) { "Exception thrown should be NetworkError type" } + + expectThat(response) + .isFailure() + .isA() } @Test @@ -260,17 +259,21 @@ class PullRequestsNetworkDataSourceTest { .setBody(jsonResponse), ) - val actualResponse = pullRequestDataSource.getReviewers( + val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", "exampleNumber", ) - val requestedReviewer = - RequestedReviewer(1, "exampleLogin") - val expected = Result.success(RequestedReviewersResponse(listOf(requestedReviewer))) - - assertEquals(expected, actualResponse) { "Data should be valid" } + expectThat(response) + .isSuccess() + .isEqualTo(RequestedReviewersResponse( + listOf( + RequestedReviewer( + 1, + "exampleLogin" + ) + ))) } @Test @@ -290,20 +293,18 @@ class PullRequestsNetworkDataSourceTest { .setBody(jsonResponse), ) - val actualResponse = pullRequestDataSource.getReviewers( + val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", "exampleNumber", ) - val expected = Result.failure( - WebException.UnknownError( + expectThat(response) + .isFailure() + .isEqualTo(WebException.UnknownError( 401, "Bad credentials", - ), - ) - - assertEquals(expected, actualResponse) { "Data should be valid" } + )) } } @@ -318,15 +319,15 @@ class PullRequestsNetworkDataSourceTest { .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) - val actualResponse = pullRequestDataSource.getReviews( + val resposne = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", "exampleNumber", ) - Assertions.assertInstanceOf( - WebException.NetworkError::class.java, - actualResponse.exceptionOrNull(), - ) { "Exception thrown should be NetworkError type" } + + expectThat(resposne) + .isFailure() + .isA() } @Test @@ -381,24 +382,21 @@ class PullRequestsNetworkDataSourceTest { .setBody(jsonResponse), ) - val actualResponse = pullRequestDataSource.getReviews( + val response = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", "exampleNumber", ) - val expected = Result.success( - listOf( - Review( - "1", - User(10000000, "exampleUser"), - ReviewState.COMMENTED, - LocalDateTime.parse("2023-03-02T10:21:36"), - ), - ), - ) - - assertEquals(expected, actualResponse) { "Data should be valid" } + expectThat(response) + .isSuccess() + .single() + .isEqualTo(Review( + "1", + User(10000000, "exampleUser"), + ReviewState.COMMENTED, + LocalDateTime.parse("2023-03-02T10:21:36"), + )) } @Test @@ -418,20 +416,20 @@ class PullRequestsNetworkDataSourceTest { .setBody(jsonResponse), ) - val actualResponse = pullRequestDataSource.getReviews( + val response = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", "exampleNumber", ) - val expected = Result.failure( - WebException.UnknownError( - 401, - "Bad credentials", - ), - ) - - assertEquals(expected, actualResponse) + expectThat(response) + .isFailure() + .isEqualTo( + WebException.UnknownError( + 401, + "Bad credentials", + ) + ) } } @@ -446,16 +444,16 @@ class PullRequestsNetworkDataSourceTest { .setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST), ) - val actualResponse = pullRequestDataSource.notify( + val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", "@ExampleUser", ) - Assertions.assertInstanceOf( - WebException.NetworkError::class.java, - actualResponse.exceptionOrNull(), - ) { "Exception thrown should be NetworkError type" } + + expectThat(response) + .isFailure() + .isA() } @Test @@ -498,14 +496,14 @@ class PullRequestsNetworkDataSourceTest { MockResponse().setResponseCode(200).setBody(jsonResponse), ) - val actual = pullRequestDataSource.notify( + val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", "@ExampleUser", ) - assertEquals(Result.success(Unit), actual) + expectThat(response).isSuccess() } @Test @@ -524,20 +522,19 @@ class PullRequestsNetworkDataSourceTest { .setBody(jsonResponse), ) - val actualResponse = pullRequestDataSource.notify( + val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", "@ExampleUser", ) - val expected = Result.failure( - WebException.UnknownError( + expectThat(response) + .isFailure() + .isEqualTo(WebException.UnknownError( 401, "Bad credentials", - ), - ) - assertEquals(expected, actualResponse) + )) } } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index a5f8e8ae2..2c96949e9 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -16,7 +16,6 @@ package com.appunite.loudius.network.datasource -import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.UserService @@ -27,8 +26,12 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.isA +import strikt.assertions.isEqualTo +import strikt.assertions.isFailure +import strikt.assertions.isSuccess @ExperimentalCoroutinesApi class UserDataSourceTest { @@ -50,11 +53,11 @@ class UserDataSourceTest { MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) - val actualResponse = userDataSource.getUser() - Assertions.assertInstanceOf( - WebException.NetworkError::class.java, - actualResponse.exceptionOrNull(), - ) { "Exception thrown should be NetworkError type" } + val response = userDataSource.getUser() + + expectThat(response) + .isFailure() + .isA() } @Test @@ -101,16 +104,14 @@ class UserDataSourceTest { MockResponse().setResponseCode(200).setBody(jsonResponse), ) - val actualResponse = userDataSource.getUser() + val response = userDataSource.getUser() - val expected = Result.success( - User( + expectThat(response) + .isSuccess() + .isEqualTo(User( id = 1, login = "exampleUser", - ), - ) - - Assertions.assertEquals(expected, actualResponse) { "Data should be valid" } + )) } @Test @@ -128,15 +129,13 @@ class UserDataSourceTest { MockResponse().setResponseCode(401).setBody(jsonResponse), ) - val actualResponse = userDataSource.getUser() + val response = userDataSource.getUser() - val expected = Result.failure( - WebException.UnknownError( + expectThat(response) + .isFailure() + .isEqualTo(WebException.UnknownError( 401, "Bad credentials", - ), - ) - - Assertions.assertEquals(expected, actualResponse) { "Data should be valid" } + )) } } diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt index 81b8fc228..d09d77ab3 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt @@ -29,10 +29,13 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow -import org.junit.jupiter.api.assertThrows import retrofit2.HttpException import retrofit2.http.GET +import strikt.api.expectCatching +import strikt.api.expectThat +import strikt.assertions.isA +import strikt.assertions.isFailure +import strikt.assertions.isSuccess class AuthFailureInterceptorTest { private val fakeAuthFailureHandler: AuthFailureHandler = mockk(relaxed = true) @@ -56,7 +59,9 @@ class AuthFailureInterceptorTest { MockResponse().setResponseCode(401).setBody(testDataJson) mockWebServer.enqueue(failureResponse) - assertThrows { service.makeARequest() } + expectCatching { service.makeARequest() } + .isFailure() + .isA() coVerify(exactly = 1) { fakeAuthFailureHandler.emitAuthFailure() } } @@ -68,9 +73,8 @@ class AuthFailureInterceptorTest { MockResponse().setResponseCode(200).setBody(testDataJson) mockWebServer.enqueue(successResponse) - assertDoesNotThrow("Should not throw Http exception") { - service.makeARequest() - } + expectCatching { service.makeARequest() } + .isSuccess() coVerify(exactly = 0) { fakeAuthFailureHandler.emitAuthFailure() } } diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt index 4454863fc..53dbb9b2a 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt @@ -26,9 +26,10 @@ import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import retrofit2.http.GET +import strikt.api.expectThat +import strikt.assertions.isEqualTo class AuthInterceptorTest { private val fakeUserRepository = FakeAuthRepository() @@ -53,9 +54,10 @@ class AuthInterceptorTest { service.test() val request = mockWebServer.takeRequest() - val header = request.getHeader("authorization") - assertEquals("Bearer validToken", header) + expectThat(request) + .get("Authorization header") { getHeader("authorization") } + .isEqualTo("Bearer validToken") } } diff --git a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt index b5a047033..0c99fdc0a 100644 --- a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt @@ -21,9 +21,11 @@ import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import com.appunite.loudius.util.MainDispatcherExtension import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import strikt.api.expectThat +import strikt.assertions.isEqualTo +import strikt.assertions.isNull @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -34,7 +36,10 @@ class MainViewModelTest { @Test fun `WHEN init THEN auth failure event is null`() = runTest { viewModel = MainViewModel(authFailureHandler) - assertEquals(null, viewModel.state.authFailureEvent) + + expectThat(viewModel.state) + .get(MainState::authFailureEvent) + .isNull() } @Test @@ -42,7 +47,9 @@ class MainViewModelTest { viewModel = MainViewModel(authFailureHandler) authFailureHandler.emitAuthFailure() - assertEquals(Unit, viewModel.state.authFailureEvent) + expectThat(viewModel.state) + .get(MainState::authFailureEvent) + .isEqualTo(Unit) } @Test @@ -52,6 +59,8 @@ class MainViewModelTest { viewModel.onAuthFailureHandled() - assertEquals(null, viewModel.state.authFailureEvent) + expectThat(viewModel.state) + .get(MainState::authFailureEvent) + .isNull() } } diff --git a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt index 6ff7a85b9..6e869d0be 100644 --- a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt @@ -28,10 +28,11 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.spyk -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import strikt.api.expectThat +import strikt.assertions.isEqualTo +import strikt.assertions.isNull @ExtendWith(MainDispatcherExtension::class) class AuthenticatingViewModelTest { @@ -46,11 +47,17 @@ class AuthenticatingViewModelTest { setupIntent("validCode") val viewModel = create() - assertNull(viewModel.state.errorScreenType) - assertEquals(AuthenticatingScreenNavigation.NavigateToPullRequests, viewModel.state.navigateTo) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isNull() + get(AuthenticatingState::navigateTo).isEqualTo(AuthenticatingScreenNavigation.NavigateToPullRequests) + } viewModel.onAction(AuthenticatingAction.OnNavigate) - assertNull(viewModel.state.navigateTo) + + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isNull() + get(AuthenticatingState::navigateTo).isNull() + } } @Test @@ -58,8 +65,10 @@ class AuthenticatingViewModelTest { setupIntent("invalidCode") val viewModel = create() - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) - assertNull(viewModel.state.navigateTo) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } } @Test @@ -70,8 +79,10 @@ class AuthenticatingViewModelTest { setupIntent("validCode") val viewModel = create() - assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) - assertNull(viewModel.state.navigateTo) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.GENERIC_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } } @Test @@ -84,35 +95,51 @@ class AuthenticatingViewModelTest { val viewModel = create() // ensure error screen is displayed - assertEquals(LoadingErrorType.GENERIC_ERROR, viewModel.state.errorScreenType) - assertNull(viewModel.state.navigateTo) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.GENERIC_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } // simulate success response and click try again clearMocks(repository) viewModel.onAction(AuthenticatingAction.OnTryAgainClick) // ensure user is navigated to the pull requests screen - assertEquals(AuthenticatingScreenNavigation.NavigateToPullRequests, viewModel.state.navigateTo) - assertNull(viewModel.state.errorScreenType) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isNull() + get(AuthenticatingState::navigateTo).isEqualTo(AuthenticatingScreenNavigation.NavigateToPullRequests) + } // clear the navigation state viewModel.onAction(AuthenticatingAction.OnNavigate) - assertNull(viewModel.state.navigateTo) - assertNull(viewModel.state.errorScreenType) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isNull() + get(AuthenticatingState::navigateTo).isNull() + } } @Test fun `GIVEN invalid screen and error is presented WHEN retry click THEN navigate to the login screen`() { setupIntent("invalidCode") val viewModel = create() - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } viewModel.onAction(AuthenticatingAction.OnTryAgainClick) - assertEquals(AuthenticatingScreenNavigation.NavigateToLogin, viewModel.state.navigateTo) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isEqualTo(AuthenticatingScreenNavigation.NavigateToLogin) + } + viewModel.onAction(AuthenticatingAction.OnNavigate) - assertNull(viewModel.state.navigateTo) - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } } @Test @@ -120,26 +147,35 @@ class AuthenticatingViewModelTest { savedStateHandle[NavController.KEY_DEEP_LINK_INTENT] = null val viewModel = create() - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) - assertNull(viewModel.state.navigateTo) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } } @Test fun `GIVEN no intent is provided and login error is presented WHEN retry is clicked THEN navigate to the login screen`() { savedStateHandle[NavController.KEY_DEEP_LINK_INTENT] = null val viewModel = create() - assertNull(viewModel.state.navigateTo) - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } viewModel.onAction(AuthenticatingAction.OnTryAgainClick) - assertEquals(AuthenticatingScreenNavigation.NavigateToLogin, viewModel.state.navigateTo) - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isEqualTo(AuthenticatingScreenNavigation.NavigateToLogin) + } viewModel.onAction(AuthenticatingAction.OnNavigate) - assertNull(viewModel.state.navigateTo) - assertEquals(LoadingErrorType.LOGIN_ERROR, viewModel.state.errorScreenType) + expectThat(viewModel.state) { + get(AuthenticatingState::errorScreenType).isEqualTo(LoadingErrorType.LOGIN_ERROR) + get(AuthenticatingState::navigateTo).isNull() + } } private fun setupIntent(intentCode: String) { diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index b13f53a70..9a24845a1 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -19,7 +19,6 @@ package com.appunite.loudius.ui.pullrequests import com.appunite.loudius.fakes.FakePullRequestRepository -import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults import com.appunite.loudius.util.MainDispatcherExtension @@ -29,12 +28,15 @@ import io.mockk.coEvery import io.mockk.spyk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import strikt.api.expectThat +import strikt.assertions.isEmpty +import strikt.assertions.isEqualTo +import strikt.assertions.isFalse +import strikt.assertions.isNull +import strikt.assertions.isTrue +import strikt.assertions.single @ExtendWith(MainDispatcherExtension::class) class PullRequestsViewModelTest { @@ -47,15 +49,22 @@ class PullRequestsViewModelTest { val viewModel = createViewModel() - assertTrue(viewModel.state.isLoading) + expectThat(viewModel.state) { + get(PullRequestState::isLoading).isTrue() + get(PullRequestState::isError).isFalse() + get(PullRequestState::pullRequests).isEmpty() + } } @Test fun `WHEN init THEN display pull requests list`() = runTest { val viewModel = createViewModel() - assertEquals(listOf(Defaults.pullRequest()), viewModel.state.pullRequests) - assertFalse(viewModel.state.isLoading) + expectThat(viewModel.state) { + get(PullRequestState::isLoading).isFalse() + get(PullRequestState::isError).isFalse() + get(PullRequestState::pullRequests).single().isEqualTo(Defaults.pullRequest()) + } } @Test @@ -63,8 +72,11 @@ class PullRequestsViewModelTest { coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { Result.failure(WebException.NetworkError()) } val viewModel = createViewModel() - assertEquals(emptyList(), viewModel.state.pullRequests) - assertTrue(viewModel.state.isError) + expectThat(viewModel.state) { + get(PullRequestState::isLoading).isFalse() + get(PullRequestState::isError).isTrue() + get(PullRequestState::pullRequests).isEmpty() + } } @Test @@ -75,25 +87,33 @@ class PullRequestsViewModelTest { clearMocks(pullRequestRepository) viewModel.onAction(PulLRequestsAction.RetryClick) - assertEquals(listOf(Defaults.pullRequest()), viewModel.state.pullRequests) - assertFalse(viewModel.state.isLoading) + expectThat(viewModel.state) { + get(PullRequestState::isLoading).isFalse() + get(PullRequestState::isError).isFalse() + get(PullRequestState::pullRequests).single().isEqualTo(Defaults.pullRequest()) + } } @Test fun `GIVEN item id WHEN item click THEN navigate the user to reviewers`() = runTest { val viewModel = createViewModel() - assertNull(viewModel.state.navigateToReviewers) + expectThat(viewModel.state) + .get(PullRequestState::navigateToReviewers) + .isNull() val pullRequest = Defaults.pullRequest() viewModel.onAction(PulLRequestsAction.ItemClick(pullRequest.id)) - val expected = NavigationPayload( - pullRequest.owner, - pullRequest.shortRepositoryName, - pullRequest.number.toString(), - pullRequest.createdAt.toString(), - ) - assertEquals(expected, viewModel.state.navigateToReviewers) + expectThat(viewModel.state) + .get(PullRequestState::navigateToReviewers) + .isEqualTo( + NavigationPayload( + pullRequest.owner, + pullRequest.shortRepositoryName, + pullRequest.number.toString(), + pullRequest.createdAt.toString(), + ) + ) } @Test @@ -104,6 +124,8 @@ class PullRequestsViewModelTest { viewModel.onAction(PulLRequestsAction.OnNavigateToReviewers) - assertNull(viewModel.state.navigateToReviewers) + expectThat(viewModel.state) + .get(PullRequestState::navigateToReviewers) + .isNull() } } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index d76d6d2bd..9b72c783a 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -31,14 +31,21 @@ import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import strikt.api.expectThat +import strikt.assertions.all +import strikt.assertions.containsExactly +import strikt.assertions.filterNot +import strikt.assertions.first +import strikt.assertions.isEmpty +import strikt.assertions.isEqualTo +import strikt.assertions.isFalse +import strikt.assertions.isNull +import strikt.assertions.isTrue import java.time.Clock import java.time.LocalDateTime import java.time.ZoneId @@ -91,27 +98,53 @@ class ReviewersViewModelTest { fun `GIVEN correct initial values WHEN init THEN pull request number is correct`() { viewModel = createViewModel() - assertEquals("correctPullRequestNumber", viewModel.state.pullRequestNumber) + expectThat(viewModel.state) + .get(ReviewersState::pullRequestNumber) + .isEqualTo("correctPullRequestNumber") } @Test fun `GIVEN correct initial values WHEN init starts THEN state is loading`() = runTest { - coEvery { repository.getReviews(any(), any(), any()) } coAnswers { neverCompletingSuspension() } + coEvery { + repository.getReviews( + any(), + any(), + any() + ) + } coAnswers { neverCompletingSuspension() } viewModel = createViewModel() - assertEquals(true, viewModel.state.isLoading) - assertEquals(emptyList(), viewModel.state.reviewers) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isTrue() + get(ReviewersState::isError).isFalse() + get(ReviewersState::reviewers).isEmpty() + } } @Test fun `GIVEN no reviewers WHEN init THEN state is correct with no reviewers`() { - coEvery { repository.getReviews(any(), any(), any()) } returns Result.success(emptyList()) - coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.success(RequestedReviewersResponse(emptyList())) + coEvery { + repository.getReviews( + any(), + any(), + any() + ) + } returns Result.success(emptyList()) + coEvery { + repository.getRequestedReviewers( + any(), + any(), + any() + ) + } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() - assertEquals(emptyList(), viewModel.state.reviewers) - assertEquals(false, viewModel.state.isLoading) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isFalse() + get(ReviewersState::isError).isFalse() + get(ReviewersState::reviewers).isEmpty() + } } @Test @@ -119,80 +152,110 @@ class ReviewersViewModelTest { runTest { viewModel = createViewModel() - val expected = listOf( - Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), - Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), - ) - val actual = viewModel.state.reviewers - - assertEquals(expected, actual) - assertEquals(false, viewModel.state.isLoading) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isFalse() + get(ReviewersState::isError).isFalse() + get(ReviewersState::reviewers).containsExactly( + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), + ) + } } @Test fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = runTest { - coEvery { repository.getReviews(any(), any(), any()) } returns Result.success(emptyList()) + coEvery { repository.getReviews(any(), any(), any()) } returns Result.success( + emptyList() + ) viewModel = createViewModel() - val expected = listOf( - Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), - ) - val actual = viewModel.state.reviewers + expectThat(viewModel.state) + .get(ReviewersState::reviewers) + .containsExactly( + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), + ) - assertEquals(expected, actual) } @Test fun `GIVEN only reviewers who done reviews WHEN init THEN list of reviewers is fetched`() = runTest { - coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.success(RequestedReviewersResponse(emptyList())) + coEvery { + repository.getRequestedReviewers( + any(), + any(), + any() + ) + } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() - val expected = listOf( - Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), - ) - val actual = viewModel.state.reviewers - assertEquals(expected, actual) + expectThat(viewModel.state) + .get(ReviewersState::reviewers) + .containsExactly( + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), + ) } @Test fun `WHEN there is an error during fetching data from 2 requests on init THEN error is shown`() = runTest { - coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) - coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) + coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure( + WebException.NetworkError() + ) + coEvery { + repository.getRequestedReviewers( + any(), + any(), + any() + ) + } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() - assertEquals(true, viewModel.state.isError) - assertEquals(emptyList(), viewModel.state.reviewers) - assertEquals(false, viewModel.state.isLoading) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isFalse() + get(ReviewersState::isError).isTrue() + get(ReviewersState::reviewers).isEmpty() + } } @Test fun `WHEN there is an error during fetching data on init only from requested reviewers request THEN error is shown`() = runTest { - coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) + coEvery { + repository.getRequestedReviewers( + any(), + any(), + any() + ) + } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() - assertEquals(true, viewModel.state.isError) - assertEquals(emptyList(), viewModel.state.reviewers) - assertEquals(false, viewModel.state.isLoading) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isFalse() + get(ReviewersState::isError).isTrue() + get(ReviewersState::reviewers).isEmpty() + } } @Test fun `WHEN there is an error during fetching data on init only from reviews request THEN error is shown`() = runTest { - coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure(WebException.NetworkError()) + coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure( + WebException.NetworkError() + ) viewModel = createViewModel() - assertEquals(true, viewModel.state.isError) - assertEquals(emptyList(), viewModel.state.reviewers) - assertEquals(false, viewModel.state.isLoading) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isFalse() + get(ReviewersState::isError).isTrue() + get(ReviewersState::reviewers).isEmpty() + } } } @@ -205,22 +268,37 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("user1")) - assertEquals(ReviewersSnackbarType.SUCCESS, viewModel.state.snackbarTypeShown) + expectThat(viewModel.state) + .get(ReviewersState::snackbarTypeShown) + .isEqualTo(ReviewersSnackbarType.SUCCESS) } @Test fun `WHEN successful notify action THEN show loading indicator`() = runTest { viewModel = createViewModel() - coEvery { repository.notify(any(), any(), any(), any()) } coAnswers { neverCompletingSuspension() } + coEvery { + repository.notify( + any(), + any(), + any(), + any() + ) + } coAnswers { neverCompletingSuspension() } viewModel.onAction(ReviewersAction.Notify("user1")) - assertTrue( - viewModel.state.reviewers.first { it.login == "user1" }.isLoading, - ) { "Clicked item should have loading indicator" } - assertTrue( - viewModel.state.reviewers.filterNot { it.login == "user1" }.none { it.isLoading }, - ) { "Only clicked item should have loading indicator" } + expectThat(viewModel.state) { + // Clicked item should have loading indicator + get(ReviewersState::reviewers) + .first { it.login == "user1" } + .get(Reviewer::isLoading) + .isTrue() + + // Other items should NOT have loading indicator + get(ReviewersState::reviewers) + .filterNot { it.login == "user1" } + .all { get(Reviewer::isLoading).isFalse() } + } } @Test @@ -228,9 +306,15 @@ class ReviewersViewModelTest { every { savedStateHandle.get("pull_request_number") } returns "nonExistingPullRequestNumber" viewModel = createViewModel() + expectThat(viewModel.state) + .get(ReviewersState::snackbarTypeShown) + .isNull() + viewModel.onAction(ReviewersAction.Notify("user1")) - assertEquals(ReviewersSnackbarType.FAILURE, viewModel.state.snackbarTypeShown) + expectThat(viewModel.state) + .get(ReviewersState::snackbarTypeShown) + .isEqualTo(ReviewersSnackbarType.FAILURE) } @Test @@ -241,7 +325,9 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("user1")) viewModel.onAction(ReviewersAction.OnSnackbarDismiss) - assertNull(viewModel.state.snackbarTypeShown) + expectThat(viewModel.state) + .get(ReviewersState::snackbarTypeShown) + .isNull() } @Test @@ -258,17 +344,16 @@ class ReviewersViewModelTest { clearMocks(repository) viewModel.onAction(ReviewersAction.OnTryAgain) - val expected = listOf( - Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), - Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), - ) - val actual = viewModel.state.reviewers - - assertEquals(expected, actual) - assertEquals(false, viewModel.state.isError) - assertEquals(false, viewModel.state.isLoading) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isFalse() + get(ReviewersState::isError).isFalse() + get(ReviewersState::reviewers).containsExactly( + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", true, 7, 5), + Reviewer(3, "user3", false, 7, null), + Reviewer(4, "user4", false, 7, null), + ) + } } @Test @@ -284,8 +369,11 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.OnTryAgain) - assertEquals(true, viewModel.state.isError) - assertEquals(false, viewModel.state.isLoading) + expectThat(viewModel.state) { + get(ReviewersState::isLoading).isFalse() + get(ReviewersState::isError).isTrue() + get(ReviewersState::reviewers).isEmpty() + } } } } From 05e229e63766bc5ad817aa96e96f1ef840c4b1e7 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 5 Apr 2023 12:55:39 +0200 Subject: [PATCH 302/526] Introduce currentUser as a model of logged in user --- .../domain/PullRequestRepositoryImpTest.kt | 9 ++++---- .../fakes/FakePullRequestDataSource.kt | 9 +------- .../fakes/FakePullRequestRepository.kt | 3 ++- .../ui/reviewers/ReviewersViewModelTest.kt | 21 +++++++----------- .../com/appunite/loudius/util/Defaults.kt | 22 ++++++++++--------- 5 files changed, 27 insertions(+), 37 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index 9fa4ed4e9..bb27ecedc 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -42,7 +42,7 @@ class PullRequestRepositoryImpTest { private val pullRequestDataSource = spyk(FakePullRequestDataSource()) private val userDataSource: UserDataSource = mockk { - coEvery { getUser() } returns Result.success(User(1, "user1")) + coEvery { getUser() } returns Result.success(Defaults.currentUser()) } private val repository = PullRequestRepositoryImpl(pullRequestDataSource, userDataSource) @@ -56,12 +56,11 @@ class PullRequestRepositoryImpTest { val actual = repository.getReviews("example", "example", pullRequestNumber) - val date1 = LocalDateTime.parse("2022-01-29T10:00:00") val expected = Result.success( listOf( - Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), - Review("5", User(2, "user2"), ReviewState.COMMENTED, date1), - Review("6", User(2, "user2"), ReviewState.APPROVED, date1), + Review("4", User(1, "user1"), ReviewState.COMMENTED, Defaults.date1), + Review("5", User(1, "user1"), ReviewState.COMMENTED, Defaults.date2), + Review("6", User(1, "user1"), ReviewState.APPROVED, Defaults.date3), ), ) assertEquals(expected, actual) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index c6b01ce2e..f7760197d 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -37,14 +37,7 @@ class FakePullRequestDataSource : PullRequestDataSource { pullRequestNumber: String, ): Result> = when (pullRequestNumber) { "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( - listOf( - Review("1", User(1, "user1"), ReviewState.CHANGES_REQUESTED, date1), - Review("2", User(1, "user1"), ReviewState.COMMENTED, date1), - Review("3", User(1, "user1"), ReviewState.APPROVED, date1), - Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), - Review("5", User(2, "user2"), ReviewState.COMMENTED, date1), - Review("6", User(2, "user2"), ReviewState.APPROVED, date1), - ), + Defaults.reviews() ) else -> Result.failure(WebException.UnknownError(404, null)) } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index ecff39438..88a90ca6e 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -28,7 +28,8 @@ class FakePullRequestRepository : PullRequestRepository { owner: String, repo: String, pullRequestNumber: String, - ): Result> = Result.success(Defaults.reviews()) + ): Result> = + Result.success(Defaults.reviews().filterNot { it.user == Defaults.currentUser() }) override suspend fun getRequestedReviewers( owner: String, diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index d76d6d2bd..4dd563221 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -29,6 +29,10 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -39,10 +43,6 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -121,9 +121,8 @@ class ReviewersViewModelTest { val expected = listOf( Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), + Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), ) val actual = viewModel.state.reviewers @@ -138,8 +137,8 @@ class ReviewersViewModelTest { viewModel = createViewModel() val expected = listOf( + Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), ) val actual = viewModel.state.reviewers @@ -152,10 +151,7 @@ class ReviewersViewModelTest { coEvery { repository.getRequestedReviewers(any(), any(), any()) } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() - val expected = listOf( - Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), - ) + val expected = listOf(Reviewer(1, "user1", true, 7, 5)) val actual = viewModel.state.reviewers assertEquals(expected, actual) @@ -260,9 +256,8 @@ class ReviewersViewModelTest { val expected = listOf( Reviewer(1, "user1", true, 7, 5), - Reviewer(2, "user2", true, 7, 5), + Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), - Reviewer(4, "user4", false, 7, null), ) val actual = viewModel.state.reviewers diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index e03bda3da..3ef477ffc 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -25,9 +25,9 @@ import com.appunite.loudius.network.model.User import java.time.LocalDateTime object Defaults { - private val date1 = LocalDateTime.parse("2022-01-29T10:00:00") - private val date2 = LocalDateTime.parse("2022-01-29T11:00:00") - private val date3 = LocalDateTime.parse("2022-01-29T12:00:00") + val date1: LocalDateTime = LocalDateTime.parse("2022-01-29T10:00:00") + val date2: LocalDateTime = LocalDateTime.parse("2022-01-29T11:00:00") + val date3: LocalDateTime = LocalDateTime.parse("2022-01-29T12:00:00") fun pullRequest(id: Int = 1) = PullRequest( id = id, @@ -39,17 +39,17 @@ object Defaults { ) fun reviews() = listOf( - Review("1", User(1, "user1"), ReviewState.CHANGES_REQUESTED, date1), - Review("2", User(1, "user1"), ReviewState.COMMENTED, date2), - Review("3", User(1, "user1"), ReviewState.APPROVED, date3), - Review("4", User(2, "user2"), ReviewState.COMMENTED, date1), - Review("5", User(2, "user2"), ReviewState.COMMENTED, date2), - Review("6", User(2, "user2"), ReviewState.APPROVED, date3), + Review("1", currentUser(), ReviewState.CHANGES_REQUESTED, date1), + Review("2", currentUser(), ReviewState.COMMENTED, date2), + Review("3", currentUser(), ReviewState.APPROVED, date3), + Review("4", User(1, "user1"), ReviewState.COMMENTED, date1), + Review("5", User(1, "user1"), ReviewState.COMMENTED, date2), + Review("6", User(1, "user1"), ReviewState.APPROVED, date3), ) fun requestedReviewers() = listOf( + RequestedReviewer(2, "user2"), RequestedReviewer(3, "user3"), - RequestedReviewer(4, "user4"), ) fun pullRequestsResponse() = PullRequestsResponse( @@ -57,4 +57,6 @@ object Defaults { totalCount = 1, items = listOf(pullRequest()), ) + + fun currentUser() = User(0, "currentUser") } From 71031ec2174696fa8a4fdcaf6f2e23f2517e217d Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 5 Apr 2023 13:04:57 +0200 Subject: [PATCH 303/526] Fix test title --- .../com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index bb27ecedc..2afa6a16d 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -142,7 +142,7 @@ class PullRequestRepositoryImpTest { } @Test - fun `GIVEN incorrect pull request number WHEN notifying THEN return success result`() = runTest { + fun `GIVEN incorrect pull request number WHEN notifying THEN return Unknown Error with 404 code`() = runTest { coEvery { repository.notify(any(), any(), any(), any()) } returns Result.failure(WebException.UnknownError(404, null)) From af2392ea51e4821bce045634a3db793a645f5a73 Mon Sep 17 00:00:00 2001 From: kezc Date: Wed, 5 Apr 2023 11:13:47 +0000 Subject: [PATCH 304/526] [MegaLinter] Apply linters fixes --- .../domain/PullRequestRepositoryImpTest.kt | 16 +++++++--------- .../loudius/fakes/FakePullRequestDataSource.kt | 4 +--- .../PullRequestsNetworkDataSourceTest.kt | 4 ++-- .../ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index 2afa6a16d..3a5179f15 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -30,7 +30,6 @@ import com.appunite.loudius.util.Defaults import io.mockk.coEvery import io.mockk.mockk import io.mockk.spyk -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -75,7 +74,7 @@ class PullRequestRepositoryImpTest { assertEquals( Result.failure>(WebException.UnknownError(404, null)), - actual + actual, ) } } @@ -117,9 +116,9 @@ class PullRequestRepositoryImpTest { assertEquals( Result.failure>( - WebException.UnknownError(404, null) + WebException.UnknownError(404, null), ), - actual + actual, ) } } @@ -157,7 +156,7 @@ class PullRequestRepositoryImpTest { assertEquals( Result.failure(WebException.UnknownError(404, null)), - actual + actual, ) } } @@ -170,8 +169,8 @@ class PullRequestRepositoryImpTest { coEvery { userDataSource.getUser() } returns Result.success( User( 0, - "correctAuthor" - ) + "correctAuthor", + ), ) val actual = repository.getCurrentUserPullRequests() @@ -190,9 +189,8 @@ class PullRequestRepositoryImpTest { assertEquals( Result.failure(WebException.NetworkError()), - actual + actual, ) } } - } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index f7760197d..35a2cbe87 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -21,8 +21,6 @@ import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.ReviewState -import com.appunite.loudius.network.model.User import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults import java.time.LocalDateTime @@ -37,7 +35,7 @@ class FakePullRequestDataSource : PullRequestDataSource { pullRequestNumber: String, ): Result> = when (pullRequestNumber) { "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( - Defaults.reviews() + Defaults.reviews(), ) else -> Result.failure(WebException.UnknownError(404, null)) } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 5f5551960..fb9061b67 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -25,7 +25,6 @@ import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -36,6 +35,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -161,7 +161,7 @@ class PullRequestsNetworkDataSourceTest { assertEquals( Result.success(Defaults.pullRequestsResponse()), - actualResponse + actualResponse, ) { "Data should be valid" } } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 4dd563221..0c03dada0 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -29,10 +29,6 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Assertions.assertEquals @@ -43,6 +39,10 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From e48f935bb69c253bf9d88dd9ad311f3d3aaf8789 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 5 Apr 2023 13:46:07 +0200 Subject: [PATCH 305/526] chore: add ktlin rule for using correct assertions library --- app/build.gradle | 3 + build.gradle | 1 + custom-ktlint-rules/.gitignore | 1 + custom-ktlint-rules/build.gradle | 19 +++++ .../loudius/rules/CustomRuleSetProvider.kt | 13 ++++ .../rules/UseStriktAssertionLibrary.kt | 36 +++++++++ ...om.pinterest.ktlint.core.RuleSetProviderV2 | 1 + .../rules/UseStriktAssertionLibraryTest.kt | 78 +++++++++++++++++++ settings.gradle | 1 + 9 files changed, 153 insertions(+) create mode 100644 custom-ktlint-rules/.gitignore create mode 100644 custom-ktlint-rules/build.gradle create mode 100644 custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt create mode 100644 custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt create mode 100644 custom-ktlint-rules/src/main/resources/META-INF/services/com.pinterest.ktlint.core.RuleSetProviderV2 create mode 100644 custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt diff --git a/app/build.gradle b/app/build.gradle index 9421f385b..96b0747c5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,6 +113,9 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + // ktlint + ktlintRuleset project(":custom-ktlint-rules") } tasks.withType(Test) { diff --git a/build.gradle b/build.gradle index 5db92eafa..6ad567c7c 100644 --- a/build.gradle +++ b/build.gradle @@ -8,4 +8,5 @@ plugins { id 'com.android.library' version '7.4.1' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'com.google.dagger.hilt.android' version '2.45' apply false + id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false } diff --git a/custom-ktlint-rules/.gitignore b/custom-ktlint-rules/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/custom-ktlint-rules/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/custom-ktlint-rules/build.gradle b/custom-ktlint-rules/build.gradle new file mode 100644 index 000000000..1380b4ed4 --- /dev/null +++ b/custom-ktlint-rules/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + compileOnly 'com.pinterest.ktlint:ktlint-core:0.48.2' + compileOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.48.2' + testImplementation 'junit:junit:4.13.2' + testImplementation 'io.strikt:strikt-core:0.34.0' + testImplementation 'org.assertj:assertj-core:3.10.0' + testImplementation 'com.pinterest.ktlint:ktlint-core:0.48.2' + testImplementation 'com.pinterest.ktlint:ktlint-test:0.48.2' +} \ No newline at end of file diff --git a/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt new file mode 100644 index 000000000..0947f1f90 --- /dev/null +++ b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt @@ -0,0 +1,13 @@ +package com.appunite.loudius.rules + +import com.pinterest.ktlint.core.RuleProvider +import com.pinterest.ktlint.core.RuleSetProviderV2 + +internal const val RULE_SET_ID = "loudius-rule-set-id" + +class CustomRuleSetProvider : RuleSetProviderV2(id = RULE_SET_ID, about = NO_ABOUT) { + override fun getRuleProviders(): Set = + setOf( + RuleProvider { UseStriktAssertionLibrary() }, + ) +} \ No newline at end of file diff --git a/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt new file mode 100644 index 000000000..e6c8a8569 --- /dev/null +++ b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt @@ -0,0 +1,36 @@ +package com.appunite.loudius.rules + +import com.pinterest.ktlint.core.Rule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.KtImportDirective +import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes + +class UseStriktAssertionLibrary : Rule("use-strikt-assertion-library") { + private val forbiddenPackageNames = listOf( + "junit.framework.TestCase", + "org.junit.jupiter.api.Assertions", + "org.junit.Assert", + "junit.framework.Assert", + ) + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { + val importDirective = node.psi as KtImportDirective + val path = importDirective.importPath?.pathStr ?: return + + forbiddenPackageNames.forEach { forbiddenPackage -> + if (path.contains(forbiddenPackage)) { + emit( + node.startOffset, + "Instead of using $forbiddenPackage use strikt.api.expectThat", + false, + ) + } + } + } + } + +} \ No newline at end of file diff --git a/custom-ktlint-rules/src/main/resources/META-INF/services/com.pinterest.ktlint.core.RuleSetProviderV2 b/custom-ktlint-rules/src/main/resources/META-INF/services/com.pinterest.ktlint.core.RuleSetProviderV2 new file mode 100644 index 000000000..c0fb3cdae --- /dev/null +++ b/custom-ktlint-rules/src/main/resources/META-INF/services/com.pinterest.ktlint.core.RuleSetProviderV2 @@ -0,0 +1 @@ +com.appunite.loudius.rules.CustomRuleSetProvider \ No newline at end of file diff --git a/custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt b/custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt new file mode 100644 index 000000000..a9f27cf2a --- /dev/null +++ b/custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt @@ -0,0 +1,78 @@ +package com.appunite.loudius.rules + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.Test + +class UseStriktAssertionLibraryTest { + + private val wrappingRuleAssertThat = assertThatRule { UseStriktAssertionLibrary() } + + @Test + fun `do not allow TestCase library`() { + //language=kotlin + val code = + """ + import a.b.c + import junit.framework.TestCase.assertEquals + import foo.bar + """.trimIndent() + + wrappingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(2, 1, "Instead of using junit.framework.TestCase use strikt.api.expectThat") + } + + + @Test + fun `do not allow jupiter assertions library`() { + //language=kotlin + val code = + """ + import a.b.c + import org.junit.jupiter.api.Assertions.assertEquals + import foo.bar + """.trimIndent() + + wrappingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(2, 1, "Instead of using org.junit.jupiter.api.Assertions use strikt.api.expectThat") + } + + @Test + fun `do not allow junit assertions library`() { + //language=kotlin + val code = + """ + import a.b.c + import org.junit.Assert.assertEquals + import foo.bar + """.trimIndent() + + wrappingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(2, 1, "Instead of using org.junit.Assert use strikt.api.expectThat") + } + @Test + fun `do not allow junit assertions framework`() { + //language=kotlin + val code = + """ + import a.b.c + import junit.framework.Assert.assertEquals + import foo.bar + """.trimIndent() + + wrappingRuleAssertThat(code) + .hasLintViolationWithoutAutoCorrect(2, 1, "Instead of using junit.framework.Assert use strikt.api.expectThat") + } + + @Test + fun `allows using skrt library`() { + //language=kotlin + val code = + """ + import a.b.c + import strikt.api.expectThat + import foo.bar + """.trimIndent() + + wrappingRuleAssertThat(code).hasNoLintViolations() + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index b46730e7a..7565b8f60 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,3 +14,4 @@ dependencyResolutionManagement { } rootProject.name = "Loudius" include ':app' +include ':custom-ktlint-rules' From 25c1a9f74906cb852897826d2d259a6b6c009bc1 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 5 Apr 2023 14:26:55 +0200 Subject: [PATCH 306/526] chore: cleanup formatting --- .../domain/PullRequestRepositoryImpTest.kt | 21 ++-- .../loudius/fakes/FakeAuthRepository.kt | 2 +- .../fakes/FakePullRequestDataSource.kt | 14 +-- .../fakes/FakePullRequestRepository.kt | 6 +- .../loudius/network/NetworkTestDoubles.kt | 4 +- .../datasource/AuthNetworkDataSourceTest.kt | 8 +- .../PullRequestsNetworkDataSourceTest.kt | 111 ++++++++++-------- .../network/datasource/UserDataSourceTest.kt | 26 ++-- .../intercept/AuthFailureInterceptorTest.kt | 3 +- .../network/intercept/AuthInterceptorTest.kt | 2 +- .../AuthenticatingViewModelTest.kt | 4 +- .../pullrequests/PullRequestsViewModelTest.kt | 2 +- .../ui/reviewers/ReviewersViewModelTest.kt | 8 +- .../com/appunite/loudius/util/Defaults.kt | 8 +- .../loudius/util/MainDispatcherExtension.kt | 2 +- 15 files changed, 116 insertions(+), 105 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index 29ca61c62..4995339ea 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -63,8 +63,8 @@ class PullRequestRepositoryImpTest { .containsExactly( Review("4", User(1, "user1"), ReviewState.COMMENTED, Defaults.date1), Review("5", User(1, "user1"), ReviewState.COMMENTED, Defaults.date2), - Review("6", User(1, "user1"), ReviewState.APPROVED, Defaults.date3), - ) + Review("6", User(1, "user1"), ReviewState.APPROVED, Defaults.date3) + ) } @Test @@ -90,15 +90,15 @@ class PullRequestRepositoryImpTest { val result = repository.getRequestedReviewers( "example", "example", - pullRequestNumber, + pullRequestNumber ) expectThat(result).isSuccess().isEqualTo( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), - RequestedReviewer(4, "user4"), - ), + RequestedReviewer(4, "user4") + ) ) ) } @@ -111,7 +111,7 @@ class PullRequestRepositoryImpTest { val response = repository.getRequestedReviewers( "example", "example", - pullRequestNumber, + pullRequestNumber ) expectThat(response) @@ -131,7 +131,7 @@ class PullRequestRepositoryImpTest { "exampleOwner", "exampleRepo", pullRequestNumber, - "@ExampleUser", + "@ExampleUser" ) expectThat(result) @@ -149,7 +149,7 @@ class PullRequestRepositoryImpTest { "exampleOwner", "exampleRepo", pullRequestNumber, - "@ExampleUser", + "@ExampleUser" ) expectThat(response) @@ -166,8 +166,8 @@ class PullRequestRepositoryImpTest { coEvery { userDataSource.getUser() } returns Result.success( User( 0, - "correctAuthor", - ), + "correctAuthor" + ) ) val response = repository.getCurrentUserPullRequests() @@ -186,7 +186,6 @@ class PullRequestRepositoryImpTest { val response = repository.getCurrentUserPullRequests() - expectThat(response) .isFailure() .isEqualTo(WebException.NetworkError()) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 026dc67ed..8bd810a93 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -24,7 +24,7 @@ class FakeAuthRepository : AuthRepository { override suspend fun fetchAccessToken( clientId: String, clientSecret: String, - code: String, + code: String ): Result = when (code) { "validCode" -> Result.success("validToken") "invalidCode" -> Result.failure(WebException.BadVerificationCodeException) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index 35a2cbe87..a62965399 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -32,10 +32,10 @@ class FakePullRequestDataSource : PullRequestDataSource { override suspend fun getReviews( owner: String, repository: String, - pullRequestNumber: String, + pullRequestNumber: String ): Result> = when (pullRequestNumber) { "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( - Defaults.reviews(), + Defaults.reviews() ) else -> Result.failure(WebException.UnknownError(404, null)) } @@ -43,15 +43,15 @@ class FakePullRequestDataSource : PullRequestDataSource { override suspend fun getReviewers( owner: String, repository: String, - pullRequestNumber: String, + pullRequestNumber: String ): Result = when (pullRequestNumber) { "correctPullRequestNumber", "onlyRequestedReviewersPullNumber" -> Result.success( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), - RequestedReviewer(4, "user4"), - ), - ), + RequestedReviewer(4, "user4") + ) + ) ) else -> Result.failure(WebException.UnknownError(404, null)) } @@ -65,7 +65,7 @@ class FakePullRequestDataSource : PullRequestDataSource { owner: String, repository: String, pullRequestNumber: String, - message: String, + message: String ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) else -> Result.failure(WebException.UnknownError(404, null)) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 88a90ca6e..4dafd2c36 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -27,14 +27,14 @@ class FakePullRequestRepository : PullRequestRepository { override suspend fun getReviews( owner: String, repo: String, - pullRequestNumber: String, + pullRequestNumber: String ): Result> = Result.success(Defaults.reviews().filterNot { it.user == Defaults.currentUser() }) override suspend fun getRequestedReviewers( owner: String, repo: String, - pullRequestNumber: String, + pullRequestNumber: String ): Result = Result.success(RequestedReviewersResponse(Defaults.requestedReviewers())) @@ -45,7 +45,7 @@ class FakePullRequestRepository : PullRequestRepository { owner: String, repo: String, pullRequestNumber: String, - message: String, + message: String ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) else -> Result.failure(WebException.UnknownError(404, null)) diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index ba9f71cd4..f8013e404 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), - authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl(), + authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl() ) = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) @@ -52,7 +52,7 @@ private fun testGson() = fun retrofitTestDouble( client: OkHttpClient = testOkHttpClient(), gson: Gson = testGson(), - mockWebServer: MockWebServer, + mockWebServer: MockWebServer ): Retrofit = Retrofit.Builder() .client(client) .addConverterFactory(GsonConverterFactory.create(gson)) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 1cd420a54..9091dd77e 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -41,7 +41,7 @@ class AuthNetworkDataSourceTest { private val mockWebServer: MockWebServer = MockWebServer() private val authService = retrofitTestDouble( mockWebServer = mockWebServer, - client = testOkHttpClient, + client = testOkHttpClient ).create(AuthService::class.java) @AfterEach @@ -60,7 +60,7 @@ class AuthNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse), + MockResponse().setResponseCode(200).setBody(jsonResponse) ) val result = @@ -80,7 +80,7 @@ class AuthNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse), + MockResponse().setResponseCode(200).setBody(jsonResponse) ) val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") @@ -99,7 +99,7 @@ class AuthNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse), + MockResponse().setResponseCode(200).setBody(jsonResponse) ) val result = authNetworkDataSource.getAccessToken("", "", "") diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 8eeec3a68..c96605dc9 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -61,11 +61,11 @@ class PullRequestsNetworkDataSourceTest { fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = runTest { mockWebServer.enqueue( - MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) ) val response = pullRequestDataSource.getPullRequestsForUser( - "exampleUser", + "exampleUser" ) expectThat(response) @@ -159,7 +159,7 @@ class PullRequestsNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse), + MockResponse().setResponseCode(200).setBody(jsonResponse) ) val response = pullRequestDataSource.getPullRequestsForUser("exampleUser") @@ -172,9 +172,9 @@ class PullRequestsNetworkDataSourceTest { incompleteResults = false, totalCount = 1, items = listOf( - Defaults.pullRequest(), - ), - ), + Defaults.pullRequest() + ) + ) ) } @@ -190,17 +190,19 @@ class PullRequestsNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(401).setBody(jsonResponse), + MockResponse().setResponseCode(401).setBody(jsonResponse) ) val response = pullRequestDataSource.getPullRequestsForUser("exampleUser") expectThat(response) .isFailure() - .isEqualTo(WebException.UnknownError( - 401, - "Bad credentials", - )) + .isEqualTo( + WebException.UnknownError( + 401, + "Bad credentials" + ) + ) } } @@ -211,13 +213,13 @@ class PullRequestsNetworkDataSourceTest { fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = runTest { mockWebServer.enqueue( - MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) ) val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", - "exampleNumber", + "exampleNumber" ) expectThat(response) @@ -260,24 +262,27 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(200) - .setBody(jsonResponse), + .setBody(jsonResponse) ) val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", - "exampleNumber", + "exampleNumber" ) expectThat(response) .isSuccess() - .isEqualTo(RequestedReviewersResponse( - listOf( - RequestedReviewer( - 1, - "exampleLogin" + .isEqualTo( + RequestedReviewersResponse( + listOf( + RequestedReviewer( + 1, + "exampleLogin" + ) ) - ))) + ) + ) } @Test @@ -294,21 +299,23 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse), + .setBody(jsonResponse) ) val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", - "exampleNumber", + "exampleNumber" ) expectThat(response) .isFailure() - .isEqualTo(WebException.UnknownError( - 401, - "Bad credentials", - )) + .isEqualTo( + WebException.UnknownError( + 401, + "Bad credentials" + ) + ) } } @@ -320,13 +327,13 @@ class PullRequestsNetworkDataSourceTest { runTest { mockWebServer.enqueue( MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) ) val resposne = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", - "exampleNumber", + "exampleNumber" ) expectThat(resposne) @@ -383,24 +390,26 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(200) - .setBody(jsonResponse), + .setBody(jsonResponse) ) val response = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", - "exampleNumber", + "exampleNumber" ) expectThat(response) .isSuccess() .single() - .isEqualTo(Review( - "1", - User(10000000, "exampleUser"), - ReviewState.COMMENTED, - LocalDateTime.parse("2023-03-02T10:21:36"), - )) + .isEqualTo( + Review( + "1", + User(10000000, "exampleUser"), + ReviewState.COMMENTED, + LocalDateTime.parse("2023-03-02T10:21:36") + ) + ) } @Test @@ -417,13 +426,13 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse), + .setBody(jsonResponse) ) val response = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", - "exampleNumber", + "exampleNumber" ) expectThat(response) @@ -431,7 +440,7 @@ class PullRequestsNetworkDataSourceTest { .isEqualTo( WebException.UnknownError( 401, - "Bad credentials", + "Bad credentials" ) ) } @@ -445,14 +454,14 @@ class PullRequestsNetworkDataSourceTest { runTest { mockWebServer.enqueue( MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST), + .setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST) ) val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser", + "@ExampleUser" ) expectThat(response) @@ -497,14 +506,14 @@ class PullRequestsNetworkDataSourceTest { } """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse), + MockResponse().setResponseCode(200).setBody(jsonResponse) ) val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser", + "@ExampleUser" ) expectThat(response).isSuccess() @@ -523,22 +532,24 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse), + .setBody(jsonResponse) ) val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser", + "@ExampleUser" ) expectThat(response) .isFailure() - .isEqualTo(WebException.UnknownError( - 401, - "Bad credentials", - )) + .isEqualTo( + WebException.UnknownError( + 401, + "Bad credentials" + ) + ) } } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index 2c96949e9..4c27de3d1 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -50,7 +50,7 @@ class UserDataSourceTest { fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = runTest { mockWebServer.enqueue( - MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) ) val response = userDataSource.getUser() @@ -101,17 +101,19 @@ class UserDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse), + MockResponse().setResponseCode(200).setBody(jsonResponse) ) val response = userDataSource.getUser() expectThat(response) .isSuccess() - .isEqualTo(User( - id = 1, - login = "exampleUser", - )) + .isEqualTo( + User( + id = 1, + login = "exampleUser" + ) + ) } @Test @@ -126,16 +128,18 @@ class UserDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(401).setBody(jsonResponse), + MockResponse().setResponseCode(401).setBody(jsonResponse) ) val response = userDataSource.getUser() expectThat(response) .isFailure() - .isEqualTo(WebException.UnknownError( - 401, - "Bad credentials", - )) + .isEqualTo( + WebException.UnknownError( + 401, + "Bad credentials" + ) + ) } } diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt index d09d77ab3..86fef5be5 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt @@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test import retrofit2.HttpException import retrofit2.http.GET import strikt.api.expectCatching -import strikt.api.expectThat import strikt.assertions.isA import strikt.assertions.isFailure import strikt.assertions.isSuccess @@ -43,7 +42,7 @@ class AuthFailureInterceptorTest { private val mockWebServer: MockWebServer = MockWebServer() private val service = retrofitTestDouble( mockWebServer = mockWebServer, - client = testOkHttpClient, + client = testOkHttpClient ).create(TestApi::class.java) @AfterEach diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt index 53dbb9b2a..5a3e095dd 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt @@ -37,7 +37,7 @@ class AuthInterceptorTest { private val mockWebServer: MockWebServer = MockWebServer() private val service = retrofitTestDouble( mockWebServer = mockWebServer, - client = testOkHttpClient, + client = testOkHttpClient ).create(TestApi::class.java) @AfterEach diff --git a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt index 6e869d0be..accc31385 100644 --- a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt @@ -74,7 +74,7 @@ class AuthenticatingViewModelTest { @Test fun `GIVEN unexpected Github behavior WHEN authenticating screen is opened THEN show generic error screen`() { coEvery { repository.fetchAccessToken(any(), any(), any()) } returns Result.failure( - WebException.UnknownError(null, null), + WebException.UnknownError(null, null) ) setupIntent("validCode") val viewModel = create() @@ -89,7 +89,7 @@ class AuthenticatingViewModelTest { fun `GIVEN unexpected error is presented WHEN try again success THEN navigate to pull requests`() { // simulate unknown error response coEvery { repository.fetchAccessToken(any(), any(), any()) } returns Result.failure( - WebException.UnknownError(null, null), + WebException.UnknownError(null, null) ) setupIntent("validCode") val viewModel = create() diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 9a24845a1..8130cc3e2 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -111,7 +111,7 @@ class PullRequestsViewModelTest { pullRequest.owner, pullRequest.shortRepositoryName, pullRequest.number.toString(), - pullRequest.createdAt.toString(), + pullRequest.createdAt.toString() ) ) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 878f515b7..29145fab5 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -158,7 +158,7 @@ class ReviewersViewModelTest { get(ReviewersState::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), - Reviewer(3, "user3", false, 7, null), + Reviewer(3, "user3", false, 7, null) ) } } @@ -175,9 +175,8 @@ class ReviewersViewModelTest { .get(ReviewersState::reviewers) .containsExactly( Reviewer(2, "user2", false, 7, null), - Reviewer(3, "user3", false, 7, null), + Reviewer(3, "user3", false, 7, null) ) - } @Test @@ -192,7 +191,6 @@ class ReviewersViewModelTest { } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() - expectThat(viewModel.state) .get(ReviewersState::reviewers) .containsExactly( @@ -348,7 +346,7 @@ class ReviewersViewModelTest { get(ReviewersState::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), - Reviewer(3, "user3", false, 7, null), + Reviewer(3, "user3", false, 7, null) ) } } diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index 3ef477ffc..42079c090 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -35,7 +35,7 @@ object Defaults { number = id, repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", - LocalDateTime.parse("2023-03-07T08:21:45").plusHours(id.toLong()), + LocalDateTime.parse("2023-03-07T08:21:45").plusHours(id.toLong()) ) fun reviews() = listOf( @@ -44,18 +44,18 @@ object Defaults { Review("3", currentUser(), ReviewState.APPROVED, date3), Review("4", User(1, "user1"), ReviewState.COMMENTED, date1), Review("5", User(1, "user1"), ReviewState.COMMENTED, date2), - Review("6", User(1, "user1"), ReviewState.APPROVED, date3), + Review("6", User(1, "user1"), ReviewState.APPROVED, date3) ) fun requestedReviewers() = listOf( RequestedReviewer(2, "user2"), - RequestedReviewer(3, "user3"), + RequestedReviewer(3, "user3") ) fun pullRequestsResponse() = PullRequestsResponse( incompleteResults = false, totalCount = 1, - items = listOf(pullRequest()), + items = listOf(pullRequest()) ) fun currentUser() = User(0, "currentUser") diff --git a/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt index b4f48d1f4..9187c24bb 100644 --- a/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt +++ b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt @@ -28,7 +28,7 @@ import org.junit.jupiter.api.extension.ExtensionContext @OptIn(ExperimentalCoroutinesApi::class) class MainDispatcherExtension( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : BeforeTestExecutionCallback, AfterTestExecutionCallback { override fun beforeTestExecution(context: ExtensionContext?) { Dispatchers.setMain(testDispatcher) From 426fba932a58e13feb931706f8866c9241d57127 Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Wed, 5 Apr 2023 12:31:16 +0000 Subject: [PATCH 307/526] [MegaLinter] Apply linters fixes --- .../domain/PullRequestRepositoryImpTest.kt | 20 ++--- .../loudius/fakes/FakeAuthRepository.kt | 2 +- .../fakes/FakePullRequestDataSource.kt | 14 ++-- .../fakes/FakePullRequestRepository.kt | 6 +- .../loudius/network/NetworkTestDoubles.kt | 4 +- .../datasource/AuthNetworkDataSourceTest.kt | 8 +- .../PullRequestsNetworkDataSourceTest.kt | 78 +++++++++---------- .../network/datasource/UserDataSourceTest.kt | 14 ++-- .../intercept/AuthFailureInterceptorTest.kt | 2 +- .../network/intercept/AuthInterceptorTest.kt | 2 +- .../AuthenticatingViewModelTest.kt | 4 +- .../pullrequests/PullRequestsViewModelTest.kt | 4 +- .../ui/reviewers/ReviewersViewModelTest.kt | 28 +++---- .../com/appunite/loudius/util/Defaults.kt | 8 +- .../loudius/util/MainDispatcherExtension.kt | 2 +- .../loudius/rules/CustomRuleSetProvider.kt | 2 +- .../rules/UseStriktAssertionLibrary.kt | 5 +- .../rules/UseStriktAssertionLibraryTest.kt | 4 +- 18 files changed, 103 insertions(+), 104 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt index 4995339ea..f118061ef 100644 --- a/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/PullRequestRepositoryImpTest.kt @@ -63,7 +63,7 @@ class PullRequestRepositoryImpTest { .containsExactly( Review("4", User(1, "user1"), ReviewState.COMMENTED, Defaults.date1), Review("5", User(1, "user1"), ReviewState.COMMENTED, Defaults.date2), - Review("6", User(1, "user1"), ReviewState.APPROVED, Defaults.date3) + Review("6", User(1, "user1"), ReviewState.APPROVED, Defaults.date3), ) } @@ -90,16 +90,16 @@ class PullRequestRepositoryImpTest { val result = repository.getRequestedReviewers( "example", "example", - pullRequestNumber + pullRequestNumber, ) expectThat(result).isSuccess().isEqualTo( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), - RequestedReviewer(4, "user4") - ) - ) + RequestedReviewer(4, "user4"), + ), + ), ) } @@ -111,7 +111,7 @@ class PullRequestRepositoryImpTest { val response = repository.getRequestedReviewers( "example", "example", - pullRequestNumber + pullRequestNumber, ) expectThat(response) @@ -131,7 +131,7 @@ class PullRequestRepositoryImpTest { "exampleOwner", "exampleRepo", pullRequestNumber, - "@ExampleUser" + "@ExampleUser", ) expectThat(result) @@ -149,7 +149,7 @@ class PullRequestRepositoryImpTest { "exampleOwner", "exampleRepo", pullRequestNumber, - "@ExampleUser" + "@ExampleUser", ) expectThat(response) @@ -166,8 +166,8 @@ class PullRequestRepositoryImpTest { coEvery { userDataSource.getUser() } returns Result.success( User( 0, - "correctAuthor" - ) + "correctAuthor", + ), ) val response = repository.getCurrentUserPullRequests() diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 8bd810a93..026dc67ed 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -24,7 +24,7 @@ class FakeAuthRepository : AuthRepository { override suspend fun fetchAccessToken( clientId: String, clientSecret: String, - code: String + code: String, ): Result = when (code) { "validCode" -> Result.success("validToken") "invalidCode" -> Result.failure(WebException.BadVerificationCodeException) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt index a62965399..35a2cbe87 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestDataSource.kt @@ -32,10 +32,10 @@ class FakePullRequestDataSource : PullRequestDataSource { override suspend fun getReviews( owner: String, repository: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result> = when (pullRequestNumber) { "correctPullRequestNumber", "onlyReviewsPullNumber" -> Result.success( - Defaults.reviews() + Defaults.reviews(), ) else -> Result.failure(WebException.UnknownError(404, null)) } @@ -43,15 +43,15 @@ class FakePullRequestDataSource : PullRequestDataSource { override suspend fun getReviewers( owner: String, repository: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber", "onlyRequestedReviewersPullNumber" -> Result.success( RequestedReviewersResponse( listOf( RequestedReviewer(3, "user3"), - RequestedReviewer(4, "user4") - ) - ) + RequestedReviewer(4, "user4"), + ), + ), ) else -> Result.failure(WebException.UnknownError(404, null)) } @@ -65,7 +65,7 @@ class FakePullRequestDataSource : PullRequestDataSource { owner: String, repository: String, pullRequestNumber: String, - message: String + message: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) else -> Result.failure(WebException.UnknownError(404, null)) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt index 4dafd2c36..88a90ca6e 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakePullRequestRepository.kt @@ -27,14 +27,14 @@ class FakePullRequestRepository : PullRequestRepository { override suspend fun getReviews( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result> = Result.success(Defaults.reviews().filterNot { it.user == Defaults.currentUser() }) override suspend fun getRequestedReviewers( owner: String, repo: String, - pullRequestNumber: String + pullRequestNumber: String, ): Result = Result.success(RequestedReviewersResponse(Defaults.requestedReviewers())) @@ -45,7 +45,7 @@ class FakePullRequestRepository : PullRequestRepository { owner: String, repo: String, pullRequestNumber: String, - message: String + message: String, ): Result = when (pullRequestNumber) { "correctPullRequestNumber" -> Result.success(Unit) else -> Result.failure(WebException.UnknownError(404, null)) diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index f8013e404..ba9f71cd4 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), - authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl() + authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl(), ) = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) @@ -52,7 +52,7 @@ private fun testGson() = fun retrofitTestDouble( client: OkHttpClient = testOkHttpClient(), gson: Gson = testGson(), - mockWebServer: MockWebServer + mockWebServer: MockWebServer, ): Retrofit = Retrofit.Builder() .client(client) .addConverterFactory(GsonConverterFactory.create(gson)) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 9091dd77e..1cd420a54 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -41,7 +41,7 @@ class AuthNetworkDataSourceTest { private val mockWebServer: MockWebServer = MockWebServer() private val authService = retrofitTestDouble( mockWebServer = mockWebServer, - client = testOkHttpClient + client = testOkHttpClient, ).create(AuthService::class.java) @AfterEach @@ -60,7 +60,7 @@ class AuthNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse) + MockResponse().setResponseCode(200).setBody(jsonResponse), ) val result = @@ -80,7 +80,7 @@ class AuthNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse) + MockResponse().setResponseCode(200).setBody(jsonResponse), ) val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") @@ -99,7 +99,7 @@ class AuthNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse) + MockResponse().setResponseCode(200).setBody(jsonResponse), ) val result = authNetworkDataSource.getAccessToken("", "", "") diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index c96605dc9..8520e7a99 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -61,11 +61,11 @@ class PullRequestsNetworkDataSourceTest { fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = runTest { mockWebServer.enqueue( - MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) val response = pullRequestDataSource.getPullRequestsForUser( - "exampleUser" + "exampleUser", ) expectThat(response) @@ -159,7 +159,7 @@ class PullRequestsNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse) + MockResponse().setResponseCode(200).setBody(jsonResponse), ) val response = pullRequestDataSource.getPullRequestsForUser("exampleUser") @@ -172,9 +172,9 @@ class PullRequestsNetworkDataSourceTest { incompleteResults = false, totalCount = 1, items = listOf( - Defaults.pullRequest() - ) - ) + Defaults.pullRequest(), + ), + ), ) } @@ -190,7 +190,7 @@ class PullRequestsNetworkDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(401).setBody(jsonResponse) + MockResponse().setResponseCode(401).setBody(jsonResponse), ) val response = pullRequestDataSource.getPullRequestsForUser("exampleUser") @@ -200,8 +200,8 @@ class PullRequestsNetworkDataSourceTest { .isEqualTo( WebException.UnknownError( 401, - "Bad credentials" - ) + "Bad credentials", + ), ) } } @@ -213,13 +213,13 @@ class PullRequestsNetworkDataSourceTest { fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = runTest { mockWebServer.enqueue( - MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", - "exampleNumber" + "exampleNumber", ) expectThat(response) @@ -262,13 +262,13 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(200) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", - "exampleNumber" + "exampleNumber", ) expectThat(response) @@ -278,10 +278,10 @@ class PullRequestsNetworkDataSourceTest { listOf( RequestedReviewer( 1, - "exampleLogin" - ) - ) - ) + "exampleLogin", + ), + ), + ), ) } @@ -299,13 +299,13 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val response = pullRequestDataSource.getReviewers( "exampleOwner", "exampleRepo", - "exampleNumber" + "exampleNumber", ) expectThat(response) @@ -313,8 +313,8 @@ class PullRequestsNetworkDataSourceTest { .isEqualTo( WebException.UnknownError( 401, - "Bad credentials" - ) + "Bad credentials", + ), ) } } @@ -327,13 +327,13 @@ class PullRequestsNetworkDataSourceTest { runTest { mockWebServer.enqueue( MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + .setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) val resposne = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", - "exampleNumber" + "exampleNumber", ) expectThat(resposne) @@ -390,13 +390,13 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(200) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val response = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", - "exampleNumber" + "exampleNumber", ) expectThat(response) @@ -407,8 +407,8 @@ class PullRequestsNetworkDataSourceTest { "1", User(10000000, "exampleUser"), ReviewState.COMMENTED, - LocalDateTime.parse("2023-03-02T10:21:36") - ) + LocalDateTime.parse("2023-03-02T10:21:36"), + ), ) } @@ -426,13 +426,13 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val response = pullRequestDataSource.getReviews( "exampleOwner", "exampleRepo", - "exampleNumber" + "exampleNumber", ) expectThat(response) @@ -440,8 +440,8 @@ class PullRequestsNetworkDataSourceTest { .isEqualTo( WebException.UnknownError( 401, - "Bad credentials" - ) + "Bad credentials", + ), ) } } @@ -454,14 +454,14 @@ class PullRequestsNetworkDataSourceTest { runTest { mockWebServer.enqueue( MockResponse() - .setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST) + .setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST), ) val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser" + "@ExampleUser", ) expectThat(response) @@ -506,14 +506,14 @@ class PullRequestsNetworkDataSourceTest { } """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse) + MockResponse().setResponseCode(200).setBody(jsonResponse), ) val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser" + "@ExampleUser", ) expectThat(response).isSuccess() @@ -532,14 +532,14 @@ class PullRequestsNetworkDataSourceTest { mockWebServer.enqueue( MockResponse() .setResponseCode(401) - .setBody(jsonResponse) + .setBody(jsonResponse), ) val response = pullRequestDataSource.notify( "exampleOwner", "exampleRepo", "exampleNumber", - "@ExampleUser" + "@ExampleUser", ) expectThat(response) @@ -547,8 +547,8 @@ class PullRequestsNetworkDataSourceTest { .isEqualTo( WebException.UnknownError( 401, - "Bad credentials" - ) + "Bad credentials", + ), ) } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index 4c27de3d1..74c9c7c1f 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -50,7 +50,7 @@ class UserDataSourceTest { fun `Given request WHEN connectivity problem occurred THEN return failure with Network error`() = runTest { mockWebServer.enqueue( - MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY) + MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY), ) val response = userDataSource.getUser() @@ -101,7 +101,7 @@ class UserDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(200).setBody(jsonResponse) + MockResponse().setResponseCode(200).setBody(jsonResponse), ) val response = userDataSource.getUser() @@ -111,8 +111,8 @@ class UserDataSourceTest { .isEqualTo( User( id = 1, - login = "exampleUser" - ) + login = "exampleUser", + ), ) } @@ -128,7 +128,7 @@ class UserDataSourceTest { """.trimIndent() mockWebServer.enqueue( - MockResponse().setResponseCode(401).setBody(jsonResponse) + MockResponse().setResponseCode(401).setBody(jsonResponse), ) val response = userDataSource.getUser() @@ -138,8 +138,8 @@ class UserDataSourceTest { .isEqualTo( WebException.UnknownError( 401, - "Bad credentials" - ) + "Bad credentials", + ), ) } } diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt index 86fef5be5..563b15279 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthFailureInterceptorTest.kt @@ -42,7 +42,7 @@ class AuthFailureInterceptorTest { private val mockWebServer: MockWebServer = MockWebServer() private val service = retrofitTestDouble( mockWebServer = mockWebServer, - client = testOkHttpClient + client = testOkHttpClient, ).create(TestApi::class.java) @AfterEach diff --git a/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt index 5a3e095dd..53dbb9b2a 100644 --- a/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/intercept/AuthInterceptorTest.kt @@ -37,7 +37,7 @@ class AuthInterceptorTest { private val mockWebServer: MockWebServer = MockWebServer() private val service = retrofitTestDouble( mockWebServer = mockWebServer, - client = testOkHttpClient + client = testOkHttpClient, ).create(TestApi::class.java) @AfterEach diff --git a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt index accc31385..6e869d0be 100644 --- a/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModelTest.kt @@ -74,7 +74,7 @@ class AuthenticatingViewModelTest { @Test fun `GIVEN unexpected Github behavior WHEN authenticating screen is opened THEN show generic error screen`() { coEvery { repository.fetchAccessToken(any(), any(), any()) } returns Result.failure( - WebException.UnknownError(null, null) + WebException.UnknownError(null, null), ) setupIntent("validCode") val viewModel = create() @@ -89,7 +89,7 @@ class AuthenticatingViewModelTest { fun `GIVEN unexpected error is presented WHEN try again success THEN navigate to pull requests`() { // simulate unknown error response coEvery { repository.fetchAccessToken(any(), any(), any()) } returns Result.failure( - WebException.UnknownError(null, null) + WebException.UnknownError(null, null), ) setupIntent("validCode") val viewModel = create() diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 8130cc3e2..de6a4dbd3 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -111,8 +111,8 @@ class PullRequestsViewModelTest { pullRequest.owner, pullRequest.shortRepositoryName, pullRequest.number.toString(), - pullRequest.createdAt.toString() - ) + pullRequest.createdAt.toString(), + ), ) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 29145fab5..3084c6d01 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -109,7 +109,7 @@ class ReviewersViewModelTest { repository.getReviews( any(), any(), - any() + any(), ) } coAnswers { neverCompletingSuspension() } viewModel = createViewModel() @@ -127,14 +127,14 @@ class ReviewersViewModelTest { repository.getReviews( any(), any(), - any() + any(), ) } returns Result.success(emptyList()) coEvery { repository.getRequestedReviewers( any(), any(), - any() + any(), ) } returns Result.success(RequestedReviewersResponse(emptyList())) @@ -158,7 +158,7 @@ class ReviewersViewModelTest { get(ReviewersState::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), - Reviewer(3, "user3", false, 7, null) + Reviewer(3, "user3", false, 7, null), ) } } @@ -167,7 +167,7 @@ class ReviewersViewModelTest { fun `GIVEN reviewers with no review done WHEN init THEN list of reviewers is fetched`() = runTest { coEvery { repository.getReviews(any(), any(), any()) } returns Result.success( - emptyList() + emptyList(), ) viewModel = createViewModel() @@ -175,7 +175,7 @@ class ReviewersViewModelTest { .get(ReviewersState::reviewers) .containsExactly( Reviewer(2, "user2", false, 7, null), - Reviewer(3, "user3", false, 7, null) + Reviewer(3, "user3", false, 7, null), ) } @@ -186,7 +186,7 @@ class ReviewersViewModelTest { repository.getRequestedReviewers( any(), any(), - any() + any(), ) } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() @@ -194,7 +194,7 @@ class ReviewersViewModelTest { expectThat(viewModel.state) .get(ReviewersState::reviewers) .containsExactly( - Reviewer(1, "user1", true, 7, 5) + Reviewer(1, "user1", true, 7, 5), ) } @@ -202,13 +202,13 @@ class ReviewersViewModelTest { fun `WHEN there is an error during fetching data from 2 requests on init THEN error is shown`() = runTest { coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure( - WebException.NetworkError() + WebException.NetworkError(), ) coEvery { repository.getRequestedReviewers( any(), any(), - any() + any(), ) } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() @@ -227,7 +227,7 @@ class ReviewersViewModelTest { repository.getRequestedReviewers( any(), any(), - any() + any(), ) } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() @@ -243,7 +243,7 @@ class ReviewersViewModelTest { fun `WHEN there is an error during fetching data on init only from reviews request THEN error is shown`() = runTest { coEvery { repository.getReviews(any(), any(), any()) } returns Result.failure( - WebException.NetworkError() + WebException.NetworkError(), ) viewModel = createViewModel() @@ -277,7 +277,7 @@ class ReviewersViewModelTest { any(), any(), any(), - any() + any(), ) } coAnswers { neverCompletingSuspension() } @@ -346,7 +346,7 @@ class ReviewersViewModelTest { get(ReviewersState::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), - Reviewer(3, "user3", false, 7, null) + Reviewer(3, "user3", false, 7, null), ) } } diff --git a/app/src/test/java/com/appunite/loudius/util/Defaults.kt b/app/src/test/java/com/appunite/loudius/util/Defaults.kt index 42079c090..3ef477ffc 100644 --- a/app/src/test/java/com/appunite/loudius/util/Defaults.kt +++ b/app/src/test/java/com/appunite/loudius/util/Defaults.kt @@ -35,7 +35,7 @@ object Defaults { number = id, repositoryUrl = "https://api.github.com/repos/exampleOwner/exampleRepo", title = "example title", - LocalDateTime.parse("2023-03-07T08:21:45").plusHours(id.toLong()) + LocalDateTime.parse("2023-03-07T08:21:45").plusHours(id.toLong()), ) fun reviews() = listOf( @@ -44,18 +44,18 @@ object Defaults { Review("3", currentUser(), ReviewState.APPROVED, date3), Review("4", User(1, "user1"), ReviewState.COMMENTED, date1), Review("5", User(1, "user1"), ReviewState.COMMENTED, date2), - Review("6", User(1, "user1"), ReviewState.APPROVED, date3) + Review("6", User(1, "user1"), ReviewState.APPROVED, date3), ) fun requestedReviewers() = listOf( RequestedReviewer(2, "user2"), - RequestedReviewer(3, "user3") + RequestedReviewer(3, "user3"), ) fun pullRequestsResponse() = PullRequestsResponse( incompleteResults = false, totalCount = 1, - items = listOf(pullRequest()) + items = listOf(pullRequest()), ) fun currentUser() = User(0, "currentUser") diff --git a/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt index 9187c24bb..b4f48d1f4 100644 --- a/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt +++ b/app/src/test/java/com/appunite/loudius/util/MainDispatcherExtension.kt @@ -28,7 +28,7 @@ import org.junit.jupiter.api.extension.ExtensionContext @OptIn(ExperimentalCoroutinesApi::class) class MainDispatcherExtension( - private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : BeforeTestExecutionCallback, AfterTestExecutionCallback { override fun beforeTestExecution(context: ExtensionContext?) { Dispatchers.setMain(testDispatcher) diff --git a/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt index 0947f1f90..0fc49db48 100644 --- a/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt +++ b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/CustomRuleSetProvider.kt @@ -10,4 +10,4 @@ class CustomRuleSetProvider : RuleSetProviderV2(id = RULE_SET_ID, about = NO_ABO setOf( RuleProvider { UseStriktAssertionLibrary() }, ) -} \ No newline at end of file +} diff --git a/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt index e6c8a8569..e84378dea 100644 --- a/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt +++ b/custom-ktlint-rules/src/main/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibrary.kt @@ -15,7 +15,7 @@ class UseStriktAssertionLibrary : Rule("use-strikt-assertion-library") { override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { if (node.elementType == KtStubElementTypes.IMPORT_DIRECTIVE) { val importDirective = node.psi as KtImportDirective @@ -32,5 +32,4 @@ class UseStriktAssertionLibrary : Rule("use-strikt-assertion-library") { } } } - -} \ No newline at end of file +} diff --git a/custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt b/custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt index a9f27cf2a..ac678534b 100644 --- a/custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt +++ b/custom-ktlint-rules/src/test/kotlin/com/appunite/loudius/rules/UseStriktAssertionLibraryTest.kt @@ -21,7 +21,6 @@ class UseStriktAssertionLibraryTest { .hasLintViolationWithoutAutoCorrect(2, 1, "Instead of using junit.framework.TestCase use strikt.api.expectThat") } - @Test fun `do not allow jupiter assertions library`() { //language=kotlin @@ -49,6 +48,7 @@ class UseStriktAssertionLibraryTest { wrappingRuleAssertThat(code) .hasLintViolationWithoutAutoCorrect(2, 1, "Instead of using org.junit.Assert use strikt.api.expectThat") } + @Test fun `do not allow junit assertions framework`() { //language=kotlin @@ -75,4 +75,4 @@ class UseStriktAssertionLibraryTest { wrappingRuleAssertThat(code).hasNoLintViolations() } -} \ No newline at end of file +} From 4a9761bdf0603fbba03691e1ad454cc763dfe308 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 5 Apr 2023 15:05:59 +0200 Subject: [PATCH 308/526] chore: review fixes --- .../PullRequestsNetworkDataSourceTest.kt | 37 ++++--------------- .../network/datasource/UserDataSourceTest.kt | 14 +------ custom-ktlint-rules/.gitignore | 2 +- custom-ktlint-rules/build.gradle | 2 +- 4 files changed, 11 insertions(+), 44 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 8520e7a99..793552bba 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -197,12 +197,7 @@ class PullRequestsNetworkDataSourceTest { expectThat(response) .isFailure() - .isEqualTo( - WebException.UnknownError( - 401, - "Bad credentials", - ), - ) + .isEqualTo(WebException.UnknownError(401, "Bad credentials")) } } @@ -276,12 +271,9 @@ class PullRequestsNetworkDataSourceTest { .isEqualTo( RequestedReviewersResponse( listOf( - RequestedReviewer( - 1, - "exampleLogin", - ), - ), - ), + RequestedReviewer(1, "exampleLogin") + ) + ) ) } @@ -310,12 +302,7 @@ class PullRequestsNetworkDataSourceTest { expectThat(response) .isFailure() - .isEqualTo( - WebException.UnknownError( - 401, - "Bad credentials", - ), - ) + .isEqualTo(WebException.UnknownError(401, "Bad credentials")) } } @@ -437,12 +424,7 @@ class PullRequestsNetworkDataSourceTest { expectThat(response) .isFailure() - .isEqualTo( - WebException.UnknownError( - 401, - "Bad credentials", - ), - ) + .isEqualTo(WebException.UnknownError(401, "Bad credentials")) } } @@ -544,12 +526,7 @@ class PullRequestsNetworkDataSourceTest { expectThat(response) .isFailure() - .isEqualTo( - WebException.UnknownError( - 401, - "Bad credentials", - ), - ) + .isEqualTo(WebException.UnknownError(401, "Bad credentials")) } } } diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index 74c9c7c1f..aabdc87ca 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -108,12 +108,7 @@ class UserDataSourceTest { expectThat(response) .isSuccess() - .isEqualTo( - User( - id = 1, - login = "exampleUser", - ), - ) + .isEqualTo(User(id = 1, login = "exampleUser")) } @Test @@ -135,11 +130,6 @@ class UserDataSourceTest { expectThat(response) .isFailure() - .isEqualTo( - WebException.UnknownError( - 401, - "Bad credentials", - ), - ) + .isEqualTo(WebException.UnknownError(401, "Bad credentials")) } } diff --git a/custom-ktlint-rules/.gitignore b/custom-ktlint-rules/.gitignore index 42afabfd2..796b96d1c 100644 --- a/custom-ktlint-rules/.gitignore +++ b/custom-ktlint-rules/.gitignore @@ -1 +1 @@ -/build \ No newline at end of file +/build diff --git a/custom-ktlint-rules/build.gradle b/custom-ktlint-rules/build.gradle index 1380b4ed4..c777e1ecc 100644 --- a/custom-ktlint-rules/build.gradle +++ b/custom-ktlint-rules/build.gradle @@ -16,4 +16,4 @@ dependencies { testImplementation 'org.assertj:assertj-core:3.10.0' testImplementation 'com.pinterest.ktlint:ktlint-core:0.48.2' testImplementation 'com.pinterest.ktlint:ktlint-test:0.48.2' -} \ No newline at end of file +} From 2693160d8377779c161c367d8506a27b22c5f12d Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Wed, 5 Apr 2023 13:10:40 +0000 Subject: [PATCH 309/526] [MegaLinter] Apply linters fixes --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 793552bba..dd7af9980 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -271,9 +271,9 @@ class PullRequestsNetworkDataSourceTest { .isEqualTo( RequestedReviewersResponse( listOf( - RequestedReviewer(1, "exampleLogin") - ) - ) + RequestedReviewer(1, "exampleLogin"), + ), + ), ) } From 3f9f3d2cb7e7b5d0d9423eeef8f200da6c2d31ee Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 5 Apr 2023 15:56:08 +0200 Subject: [PATCH 310/526] chore: make megalinter read custom linters --- .github/workflows/run-code-quality-check.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml index 8d82d8309..fbccd0a85 100644 --- a/.github/workflows/run-code-quality-check.yml +++ b/.github/workflows/run-code-quality-check.yml @@ -35,6 +35,16 @@ jobs: token: ${{secrets.PAT || secrets.GITHUB_TOKEN }} fetch-depth: 0 # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to improve performances + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: "adopt" + cache: gradle + + - name: Build custom rules + run: ./gradlew :custom-ktlint-rules:assemble + # MegaLinter - name: MegaLinter id: ml @@ -46,6 +56,7 @@ jobs: # https://megalinter.github.io/configuration/ VALIDATE_ALL_CODEBASE: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} # Validates all source when push on main, else just the git diff with main. Override with true if you always want to lint all sources GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + KOTLIN_KTLINT_ARGUMENTS: "--ruleset=custom-ktlint-rules/build/libs/custom-ktlint-rules.jar" # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY # DISABLE: COPYPASTE,SPELL # Uncomment to disable copy-paste and spell checks From c32c939d2c712a2520c87351f9877c6aae92a3f8 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Wed, 5 Apr 2023 16:12:25 +0200 Subject: [PATCH 311/526] chore: oops in resolving merge conflicts --- .../datasource/PullRequestsNetworkDataSourceTest.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index dd7af9980..68609220d 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -166,16 +166,6 @@ class PullRequestsNetworkDataSourceTest { expectThat(response).isSuccess() .isEqualTo(Defaults.pullRequestsResponse()) - - val expected = Result.success( - PullRequestsResponse( - incompleteResults = false, - totalCount = 1, - items = listOf( - Defaults.pullRequest(), - ), - ), - ) } @Test From d08baecaec6589ba9b09d7a7a8788207e7908db6 Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Wed, 5 Apr 2023 14:16:33 +0000 Subject: [PATCH 312/526] [MegaLinter] Apply linters fixes --- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 68609220d..b09d5c88e 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -16,7 +16,6 @@ package com.appunite.loudius.network.datasource -import com.appunite.loudius.network.model.PullRequestsResponse import com.appunite.loudius.network.model.RequestedReviewer import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review From c9ec32ec396ee15e4d2be77530619b6971c6f560 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 09:03:20 +0200 Subject: [PATCH 313/526] Rename LoudiusErrorScreen.kt into LoudiusFullScreenError.kt --- .../loudius/ui/authenticating/AuthenticatingScreen.kt | 6 +++--- .../{LoudiusErrorScreen.kt => LoudiusFullScreenError.kt} | 4 ++-- .../appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 4 ++-- .../com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename app/src/main/java/com/appunite/loudius/ui/components/{LoudiusErrorScreen.kt => LoudiusFullScreenError.kt} (97%) diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 10cdb77e1..4e5f75ade 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.theme.LoudiusTheme @@ -67,7 +67,7 @@ fun AuthenticatingScreenStateless( private fun ShowLoudiusLoginErrorScreen( onTryAgainClick: () -> Unit, ) { - LoudiusErrorScreen( + LoudiusFullScreenError( errorText = stringResource(id = R.string.error_login_text), buttonText = stringResource(id = R.string.go_to_login), onButtonClick = onTryAgainClick, @@ -78,7 +78,7 @@ private fun ShowLoudiusLoginErrorScreen( private fun ShowLoudiusGenericErrorScreen( onTryAgainClick: () -> Unit, ) { - LoudiusErrorScreen(onButtonClick = onTryAgainClick) + LoudiusFullScreenError(onButtonClick = onTryAgainClick) } @Preview(showSystemUi = true) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt similarity index 97% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt rename to app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index 18ab8825c..246dffc08 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -32,7 +32,7 @@ import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme @Composable -fun LoudiusErrorScreen( +fun LoudiusFullScreenError( errorText: String = stringResource(id = R.string.error_dialog_text), buttonText: String = stringResource(id = R.string.try_again), onButtonClick: () -> Unit, @@ -74,7 +74,7 @@ private fun ErrorText(text: String) { @Composable fun LoudiusErrorScreenPreview() { LoudiusTheme { - LoudiusErrorScreen( + LoudiusFullScreenError( errorText = stringResource(id = R.string.error_dialog_text), buttonText = stringResource(R.string.try_again), onButtonClick = {}, diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 0e3d0b06a..1e627f39b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -37,7 +37,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest -import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator @@ -83,7 +83,7 @@ private fun PullRequestsScreenStateless( }, content = { padding -> when { - isError -> LoudiusErrorScreen( + isError -> LoudiusFullScreenError( onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 13c4fc811..6ef2642df 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.ui.components.LoudiusErrorScreen +import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator @@ -125,7 +125,7 @@ private fun ReviewersScreenStateless( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, content = { padding -> when { - isError -> LoudiusErrorScreen(onButtonClick = { onAction(ReviewersAction.OnTryAgain) }) + isError -> LoudiusFullScreenError(onButtonClick = { onAction(ReviewersAction.OnTryAgain) }) isLoading -> LoudiusLoadingIndicator() reviewers.isEmpty() -> EmptyListPlaceholder(padding) else -> ReviewersScreenContent( From d5d06e5d585664ec15eadb66502ea541b3d989c4 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 09:17:56 +0200 Subject: [PATCH 314/526] Remove passing a padding value into LoudiusPlaceholderText.kt. --- .../loudius/ui/components/LoudiusPlaceholderText.kt | 6 ++---- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 10 ++++++---- .../appunite/loudius/ui/reviewers/ReviewersScreen.kt | 9 +++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt index f7bb67e96..06bb70c3b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt @@ -18,7 +18,6 @@ package com.appunite.loudius.ui.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -31,10 +30,9 @@ import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme @Composable -fun LoudiusPlaceholderText(@StringRes textId: Int, padding: PaddingValues) { +fun LoudiusPlaceholderText(@StringRes textId: Int) { Box( modifier = Modifier - .padding(padding) .fillMaxSize() .padding(16.dp), contentAlignment = Alignment.Center, @@ -50,6 +48,6 @@ fun LoudiusPlaceholderText(@StringRes textId: Int, padding: PaddingValues) { @Composable fun PreviewLoudiusPlaceholderText() { LoudiusTheme { - LoudiusPlaceholderText(R.string.you_dont_have_any_pull_request, PaddingValues(0.dp)) + LoudiusPlaceholderText(R.string.you_dont_have_any_pull_request) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 1e627f39b..cfc2e7b82 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -19,6 +19,7 @@ package com.appunite.loudius.ui.pullrequests import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -163,10 +164,11 @@ private fun RepoDetails(modifier: Modifier, pullRequestTitle: String, repository @Composable private fun EmptyListPlaceholder(padding: PaddingValues) { - LoudiusPlaceholderText( - textId = R.string.you_dont_have_any_pull_request, - padding = padding, - ) + Box(modifier = Modifier.padding(padding)) { + LoudiusPlaceholderText( + textId = R.string.you_dont_have_any_pull_request, + ) + } } @Preview("Pull requests - filled list") diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 6ef2642df..15c969dee 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -231,10 +231,11 @@ private fun ReviewerName(reviewer: Reviewer) { @Composable private fun EmptyListPlaceholder(padding: PaddingValues) { - LoudiusPlaceholderText( - textId = R.string.you_dont_have_any_reviewers, - padding = padding, - ) + Box(modifier = Modifier.padding(padding)) { + LoudiusPlaceholderText( + textId = R.string.you_dont_have_any_reviewers, + ) + } } @Preview(showBackground = true) From 8e45618f4cbe33e752b87bedfba862f662011974 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 09:28:01 +0200 Subject: [PATCH 315/526] Rename function param - onTryAgainClick into navigateToLogin. --- .../loudius/ui/authenticating/AuthenticatingScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 4e5f75ade..22d50bf27 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -65,12 +65,12 @@ fun AuthenticatingScreenStateless( @Composable private fun ShowLoudiusLoginErrorScreen( - onTryAgainClick: () -> Unit, + navigateToLogin: () -> Unit, ) { LoudiusFullScreenError( errorText = stringResource(id = R.string.error_login_text), buttonText = stringResource(id = R.string.go_to_login), - onButtonClick = onTryAgainClick, + onButtonClick = navigateToLogin, ) } From 439e75a5ca9556d9e6055792f6428873d8ad90c6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 10:39:51 +0200 Subject: [PATCH 316/526] Move building authorization url to the Constants.kt. --- .../java/com/appunite/loudius/common/Constants.kt | 2 ++ .../com/appunite/loudius/ui/login/LoginScreen.kt | 13 +++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/common/Constants.kt b/app/src/main/java/com/appunite/loudius/common/Constants.kt index 3fed85a92..db864697c 100644 --- a/app/src/main/java/com/appunite/loudius/common/Constants.kt +++ b/app/src/main/java/com/appunite/loudius/common/Constants.kt @@ -25,4 +25,6 @@ object Constants { const val SCOPE_PARAM = "&scope=repo" const val CLIENT_ID = "91131449e417c7e29912" const val REDIRECT_URL = "loudius://callback" + const val AUTHORIZATION_URL = + AUTH_API_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + SCOPE_PARAM } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 0c3f278f5..56c0f303c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -33,11 +33,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.appunite.loudius.R -import com.appunite.loudius.common.Constants.AUTH_API_URL -import com.appunite.loudius.common.Constants.AUTH_PATH -import com.appunite.loudius.common.Constants.CLIENT_ID -import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID -import com.appunite.loudius.common.Constants.SCOPE_PARAM +import com.appunite.loudius.common.Constants.AUTHORIZATION_URL import com.appunite.loudius.ui.components.LoudiusOutlinedButton import com.appunite.loudius.ui.components.LoudiusOutlinedButtonIcon import com.appunite.loudius.ui.components.LoudiusOutlinedButtonStyle @@ -63,7 +59,7 @@ fun LoginScreen() { ) }, - ) + ) } } @@ -78,13 +74,10 @@ fun LoginImage() { } private fun startAuthorizing(context: Context) { - val url = buildAuthorizationUrl() - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(AUTHORIZATION_URL)) context.startActivity(intent) } -private fun buildAuthorizationUrl() = AUTH_API_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + SCOPE_PARAM - @Preview(showSystemUi = true, showBackground = true) @Composable fun LoginScreenPreview() { From be90f7c183f6c0d589cb00be09023e2e4be19b23 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 11:19:55 +0200 Subject: [PATCH 317/526] Use scaffold padding within every optional composable. --- .../loudius/ui/authenticating/AuthenticatingViewModel.kt | 4 ++-- .../loudius/ui/components/LoudiusFullScreenError.kt | 3 ++- .../loudius/ui/components/LoudiusLoadingIndicator.kt | 4 ++-- .../appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 3 ++- .../com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 7 +++++-- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt index 20c1138bb..d7645dd38 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt @@ -28,8 +28,8 @@ import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.utils.WebException import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class AuthenticatingAction { @@ -56,7 +56,7 @@ sealed class AuthenticatingScreenNavigation { @HiltViewModel class AuthenticatingViewModel @Inject constructor( private val authRepository: AuthRepository, - private val savedStateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, ) : ViewModel() { private val code = Screen.Authenticating.getCode(savedStateHandle) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index 246dffc08..ea28b1da1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -33,12 +33,13 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusFullScreenError( + modifier: Modifier = Modifier, errorText: String = stringResource(id = R.string.error_dialog_text), buttonText: String = stringResource(id = R.string.try_again), onButtonClick: () -> Unit, ) { Column( - modifier = Modifier + modifier = modifier .padding(top = 142.dp) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index aea8523de..f89baabc5 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -34,14 +34,14 @@ import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme @Composable -fun LoudiusLoadingIndicator() { +fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) val progress by animateLottieCompositionAsState( composition = composition, iterations = LottieConstants.IterateForever, ) Box( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), ) { LottieAnimation( composition = composition, diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index cfc2e7b82..a6086b5b3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -85,10 +85,11 @@ private fun PullRequestsScreenStateless( content = { padding -> when { isError -> LoudiusFullScreenError( + modifier = Modifier.padding(padding), onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) - isLoading -> LoudiusLoadingIndicator() + isLoading -> LoudiusLoadingIndicator(Modifier.padding(padding)) pullRequests.isEmpty() -> EmptyListPlaceholder(padding) else -> PullRequestsList( pullRequests = pullRequests, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 15c969dee..9fe27f08a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -125,8 +125,11 @@ private fun ReviewersScreenStateless( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, content = { padding -> when { - isError -> LoudiusFullScreenError(onButtonClick = { onAction(ReviewersAction.OnTryAgain) }) - isLoading -> LoudiusLoadingIndicator() + isError -> LoudiusFullScreenError( + modifier = Modifier.padding(padding), + onButtonClick = { onAction(ReviewersAction.OnTryAgain) } + ) + isLoading -> LoudiusLoadingIndicator(Modifier.padding(padding)) reviewers.isEmpty() -> EmptyListPlaceholder(padding) else -> ReviewersScreenContent( reviewers = reviewers, From 973de59a28be066587be18b78dbb8ebf53556979 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 6 Apr 2023 09:23:52 +0000 Subject: [PATCH 318/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/authenticating/AuthenticatingViewModel.kt | 2 +- app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 2 +- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt index d7645dd38..b0bf68b9d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt @@ -28,8 +28,8 @@ import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.utils.WebException import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class AuthenticatingAction { diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 56c0f303c..c0179fbe3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -59,7 +59,7 @@ fun LoginScreen() { ) }, - ) + ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 9fe27f08a..3f19ec86c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -127,7 +127,7 @@ private fun ReviewersScreenStateless( when { isError -> LoudiusFullScreenError( modifier = Modifier.padding(padding), - onButtonClick = { onAction(ReviewersAction.OnTryAgain) } + onButtonClick = { onAction(ReviewersAction.OnTryAgain) }, ) isLoading -> LoudiusLoadingIndicator(Modifier.padding(padding)) reviewers.isEmpty() -> EmptyListPlaceholder(padding) From 30b9df4f8d47913c8b1bc2c9aab04f26693bd487 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 12:10:10 +0200 Subject: [PATCH 319/526] Move ReviewersViewModel.kt getting init values to the Screen class. --- .../com/appunite/loudius/common/Screen.kt | 33 ++++++++++++++----- .../ui/reviewers/ReviewersViewModel.kt | 28 ++++------------ 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index 68934b30d..6811eca53 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -37,21 +37,22 @@ sealed class Screen(val route: String) { return code?.let { Result.success(it) } ?: Result.failure(Exception("No error code")) } - val deepLinks: List get() = listOf( - navDeepLink { - uriPattern = Constants.REDIRECT_URL - }, - ) + val deepLinks: List + get() = listOf( + navDeepLink { + uriPattern = Constants.REDIRECT_URL + }, + ) } object PullRequests : Screen("pull_requests_screen") object Reviewers : Screen("reviewers_screen/{pull_request_number}/{owner}/{repo}/{submission_date}") { - const val pullRequestNumberArg = "pull_request_number" - const val ownerArg = "owner" - const val repoArg = "repo" - const val submissionDateArg = "submission_date" + private const val pullRequestNumberArg = "pull_request_number" + private const val ownerArg = "owner" + private const val repoArg = "repo" + private const val submissionDateArg = "submission_date" override val arguments: List get() { @@ -69,5 +70,19 @@ sealed class Screen(val route: String) { pullRequestNumber: String, submissionDate: String, ): String = "reviewers_screen/$pullRequestNumber/$owner/$repo/$submissionDate" + + fun getInitialValues(savedStateHandle: SavedStateHandle) = ReviewersInitialValues( + checkNotNull(savedStateHandle[ownerArg]), + checkNotNull(savedStateHandle[repoArg]), + checkNotNull(savedStateHandle[pullRequestNumberArg]), + checkNotNull(savedStateHandle[submissionDateArg]), + ) + + data class ReviewersInitialValues( + val owner: String, + val repo: String, + val pullRequestNumber: String, + val submissionTime: String, + ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 2d31f4141..803a9fbaf 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.appunite.loudius.common.Screen +import com.appunite.loudius.common.Screen.Reviewers.getInitialValues import com.appunite.loudius.common.flatMap import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.RequestedReviewersResponse @@ -30,12 +30,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -60,7 +60,7 @@ class ReviewersViewModel @Inject constructor( private val repository: PullRequestRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val initialValues: InitialValues = getInitialValues(savedStateHandle) + private val initialValues = getInitialValues(savedStateHandle) var state by mutableStateOf(ReviewersState()) private set @@ -70,22 +70,15 @@ class ReviewersViewModel @Inject constructor( fetchData() } - private fun getInitialValues(savedStateHandle: SavedStateHandle) = InitialValues( - checkNotNull(savedStateHandle[Screen.Reviewers.ownerArg]), - checkNotNull(savedStateHandle[Screen.Reviewers.repoArg]), - checkNotNull(savedStateHandle[Screen.Reviewers.pullRequestNumberArg]), - checkNotNull(savedStateHandle[Screen.Reviewers.submissionDateArg]), - ) - private fun fetchData() { viewModelScope.launch { getMergedData() - .onSuccess { state = state.copy(reviewers = it.orEmpty(), isLoading = false) } + .onSuccess { state = state.copy(reviewers = it, isLoading = false) } .onFailure { state = state.copy(isError = true, isLoading = false) } } } - private suspend fun getMergedData(): Result?> = + private suspend fun getMergedData(): Result> = coroutineScope { state = state.copy(isLoading = true, isError = false) val requestedReviewersDeferred = async { fetchRequestedReviewers() } @@ -175,11 +168,4 @@ class ReviewersViewModel @Inject constructor( private fun dismissSnackbar() { state = state.copy(snackbarTypeShown = null) } - - private data class InitialValues( - val owner: String, - val repo: String, - val pullRequestNumber: String, - val submissionTime: String, - ) } From 007f4f35213484b8bf056a882b481b2f1c27d9be Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 6 Apr 2023 11:45:19 +0200 Subject: [PATCH 320/526] chore: Fix log-in on xiaomi devices --- app/src/main/AndroidManifest.xml | 3 + .../loudius/ui/components/LoudiusDialog.kt | 71 +++++++++++++ .../ui/components/LoudiusErrorDialog.kt | 28 +----- .../appunite/loudius/ui/login/GithubHelper.kt | 84 ++++++++++++++++ .../appunite/loudius/ui/login/LoginScreen.kt | 99 ++++++++++++++++--- .../loudius/ui/login/LoginScreenViewModel.kt | 65 ++++++++++++ app/src/main/res/values/strings.xml | 11 ++- .../ui/login/LoginScreenViewModelTest.kt | 74 ++++++++++++++ 8 files changed, 394 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt create mode 100644 app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d4efb848..33f1f9148 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,9 @@ + + + Unit, + confirmButton: @Composable () -> Unit, + modifier: Modifier = Modifier, + title: String, + dismissButton: @Composable (() -> Unit)? = null, + /** + * For text {@see LoudiusTextStyle#ScreenContent} should be used + */ + text: @Composable (() -> Unit)? = null, + + ) { + AlertDialog( + onDismissRequest = onDismissRequest, + modifier = modifier, + title = { + LoudiusText(style = LoudiusTextStyle.TitleLarge, text = title) + }, + text = text, + confirmButton = confirmButton, + dismissButton = dismissButton + ) +} + +@Composable +@Preview +fun LoudiusDialogSimplePreview() { + LoudiusTheme { + LoudiusDialog( + onDismissRequest = { }, + title = "Title", + confirmButton = { + LoudiusOutlinedButton(text = "Confirm") { + } + }, + ) + } +} +@Composable +@Preview +fun LoudiusDialogAdvancedPreview() { + LoudiusTheme { + LoudiusDialog( + onDismissRequest = { }, + title = "Title", + text = { + LoudiusText( + style = LoudiusTextStyle.ScreenContent, + text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse dapibus elit justo, at pharetra nulla mattis vel. Integer gravida tortor sed fringilla viverra. Duis scelerisque ante neque, a pretium eros." + ) + }, + confirmButton = { + LoudiusOutlinedButton(text = "Confirm") { + } + }, + dismissButton = { + LoudiusOutlinedButton(text = "Dismiss") { + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index 4b7806270..70867d15b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -40,37 +40,17 @@ fun LoudiusErrorDialog( ) { var openDialog by remember { mutableStateOf(true) } if (openDialog) { - AlertDialog( + LoudiusDialog( onDismissRequest = { openDialog = false }, - title = { Text(text = dialogTitle) }, - text = { Text(text = dialogText) }, + title = dialogTitle, + text = { LoudiusText(style = LoudiusTextStyle.ScreenContent, text = dialogText) }, confirmButton = { - ConfirmButton( - confirmText = confirmText, - confirm = onConfirmButtonClick, - ) + LoudiusOutlinedButton(text = confirmText, onClick = onConfirmButtonClick) }, - containerColor = MaterialTheme.colorScheme.surface, ) } } -@Composable -private fun ConfirmButton( - confirmText: String, - confirm: () -> Unit, -) { - Button( - onClick = confirm, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.tertiary, - ), - ) { - Text(text = confirmText) - } -} - @Preview @Composable fun LoudiusErrorDialogPreview() { diff --git a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt new file mode 100644 index 000000000..738fee1d7 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt @@ -0,0 +1,84 @@ +package com.appunite.loudius.ui.login + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +/** + * Github app currently have a bug on Xiaomi devices where + * "Display pup-up windows while running in the background" permission needs to be granted on those + * devices to allow log-in if the app is installed on those devices. This class helps you find those + * situations and ask for the permissions. + * + * Those steps are necessary to reproduce the Github App issue that is fixed by the class: + * - Use Xiaomi device + * - Install github app + * - Kill the app + * - Open the Loudius app + * - Click Log-in + * - If nothing happens, or you can't continue to log-in the issue persist. + * + * We've checked 1.107.0 version of Github from 2023-04-06. + * If you won't be able to reproduce the issue without the fix, it can be removed. + */ +class GithubHelper @Inject constructor(@ApplicationContext private val context: Context) { + companion object { + private const val GITHUB_APP_PACKAGE_NAME = "com.github.android" + + fun xiaomiPermissionManagerForGithub(): Intent = + Intent("miui.intent.action.APP_PERM_EDITOR") + .setClassName( + "com.miui.securitycenter", + "com.miui.permcenter.permissions.PermissionsEditorActivity" + ) + .putExtra("extra_pkgname", GITHUB_APP_PACKAGE_NAME) + + } + + fun shouldAskForXiaomiIntent(): Boolean { + if (!Build.MANUFACTURER.equals("Xiaomi", ignoreCase = true)) { + return false + } + + if (!isGithubAppInstalled()) { + return false + } + + if (isAlertWindowPermissionGranted()) { + return false + } + return true + } + + private fun isAlertWindowPermissionGranted(): Boolean { + val permissionNeeded = "Manifest.permission.SYSTEM_ALERT_WINDOW" + return context.packageManager.checkPermission( + permissionNeeded, + GITHUB_APP_PACKAGE_NAME + ) == PackageManager.PERMISSION_GRANTED + } + + + private fun isGithubAppInstalled(): Boolean { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.getPackageInfo( + GITHUB_APP_PACKAGE_NAME, + PackageManager.PackageInfoFlags.of(PackageManager.GET_META_DATA.toLong()) + ) + } else { + @Suppress("DEPRECATION") + context.packageManager.getPackageInfo( + GITHUB_APP_PACKAGE_NAME, + PackageManager.GET_META_DATA, + ) + } + true + } catch (e: PackageManager.NameNotFoundException) { + false + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 0c3f278f5..6b96a2b19 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -16,7 +16,6 @@ package com.appunite.loudius.ui.login -import android.content.Context import android.content.Intent import android.net.Uri import androidx.compose.foundation.Image @@ -26,44 +25,106 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTH_API_URL import com.appunite.loudius.common.Constants.AUTH_PATH import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Constants.NAME_PARAM_CLIENT_ID import com.appunite.loudius.common.Constants.SCOPE_PARAM +import com.appunite.loudius.ui.components.LoudiusDialog import com.appunite.loudius.ui.components.LoudiusOutlinedButton import com.appunite.loudius.ui.components.LoudiusOutlinedButtonIcon import com.appunite.loudius.ui.components.LoudiusOutlinedButtonStyle +import com.appunite.loudius.ui.components.LoudiusText +import com.appunite.loudius.ui.components.LoudiusTextStyle + @Composable -fun LoginScreen() { +fun LoginScreen( + viewModel: LoginScreenViewModel = hiltViewModel(), + ) { val context = LocalContext.current + val navigateTo = viewModel.state.navigateTo + LaunchedEffect(navigateTo) { + when (navigateTo) { + LoginNavigateTo.OpenGithubAuth -> { + context.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse(buildAuthorizationUrl()) + ) + ) + viewModel.onAction(LoginAction.ClearNavigation) + } + LoginNavigateTo.OpenXiaomiPermissionManager -> { + context.startActivity(GithubHelper.xiaomiPermissionManagerForGithub()) + } + null -> Unit + } + } + LoginScreenStateless( + state = viewModel.state, + onAction = viewModel::onAction, + ) +} + +@Composable +fun LoginScreenStateless( + state: LoginState, + onAction: (LoginAction) -> Unit, + ) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally, + horizontalAlignment = Alignment.CenterHorizontally ) { LoginImage() LoudiusOutlinedButton( modifier = Modifier.fillMaxWidth(), - onClick = { startAuthorizing(context) }, - text = stringResource(id = R.string.login), + onClick = { + onAction(LoginAction.ClickLogIn) + + }, + text = stringResource(id = R.string.login_screen_login), style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( painter = painterResource(id = R.drawable.ic_github), - contentDescription = stringResource(R.string.github_icon), + contentDescription = stringResource(R.string.github_icon) ) - }, + } ) + if (state.showXiaomiPermissionDialog) { + LoudiusDialog( + onDismissRequest = { onAction(LoginAction.XiaomiPermissionDialogDismiss) }, + title = stringResource(R.string.login_screen_xiaomi_dialog_title), + text = { + LoudiusText( + style = LoudiusTextStyle.ScreenContent, + text = stringResource(R.string.login_screen_xiaomi_dialog_text) + ) + }, + confirmButton = { + LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_grant_permission)) { + onAction(LoginAction.XiaomiPermissionDialogGrantPermission) + } + }, + dismissButton = { + LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_cancel)) { + onAction(LoginAction.XiaomiPermissionDialogDismiss) + } + } + ) + } } } @@ -72,16 +133,11 @@ fun LoginImage() { Image( painter = painterResource(id = R.drawable.loudius_logo), contentDescription = stringResource( - R.string.login_screen, - ), + R.string.login_screen_loudius_logo_content_description + ) ) } -private fun startAuthorizing(context: Context) { - val url = buildAuthorizationUrl() - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - context.startActivity(intent) -} private fun buildAuthorizationUrl() = AUTH_API_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + SCOPE_PARAM @@ -89,6 +145,19 @@ private fun buildAuthorizationUrl() = AUTH_API_URL + AUTH_PATH + NAME_PARAM_CLIE @Composable fun LoginScreenPreview() { MaterialTheme { - LoginScreen() + LoginScreenStateless( + state = LoginState(showXiaomiPermissionDialog = false, navigateTo = null), + onAction = {} + ) + } +} +@Preview(showSystemUi = true, showBackground = true) +@Composable +fun LoginScreenPreviewWithDialog() { + MaterialTheme { + LoginScreenStateless( + state = LoginState(showXiaomiPermissionDialog = true, navigateTo = null), + onAction = {} + ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt new file mode 100644 index 000000000..4ed2ee917 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt @@ -0,0 +1,65 @@ +package com.appunite.loudius.ui.login + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +sealed class LoginAction { + object ClearNavigation : LoginAction() + object ClickLogIn : LoginAction() + object XiaomiPermissionDialogDismiss : LoginAction() + object XiaomiPermissionDialogGrantPermission : LoginAction() +} + +sealed class LoginNavigateTo { + object OpenXiaomiPermissionManager : LoginNavigateTo() + object OpenGithubAuth : LoginNavigateTo() +} + +data class LoginState( + val showXiaomiPermissionDialog: Boolean, + val navigateTo: LoginNavigateTo?, +) + +@HiltViewModel +class LoginScreenViewModel @Inject constructor( + private val githubHelper: GithubHelper +) : ViewModel() { + + var state by mutableStateOf(LoginState(showXiaomiPermissionDialog = false, navigateTo = null)) + private set + + fun onAction(action: LoginAction) { + when (action) { + LoginAction.ClearNavigation -> { + state = state.copy(navigateTo = null) + } + + LoginAction.ClickLogIn -> { + if (githubHelper.shouldAskForXiaomiIntent()) { + state = state.copy( + showXiaomiPermissionDialog = true + ) + } else { + state = state.copy( + navigateTo = LoginNavigateTo.OpenGithubAuth + ) + } + } + + LoginAction.XiaomiPermissionDialogDismiss -> { + state = state.copy(showXiaomiPermissionDialog = false) + } + + LoginAction.XiaomiPermissionDialogGrantPermission -> { + state = state.copy( + showXiaomiPermissionDialog = false, + navigateTo = LoginNavigateTo.OpenXiaomiPermissionManager, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b33dd53e9..0523cfe29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,6 @@ Loudius Back button - Log in Pull request User image Notify @@ -9,7 +8,6 @@ Not reviewed for %d h. Pull request # %s Github icon - Loudius logo Error Something went wrong… OK @@ -22,4 +20,13 @@ Unauthorized collaborator detected! Please login again. Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 + + + Log in + Loudius logo + Grant Permission + You\'re using a Xiaomi device and you have Github App installed, please note that there\'s a known bug that requires you to grant the "Display pup-up windows while running in the background" permission. This will allow you to continue using the app without any interruptions. + Necessary permissions + Cancel + diff --git a/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt new file mode 100644 index 000000000..22989ebbe --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt @@ -0,0 +1,74 @@ +package com.appunite.loudius.ui.login + +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import strikt.api.expectThat +import strikt.assertions.isEqualTo +import strikt.assertions.isFalse +import strikt.assertions.isNull +import strikt.assertions.isTrue + +class LoginScreenViewModelTest { + + private val githubHelper = mockk { + every { shouldAskForXiaomiIntent() } returns false + } + private fun create() = LoginScreenViewModel(githubHelper) + + @Test + fun `WHEN log-in click THEN open github authorization`() { + val viewModel = create() + + viewModel.onAction(LoginAction.ClickLogIn) + + expectThat(viewModel.state) { + get(LoginState::navigateTo).isEqualTo(LoginNavigateTo.OpenGithubAuth) + get(LoginState::showXiaomiPermissionDialog).isFalse() + } + } + + @Test + fun `GIVEN should ask for xiaomi intent WHEN log-in click THEN show xiaomi permission dialog`() { + every { githubHelper.shouldAskForXiaomiIntent() } returns true + val viewModel = create() + + viewModel.onAction(LoginAction.ClickLogIn) + + expectThat(viewModel.state) { + get(LoginState::navigateTo).isNull() + get(LoginState::showXiaomiPermissionDialog).isTrue() + } + } + + @Test + fun `GIVEN xiaomi permission dialog is displayed WHEN dismisses dialog THEN hide the dialog`() { + every { githubHelper.shouldAskForXiaomiIntent() } returns true + val viewModel = create() + viewModel.onAction(LoginAction.ClickLogIn) + expectThat(viewModel.state).get(LoginState::showXiaomiPermissionDialog).isTrue() + + viewModel.onAction(LoginAction.XiaomiPermissionDialogDismiss) + + expectThat(viewModel.state) { + get(LoginState::navigateTo).isNull() + get(LoginState::showXiaomiPermissionDialog).isFalse() + } + } + + @Test + fun `GIVEN xiaomi permission dialog is displayed WHEN grant permission THEN navigate to xiaomi permissions manager`() { + every { githubHelper.shouldAskForXiaomiIntent() } returns true + val viewModel = create() + viewModel.onAction(LoginAction.ClickLogIn) + expectThat(viewModel.state).get(LoginState::showXiaomiPermissionDialog).isTrue() + + viewModel.onAction(LoginAction.XiaomiPermissionDialogGrantPermission) + + expectThat(viewModel.state) { + get(LoginState::navigateTo).isEqualTo(LoginNavigateTo.OpenXiaomiPermissionManager) + get(LoginState::showXiaomiPermissionDialog).isFalse() + } + } + +} \ No newline at end of file From 0e5eaef6ed01ea7d7860af2ab92b827e315d3aa3 Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Thu, 6 Apr 2023 10:56:04 +0000 Subject: [PATCH 321/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/components/LoudiusDialog.kt | 11 ++++--- .../ui/components/LoudiusErrorDialog.kt | 5 --- .../appunite/loudius/ui/login/GithubHelper.kt | 10 +++--- .../appunite/loudius/ui/login/LoginScreen.kt | 32 +++++++++---------- .../loudius/ui/login/LoginScreenViewModel.kt | 8 ++--- .../ui/login/LoginScreenViewModelTest.kt | 3 +- 6 files changed, 30 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt index 7c3995547..08dc7e570 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt @@ -18,7 +18,7 @@ fun LoudiusDialog( */ text: @Composable (() -> Unit)? = null, - ) { +) { AlertDialog( onDismissRequest = onDismissRequest, modifier = modifier, @@ -27,7 +27,7 @@ fun LoudiusDialog( }, text = text, confirmButton = confirmButton, - dismissButton = dismissButton + dismissButton = dismissButton, ) } @@ -45,6 +45,7 @@ fun LoudiusDialogSimplePreview() { ) } } + @Composable @Preview fun LoudiusDialogAdvancedPreview() { @@ -55,7 +56,7 @@ fun LoudiusDialogAdvancedPreview() { text = { LoudiusText( style = LoudiusTextStyle.ScreenContent, - text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse dapibus elit justo, at pharetra nulla mattis vel. Integer gravida tortor sed fringilla viverra. Duis scelerisque ante neque, a pretium eros." + text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse dapibus elit justo, at pharetra nulla mattis vel. Integer gravida tortor sed fringilla viverra. Duis scelerisque ante neque, a pretium eros.", ) }, confirmButton = { @@ -65,7 +66,7 @@ fun LoudiusDialogAdvancedPreview() { dismissButton = { LoudiusOutlinedButton(text = "Dismiss") { } - } + }, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index 70867d15b..a4914802f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -16,11 +16,6 @@ package com.appunite.loudius.ui.components -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt index 738fee1d7..d5e039508 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt @@ -32,10 +32,9 @@ class GithubHelper @Inject constructor(@ApplicationContext private val context: Intent("miui.intent.action.APP_PERM_EDITOR") .setClassName( "com.miui.securitycenter", - "com.miui.permcenter.permissions.PermissionsEditorActivity" + "com.miui.permcenter.permissions.PermissionsEditorActivity", ) .putExtra("extra_pkgname", GITHUB_APP_PACKAGE_NAME) - } fun shouldAskForXiaomiIntent(): Boolean { @@ -57,17 +56,16 @@ class GithubHelper @Inject constructor(@ApplicationContext private val context: val permissionNeeded = "Manifest.permission.SYSTEM_ALERT_WINDOW" return context.packageManager.checkPermission( permissionNeeded, - GITHUB_APP_PACKAGE_NAME + GITHUB_APP_PACKAGE_NAME, ) == PackageManager.PERMISSION_GRANTED } - private fun isGithubAppInstalled(): Boolean { return try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.packageManager.getPackageInfo( GITHUB_APP_PACKAGE_NAME, - PackageManager.PackageInfoFlags.of(PackageManager.GET_META_DATA.toLong()) + PackageManager.PackageInfoFlags.of(PackageManager.GET_META_DATA.toLong()), ) } else { @Suppress("DEPRECATION") @@ -81,4 +79,4 @@ class GithubHelper @Inject constructor(@ApplicationContext private val context: false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 6b96a2b19..cc5d3117e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -46,11 +46,10 @@ import com.appunite.loudius.ui.components.LoudiusOutlinedButtonStyle import com.appunite.loudius.ui.components.LoudiusText import com.appunite.loudius.ui.components.LoudiusTextStyle - @Composable fun LoginScreen( viewModel: LoginScreenViewModel = hiltViewModel(), - ) { +) { val context = LocalContext.current val navigateTo = viewModel.state.navigateTo LaunchedEffect(navigateTo) { @@ -59,8 +58,8 @@ fun LoginScreen( context.startActivity( Intent( Intent.ACTION_VIEW, - Uri.parse(buildAuthorizationUrl()) - ) + Uri.parse(buildAuthorizationUrl()), + ), ) viewModel.onAction(LoginAction.ClearNavigation) } @@ -80,37 +79,36 @@ fun LoginScreen( fun LoginScreenStateless( state: LoginState, onAction: (LoginAction) -> Unit, - ) { +) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceEvenly, - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { LoginImage() LoudiusOutlinedButton( modifier = Modifier.fillMaxWidth(), onClick = { onAction(LoginAction.ClickLogIn) - }, text = stringResource(id = R.string.login_screen_login), style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( painter = painterResource(id = R.drawable.ic_github), - contentDescription = stringResource(R.string.github_icon) + contentDescription = stringResource(R.string.github_icon), ) - } + }, ) - if (state.showXiaomiPermissionDialog) { + if (state.showXiaomiPermissionDialog) { LoudiusDialog( onDismissRequest = { onAction(LoginAction.XiaomiPermissionDialogDismiss) }, title = stringResource(R.string.login_screen_xiaomi_dialog_title), text = { LoudiusText( style = LoudiusTextStyle.ScreenContent, - text = stringResource(R.string.login_screen_xiaomi_dialog_text) + text = stringResource(R.string.login_screen_xiaomi_dialog_text), ) }, confirmButton = { @@ -122,7 +120,7 @@ fun LoginScreenStateless( LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_cancel)) { onAction(LoginAction.XiaomiPermissionDialogDismiss) } - } + }, ) } } @@ -133,12 +131,11 @@ fun LoginImage() { Image( painter = painterResource(id = R.drawable.loudius_logo), contentDescription = stringResource( - R.string.login_screen_loudius_logo_content_description - ) + R.string.login_screen_loudius_logo_content_description, + ), ) } - private fun buildAuthorizationUrl() = AUTH_API_URL + AUTH_PATH + NAME_PARAM_CLIENT_ID + CLIENT_ID + SCOPE_PARAM @Preview(showSystemUi = true, showBackground = true) @@ -147,17 +144,18 @@ fun LoginScreenPreview() { MaterialTheme { LoginScreenStateless( state = LoginState(showXiaomiPermissionDialog = false, navigateTo = null), - onAction = {} + onAction = {}, ) } } + @Preview(showSystemUi = true, showBackground = true) @Composable fun LoginScreenPreviewWithDialog() { MaterialTheme { LoginScreenStateless( state = LoginState(showXiaomiPermissionDialog = true, navigateTo = null), - onAction = {} + onAction = {}, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt index 4ed2ee917..08b5b67dc 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt @@ -26,7 +26,7 @@ data class LoginState( @HiltViewModel class LoginScreenViewModel @Inject constructor( - private val githubHelper: GithubHelper + private val githubHelper: GithubHelper, ) : ViewModel() { var state by mutableStateOf(LoginState(showXiaomiPermissionDialog = false, navigateTo = null)) @@ -41,11 +41,11 @@ class LoginScreenViewModel @Inject constructor( LoginAction.ClickLogIn -> { if (githubHelper.shouldAskForXiaomiIntent()) { state = state.copy( - showXiaomiPermissionDialog = true + showXiaomiPermissionDialog = true, ) } else { state = state.copy( - navigateTo = LoginNavigateTo.OpenGithubAuth + navigateTo = LoginNavigateTo.OpenGithubAuth, ) } } @@ -62,4 +62,4 @@ class LoginScreenViewModel @Inject constructor( } } } -} \ No newline at end of file +} diff --git a/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt index 22989ebbe..0061488f9 100644 --- a/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt @@ -70,5 +70,4 @@ class LoginScreenViewModelTest { get(LoginState::showXiaomiPermissionDialog).isFalse() } } - -} \ No newline at end of file +} From 2530588b23e2ea0343ce76ec2cdadc818336ff6a Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 13:44:38 +0200 Subject: [PATCH 322/526] Refactor fetching data on the ReviewersViewModel.kt - separate downloading the data from mapping the data. --- .../ui/reviewers/ReviewersViewModel.kt | 86 +++++++++++-------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 803a9fbaf..6c32c3a14 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -72,64 +72,74 @@ class ReviewersViewModel @Inject constructor( private fun fetchData() { viewModelScope.launch { + state = state.copy(isLoading = true, isError = false) + getMergedData() .onSuccess { state = state.copy(reviewers = it, isLoading = false) } .onFailure { state = state.copy(isError = true, isLoading = false) } } } - private suspend fun getMergedData(): Result> = - coroutineScope { - state = state.copy(isLoading = true, isError = false) - val requestedReviewersDeferred = async { fetchRequestedReviewers() } - val reviewersDeferred = async { fetchReviews() } + private suspend fun getMergedData(): Result> = downloadData() + .mapData() - val requestedReviewerResult = requestedReviewersDeferred.await() - val reviewersResult = reviewersDeferred.await() - requestedReviewerResult.flatMap { requestedReviewers -> - reviewersResult.map { it + requestedReviewers } - } - } - - private suspend fun fetchRequestedReviewers(): Result> { - val (owner, repo, pullRequestNumber, submissionTime) = initialValues - - return repository.getRequestedReviewers(owner, repo, pullRequestNumber) - .map { it.mapToReviewers(submissionTime) } - } + private suspend fun downloadData(): Pair, Result>> = + coroutineScope { + val (owner, repo, pullRequestNumber) = initialValues - private fun RequestedReviewersResponse.mapToReviewers(submissionTime: String): List { - val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) + val requestedReviewersDeferred = + async { repository.getRequestedReviewers(owner, repo, pullRequestNumber) } + val reviewsDeferred = async { repository.getReviews(owner, repo, pullRequestNumber) } - return users.map { - Reviewer(it.id, it.login, false, hoursFromPRStart, null) + Pair(requestedReviewersDeferred.await(), reviewsDeferred.await()) } - } - private suspend fun fetchReviews(): Result> { - val (owner, repo, pullRequestNumber, submissionTime) = initialValues - return repository.getReviews(owner, repo, pullRequestNumber) - .map { it.mapToReviewers(submissionTime) } - } + private fun Pair, Result>>.mapData() = + mergeData(first.mapRequestedReviewers(), second.mapReviews()) - private fun List.mapToReviewers(submissionTime: String): List { - val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(submissionTime)) + private fun mergeData( + requestedReviewers: Result>, + reviewersWithReviews: Result> + ) = requestedReviewers.flatMap { list -> + reviewersWithReviews.map { it + list } + } - return groupBy { it.user.id } - .map { reviewsForSingleUser -> - val latestReview = reviewsForSingleUser.value.minBy { it.submittedAt } - val hoursFromReviewDone = countHoursTillNow(latestReview.submittedAt) + private fun Result.mapRequestedReviewers(): Result> = + map { + val hoursFromPRStart = + countHoursTillNow(LocalDateTime.parse(initialValues.submissionTime)) + it.users.map { requestedReviewer -> Reviewer( - latestReview.user.id, - latestReview.user.login, - true, + requestedReviewer.id, + requestedReviewer.login, + false, hoursFromPRStart, - hoursFromReviewDone, + null ) } + } + + private fun Result>.mapReviews(): Result> { + val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(initialValues.submissionTime)) + + return map { list -> + list.groupBy { it.user.id } + .map { reviewsForSingleUser -> + val latestReview = reviewsForSingleUser.value.minBy { it.submittedAt } + val hoursFromReviewDone = countHoursTillNow(latestReview.submittedAt) + + Reviewer( + latestReview.user.id, + latestReview.user.login, + true, + hoursFromPRStart, + hoursFromReviewDone, + ) + } + } } private fun countHoursTillNow(submissionTime: LocalDateTime): Long = From 804170ba95ad9af3986bbc9050bbb84ea6eb72dc Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 6 Apr 2023 13:55:42 +0200 Subject: [PATCH 323/526] Make updateReviewerLoading method stateless and change the name. --- .../ui/reviewers/ReviewersViewModel.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 6c32c3a14..cc3c0f918 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -155,25 +155,35 @@ class ReviewersViewModel @Inject constructor( val (owner, repo, pullRequestNumber) = initialValues viewModelScope.launch { - state = state.copy(reviewers = updateReviewerLoadingState(userLogin, true)) + state = state.copy(reviewers = state.reviewers.updateLoadingState(userLogin, true)) + repository.notify(owner, repo, pullRequestNumber, "@$userLogin") .onSuccess { state = state.copy( snackbarTypeShown = SUCCESS, - reviewers = updateReviewerLoadingState(userLogin, false), + reviewers = state.reviewers.updateLoadingState(userLogin, false), ) } .onFailure { state = state.copy( snackbarTypeShown = FAILURE, - reviewers = updateReviewerLoadingState(userLogin, false), + reviewers = state.reviewers.updateLoadingState(userLogin, false), ) } } } - private fun updateReviewerLoadingState(userLogin: String, isLoading: Boolean) = - state.reviewers.map { if (it.login == userLogin) it.copy(isLoading = isLoading) else it } + private fun List.updateLoadingState( + userLogin: String, + isLoading: Boolean + ): List = map { + if (it.login == userLogin) { + it.copy(isLoading = isLoading) + } else { + it + } + } + private fun dismissSnackbar() { state = state.copy(snackbarTypeShown = null) From 989252af5c916b1e4bd64cbeb3b0d9715026090e Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 6 Apr 2023 12:06:31 +0000 Subject: [PATCH 324/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/reviewers/ReviewersViewModel.kt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index cc3c0f918..39005408a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -30,12 +30,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -83,7 +83,6 @@ class ReviewersViewModel @Inject constructor( private suspend fun getMergedData(): Result> = downloadData() .mapData() - private suspend fun downloadData(): Pair, Result>> = coroutineScope { val (owner, repo, pullRequestNumber) = initialValues @@ -95,13 +94,12 @@ class ReviewersViewModel @Inject constructor( Pair(requestedReviewersDeferred.await(), reviewsDeferred.await()) } - private fun Pair, Result>>.mapData() = mergeData(first.mapRequestedReviewers(), second.mapReviews()) private fun mergeData( requestedReviewers: Result>, - reviewersWithReviews: Result> + reviewersWithReviews: Result>, ) = requestedReviewers.flatMap { list -> reviewersWithReviews.map { it + list } } @@ -117,7 +115,7 @@ class ReviewersViewModel @Inject constructor( requestedReviewer.login, false, hoursFromPRStart, - null + null, ) } } @@ -175,7 +173,7 @@ class ReviewersViewModel @Inject constructor( private fun List.updateLoadingState( userLogin: String, - isLoading: Boolean + isLoading: Boolean, ): List = map { if (it.login == userLogin) { it.copy(isLoading = isLoading) @@ -184,7 +182,6 @@ class ReviewersViewModel @Inject constructor( } } - private fun dismissSnackbar() { state = state.copy(snackbarTypeShown = null) } From 2288c2bc5f43fafa34b5127659abd57d01345ed0 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 6 Apr 2023 15:00:41 +0200 Subject: [PATCH 325/526] chore: always present dialog on xiaomi --- .../appunite/loudius/ui/login/GithubHelper.kt | 11 ----- .../appunite/loudius/ui/login/LoginScreen.kt | 4 +- .../loudius/ui/login/LoginScreenViewModel.kt | 9 +++- app/src/main/res/values/strings.xml | 6 +-- .../ui/login/LoginScreenViewModelTest.kt | 49 +++++++++++++++++++ 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt index d5e039508..8eae0c18f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt @@ -46,20 +46,9 @@ class GithubHelper @Inject constructor(@ApplicationContext private val context: return false } - if (isAlertWindowPermissionGranted()) { - return false - } return true } - private fun isAlertWindowPermissionGranted(): Boolean { - val permissionNeeded = "Manifest.permission.SYSTEM_ALERT_WINDOW" - return context.packageManager.checkPermission( - permissionNeeded, - GITHUB_APP_PACKAGE_NAME, - ) == PackageManager.PERMISSION_GRANTED - } - private fun isGithubAppInstalled(): Boolean { return try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 42d7df986..e50515dc2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -111,8 +111,8 @@ fun LoginScreenStateless( } }, dismissButton = { - LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_cancel)) { - onAction(LoginAction.XiaomiPermissionDialogDismiss) + LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_already_granted)) { + onAction(LoginAction.XiaomiPermissionDialogAlreadyGrantedPermission) } }, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt index 08b5b67dc..216c00dbc 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt @@ -12,6 +12,7 @@ sealed class LoginAction { object ClickLogIn : LoginAction() object XiaomiPermissionDialogDismiss : LoginAction() object XiaomiPermissionDialogGrantPermission : LoginAction() + object XiaomiPermissionDialogAlreadyGrantedPermission : LoginAction() } sealed class LoginNavigateTo { @@ -56,10 +57,16 @@ class LoginScreenViewModel @Inject constructor( LoginAction.XiaomiPermissionDialogGrantPermission -> { state = state.copy( - showXiaomiPermissionDialog = false, navigateTo = LoginNavigateTo.OpenXiaomiPermissionManager, ) } + + LoginAction.XiaomiPermissionDialogAlreadyGrantedPermission -> { + state = state.copy( + showXiaomiPermissionDialog = false, + navigateTo = LoginNavigateTo.OpenGithubAuth, + ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0523cfe29..8cfe38d44 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,9 +24,9 @@ Log in Loudius logo - Grant Permission - You\'re using a Xiaomi device and you have Github App installed, please note that there\'s a known bug that requires you to grant the "Display pup-up windows while running in the background" permission. This will allow you to continue using the app without any interruptions. + Grant permission + You\'re using a Xiaomi device and you have Github App installed, please note that there\'s a known bug that requires you to grant the \"Display pup-up windows while running in the background\" permission. This will allow you to continue using the app without any interruptions. Necessary permissions - Cancel + I\'ve already granted diff --git a/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt index 0061488f9..f7f96c24d 100644 --- a/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt @@ -67,7 +67,56 @@ class LoginScreenViewModelTest { expectThat(viewModel.state) { get(LoginState::navigateTo).isEqualTo(LoginNavigateTo.OpenXiaomiPermissionManager) + get(LoginState::showXiaomiPermissionDialog).isTrue() + } + } + + @Test + fun `GIVEN xiaomi permission dialog is displayed WHEN user click already granted THEN navigate github auth`() { + every { githubHelper.shouldAskForXiaomiIntent() } returns true + val viewModel = create() + viewModel.onAction(LoginAction.ClickLogIn) + expectThat(viewModel.state).get(LoginState::showXiaomiPermissionDialog).isTrue() + + viewModel.onAction(LoginAction.XiaomiPermissionDialogAlreadyGrantedPermission) + + expectThat(viewModel.state) { + get(LoginState::navigateTo).isEqualTo(LoginNavigateTo.OpenGithubAuth) + get(LoginState::showXiaomiPermissionDialog).isFalse() + } + } + + @Test + fun `test the whole xiaomi flow`() { + // on Xiaomin with GitHub App + every { githubHelper.shouldAskForXiaomiIntent() } returns true + val viewModel = create() + + // When user click log-in screen + viewModel.onAction(LoginAction.ClickLogIn) + + // Then display xiaomi permission dialog + expectThat(viewModel.state) { + get(LoginState::showXiaomiPermissionDialog).isTrue() + get(LoginState::navigateTo).isNull() + } + + // When user clicks grant permission + viewModel.onAction(LoginAction.XiaomiPermissionDialogGrantPermission) + + // Then show xiaomi preferences menager + expectThat(viewModel.state) { + get(LoginState::showXiaomiPermissionDialog).isTrue() + get(LoginState::navigateTo).isEqualTo(LoginNavigateTo.OpenXiaomiPermissionManager) + } + + // When user goes back to the app and click button with granted info + viewModel.onAction(LoginAction.XiaomiPermissionDialogAlreadyGrantedPermission) + + // Then show xiaomi preferences menager + expectThat(viewModel.state) { get(LoginState::showXiaomiPermissionDialog).isFalse() + get(LoginState::navigateTo).isEqualTo(LoginNavigateTo.OpenGithubAuth) } } } From fef1fb4bf6c2f28009b6b0fec06460995bcce796 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 7 Apr 2023 11:43:23 +0200 Subject: [PATCH 326/526] Implement code review suggestions. --- .../com/appunite/loudius/common/Screen.kt | 13 +++++---- .../ui/reviewers/ReviewersViewModel.kt | 29 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index 6811eca53..7386d5141 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -24,6 +24,7 @@ import androidx.navigation.NavDeepLink import androidx.navigation.NavType import androidx.navigation.navArgument import androidx.navigation.navDeepLink +import java.time.LocalDateTime sealed class Screen(val route: String) { open val arguments: List = emptyList() @@ -72,17 +73,19 @@ sealed class Screen(val route: String) { ): String = "reviewers_screen/$pullRequestNumber/$owner/$repo/$submissionDate" fun getInitialValues(savedStateHandle: SavedStateHandle) = ReviewersInitialValues( - checkNotNull(savedStateHandle[ownerArg]), - checkNotNull(savedStateHandle[repoArg]), - checkNotNull(savedStateHandle[pullRequestNumberArg]), - checkNotNull(savedStateHandle[submissionDateArg]), + owner = checkNotNull(savedStateHandle[ownerArg]), + repo = checkNotNull(savedStateHandle[repoArg]), + pullRequestNumber = checkNotNull(savedStateHandle[pullRequestNumberArg]), + submissionTime = checkNotNull( + LocalDateTime.parse(savedStateHandle[submissionDateArg]) + ) ) data class ReviewersInitialValues( val owner: String, val repo: String, val pullRequestNumber: String, - val submissionTime: String, + val submissionTime: LocalDateTime, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 39005408a..1bfde3db4 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -30,12 +30,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -85,11 +85,20 @@ class ReviewersViewModel @Inject constructor( private suspend fun downloadData(): Pair, Result>> = coroutineScope { - val (owner, repo, pullRequestNumber) = initialValues - - val requestedReviewersDeferred = - async { repository.getRequestedReviewers(owner, repo, pullRequestNumber) } - val reviewsDeferred = async { repository.getReviews(owner, repo, pullRequestNumber) } + val requestedReviewersDeferred = async { + repository.getRequestedReviewers( + initialValues.owner, + initialValues.repo, + initialValues.pullRequestNumber + ) + } + val reviewsDeferred = async { + repository.getReviews( + initialValues.owner, + initialValues.repo, + initialValues.pullRequestNumber + ) + } Pair(requestedReviewersDeferred.await(), reviewsDeferred.await()) } @@ -107,7 +116,7 @@ class ReviewersViewModel @Inject constructor( private fun Result.mapRequestedReviewers(): Result> = map { val hoursFromPRStart = - countHoursTillNow(LocalDateTime.parse(initialValues.submissionTime)) + countHoursTillNow(initialValues.submissionTime) it.users.map { requestedReviewer -> Reviewer( @@ -121,7 +130,7 @@ class ReviewersViewModel @Inject constructor( } private fun Result>.mapReviews(): Result> { - val hoursFromPRStart = countHoursTillNow(LocalDateTime.parse(initialValues.submissionTime)) + val hoursFromPRStart = countHoursTillNow(initialValues.submissionTime) return map { list -> list.groupBy { it.user.id } From 6790601ba12dbe2e72d9e792ec9c696affe4f7ea Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Fri, 7 Apr 2023 09:47:26 +0000 Subject: [PATCH 327/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/common/Screen.kt | 4 ++-- .../loudius/ui/reviewers/ReviewersViewModel.kt | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/common/Screen.kt b/app/src/main/java/com/appunite/loudius/common/Screen.kt index 7386d5141..f29b7d1cf 100644 --- a/app/src/main/java/com/appunite/loudius/common/Screen.kt +++ b/app/src/main/java/com/appunite/loudius/common/Screen.kt @@ -77,8 +77,8 @@ sealed class Screen(val route: String) { repo = checkNotNull(savedStateHandle[repoArg]), pullRequestNumber = checkNotNull(savedStateHandle[pullRequestNumberArg]), submissionTime = checkNotNull( - LocalDateTime.parse(savedStateHandle[submissionDateArg]) - ) + LocalDateTime.parse(savedStateHandle[submissionDateArg]), + ), ) data class ReviewersInitialValues( diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 1bfde3db4..6f9b48370 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -30,12 +30,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -89,14 +89,14 @@ class ReviewersViewModel @Inject constructor( repository.getRequestedReviewers( initialValues.owner, initialValues.repo, - initialValues.pullRequestNumber + initialValues.pullRequestNumber, ) } val reviewsDeferred = async { repository.getReviews( initialValues.owner, initialValues.repo, - initialValues.pullRequestNumber + initialValues.pullRequestNumber, ) } From bd8e90b7553e75e0d7e0b5ceaeb4527a889a7848 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 7 Apr 2023 12:39:50 +0200 Subject: [PATCH 328/526] Create simple test for checking login screen --- app/build.gradle | 16 +++++-- .../loudius/ExampleInstrumentedTest.kt | 41 ----------------- .../com/appunite/loudius/LoginScreenTest.kt | 44 +++++++++++++++++++ .../appunite/loudius/util/HiltTestRunner.kt | 13 ++++++ app/src/debug/AndroidManifest.xml | 9 ++++ .../java/com/appunite/loudius/TestActivity.kt | 7 +++ 6 files changed, 85 insertions(+), 45 deletions(-) delete mode 100644 app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt create mode 100644 app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt create mode 100644 app/src/debug/AndroidManifest.xml create mode 100644 app/src/debug/java/com/appunite/loudius/TestActivity.kt diff --git a/app/build.gradle b/app/build.gradle index 96b0747c5..babb53b15 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,7 +17,7 @@ android { versionCode 1 versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "com.appunite.loudius.util.HiltTestRunner" vectorDrawables { useSupportLibrary true } @@ -88,6 +88,10 @@ dependencies { testImplementation 'com.google.dagger:hilt-android-testing:2.45' kaptTest 'com.google.dagger:hilt-compiler:2.45' + // DI - For instrumented tests. + androidTestImplementation 'com.google.dagger:hilt-android-testing:2.45' + kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.45' + //coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" @@ -102,9 +106,11 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' // assertion library - testImplementation 'io.strikt:strikt-core:0.34.0' - testImplementation 'io.strikt:strikt-mockk:0.34.0' - androidTestImplementation 'io.strikt:strikt-core:0.34.0' + // cannot use 0.34.0 due to an existing bug + // https://github.com/robfletcher/strikt/issues/259 + testImplementation 'io.strikt:strikt-core:0.33.0' + testImplementation 'io.strikt:strikt-mockk:0.33.0' + androidTestImplementation 'io.strikt:strikt-core:0.33.0' //testing testImplementation "io.mockk:mockk:1.13.3" @@ -113,6 +119,8 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") +// debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version") // ktlint ktlintRuleset project(":custom-ktlint-rules") diff --git a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt deleted file mode 100644 index b8c78bdba..000000000 --- a/app/src/androidTest/java/com/appunite/loudius/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Test -import org.junit.runner.RunWith -import strikt.api.expectThat -import strikt.assertions.isEqualTo - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - - expectThat(appContext.packageName) - .isEqualTo("com.appunite.loudius") - } -} diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt new file mode 100644 index 000000000..8a723650b --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -0,0 +1,44 @@ +package com.appunite.loudius + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.ui.login.LoginScreen +import com.appunite.loudius.ui.theme.LoudiusTheme +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +class LoginScreenTest { + +// @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) +// @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule() + @get:Rule + val rule: RuleChain = RuleChain.outerRule(hiltRule) + .around(composeTestRule) + + @Before + fun setUp() { + hiltRule.inject() + } + + @Test + fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { + composeTestRule.setContent { + LoudiusTheme { + LoginScreen() + } + } + + composeTestRule.onNodeWithText("Log in").assertIsDisplayed() + } +} diff --git a/app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt b/app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt new file mode 100644 index 000000000..c20be7212 --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt @@ -0,0 +1,13 @@ +package com.appunite.loudius.util + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +class HiltTestRunner : AndroidJUnitRunner() { + + override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..a27e8c399 --- /dev/null +++ b/app/src/debug/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/app/src/debug/java/com/appunite/loudius/TestActivity.kt b/app/src/debug/java/com/appunite/loudius/TestActivity.kt new file mode 100644 index 000000000..0b178bb06 --- /dev/null +++ b/app/src/debug/java/com/appunite/loudius/TestActivity.kt @@ -0,0 +1,7 @@ +package com.appunite.loudius + +import androidx.activity.ComponentActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class TestActivity : ComponentActivity() From f2175fbc591230ad2da3bf4fcccb02bb3b9e3782 Mon Sep 17 00:00:00 2001 From: kezc Date: Fri, 7 Apr 2023 11:56:27 +0000 Subject: [PATCH 329/526] [MegaLinter] Apply linters fixes --- .../androidTest/java/com/appunite/loudius/LoginScreenTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 8a723650b..bd1f610e8 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -20,8 +20,10 @@ class LoginScreenTest { // @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) + // @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() + @get:Rule val rule: RuleChain = RuleChain.outerRule(hiltRule) .around(composeTestRule) From 8f010838085ee919dd0b215728ff43879e6cdcdc Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Fri, 7 Apr 2023 15:33:10 +0200 Subject: [PATCH 330/526] chore: review changes --- .../loudius/ui/components/LoudiusDialog.kt | 27 ++++++++--- .../appunite/loudius/ui/login/GithubHelper.kt | 30 ++++++++---- .../appunite/loudius/ui/login/LoginScreen.kt | 47 ++++++++++--------- .../loudius/ui/login/LoginScreenViewModel.kt | 30 ++++++++---- .../ui/login/LoginScreenViewModelTest.kt | 16 +++++++ 5 files changed, 102 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt index 08dc7e570..a09538499 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.components import androidx.compose.material3.AlertDialog @@ -14,7 +30,7 @@ fun LoudiusDialog( title: String, dismissButton: @Composable (() -> Unit)? = null, /** - * For text {@see LoudiusTextStyle#ScreenContent} should be used + * For text [com.appunite.loudius.ui.components.LoudiusTextStyle.ScreenContent] should be used */ text: @Composable (() -> Unit)? = null, @@ -39,8 +55,7 @@ fun LoudiusDialogSimplePreview() { onDismissRequest = { }, title = "Title", confirmButton = { - LoudiusOutlinedButton(text = "Confirm") { - } + LoudiusOutlinedButton(text = "Confirm") {} }, ) } @@ -60,12 +75,10 @@ fun LoudiusDialogAdvancedPreview() { ) }, confirmButton = { - LoudiusOutlinedButton(text = "Confirm") { - } + LoudiusOutlinedButton(text = "Confirm") {} }, dismissButton = { - LoudiusOutlinedButton(text = "Dismiss") { - } + LoudiusOutlinedButton(text = "Dismiss") {} }, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt index 8eae0c18f..f4ff14007 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.login import android.content.Context @@ -37,16 +53,10 @@ class GithubHelper @Inject constructor(@ApplicationContext private val context: .putExtra("extra_pkgname", GITHUB_APP_PACKAGE_NAME) } - fun shouldAskForXiaomiIntent(): Boolean { - if (!Build.MANUFACTURER.equals("Xiaomi", ignoreCase = true)) { - return false - } - - if (!isGithubAppInstalled()) { - return false - } - - return true + fun shouldAskForXiaomiIntent(): Boolean = when { + !Build.MANUFACTURER.equals("Xiaomi", ignoreCase = true) -> false + !isGithubAppInstalled() -> false + else -> true } private fun isGithubAppInstalled(): Boolean { diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index e50515dc2..f5d1d679e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -56,7 +56,6 @@ fun LoginScreen( ) viewModel.onAction(LoginAction.ClearNavigation) } - LoginNavigateTo.OpenXiaomiPermissionManager -> { context.startActivity(GithubHelper.xiaomiPermissionManagerForGithub()) } @@ -93,33 +92,37 @@ fun LoginScreenStateless( contentDescription = stringResource(R.string.github_icon), ) }, - ) if (state.showXiaomiPermissionDialog) { - LoudiusDialog( - onDismissRequest = { onAction(LoginAction.XiaomiPermissionDialogDismiss) }, - title = stringResource(R.string.login_screen_xiaomi_dialog_title), - text = { - LoudiusText( - style = LoudiusTextStyle.ScreenContent, - text = stringResource(R.string.login_screen_xiaomi_dialog_text), - ) - }, - confirmButton = { - LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_grant_permission)) { - onAction(LoginAction.XiaomiPermissionDialogGrantPermission) - } - }, - dismissButton = { - LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_already_granted)) { - onAction(LoginAction.XiaomiPermissionDialogAlreadyGrantedPermission) - } - }, - ) + XiaomiPermissionDialog(onAction) } } } +@Composable +private fun XiaomiPermissionDialog(onAction: (LoginAction) -> Unit) { + LoudiusDialog( + onDismissRequest = { onAction(LoginAction.XiaomiPermissionDialogDismiss) }, + title = stringResource(R.string.login_screen_xiaomi_dialog_title), + text = { + LoudiusText( + style = LoudiusTextStyle.ScreenContent, + text = stringResource(R.string.login_screen_xiaomi_dialog_text), + ) + }, + confirmButton = { + LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_grant_permission)) { + onAction(LoginAction.XiaomiPermissionDialogGrantPermission) + } + }, + dismissButton = { + LoudiusOutlinedButton(text = stringResource(R.string.login_screen_xiaomi_dialog_already_granted)) { + onAction(LoginAction.XiaomiPermissionDialogAlreadyGrantedPermission) + } + }, + ) +} + @Composable fun LoginImage() { Image( diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt index 216c00dbc..4fd6593b1 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreenViewModel.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.login import androidx.compose.runtime.getValue @@ -21,8 +37,8 @@ sealed class LoginNavigateTo { } data class LoginState( - val showXiaomiPermissionDialog: Boolean, - val navigateTo: LoginNavigateTo?, + val showXiaomiPermissionDialog: Boolean = false, + val navigateTo: LoginNavigateTo? = null, ) @HiltViewModel @@ -30,7 +46,7 @@ class LoginScreenViewModel @Inject constructor( private val githubHelper: GithubHelper, ) : ViewModel() { - var state by mutableStateOf(LoginState(showXiaomiPermissionDialog = false, navigateTo = null)) + var state by mutableStateOf(LoginState()) private set fun onAction(action: LoginAction) { @@ -45,9 +61,7 @@ class LoginScreenViewModel @Inject constructor( showXiaomiPermissionDialog = true, ) } else { - state = state.copy( - navigateTo = LoginNavigateTo.OpenGithubAuth, - ) + state = state.copy(navigateTo = LoginNavigateTo.OpenGithubAuth) } } @@ -56,9 +70,7 @@ class LoginScreenViewModel @Inject constructor( } LoginAction.XiaomiPermissionDialogGrantPermission -> { - state = state.copy( - navigateTo = LoginNavigateTo.OpenXiaomiPermissionManager, - ) + state = state.copy(navigateTo = LoginNavigateTo.OpenXiaomiPermissionManager) } LoginAction.XiaomiPermissionDialogAlreadyGrantedPermission -> { diff --git a/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt index f7f96c24d..1ec09d03a 100644 --- a/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/login/LoginScreenViewModelTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.ui.login import io.mockk.every From b2db600206193ddb63105cea86df3af0db513cad Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 11 Apr 2023 09:27:28 +0200 Subject: [PATCH 331/526] Add copyright header --- .../java/com/appunite/loudius/LoginScreenTest.kt | 16 ++++++++++++++++ .../com/appunite/loudius/util/HiltTestRunner.kt | 16 ++++++++++++++++ .../java/com/appunite/loudius/TestActivity.kt | 16 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index bd1f610e8..ced86a789 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed diff --git a/app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt b/app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt index c20be7212..8e4fc9fb6 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/HiltTestRunner.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.util import android.app.Application diff --git a/app/src/debug/java/com/appunite/loudius/TestActivity.kt b/app/src/debug/java/com/appunite/loudius/TestActivity.kt index 0b178bb06..f5d2c9ea6 100644 --- a/app/src/debug/java/com/appunite/loudius/TestActivity.kt +++ b/app/src/debug/java/com/appunite/loudius/TestActivity.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius import androidx.activity.ComponentActivity From 5f511d5cea31ccf87eced98d166bf7d5eb42a8c0 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 11 Apr 2023 10:52:22 +0200 Subject: [PATCH 332/526] Catch JsonParseException in the safeApiCall() method. --- .../java/com/appunite/loudius/network/utils/ApiCallUtil.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index 065ee7fea..8edba6fc8 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -18,9 +18,10 @@ package com.appunite.loudius.network.utils import com.appunite.loudius.network.model.error.DefaultErrorResponse import com.google.gson.Gson +import com.google.gson.JsonParseException +import java.io.IOException import org.json.JSONException import retrofit2.HttpException -import java.io.IOException suspend fun safeApiCall( errorParser: RequestErrorParser = DefaultErrorParser, @@ -34,6 +35,8 @@ suspend fun safeApiCall( Result.failure(errorParser(throwable.code(), message ?: throwable.message())) } catch (throwable: IOException) { Result.failure(WebException.NetworkError(throwable)) + } catch (throwable: JsonParseException) { + Result.failure(WebException.UnknownError(null, throwable.message)) } } From b4397c5c8c0842224053ce7555a3b0f42b85acb1 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 11 Apr 2023 11:07:52 +0200 Subject: [PATCH 333/526] Wrap safeApiCall in ApiRequester class and reuse Gson instance. --- .../appunite/loudius/di/DataSourceModule.kt | 18 ++++-- .../com/appunite/loudius/di/NetworkModule.kt | 8 ++- .../network/datasource/AuthDataSource.kt | 5 +- .../PullRequestsNetworkDataSource.kt | 15 +++-- .../network/datasource/UserDataSource.kt | 9 ++- .../loudius/network/utils/ApiCallUtil.kt | 59 ++++++++++--------- 6 files changed, 69 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 050a4be3f..e42cbbfa1 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -28,6 +28,7 @@ import com.appunite.loudius.network.datasource.UserDataSourceImpl import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.services.UserService +import com.appunite.loudius.network.utils.ApiRequester import com.appunite.loudius.network.utils.AuthFailureHandler import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import dagger.Module @@ -43,13 +44,19 @@ object DataSourceModule { @Provides @Singleton - fun providePullRequestNetworkDataSource(service: PullRequestsService): PullRequestDataSource = - PullRequestsNetworkDataSource(service) + fun providePullRequestNetworkDataSource( + service: PullRequestsService, + apiRequester: ApiRequester + ): PullRequestDataSource = + PullRequestsNetworkDataSource(service, apiRequester) @Provides @Singleton - fun provideUserDataSource(userService: UserService): UserDataSource = - UserDataSourceImpl(userService) + fun provideUserDataSource( + userService: UserService, + apiRequester: ApiRequester + ): UserDataSource = + UserDataSourceImpl(userService, apiRequester) @Singleton @Provides @@ -60,7 +67,8 @@ object DataSourceModule { @Provides fun provideAuthNetworkDataSource( service: AuthService, - ): AuthDataSource = AuthNetworkDataSource(service) + apiRequester: ApiRequester + ): AuthDataSource = AuthNetworkDataSource(service, apiRequester) @Singleton @Provides diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index c0fca4070..4ccf4d2fa 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -19,6 +19,7 @@ package com.appunite.loudius.di import com.appunite.loudius.common.Constants import com.appunite.loudius.network.intercept.AuthFailureInterceptor import com.appunite.loudius.network.intercept.AuthInterceptor +import com.appunite.loudius.network.utils.ApiRequester import com.appunite.loudius.network.utils.AuthFailureHandler import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy @@ -28,12 +29,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import java.time.LocalDateTime +import javax.inject.Singleton import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module @@ -100,4 +101,7 @@ object NetworkModule { GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) .create() + + @Provides + fun provideApiRequester(gson: Gson): ApiRequester = ApiRequester(gson) } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index e66398772..5cdefa76c 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -20,8 +20,8 @@ import com.appunite.loudius.common.flatMap import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.model.AccessTokenResponse import com.appunite.loudius.network.services.AuthService +import com.appunite.loudius.network.utils.ApiRequester import com.appunite.loudius.network.utils.WebException -import com.appunite.loudius.network.utils.safeApiCall import javax.inject.Inject import javax.inject.Singleton @@ -37,6 +37,7 @@ interface AuthDataSource { @Singleton class AuthNetworkDataSource @Inject constructor( private val authService: AuthService, + private val apiRequester: ApiRequester, ) : AuthDataSource { companion object { @@ -48,7 +49,7 @@ class AuthNetworkDataSource @Inject constructor( clientSecret: String, code: String, ): Result = - safeApiCall { authService.getAccessToken(clientId, clientSecret, code) } + apiRequester.safeApiCall { authService.getAccessToken(clientId, clientSecret, code) } .flatMap { response -> if (response.accessToken != null) { Result.success(response.accessToken) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 17efb604e..118298de8 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -21,7 +21,7 @@ import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.model.Review import com.appunite.loudius.network.model.request.NotifyRequestBody import com.appunite.loudius.network.services.PullRequestsService -import com.appunite.loudius.network.utils.safeApiCall +import com.appunite.loudius.network.utils.ApiRequester import javax.inject.Inject import javax.inject.Singleton @@ -49,10 +49,13 @@ interface PullRequestDataSource { } @Singleton -class PullRequestsNetworkDataSource @Inject constructor(private val service: PullRequestsService) : +class PullRequestsNetworkDataSource @Inject constructor( + private val service: PullRequestsService, + private val apiRequester: ApiRequester +) : PullRequestDataSource { override suspend fun getPullRequestsForUser(author: String): Result = - safeApiCall { + apiRequester.safeApiCall { service.getPullRequestsForUser("author:$author type:pr state:open") } @@ -60,7 +63,7 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Pul owner: String, repository: String, pullRequestNumber: String, - ): Result = safeApiCall { + ): Result = apiRequester.safeApiCall { service.getReviewers(owner, repository, pullRequestNumber) } @@ -68,7 +71,7 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Pul owner: String, repository: String, pullRequestNumber: String, - ): Result> = safeApiCall { + ): Result> = apiRequester.safeApiCall { service.getReviews(owner, repository, pullRequestNumber) } @@ -77,7 +80,7 @@ class PullRequestsNetworkDataSource @Inject constructor(private val service: Pul repository: String, pullRequestNumber: String, message: String, - ): Result = safeApiCall { + ): Result = apiRequester.safeApiCall { service.notify(owner, repository, pullRequestNumber, NotifyRequestBody(message)) } } diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt index 4bfacf3c6..3eb01ecb0 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt @@ -18,7 +18,7 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.User import com.appunite.loudius.network.services.UserService -import com.appunite.loudius.network.utils.safeApiCall +import com.appunite.loudius.network.utils.ApiRequester import javax.inject.Inject import javax.inject.Singleton @@ -27,9 +27,12 @@ interface UserDataSource { } @Singleton -class UserDataSourceImpl @Inject constructor(private val userService: UserService) : +class UserDataSourceImpl @Inject constructor( + private val userService: UserService, + private val apiRequester: ApiRequester +) : UserDataSource { - override suspend fun getUser(): Result = safeApiCall { + override suspend fun getUser(): Result = apiRequester.safeApiCall { userService.getUser() } } diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt index 8edba6fc8..f389e77c0 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt @@ -20,38 +20,43 @@ import com.appunite.loudius.network.model.error.DefaultErrorResponse import com.google.gson.Gson import com.google.gson.JsonParseException import java.io.IOException +import javax.inject.Inject import org.json.JSONException import retrofit2.HttpException -suspend fun safeApiCall( - errorParser: RequestErrorParser = DefaultErrorParser, - apiCall: suspend () -> T, -): Result { - return try { - val response = apiCall() - Result.success(response) - } catch (throwable: HttpException) { - val message = getApiErrorMessageIfExist(throwable) - Result.failure(errorParser(throwable.code(), message ?: throwable.message())) - } catch (throwable: IOException) { - Result.failure(WebException.NetworkError(throwable)) - } catch (throwable: JsonParseException) { - Result.failure(WebException.UnknownError(null, throwable.message)) +class ApiRequester @Inject constructor(private val gson: Gson) { + + suspend fun safeApiCall( + errorParser: RequestErrorParser = DefaultErrorParser, + apiCall: suspend () -> T, + ): Result { + return try { + val response = apiCall() + Result.success(response) + } catch (throwable: HttpException) { + val message = getApiErrorMessageIfExist(throwable) + Result.failure(errorParser(throwable.code(), message ?: throwable.message())) + } catch (throwable: IOException) { + Result.failure(WebException.NetworkError(throwable)) + } catch (throwable: JsonParseException) { + Result.failure(WebException.UnknownError(null, throwable.message)) + } } -} -private fun getApiErrorMessageIfExist(throwable: HttpException) = try { - val errorResponse = Gson().fromJson( - throwable.response()?.errorBody()?.string(), - DefaultErrorResponse::class.java, - ) - errorResponse.message -} catch (throwable: JSONException) { - null -} + private fun getApiErrorMessageIfExist(throwable: HttpException) = try { + val errorResponse = gson.fromJson( + throwable.response()?.errorBody()?.charStream(), + DefaultErrorResponse::class.java, + ) + errorResponse.message + } catch (throwable: JSONException) { + null + } + + object DefaultErrorParser : RequestErrorParser { -object DefaultErrorParser : RequestErrorParser { + override fun invoke(responseCode: Int, responseMessage: String): Exception = + WebException.UnknownError(responseCode, responseMessage) + } - override fun invoke(responseCode: Int, responseMessage: String): Exception = - WebException.UnknownError(responseCode, responseMessage) } From bec5d6ed0b88989c1022085d4200925910469960 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 11 Apr 2023 11:09:52 +0200 Subject: [PATCH 334/526] Improve LocalDateTimeDeserializer.kt implementation. --- .../loudius/network/utils/LocalDateTimeDeserializer.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt index 9701839fe..d5fcc5383 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/LocalDateTimeDeserializer.kt @@ -29,11 +29,12 @@ class LocalDateTimeDeserializer : JsonDeserializer { override fun deserialize( json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext?, + typeOfT: Type, + context: JsonDeserializationContext, ): LocalDateTime { try { - val dateString = json?.asJsonPrimitive?.asString + json ?: throw JsonParseException("Cannot deserialize null value") + val dateString = json.asJsonPrimitive.asString val offsetDateTime = OffsetDateTime.parse(dateString, ISO_OFFSET_DATE_TIME) return offsetDateTime.toLocalDateTime() } catch (e: Exception) { From a2e5d933710a9acfcad1731c92a68b2deca0497d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 11 Apr 2023 11:28:36 +0200 Subject: [PATCH 335/526] Extract BadVerificationCodeException from WebException.kt. --- .../appunite/loudius/network/datasource/AuthDataSource.kt | 7 ++++++- .../com/appunite/loudius/network/utils/WebException.kt | 5 ----- .../loudius/ui/authenticating/AuthenticatingViewModel.kt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt index 5cdefa76c..8ef828325 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/AuthDataSource.kt @@ -60,8 +60,13 @@ class AuthNetworkDataSource @Inject constructor( private fun AccessTokenResponse.mapErrorToException(): Exception { return when (error) { - BAD_VERIFICATION_CODE_ERROR -> WebException.BadVerificationCodeException + BAD_VERIFICATION_CODE_ERROR -> BadVerificationCodeException else -> WebException.UnknownError(null, error) } } } + +/** + * Thrown during authorization with incorrect verification code. + */ +object BadVerificationCodeException : Exception() diff --git a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt index d2521e9f5..29f6c3440 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/WebException.kt @@ -30,9 +30,4 @@ sealed class WebException : Exception() { * For example [IOException]. */ data class NetworkError(override val cause: Throwable? = null) : WebException() - - /** - * Thrown during authorization with incorrect verification code. - */ - object BadVerificationCodeException : WebException() } diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt index b0bf68b9d..c5b0eb9ed 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt @@ -26,10 +26,10 @@ import com.appunite.loudius.BuildConfig import com.appunite.loudius.common.Constants.CLIENT_ID import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.repository.AuthRepository -import com.appunite.loudius.network.utils.WebException +import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class AuthenticatingAction { @@ -107,7 +107,7 @@ class AuthenticatingViewModel @Inject constructor( } private fun resolveErrorType(it: Throwable) = when (it) { - is WebException.BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR + is BadVerificationCodeException -> LoadingErrorType.LOGIN_ERROR else -> LoadingErrorType.GENERIC_ERROR } } From 215485b79af10a656fa09cce2884d69dd842ebef Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 11 Apr 2023 11:43:22 +0200 Subject: [PATCH 336/526] Remove unused dependency --- app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index babb53b15..87c904f98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,7 +120,6 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") -// debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version") // ktlint ktlintRuleset project(":custom-ktlint-rules") From a437fc74b8d7790a0bbfcdabbc21eab6c10e7ab9 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 11 Apr 2023 12:14:53 +0200 Subject: [PATCH 337/526] Change rules type --- .../java/com/appunite/loudius/LoginScreenTest.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index ced86a789..dac4cb99b 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -34,16 +34,12 @@ import org.junit.runner.RunWith @HiltAndroidTest class LoginScreenTest { -// @get:Rule(order = 0) + @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) -// @get:Rule(order = 1) + @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() - @get:Rule - val rule: RuleChain = RuleChain.outerRule(hiltRule) - .around(composeTestRule) - @Before fun setUp() { hiltRule.inject() From 330473e1e62501bc787fd28dde49569cbe55006e Mon Sep 17 00:00:00 2001 From: kezc Date: Tue, 11 Apr 2023 10:18:36 +0000 Subject: [PATCH 338/526] [MegaLinter] Apply linters fixes --- app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index dac4cb99b..5ae5694b0 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -27,7 +27,6 @@ import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule import org.junit.Test -import org.junit.rules.RuleChain import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) From a891a92de25d362df1e0077790f403b419c0f4fe Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 11 Apr 2023 13:47:29 +0200 Subject: [PATCH 339/526] Correct formatting. --- .../main/java/com/appunite/loudius/di/DataSourceModule.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index e42cbbfa1..3438b9c9b 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -47,16 +47,14 @@ object DataSourceModule { fun providePullRequestNetworkDataSource( service: PullRequestsService, apiRequester: ApiRequester - ): PullRequestDataSource = - PullRequestsNetworkDataSource(service, apiRequester) + ): PullRequestDataSource = PullRequestsNetworkDataSource(service, apiRequester) @Provides @Singleton fun provideUserDataSource( userService: UserService, apiRequester: ApiRequester - ): UserDataSource = - UserDataSourceImpl(userService, apiRequester) + ): UserDataSource = UserDataSourceImpl(userService, apiRequester) @Singleton @Provides From 27d6d3f9100ad1c8c1cc7e3f7f9887492ff58cc4 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 11 Apr 2023 13:58:49 +0200 Subject: [PATCH 340/526] Repair failing tests. --- .../network/utils/{ApiCallUtil.kt => ApiRequester.kt} | 0 .../java/com/appunite/loudius/fakes/FakeAuthRepository.kt | 3 ++- .../com/appunite/loudius/network/NetworkTestDoubles.kt | 7 +++++-- .../network/datasource/AuthNetworkDataSourceTest.kt | 8 +++++--- .../datasource/PullRequestsNetworkDataSourceTest.kt | 6 ++++-- .../loudius/network/datasource/UserDataSourceTest.kt | 3 ++- 6 files changed, 18 insertions(+), 9 deletions(-) rename app/src/main/java/com/appunite/loudius/network/utils/{ApiCallUtil.kt => ApiRequester.kt} (100%) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt similarity index 100% rename from app/src/main/java/com/appunite/loudius/network/utils/ApiCallUtil.kt rename to app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt index 026dc67ed..256971123 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeAuthRepository.kt @@ -17,6 +17,7 @@ package com.appunite.loudius.fakes import com.appunite.loudius.domain.repository.AuthRepository +import com.appunite.loudius.network.datasource.BadVerificationCodeException import com.appunite.loudius.network.model.AccessToken import com.appunite.loudius.network.utils.WebException @@ -27,7 +28,7 @@ class FakeAuthRepository : AuthRepository { code: String, ): Result = when (code) { "validCode" -> Result.success("validToken") - "invalidCode" -> Result.failure(WebException.BadVerificationCodeException) + "invalidCode" -> Result.failure(BadVerificationCodeException) else -> Result.failure(WebException.UnknownError(null, null)) } diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index ba9f71cd4..350e121fb 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -20,18 +20,19 @@ import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.fakes.FakeAuthRepository import com.appunite.loudius.network.intercept.AuthFailureInterceptor import com.appunite.loudius.network.intercept.AuthInterceptor +import com.appunite.loudius.network.utils.ApiRequester import com.appunite.loudius.network.utils.AuthFailureHandler import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), @@ -49,6 +50,8 @@ private fun testGson() = .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() +fun testRequester() = ApiRequester(testGson()) + fun retrofitTestDouble( client: OkHttpClient = testOkHttpClient(), gson: Gson = testGson(), diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt index 1cd420a54..26a6a85ac 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/AuthNetworkDataSourceTest.kt @@ -22,6 +22,7 @@ import com.appunite.loudius.fakes.FakeAuthRepository import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.testOkHttpClient +import com.appunite.loudius.network.testRequester import com.appunite.loudius.network.utils.WebException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -49,7 +50,7 @@ class AuthNetworkDataSourceTest { mockWebServer.shutdown() } - private val authNetworkDataSource = AuthNetworkDataSource(authService) + private val authNetworkDataSource = AuthNetworkDataSource(authService, testRequester()) @Test fun `GIVEN correct data WHEN accessing token THEN return success with new valid token`() = @@ -83,11 +84,12 @@ class AuthNetworkDataSourceTest { MockResponse().setResponseCode(200).setBody(jsonResponse), ) - val result = authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") + val result = + authNetworkDataSource.getAccessToken("clientId", "clientSecret", "incorrectCode") expectThat(result) .isFailure() - .isEqualTo(WebException.BadVerificationCodeException) + .isEqualTo(BadVerificationCodeException) } @Test diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index b09d5c88e..61532c123 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -23,8 +23,10 @@ import com.appunite.loudius.network.model.ReviewState import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.PullRequestsService +import com.appunite.loudius.network.testRequester import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults +import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -39,7 +41,6 @@ import strikt.assertions.isEqualTo import strikt.assertions.isFailure import strikt.assertions.isSuccess import strikt.assertions.single -import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { @@ -47,7 +48,8 @@ class PullRequestsNetworkDataSourceTest { private val mockWebServer: MockWebServer = MockWebServer() private val pullRequestsService = retrofitTestDouble(mockWebServer = mockWebServer).create(PullRequestsService::class.java) - private val pullRequestDataSource = PullRequestsNetworkDataSource(pullRequestsService) + private val pullRequestDataSource = + PullRequestsNetworkDataSource(pullRequestsService, testRequester()) @AfterEach fun tearDown() { diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt index aabdc87ca..e02866795 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/UserDataSourceTest.kt @@ -19,6 +19,7 @@ package com.appunite.loudius.network.datasource import com.appunite.loudius.network.model.User import com.appunite.loudius.network.retrofitTestDouble import com.appunite.loudius.network.services.UserService +import com.appunite.loudius.network.testRequester import com.appunite.loudius.network.utils.WebException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -39,7 +40,7 @@ class UserDataSourceTest { private val mockWebServer: MockWebServer = MockWebServer() private val userService = retrofitTestDouble(mockWebServer = mockWebServer).create(UserService::class.java) - private val userDataSource = UserDataSourceImpl(userService) + private val userDataSource = UserDataSourceImpl(userService, testRequester()) @AfterEach fun tearDown() { From f46444ae7c412a02ada54c15c264a3425053ea06 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Tue, 11 Apr 2023 12:34:36 +0000 Subject: [PATCH 341/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/di/DataSourceModule.kt | 6 +++--- app/src/main/java/com/appunite/loudius/di/NetworkModule.kt | 4 ++-- .../network/datasource/PullRequestsNetworkDataSource.kt | 2 +- .../appunite/loudius/network/datasource/UserDataSource.kt | 2 +- .../java/com/appunite/loudius/network/utils/ApiRequester.kt | 5 ++--- .../loudius/ui/authenticating/AuthenticatingViewModel.kt | 2 +- .../java/com/appunite/loudius/network/NetworkTestDoubles.kt | 4 ++-- .../network/datasource/PullRequestsNetworkDataSourceTest.kt | 2 +- 8 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index 3438b9c9b..fb1103af2 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -46,14 +46,14 @@ object DataSourceModule { @Singleton fun providePullRequestNetworkDataSource( service: PullRequestsService, - apiRequester: ApiRequester + apiRequester: ApiRequester, ): PullRequestDataSource = PullRequestsNetworkDataSource(service, apiRequester) @Provides @Singleton fun provideUserDataSource( userService: UserService, - apiRequester: ApiRequester + apiRequester: ApiRequester, ): UserDataSource = UserDataSourceImpl(userService, apiRequester) @Singleton @@ -65,7 +65,7 @@ object DataSourceModule { @Provides fun provideAuthNetworkDataSource( service: AuthService, - apiRequester: ApiRequester + apiRequester: ApiRequester, ): AuthDataSource = AuthNetworkDataSource(service, apiRequester) @Singleton diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 4ccf4d2fa..c1f412221 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -29,12 +29,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import java.time.LocalDateTime -import javax.inject.Singleton import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt index 118298de8..284b54740 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSource.kt @@ -51,7 +51,7 @@ interface PullRequestDataSource { @Singleton class PullRequestsNetworkDataSource @Inject constructor( private val service: PullRequestsService, - private val apiRequester: ApiRequester + private val apiRequester: ApiRequester, ) : PullRequestDataSource { override suspend fun getPullRequestsForUser(author: String): Result = diff --git a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt index 3eb01ecb0..d02467e56 100644 --- a/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt +++ b/app/src/main/java/com/appunite/loudius/network/datasource/UserDataSource.kt @@ -29,7 +29,7 @@ interface UserDataSource { @Singleton class UserDataSourceImpl @Inject constructor( private val userService: UserService, - private val apiRequester: ApiRequester + private val apiRequester: ApiRequester, ) : UserDataSource { override suspend fun getUser(): Result = apiRequester.safeApiCall { diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt index f389e77c0..a4d0a254b 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt @@ -19,10 +19,10 @@ package com.appunite.loudius.network.utils import com.appunite.loudius.network.model.error.DefaultErrorResponse import com.google.gson.Gson import com.google.gson.JsonParseException -import java.io.IOException -import javax.inject.Inject import org.json.JSONException import retrofit2.HttpException +import java.io.IOException +import javax.inject.Inject class ApiRequester @Inject constructor(private val gson: Gson) { @@ -58,5 +58,4 @@ class ApiRequester @Inject constructor(private val gson: Gson) { override fun invoke(responseCode: Int, responseMessage: String): Exception = WebException.UnknownError(responseCode, responseMessage) } - } diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt index c5b0eb9ed..c3493ee1a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingViewModel.kt @@ -28,8 +28,8 @@ import com.appunite.loudius.common.Screen import com.appunite.loudius.domain.repository.AuthRepository import com.appunite.loudius.network.datasource.BadVerificationCodeException import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class AuthenticatingAction { diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 350e121fb..6ebb3ddf6 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -27,12 +27,12 @@ import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), diff --git a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt index 61532c123..5b395555b 100644 --- a/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/network/datasource/PullRequestsNetworkDataSourceTest.kt @@ -26,7 +26,6 @@ import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.testRequester import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults -import java.time.LocalDateTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.mockwebserver.MockResponse @@ -41,6 +40,7 @@ import strikt.assertions.isEqualTo import strikt.assertions.isFailure import strikt.assertions.isSuccess import strikt.assertions.single +import java.time.LocalDateTime @ExperimentalCoroutinesApi class PullRequestsNetworkDataSourceTest { From bdb81c7659ef565df7080b1565c2836bd746e629 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 13 Apr 2023 10:04:51 +0200 Subject: [PATCH 342/526] Remove redundant provides method for ApiRequester. --- app/src/main/java/com/appunite/loudius/di/NetworkModule.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index c1f412221..c0fca4070 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -19,7 +19,6 @@ package com.appunite.loudius.di import com.appunite.loudius.common.Constants import com.appunite.loudius.network.intercept.AuthFailureInterceptor import com.appunite.loudius.network.intercept.AuthInterceptor -import com.appunite.loudius.network.utils.ApiRequester import com.appunite.loudius.network.utils.AuthFailureHandler import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy @@ -101,7 +100,4 @@ object NetworkModule { GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeDeserializer()) .create() - - @Provides - fun provideApiRequester(gson: Gson): ApiRequester = ApiRequester(gson) } From 3d55923f61794b5fa42ca9f254723af391cf45fb Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 12 Apr 2023 12:14:30 +0200 Subject: [PATCH 343/526] Add github workflow to run UI tests --- .../actions/clean-up-gradle-cache/action.yml | 10 +++++ .../actions/prepare-android-env/action.yml | 22 +++++++++++ .github/tests.yml | 9 +++++ .github/workflows/run-ui-test.yml | 38 +++++++++++++++++++ .github/workflows/run-unit-test.yml | 22 ++--------- 5 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 .github/actions/clean-up-gradle-cache/action.yml create mode 100644 .github/actions/prepare-android-env/action.yml create mode 100644 .github/tests.yml create mode 100644 .github/workflows/run-ui-test.yml diff --git a/.github/actions/clean-up-gradle-cache/action.yml b/.github/actions/clean-up-gradle-cache/action.yml new file mode 100644 index 000000000..f807ddfd6 --- /dev/null +++ b/.github/actions/clean-up-gradle-cache/action.yml @@ -0,0 +1,10 @@ +name: "Clean up gradle cache workflow" +description: "Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. Restoring these files from a GitHub Actions cache might cause problems for future builds." +runs: + using: "composite" + steps: + - name: Clean-up Gradle cache + shell: bash + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.github/actions/prepare-android-env/action.yml b/.github/actions/prepare-android-env/action.yml new file mode 100644 index 000000000..07db52bed --- /dev/null +++ b/.github/actions/prepare-android-env/action.yml @@ -0,0 +1,22 @@ +name: "Prepare Environment to build Android app" +description: "Cancels previous runs, set ups jdk, validates gradle wrapper and uses gradle cache" +runs: + using: "composite" + steps: + - name: Cancel previous runs for the same branch + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + java-version: "11" + distribution: "adopt" + cache: gradle + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Gradle cache + uses: gradle/gradle-build-action@v2 diff --git a/.github/tests.yml b/.github/tests.yml new file mode 100644 index 000000000..34c9b6fdb --- /dev/null +++ b/.github/tests.yml @@ -0,0 +1,9 @@ +android-pixel-2: + type: instrumentation + app: app/build/outputs/apk/debug/app-debug.apk + test: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk + device: + - model: Pixel2 + version: 30 + locale: 'en' + orientation: portrait diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml new file mode 100644 index 000000000..42217d678 --- /dev/null +++ b/.github/workflows/run-ui-test.yml @@ -0,0 +1,38 @@ +name: Android Release + +on: + pull_request: + push: + branches: + - "develop" + - "main" + +permissions: read-all + +jobs: + apk: + name: Run UI tests on Firebase Test Lab + runs-on: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Prepare Android Environment + uses: ./.github/actions/prepare-android-env + + - name: Assemble app debug APK + run: ./gradlew assembleDebug --stacktrace + + - name: Assemble Android Instrumentation Tests + run: ./gradlew assembleDebugAndroidTest + + - name: Run tests on Firebase Test Lab + uses: asadmansr/Firebase-Test-Lab-Action@v1.0 + with: + arg-spec: '.github/tests.yml:android-pixel-2' + env: + SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + + - name: Clean-up Gradle cache + uses: ./.github/actions/clean-up-gradle-cache diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 7e4285dbe..ba46d4210 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -14,23 +14,11 @@ jobs: runs-on: ubuntu-20.04 steps: - - name: Cancel previous runs for the same branch - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - name: Checkout uses: actions/checkout@v3 - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: "11" - distribution: "adopt" - cache: gradle - - - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Prepare Android Environment + uses: ./.github/actions/prepare-android-env - name: Gradle cache uses: gradle/gradle-build-action@v2 @@ -42,8 +30,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Clean-up Gradle cache - # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. - # Restoring these files from a GitHub Actions cache might cause problems for future builds. - run: | - rm -f ~/.gradle/caches/modules-2/modules-2.lock - rm -f ~/.gradle/caches/modules-2/gc.properties + uses: ./.github/actions/clean-up-gradle-cache From 08932913ede82e1e1b1899586638d05e9ee1f662 Mon Sep 17 00:00:00 2001 From: kezc Date: Fri, 14 Apr 2023 09:00:29 +0000 Subject: [PATCH 344/526] [MegaLinter] Apply linters fixes --- .github/tests.yml | 2 +- .github/workflows/run-ui-test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/tests.yml b/.github/tests.yml index 34c9b6fdb..7c166fa19 100644 --- a/.github/tests.yml +++ b/.github/tests.yml @@ -5,5 +5,5 @@ android-pixel-2: device: - model: Pixel2 version: 30 - locale: 'en' + locale: "en" orientation: portrait diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index 42217d678..be9129d8a 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -30,7 +30,7 @@ jobs: - name: Run tests on Firebase Test Lab uses: asadmansr/Firebase-Test-Lab-Action@v1.0 with: - arg-spec: '.github/tests.yml:android-pixel-2' + arg-spec: ".github/tests.yml:android-pixel-2" env: SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} From 3a2838efdcb6145bdf7f0c66eb165082e33049dc Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 17 Apr 2023 09:11:34 +0200 Subject: [PATCH 345/526] Repair package for a CoroutinesHelpers.kt. --- .../ui/pullrequests/PullRequestsViewModelTest.kt | 2 +- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 10 +++++----- .../com/appunite/loudius/util/CoroutinesHelpers.kt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index de6a4dbd3..5eeefa948 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -22,7 +22,7 @@ import com.appunite.loudius.fakes.FakePullRequestRepository import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.Defaults import com.appunite.loudius.util.MainDispatcherExtension -import com.appunite.loudius.utils.neverCompletingSuspension +import com.appunite.loudius.util.neverCompletingSuspension import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.spyk diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 3084c6d01..1b797b4b4 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -21,7 +21,7 @@ import com.appunite.loudius.fakes.FakePullRequestRepository import com.appunite.loudius.network.model.RequestedReviewersResponse import com.appunite.loudius.network.utils.WebException import com.appunite.loudius.util.MainDispatcherExtension -import com.appunite.loudius.utils.neverCompletingSuspension +import com.appunite.loudius.util.neverCompletingSuspension import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.every @@ -29,6 +29,10 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach @@ -46,10 +50,6 @@ import strikt.assertions.isEqualTo import strikt.assertions.isFalse import strikt.assertions.isNull import strikt.assertions.isTrue -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) diff --git a/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt b/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt index a8e7ff817..c2c36dc2c 100644 --- a/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt +++ b/app/src/test/java/com/appunite/loudius/util/CoroutinesHelpers.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.utils +package com.appunite.loudius.util import kotlinx.coroutines.suspendCancellableCoroutine From 44ac32b0b3531e3b4486b453ad4394d56c0fd925 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 17 Apr 2023 09:16:36 +0200 Subject: [PATCH 346/526] Remove interface AuthFailureHandler - which had only one implementation. --- .../com/appunite/loudius/di/DataSourceModule.kt | 6 ------ .../loudius/network/utils/AuthFailureHandler.kt | 15 +++++---------- .../loudius/network/NetworkTestDoubles.kt | 7 +++---- .../com/appunite/loudius/ui/MainViewModelTest.kt | 3 +-- 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt index fb1103af2..b3d662f30 100644 --- a/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DataSourceModule.kt @@ -29,8 +29,6 @@ import com.appunite.loudius.network.services.AuthService import com.appunite.loudius.network.services.PullRequestsService import com.appunite.loudius.network.services.UserService import com.appunite.loudius.network.utils.ApiRequester -import com.appunite.loudius.network.utils.AuthFailureHandler -import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -67,8 +65,4 @@ object DataSourceModule { service: AuthService, apiRequester: ApiRequester, ): AuthDataSource = AuthNetworkDataSource(service, apiRequester) - - @Singleton - @Provides - fun provideAuthManager(): AuthFailureHandler = AuthFailureHandlerImpl() } diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt index f7011e844..9d4591029 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -16,23 +16,18 @@ package com.appunite.loudius.network.utils +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import javax.inject.Singleton - -interface AuthFailureHandler { - suspend fun emitAuthFailure() - - val authFailureFlow: SharedFlow -} @Singleton -class AuthFailureHandlerImpl : AuthFailureHandler { +class AuthFailureHandler @Inject constructor() { private val _authFailureFlow = MutableSharedFlow() - override val authFailureFlow: SharedFlow = _authFailureFlow + val authFailureFlow: SharedFlow = _authFailureFlow - override suspend fun emitAuthFailure() { + suspend fun emitAuthFailure() { _authFailureFlow.emit(Unit) } } diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 6ebb3ddf6..f72a85bc6 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -22,21 +22,20 @@ import com.appunite.loudius.network.intercept.AuthFailureInterceptor import com.appunite.loudius.network.intercept.AuthInterceptor import com.appunite.loudius.network.utils.ApiRequester import com.appunite.loudius.network.utils.AuthFailureHandler -import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), - authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl(), + authFailureHandler: AuthFailureHandler = AuthFailureHandler(), ) = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) diff --git a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt index 0c99fdc0a..cc9576f04 100644 --- a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt @@ -17,7 +17,6 @@ package com.appunite.loudius.ui import com.appunite.loudius.network.utils.AuthFailureHandler -import com.appunite.loudius.network.utils.AuthFailureHandlerImpl import com.appunite.loudius.util.MainDispatcherExtension import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -30,7 +29,7 @@ import strikt.assertions.isNull @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) class MainViewModelTest { - private val authFailureHandler: AuthFailureHandler = AuthFailureHandlerImpl() + private val authFailureHandler: AuthFailureHandler = AuthFailureHandler() private lateinit var viewModel: MainViewModel @Test From 86faab4a7d2c7b80043b16bbfeabaea4f7b5d3ac Mon Sep 17 00:00:00 2001 From: Wojtek Date: Mon, 17 Apr 2023 10:31:18 +0200 Subject: [PATCH 347/526] Remove Clean up gradle cache ation --- .github/actions/clean-up-gradle-cache/action.yml | 10 ---------- .github/workflows/run-ui-test.yml | 3 --- .github/workflows/run-unit-test.yml | 3 --- 3 files changed, 16 deletions(-) delete mode 100644 .github/actions/clean-up-gradle-cache/action.yml diff --git a/.github/actions/clean-up-gradle-cache/action.yml b/.github/actions/clean-up-gradle-cache/action.yml deleted file mode 100644 index f807ddfd6..000000000 --- a/.github/actions/clean-up-gradle-cache/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Clean up gradle cache workflow" -description: "Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. Restoring these files from a GitHub Actions cache might cause problems for future builds." -runs: - using: "composite" - steps: - - name: Clean-up Gradle cache - shell: bash - run: | - rm -f ~/.gradle/caches/modules-2/modules-2.lock - rm -f ~/.gradle/caches/modules-2/gc.properties diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index be9129d8a..3d5c6aae0 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -33,6 +33,3 @@ jobs: arg-spec: ".github/tests.yml:android-pixel-2" env: SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} - - - name: Clean-up Gradle cache - uses: ./.github/actions/clean-up-gradle-cache diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index ba46d4210..58b26faa9 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -28,6 +28,3 @@ jobs: env: GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Clean-up Gradle cache - uses: ./.github/actions/clean-up-gradle-cache From d8477d68e3ec56dd518337234fad9e2649670649 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Mon, 17 Apr 2023 10:32:55 +0200 Subject: [PATCH 348/526] Remove redundant Gradle cache action from run-unit-test --- .github/workflows/run-unit-test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 58b26faa9..35ec114c5 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -20,9 +20,6 @@ jobs: - name: Prepare Android Environment uses: ./.github/actions/prepare-android-env - - name: Gradle cache - uses: gradle/gradle-build-action@v2 - - name: Run test run: ./gradlew testDebugUnitTest env: From ff12bb45d9c281c22556ecd1ab294871123881a0 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Mon, 17 Apr 2023 11:10:43 +0200 Subject: [PATCH 349/526] Combine gradle commands --- .github/workflows/run-ui-test.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index 3d5c6aae0..1043cad24 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -21,11 +21,8 @@ jobs: - name: Prepare Android Environment uses: ./.github/actions/prepare-android-env - - name: Assemble app debug APK - run: ./gradlew assembleDebug --stacktrace - - - name: Assemble Android Instrumentation Tests - run: ./gradlew assembleDebugAndroidTest + - name: Assemble App Debug APK and Android Instrumentation Tests + run: ./gradlew assembleDebug assembleDebugAndroidTest - name: Run tests on Firebase Test Lab uses: asadmansr/Firebase-Test-Lab-Action@v1.0 From 24fe42f0ba1d24bbeb627f89e4d10246fda45502 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 17 Apr 2023 12:40:17 +0200 Subject: [PATCH 350/526] Make AuthFailureInterceptor coroutine less. --- .../com/appunite/loudius/di/GeneralModule.kt | 32 +++++++++++++++++++ .../com/appunite/loudius/di/NetworkModule.kt | 4 +-- .../intercept/AuthFailureInterceptor.kt | 11 ++----- .../network/utils/AuthFailureHandler.kt | 14 +++++--- .../loudius/network/NetworkTestDoubles.kt | 3 +- .../appunite/loudius/ui/MainViewModelTest.kt | 3 +- 6 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/di/GeneralModule.kt diff --git a/app/src/main/java/com/appunite/loudius/di/GeneralModule.kt b/app/src/main/java/com/appunite/loudius/di/GeneralModule.kt new file mode 100644 index 000000000..80ee2d38e --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/GeneralModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +@InstallIn(SingletonComponent::class) +@Module +object GeneralModule { + + @Provides + fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default +} diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index c0fca4070..809f99bdf 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -28,12 +28,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import java.time.LocalDateTime +import javax.inject.Singleton import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory -import java.time.LocalDateTime -import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt index ca2645932..34450ff69 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt @@ -17,26 +17,19 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.network.utils.AuthFailureHandler -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Response -import javax.inject.Inject class AuthFailureInterceptor @Inject constructor( private val authFailureHandler: AuthFailureHandler, - private val dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val response = chain.proceed(request) if (response.code == 401) { - CoroutineScope(dispatcher).launch { - authFailureHandler.emitAuthFailure() - } + authFailureHandler.emitAuthFailure() } return response diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt index 9d4591029..3b95f9c99 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -18,16 +18,22 @@ package com.appunite.loudius.network.utils import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch @Singleton -class AuthFailureHandler @Inject constructor() { - +class AuthFailureHandler @Inject constructor( + private val dispatcher: CoroutineDispatcher, +) { private val _authFailureFlow = MutableSharedFlow() val authFailureFlow: SharedFlow = _authFailureFlow - suspend fun emitAuthFailure() { - _authFailureFlow.emit(Unit) + fun emitAuthFailure() { + CoroutineScope(dispatcher).launch { + _authFailureFlow.emit(Unit) + } } } diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index f72a85bc6..7f21f1782 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -28,6 +28,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import java.time.LocalDateTime import java.util.concurrent.TimeUnit +import kotlinx.coroutines.Dispatchers import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit @@ -35,7 +36,7 @@ import retrofit2.converter.gson.GsonConverterFactory fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), - authFailureHandler: AuthFailureHandler = AuthFailureHandler(), + authFailureHandler: AuthFailureHandler = AuthFailureHandler(Dispatchers.Unconfined) ) = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) diff --git a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt index cc9576f04..00a09e332 100644 --- a/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/MainViewModelTest.kt @@ -18,6 +18,7 @@ package com.appunite.loudius.ui import com.appunite.loudius.network.utils.AuthFailureHandler import com.appunite.loudius.util.MainDispatcherExtension +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test @@ -29,7 +30,7 @@ import strikt.assertions.isNull @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) class MainViewModelTest { - private val authFailureHandler: AuthFailureHandler = AuthFailureHandler() + private val authFailureHandler: AuthFailureHandler = AuthFailureHandler(Dispatchers.Unconfined) private lateinit var viewModel: MainViewModel @Test From 1a2c409d08251acb8897702b58c19752350b56a1 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 17 Apr 2023 10:54:11 +0000 Subject: [PATCH 351/526] [MegaLinter] Apply linters fixes --- .../main/java/com/appunite/loudius/di/NetworkModule.kt | 4 ++-- .../loudius/network/intercept/AuthFailureInterceptor.kt | 2 +- .../appunite/loudius/network/utils/AuthFailureHandler.kt | 4 ++-- .../com/appunite/loudius/network/NetworkTestDoubles.kt | 6 +++--- .../loudius/ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index 809f99bdf..c0fca4070 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -28,12 +28,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import java.time.LocalDateTime -import javax.inject.Singleton import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module diff --git a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt index 34450ff69..4269a6b28 100644 --- a/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/network/intercept/AuthFailureInterceptor.kt @@ -17,9 +17,9 @@ package com.appunite.loudius.network.intercept import com.appunite.loudius.network.utils.AuthFailureHandler -import javax.inject.Inject import okhttp3.Interceptor import okhttp3.Response +import javax.inject.Inject class AuthFailureInterceptor @Inject constructor( private val authFailureHandler: AuthFailureHandler, diff --git a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt index 3b95f9c99..dc0be7fc7 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/AuthFailureHandler.kt @@ -16,13 +16,13 @@ package com.appunite.loudius.network.utils -import javax.inject.Inject -import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton @Singleton class AuthFailureHandler @Inject constructor( diff --git a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt index 7f21f1782..259812669 100644 --- a/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt +++ b/app/src/test/java/com/appunite/loudius/network/NetworkTestDoubles.kt @@ -26,17 +26,17 @@ import com.appunite.loudius.network.utils.LocalDateTimeDeserializer import com.google.gson.FieldNamingPolicy import com.google.gson.Gson import com.google.gson.GsonBuilder -import java.time.LocalDateTime -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockWebServer import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit fun testOkHttpClient( authRepository: AuthRepository = FakeAuthRepository(), - authFailureHandler: AuthFailureHandler = AuthFailureHandler(Dispatchers.Unconfined) + authFailureHandler: AuthFailureHandler = AuthFailureHandler(Dispatchers.Unconfined), ) = OkHttpClient.Builder() .connectTimeout(1, TimeUnit.SECONDS) .readTimeout(1, TimeUnit.SECONDS) diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 1b797b4b4..0ee366949 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -29,10 +29,6 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach @@ -50,6 +46,10 @@ import strikt.assertions.isEqualTo import strikt.assertions.isFalse import strikt.assertions.isNull import strikt.assertions.isTrue +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From 95983316c143bd7f64c4ab61d16f8709c14047fe Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 17 Apr 2023 13:32:43 +0200 Subject: [PATCH 352/526] Use fake shared preferences in the UserLocalDataSourceTest instead of mock. --- .../loudius/domain/UserLocalDataSourceTest.kt | 27 ++--- .../loudius/fakes/FakeSharedPreferences.kt | 105 ++++++++++++++++++ 2 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt diff --git a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt index 6770beec7..bbcf303eb 100644 --- a/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt +++ b/app/src/test/java/com/appunite/loudius/domain/UserLocalDataSourceTest.kt @@ -17,46 +17,43 @@ package com.appunite.loudius.domain import android.content.Context -import android.content.SharedPreferences import com.appunite.loudius.domain.store.UserLocalDataSourceImpl +import com.appunite.loudius.fakes.FakeSharedPreferences import io.mockk.every import io.mockk.mockk -import io.mockk.verify import org.junit.jupiter.api.Test import strikt.api.expectThat import strikt.assertions.isEmpty import strikt.assertions.isEqualTo class UserLocalDataSourceTest { - private val sharedPreferences = mockk(relaxed = true) { - every { getString("access_token", null) } returns "exampleAccessToken" - } + private val sharedPreferences = FakeSharedPreferences() private val context = mockk { every { getSharedPreferences(any(), any()) } returns sharedPreferences } private val userLocalDataSource = UserLocalDataSourceImpl(context) @Test - fun `GIVEN filled data source WHEN getting access token THEN return access token`() { + fun `GIVEN the app is started first time WHEN getting access token THEN token is empty`() { val result = userLocalDataSource.getAccessToken() - expectThat(result).isEqualTo("exampleAccessToken") + expectThat(result).isEmpty() } @Test - fun `GIVEN not filled data source WHEN getting access token THEN return empty string`() { - every { sharedPreferences.getString("access_token", null) } returns null + fun `WHEN token is set THEN token can be retrieved`() { + userLocalDataSource.saveAccessToken("someToken") val result = userLocalDataSource.getAccessToken() - expectThat(result).isEmpty() + expectThat(result).isEqualTo("someToken") } @Test - fun `GIVEN access token WHEN saving access token THEN shared preferences are edited`() { - userLocalDataSource.saveAccessToken("exampleAccessToken") + fun `GIVEN token is stored WHEN token is cleared THEN no token to retrieve`() { + userLocalDataSource.saveAccessToken("someToken") + userLocalDataSource.saveAccessToken("") - verify(exactly = 1) { - sharedPreferences.edit().putString("access_token", "exampleAccessToken") - } + val result = userLocalDataSource.getAccessToken() + expectThat(result).isEmpty() } } diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt new file mode 100644 index 000000000..efdebfd69 --- /dev/null +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.fakes + +import android.content.SharedPreferences + +class FakeSharedPreferences : SharedPreferences { + + private val map = mutableMapOf() + + private inner class Editor : SharedPreferences.Editor { + private val updates = mutableMapOf() + + override fun putString(key: String, value: String?): SharedPreferences.Editor { + updates[key] = value + return this + } + + override fun putStringSet( + key: String, + value: MutableSet? + ): SharedPreferences.Editor = + TODO("Not yet implemented") + + override fun putInt(key: String, value: Int): SharedPreferences.Editor = + TODO("Not yet implemented") + + override fun putLong(key: String, value: Long): SharedPreferences.Editor = + TODO("Not yet implemented") + + override fun putFloat(key: String, value: Float): SharedPreferences.Editor = + TODO("Not yet implemented") + + override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = + TODO("Not yet implemented") + + override fun remove(key: String?): SharedPreferences.Editor = TODO("Not yet implemented") + + override fun clear(): SharedPreferences.Editor = TODO("Not yet implemented") + + override fun commit(): Boolean { + apply() + return true + } + + override fun apply() { + map.putAll(updates) + } + } + + override fun getAll(): MutableMap { + TODO("Not yet implemented") + } + + override fun getString(key: String?, defValue: String?): String? = + map[key] as String? ?: defValue + + override fun getStringSet(key: String?, defValues: MutableSet?): MutableSet? { + TODO("Not yet implemented") + } + + override fun getInt(key: String?, defValue: Int): Int { + TODO("Not yet implemented") + } + + override fun getLong(key: String?, defValue: Long): Long { + TODO("Not yet implemented") + } + + override fun getFloat(key: String?, defValue: Float): Float { + TODO("Not yet implemented") + } + + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + TODO("Not yet implemented") + } + + override fun contains(key: String?): Boolean { + TODO("Not yet implemented") + } + + override fun edit(): SharedPreferences.Editor = Editor() + + override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) { + TODO("Not yet implemented") + } + + override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) { + TODO("Not yet implemented") + } +} From 82b04073a97bfbc5ed0c0a183e90d7569244bfc0 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 17 Apr 2023 12:04:24 +0000 Subject: [PATCH 353/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/fakes/FakeSharedPreferences.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt b/app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt index efdebfd69..08d62e574 100644 --- a/app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt +++ b/app/src/test/java/com/appunite/loudius/fakes/FakeSharedPreferences.kt @@ -32,7 +32,7 @@ class FakeSharedPreferences : SharedPreferences { override fun putStringSet( key: String, - value: MutableSet? + value: MutableSet?, ): SharedPreferences.Editor = TODO("Not yet implemented") From 0ef153f986c2bd3846c8fc40fa31ebbcdffd8319 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 18 Apr 2023 10:54:09 +0200 Subject: [PATCH 354/526] Rename GeneralModule.kt into DispatchersModule. --- .../loudius/di/{GeneralModule.kt => DispatchersModule.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/main/java/com/appunite/loudius/di/{GeneralModule.kt => DispatchersModule.kt} (97%) diff --git a/app/src/main/java/com/appunite/loudius/di/GeneralModule.kt b/app/src/main/java/com/appunite/loudius/di/DispatchersModule.kt similarity index 97% rename from app/src/main/java/com/appunite/loudius/di/GeneralModule.kt rename to app/src/main/java/com/appunite/loudius/di/DispatchersModule.kt index 80ee2d38e..36afd4649 100644 --- a/app/src/main/java/com/appunite/loudius/di/GeneralModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/DispatchersModule.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.Dispatchers @InstallIn(SingletonComponent::class) @Module -object GeneralModule { +object DispatchersModule { @Provides fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default From ba3189096470197269de91b7714c9329fb195620 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 18 Apr 2023 21:11:59 +0200 Subject: [PATCH 355/526] add variable kinds of error screen --- .../ui/components/LoudiusFullScreenError.kt | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index ea28b1da1..d10f940b6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -19,11 +19,15 @@ package com.appunite.loudius.ui.components import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -31,19 +35,76 @@ import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme +private const val minHeightToFitSpacer = 1856 + @Composable fun LoudiusFullScreenError( modifier: Modifier = Modifier, errorText: String = stringResource(id = R.string.error_dialog_text), buttonText: String = stringResource(id = R.string.try_again), onButtonClick: () -> Unit, +) { + val density = LocalDensity.current + val configuration = LocalConfiguration.current + val screenHeight = with(density) { configuration.screenHeightDp.dp.roundToPx() } + + if (screenHeight >= minHeightToFitSpacer) { + ScreenErrorWithSpacers( + modifier = modifier, + errorText = errorText, + buttonText = buttonText, + onButtonClick = onButtonClick + ) + } else { + ScreenErrorWithoutSpacers( + modifier = modifier, + errorText = errorText, + buttonText = buttonText, + onButtonClick = onButtonClick + ) + } +} + +@Composable +fun ScreenErrorWithSpacers( + modifier: Modifier, + errorText: String, + buttonText: String, + onButtonClick: () -> Unit, +) { + Column( + modifier = modifier.fillMaxSize() + ) { + Spacer(modifier = Modifier.weight(weight = 0.170f)) + Column( + modifier = modifier + .weight(weight = 0.550f) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + ErrorImage() + ErrorText(text = errorText) + LoudiusOutlinedButton( + onClick = onButtonClick, + text = buttonText, + ) + } + Spacer(modifier = Modifier.weight(weight = 0.280f)) + } +} + +@Composable +fun ScreenErrorWithoutSpacers( + modifier: Modifier, + errorText: String, + buttonText: String, + onButtonClick: () -> Unit, ) { Column( - modifier = modifier - .padding(top = 142.dp) - .fillMaxSize(), + modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(56.dp), + verticalArrangement = Arrangement.SpaceEvenly, ) { ErrorImage() ErrorText(text = errorText) From 528dee590832200c28d40175c5f83118538bc724 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 18 Apr 2023 19:16:34 +0000 Subject: [PATCH 356/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/components/LoudiusFullScreenError.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index d10f940b6..ed3fcbf7c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -53,14 +53,14 @@ fun LoudiusFullScreenError( modifier = modifier, errorText = errorText, buttonText = buttonText, - onButtonClick = onButtonClick + onButtonClick = onButtonClick, ) } else { ScreenErrorWithoutSpacers( modifier = modifier, errorText = errorText, buttonText = buttonText, - onButtonClick = onButtonClick + onButtonClick = onButtonClick, ) } } @@ -73,7 +73,7 @@ fun ScreenErrorWithSpacers( onButtonClick: () -> Unit, ) { Column( - modifier = modifier.fillMaxSize() + modifier = modifier.fillMaxSize(), ) { Spacer(modifier = Modifier.weight(weight = 0.170f)) Column( From 8f182e681b784b6b0448401cf09291ea4407ed7f Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 19 Apr 2023 16:00:11 +0200 Subject: [PATCH 357/526] separate content of error screen --- .../ui/components/LoudiusFullScreenError.kt | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index ed3fcbf7c..c512878e9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -73,13 +72,13 @@ fun ScreenErrorWithSpacers( onButtonClick: () -> Unit, ) { Column( - modifier = modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { - Spacer(modifier = Modifier.weight(weight = 0.170f)) + Spacer(modifier = Modifier.weight(weight = 0.15f)) Column( modifier = modifier - .weight(weight = 0.550f) - .fillMaxWidth(), + .weight(weight = 0.6f) + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { @@ -90,7 +89,7 @@ fun ScreenErrorWithSpacers( text = buttonText, ) } - Spacer(modifier = Modifier.weight(weight = 0.280f)) + Spacer(modifier = Modifier.weight(weight = 0.25f)) } } @@ -106,15 +105,28 @@ fun ScreenErrorWithoutSpacers( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly, ) { - ErrorImage() - ErrorText(text = errorText) - LoudiusOutlinedButton( - onClick = onButtonClick, - text = buttonText, + ScreenErrorContent( + errorText = errorText, + buttonText = buttonText, + onButtonClick = onButtonClick ) } } +@Composable +fun ScreenErrorContent( + errorText: String, + buttonText: String, + onButtonClick: () -> Unit, +) { + ErrorImage() + ErrorText(text = errorText) + LoudiusOutlinedButton( + onClick = onButtonClick, + text = buttonText, + ) +} + @Composable private fun ErrorImage() { Image( From 981e74eea60abf47c671485f68a0bda3f37a5d54 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 19 Apr 2023 14:04:07 +0000 Subject: [PATCH 358/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/ui/components/LoudiusFullScreenError.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index c512878e9..1eee11aad 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -108,7 +108,7 @@ fun ScreenErrorWithoutSpacers( ScreenErrorContent( errorText = errorText, buttonText = buttonText, - onButtonClick = onButtonClick + onButtonClick = onButtonClick, ) } } From a8d835893fc9172212674831805099c7256a0148 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 17 Apr 2023 15:14:46 +0200 Subject: [PATCH 359/526] Use view state as sealed class Loading/Error/Loaded in the PullRequestScreen. --- .../ui/pullrequests/PullRequestsScreen.kt | 143 ++++++++++-------- .../ui/pullrequests/PullRequestsViewModel.kt | 34 +++-- 2 files changed, 98 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index a6086b5b3..64c8f0cf2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -57,50 +57,67 @@ fun PullRequestsScreen( navigateToReviewers: NavigateToReviewers, ) { val state = viewModel.state - LaunchedEffect(state.navigateToReviewers) { - state.navigateToReviewers?.let { - navigateToReviewers(it.owner, it.repo, it.pullRequestNumber, it.submissionTime) - viewModel.onAction(PulLRequestsAction.OnNavigateToReviewers) - } - } PullRequestsScreenStateless( - pullRequests = state.pullRequests, + state = state, onAction = viewModel::onAction, - isLoading = state.isLoading, - isError = state.isError, + navigateToReviewers = navigateToReviewers, ) } @Composable private fun PullRequestsScreenStateless( - pullRequests: List, + state: PullRequestState, onAction: (PulLRequestsAction) -> Unit, - isLoading: Boolean, - isError: Boolean, + navigateToReviewers: NavigateToReviewers, ) { Scaffold( topBar = { LoudiusTopAppBar(title = stringResource(R.string.app_name)) }, content = { padding -> - when { - isError -> LoudiusFullScreenError( + when (state) { + is PullRequestState.Error -> LoudiusFullScreenError( modifier = Modifier.padding(padding), onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) - isLoading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - pullRequests.isEmpty() -> EmptyListPlaceholder(padding) - else -> PullRequestsList( - pullRequests = pullRequests, - modifier = Modifier.padding(padding), - onItemClick = onAction, - ) + is PullRequestState.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) + is PullRequestState.Loaded -> { + PullRequestContent(state, padding, onAction) + LaunchedEffect(state.navigateToReviewers) { + state.navigateToReviewers?.let { + navigateToReviewers( + it.owner, + it.repo, + it.pullRequestNumber, + it.submissionTime + ) + onAction(PulLRequestsAction.OnNavigateToReviewers) + } + } + } } }, ) } +@Composable +private fun PullRequestContent( + state: PullRequestState.Loaded, + padding: PaddingValues, + onAction: (PulLRequestsAction) -> Unit +) { + if (state.pullRequests.isEmpty()) { + EmptyListPlaceholder(padding) + } else { + PullRequestsList( + pullRequests = state.pullRequests, + modifier = Modifier.padding(padding), + onItemClick = onAction, + ) + } +} + @Composable private fun PullRequestsList( pullRequests: List, @@ -177,43 +194,44 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { fun PullRequestsScreenPreview() { LoudiusTheme { PullRequestsScreenStateless( - isLoading = false, - isError = false, - pullRequests = listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), - ), - PullRequest( - id = 2, - draft = false, - number = 2, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", - title = "[SIL-73] Storing access token", - createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), - ), - PullRequest( - id = 3, - draft = false, - number = 3, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", - title = "[SIL-62/SIL-75] Provide new annotation for API instances", - createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), + state = PullRequestState.Loaded( + listOf( + PullRequest( + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", + title = "[SIL-67] Details screen - network layer", + createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), + ), + PullRequest( + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", + title = "[SIL-66] Add client secret to build config", + createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + ), + PullRequest( + id = 2, + draft = false, + number = 2, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", + title = "[SIL-73] Storing access token", + createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), + ), + PullRequest( + id = 3, + draft = false, + number = 3, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", + title = "[SIL-62/SIL-75] Provide new annotation for API instances", + createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), + ), ), ), onAction = {}, + navigateToReviewers = { _, _, _, _ -> }, ) } } @@ -223,10 +241,9 @@ fun PullRequestsScreenPreview() { fun PullRequestsScreenEmptyListPreview() { LoudiusTheme { PullRequestsScreenStateless( - emptyList(), - isLoading = false, - isError = false, + PullRequestState.Loaded(emptyList()), onAction = {}, + navigateToReviewers = { _, _, _, _ -> }, ) } } @@ -236,10 +253,9 @@ fun PullRequestsScreenEmptyListPreview() { fun PullRequestsScreenLoadingPreview() { LoudiusTheme { PullRequestsScreenStateless( - emptyList(), - isLoading = true, - isError = false, + PullRequestState.Loading, onAction = {}, + navigateToReviewers = { _, _, _, _ -> }, ) } } @@ -249,10 +265,9 @@ fun PullRequestsScreenLoadingPreview() { fun PullRequestsScreenErrorPreview() { LoudiusTheme { PullRequestsScreenStateless( - emptyList(), - isLoading = false, - isError = true, + PullRequestState.Error, onAction = {}, + navigateToReviewers = { _, _, _, _ -> }, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 7da619eb6..a3fd9fc3d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() @@ -33,12 +33,14 @@ sealed class PulLRequestsAction { object RetryClick : PulLRequestsAction() } -data class PullRequestState( - val pullRequests: List = emptyList(), - val navigateToReviewers: NavigationPayload? = null, - val isLoading: Boolean = false, - val isError: Boolean = false, -) +sealed class PullRequestState { + object Loading : PullRequestState() + object Error : PullRequestState() + data class Loaded( + val pullRequests: List = emptyList(), + val navigateToReviewers: NavigationPayload? = null, + ) : PullRequestState() +} data class NavigationPayload( val owner: String, @@ -51,7 +53,7 @@ data class NavigationPayload( class PullRequestsViewModel @Inject constructor( private val pullRequestsRepository: PullRequestRepository, ) : ViewModel() { - var state by mutableStateOf(PullRequestState()) + var state: PullRequestState by mutableStateOf(PullRequestState.Loading) private set init { @@ -60,12 +62,12 @@ class PullRequestsViewModel @Inject constructor( private fun fetchData() { viewModelScope.launch { - state = state.copy(isLoading = true, isError = false) + state = PullRequestState.Loading pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { - state = state.copy(pullRequests = it.items, isLoading = false) + state = PullRequestState.Loaded(it.items) }.onFailure { - state = state.copy(isLoading = false, isError = true) + state = PullRequestState.Error } } } @@ -77,9 +79,10 @@ class PullRequestsViewModel @Inject constructor( } private fun navigateToReviewers(itemClickedId: Int) { - val index = state.pullRequests.indexOfFirst { it.id == itemClickedId } - val itemClickedData = state.pullRequests[index] - state = state.copy( + val loadedState = state as? PullRequestState.Loaded ?: return + val index = loadedState.pullRequests.indexOfFirst { it.id == itemClickedId } + val itemClickedData = loadedState.pullRequests[index] + state = loadedState.copy( navigateToReviewers = NavigationPayload( itemClickedData.owner, itemClickedData.shortRepositoryName, @@ -90,6 +93,7 @@ class PullRequestsViewModel @Inject constructor( } private fun resetNavigationState() { - state = state.copy(navigateToReviewers = null) + val loadedState = state as? PullRequestState.Loaded ?: return + state = loadedState.copy(navigateToReviewers = null) } } From 8d107f5b1820ee16fc3e3d50050b7d9a3f263172 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 17 Apr 2023 13:34:28 +0000 Subject: [PATCH 360/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 4 ++-- .../appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 64c8f0cf2..e7a0b25f8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -90,7 +90,7 @@ private fun PullRequestsScreenStateless( it.owner, it.repo, it.pullRequestNumber, - it.submissionTime + it.submissionTime, ) onAction(PulLRequestsAction.OnNavigateToReviewers) } @@ -105,7 +105,7 @@ private fun PullRequestsScreenStateless( private fun PullRequestContent( state: PullRequestState.Loaded, padding: PaddingValues, - onAction: (PulLRequestsAction) -> Unit + onAction: (PulLRequestsAction) -> Unit, ) { if (state.pullRequests.isEmpty()) { EmptyListPlaceholder(padding) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index a3fd9fc3d..2b924c40f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() From 0aa7f05f8901221cdc1084ab567486ab6e7c612d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 19 Apr 2023 13:55:00 +0200 Subject: [PATCH 361/526] Use conception with view state wrapped in Data class. --- .../ui/pullrequests/PullRequestsScreen.kt | 124 +++++++++--------- .../ui/pullrequests/PullRequestsViewModel.kt | 37 +++--- 2 files changed, 82 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index e7a0b25f8..ac404fb3f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -60,42 +60,46 @@ fun PullRequestsScreen( PullRequestsScreenStateless( state = state, onAction = viewModel::onAction, - navigateToReviewers = navigateToReviewers, ) + LaunchedEffect(state.navigateToReviewers) { + navigateToReviewers(state, navigateToReviewers, viewModel) + } +} + +private fun navigateToReviewers( + state: PullRequestState, + navigateToReviewers: NavigateToReviewers, + viewModel: PullRequestsViewModel +) { + state.navigateToReviewers?.let { + navigateToReviewers( + it.owner, + it.repo, + it.pullRequestNumber, + it.submissionTime, + ) + viewModel.onAction(PulLRequestsAction.OnNavigateToReviewers) + } } @Composable private fun PullRequestsScreenStateless( state: PullRequestState, onAction: (PulLRequestsAction) -> Unit, - navigateToReviewers: NavigateToReviewers, ) { Scaffold( topBar = { LoudiusTopAppBar(title = stringResource(R.string.app_name)) }, content = { padding -> - when (state) { - is PullRequestState.Error -> LoudiusFullScreenError( + when (state.data) { + is Data.Error -> LoudiusFullScreenError( modifier = Modifier.padding(padding), onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) - is PullRequestState.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is PullRequestState.Loaded -> { - PullRequestContent(state, padding, onAction) - LaunchedEffect(state.navigateToReviewers) { - state.navigateToReviewers?.let { - navigateToReviewers( - it.owner, - it.repo, - it.pullRequestNumber, - it.submissionTime, - ) - onAction(PulLRequestsAction.OnNavigateToReviewers) - } - } - } + is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) + is Data.Loaded -> PullRequestContent(state.data, padding, onAction) } }, ) @@ -103,7 +107,7 @@ private fun PullRequestsScreenStateless( @Composable private fun PullRequestContent( - state: PullRequestState.Loaded, + state: Data.Loaded, padding: PaddingValues, onAction: (PulLRequestsAction) -> Unit, ) { @@ -194,44 +198,45 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { fun PullRequestsScreenPreview() { LoudiusTheme { PullRequestsScreenStateless( - state = PullRequestState.Loaded( - listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + state = PullRequestState( + Data.Loaded( + listOf( + PullRequest( + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", + title = "[SIL-67] Details screen - network layer", + createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), + ), + PullRequest( + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", + title = "[SIL-66] Add client secret to build config", + createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + ), + PullRequest( + id = 2, + draft = false, + number = 2, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", + title = "[SIL-73] Storing access token", + createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), + ), + PullRequest( + id = 3, + draft = false, + number = 3, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", + title = "[SIL-62/SIL-75] Provide new annotation for API instances", + createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), + ), ), - PullRequest( - id = 2, - draft = false, - number = 2, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", - title = "[SIL-73] Storing access token", - createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), - ), - PullRequest( - id = 3, - draft = false, - number = 3, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", - title = "[SIL-62/SIL-75] Provide new annotation for API instances", - createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), - ), - ), + ) ), onAction = {}, - navigateToReviewers = { _, _, _, _ -> }, ) } } @@ -241,9 +246,8 @@ fun PullRequestsScreenPreview() { fun PullRequestsScreenEmptyListPreview() { LoudiusTheme { PullRequestsScreenStateless( - PullRequestState.Loaded(emptyList()), + PullRequestState(Data.Loaded(emptyList())), onAction = {}, - navigateToReviewers = { _, _, _, _ -> }, ) } } @@ -253,9 +257,8 @@ fun PullRequestsScreenEmptyListPreview() { fun PullRequestsScreenLoadingPreview() { LoudiusTheme { PullRequestsScreenStateless( - PullRequestState.Loading, + PullRequestState(Data.Loading), onAction = {}, - navigateToReviewers = { _, _, _, _ -> }, ) } } @@ -265,9 +268,8 @@ fun PullRequestsScreenLoadingPreview() { fun PullRequestsScreenErrorPreview() { LoudiusTheme { PullRequestsScreenStateless( - PullRequestState.Error, + PullRequestState(Data.Error), onAction = {}, - navigateToReviewers = { _, _, _, _ -> }, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 2b924c40f..67b57186e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() @@ -33,15 +33,17 @@ sealed class PulLRequestsAction { object RetryClick : PulLRequestsAction() } -sealed class PullRequestState { - object Loading : PullRequestState() - object Error : PullRequestState() - data class Loaded( - val pullRequests: List = emptyList(), - val navigateToReviewers: NavigationPayload? = null, - ) : PullRequestState() +sealed class Data { + object Loading : Data() + object Error : Data() + data class Loaded(val pullRequests: List) : Data() } +data class PullRequestState( + val data: Data = Data.Loading, + val navigateToReviewers: NavigationPayload? = null, +) + data class NavigationPayload( val owner: String, val repo: String, @@ -53,7 +55,7 @@ data class NavigationPayload( class PullRequestsViewModel @Inject constructor( private val pullRequestsRepository: PullRequestRepository, ) : ViewModel() { - var state: PullRequestState by mutableStateOf(PullRequestState.Loading) + var state: PullRequestState by mutableStateOf(PullRequestState()) private set init { @@ -62,12 +64,12 @@ class PullRequestsViewModel @Inject constructor( private fun fetchData() { viewModelScope.launch { - state = PullRequestState.Loading + state = PullRequestState() pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { - state = PullRequestState.Loaded(it.items) + state = state.copy(data = Data.Loaded(it.items)) }.onFailure { - state = PullRequestState.Error + state = state.copy(data = Data.Error) } } } @@ -79,10 +81,10 @@ class PullRequestsViewModel @Inject constructor( } private fun navigateToReviewers(itemClickedId: Int) { - val loadedState = state as? PullRequestState.Loaded ?: return - val index = loadedState.pullRequests.indexOfFirst { it.id == itemClickedId } - val itemClickedData = loadedState.pullRequests[index] - state = loadedState.copy( + val loadedData = state.data as? Data.Loaded ?: return + val index = loadedData.pullRequests.indexOfFirst { it.id == itemClickedId } + val itemClickedData = loadedData.pullRequests[index] + state = state.copy( navigateToReviewers = NavigationPayload( itemClickedData.owner, itemClickedData.shortRepositoryName, @@ -93,7 +95,6 @@ class PullRequestsViewModel @Inject constructor( } private fun resetNavigationState() { - val loadedState = state as? PullRequestState.Loaded ?: return - state = loadedState.copy(navigateToReviewers = null) + state = state.copy(navigateToReviewers = null) } } From f3a1a43d7da1f8619b149a426693e9ea3dba4d62 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 19 Apr 2023 14:24:10 +0200 Subject: [PATCH 362/526] Repair PullRequestsViewModelTest. --- .../ui/pullrequests/PullRequestsScreen.kt | 2 +- .../pullrequests/PullRequestsViewModelTest.kt | 42 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index ac404fb3f..c70888ea7 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -97,10 +97,10 @@ private fun PullRequestsScreenStateless( modifier = Modifier.padding(padding), onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) - is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) is Data.Loaded -> PullRequestContent(state.data, padding, onAction) } + }, ) } diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 5eeefa948..92f7b3eec 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -14,7 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) package com.appunite.loudius.ui.pullrequests @@ -31,13 +30,12 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import strikt.api.expectThat -import strikt.assertions.isEmpty +import strikt.assertions.isA import strikt.assertions.isEqualTo -import strikt.assertions.isFalse import strikt.assertions.isNull -import strikt.assertions.isTrue import strikt.assertions.single +@OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) class PullRequestsViewModelTest { private val pullRequestRepository = spyk(FakePullRequestRepository()) @@ -49,48 +47,44 @@ class PullRequestsViewModelTest { val viewModel = createViewModel() - expectThat(viewModel.state) { - get(PullRequestState::isLoading).isTrue() - get(PullRequestState::isError).isFalse() - get(PullRequestState::pullRequests).isEmpty() - } + expectThat(viewModel.state.data).isA() } @Test fun `WHEN init THEN display pull requests list`() = runTest { val viewModel = createViewModel() - expectThat(viewModel.state) { - get(PullRequestState::isLoading).isFalse() - get(PullRequestState::isError).isFalse() - get(PullRequestState::pullRequests).single().isEqualTo(Defaults.pullRequest()) + expectThat(viewModel.state.data).isA().and { + get(Data.Loaded::pullRequests).single().isEqualTo(Defaults.pullRequest()) } } @Test fun `WHEN fetching data failed on init THEN display error`() = runTest { - coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { Result.failure(WebException.NetworkError()) } + coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { + Result.failure( + WebException.NetworkError() + ) + } val viewModel = createViewModel() - expectThat(viewModel.state) { - get(PullRequestState::isLoading).isFalse() - get(PullRequestState::isError).isTrue() - get(PullRequestState::pullRequests).isEmpty() - } + expectThat(viewModel.state.data).isA() } @Test fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() = runTest { - coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { Result.failure(WebException.NetworkError()) } + coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { + Result.failure( + WebException.NetworkError() + ) + } val viewModel = createViewModel() clearMocks(pullRequestRepository) viewModel.onAction(PulLRequestsAction.RetryClick) - expectThat(viewModel.state) { - get(PullRequestState::isLoading).isFalse() - get(PullRequestState::isError).isFalse() - get(PullRequestState::pullRequests).single().isEqualTo(Defaults.pullRequest()) + expectThat(viewModel.state.data).isA().and { + get(Data.Loaded::pullRequests).single().isEqualTo(Defaults.pullRequest()) } } From 18c6869930047fd312219b04949bf5361ea150bb Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Wed, 19 Apr 2023 14:59:40 +0000 Subject: [PATCH 363/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/ui/pullrequests/PullRequestsScreen.kt | 5 ++--- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsViewModelTest.kt | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index c70888ea7..b551c7aee 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -69,7 +69,7 @@ fun PullRequestsScreen( private fun navigateToReviewers( state: PullRequestState, navigateToReviewers: NavigateToReviewers, - viewModel: PullRequestsViewModel + viewModel: PullRequestsViewModel, ) { state.navigateToReviewers?.let { navigateToReviewers( @@ -100,7 +100,6 @@ private fun PullRequestsScreenStateless( is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) is Data.Loaded -> PullRequestContent(state.data, padding, onAction) } - }, ) } @@ -234,7 +233,7 @@ fun PullRequestsScreenPreview() { createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), ), ), - ) + ), ), onAction = {}, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 67b57186e..5a300f4d7 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 92f7b3eec..49b2aea33 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -14,7 +14,6 @@ * limitations under the License. */ - package com.appunite.loudius.ui.pullrequests import com.appunite.loudius.fakes.FakePullRequestRepository @@ -63,7 +62,7 @@ class PullRequestsViewModelTest { fun `WHEN fetching data failed on init THEN display error`() = runTest { coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { Result.failure( - WebException.NetworkError() + WebException.NetworkError(), ) } val viewModel = createViewModel() @@ -75,7 +74,7 @@ class PullRequestsViewModelTest { fun `GIVEN error state WHEN retry THEN fetch pull requests list again`() = runTest { coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { Result.failure( - WebException.NetworkError() + WebException.NetworkError(), ) } val viewModel = createViewModel() From ee6d7b655b49296cda85a08f1fb20d85c5bcff24 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 20 Apr 2023 10:13:22 +0200 Subject: [PATCH 364/526] Use concept of a view state in a sealed class in the ReviewersViewModel. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 47 ++++++----- .../ui/reviewers/ReviewersViewModel.kt | 81 ++++++++++++------ .../ui/reviewers/ReviewersViewModelTest.kt | 82 +++++++------------ 3 files changed, 113 insertions(+), 97 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 3f19ec86c..52acd2ba4 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -65,9 +65,7 @@ fun ReviewersScreen( ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, - reviewers = state.reviewers, - isLoading = state.isLoading, - isError = state.isError, + data = state.data, onClickBackArrow = navigateBack, snackbarHostState = snackbarHostState, onAction = viewModel::onAction, @@ -108,9 +106,7 @@ private fun resolveSnackbarMessage(snackbarTypeShown: ReviewersSnackbarType) = @Composable private fun ReviewersScreenStateless( pullRequestNumber: String, - reviewers: List, - isLoading: Boolean, - isError: Boolean, + data: Data, onClickBackArrow: () -> Unit, snackbarHostState: SnackbarHostState, onAction: (ReviewersAction) -> Unit, @@ -124,18 +120,14 @@ private fun ReviewersScreenStateless( }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, content = { padding -> - when { - isError -> LoudiusFullScreenError( + when (data) { + is Data.Error -> LoudiusFullScreenError( modifier = Modifier.padding(padding), onButtonClick = { onAction(ReviewersAction.OnTryAgain) }, ) - isLoading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - reviewers.isEmpty() -> EmptyListPlaceholder(padding) - else -> ReviewersScreenContent( - reviewers = reviewers, - modifier = Modifier.padding(padding), - onNotifyClick = onAction, - ) + + is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) + is Data.Loaded -> ReviewersScreenContent(data, padding, onAction) } }, ) @@ -143,6 +135,23 @@ private fun ReviewersScreenStateless( @Composable private fun ReviewersScreenContent( + data: Data.Loaded, + padding: PaddingValues, + onAction: (ReviewersAction) -> Unit +) { + if (data.reviewers.isNotEmpty()) { + ReviewersList( + reviewers = data.reviewers, + modifier = Modifier.padding(padding), + onNotifyClick = onAction, + ) + } else { + EmptyListPlaceholder(padding) + } +} + +@Composable +private fun ReviewersList( reviewers: List, modifier: Modifier, onNotifyClick: (ReviewersAction) -> Unit, @@ -263,9 +272,7 @@ fun DetailsScreenPreview() { LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - reviewers = reviewers, - isError = false, - isLoading = false, + data = Data.Loaded(reviewers), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, @@ -279,9 +286,7 @@ fun DetailsScreenNoReviewsPreview() { LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - reviewers = emptyList(), - isError = false, - isLoading = false, + data = Data.Loaded(emptyList()), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 6f9b48370..6f3a05cbf 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -30,12 +30,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -44,13 +44,17 @@ sealed class ReviewersAction { } data class ReviewersState( - val reviewers: List = emptyList(), + val data: Data = Data.Loading, val pullRequestNumber: String = "", val snackbarTypeShown: ReviewersSnackbarType? = null, - val isLoading: Boolean = false, - val isError: Boolean = false, ) +sealed class Data { + object Loading : Data() + object Error : Data() + data class Loaded(val reviewers: List = emptyList()) : Data() +} + enum class ReviewersSnackbarType { SUCCESS, FAILURE } @@ -62,7 +66,7 @@ class ReviewersViewModel @Inject constructor( ) : ViewModel() { private val initialValues = getInitialValues(savedStateHandle) - var state by mutableStateOf(ReviewersState()) + var state: ReviewersState by mutableStateOf(ReviewersState()) private set init { @@ -72,11 +76,16 @@ class ReviewersViewModel @Inject constructor( private fun fetchData() { viewModelScope.launch { - state = state.copy(isLoading = true, isError = false) + state = state.copy(data = Data.Loading) getMergedData() - .onSuccess { state = state.copy(reviewers = it, isLoading = false) } - .onFailure { state = state.copy(isError = true, isLoading = false) } + .onSuccess { + state = state.copy( + data = Data.Loaded(reviewers = it), + pullRequestNumber = initialValues.pullRequestNumber + ) + } + .onFailure { state = state.copy(data = Data.Error) } } } @@ -160,26 +169,52 @@ class ReviewersViewModel @Inject constructor( private fun notifyUser(userLogin: String) { val (owner, repo, pullRequestNumber) = initialValues + val loadedData = state.data as? Data.Loaded ?: return viewModelScope.launch { - state = state.copy(reviewers = state.reviewers.updateLoadingState(userLogin, true)) + setUserItemLoading(loadedData, userLogin) repository.notify(owner, repo, pullRequestNumber, "@$userLogin") - .onSuccess { - state = state.copy( - snackbarTypeShown = SUCCESS, - reviewers = state.reviewers.updateLoadingState(userLogin, false), - ) - } - .onFailure { - state = state.copy( - snackbarTypeShown = FAILURE, - reviewers = state.reviewers.updateLoadingState(userLogin, false), - ) - } + .onSuccess { onNotifyUserSuccess(loadedData, userLogin) } + .onFailure { onNotifyUserFailure(loadedData, userLogin) } } } + private fun setUserItemLoading( + loadedData: Data.Loaded, + userLogin: String + ) { + state = state.copy( + data = Data.Loaded( + reviewers = loadedData.reviewers.updateLoadingState(userLogin, true) + ) + ) + } + + private fun onNotifyUserFailure( + loadedData: Data.Loaded, + userLogin: String + ) { + state = state.copy( + snackbarTypeShown = FAILURE, + data = Data.Loaded( + reviewers = loadedData.reviewers.updateLoadingState(userLogin, false), + ) + ) + } + + private fun onNotifyUserSuccess( + loadedData: Data.Loaded, + userLogin: String + ) { + state = state.copy( + snackbarTypeShown = SUCCESS, + data = Data.Loaded( + loadedData.reviewers.updateLoadingState(userLogin, false), + ) + ) + } + private fun List.updateLoadingState( userLogin: String, isLoading: Boolean, diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 0ee366949..66de777af 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -29,6 +29,10 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach @@ -41,15 +45,12 @@ import strikt.assertions.all import strikt.assertions.containsExactly import strikt.assertions.filterNot import strikt.assertions.first +import strikt.assertions.isA import strikt.assertions.isEmpty import strikt.assertions.isEqualTo import strikt.assertions.isFalse import strikt.assertions.isNull import strikt.assertions.isTrue -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) @@ -114,11 +115,7 @@ class ReviewersViewModelTest { } coAnswers { neverCompletingSuspension() } viewModel = createViewModel() - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isTrue() - get(ReviewersState::isError).isFalse() - get(ReviewersState::reviewers).isEmpty() - } + expectThat(viewModel.state.data).isA() } @Test @@ -140,10 +137,8 @@ class ReviewersViewModelTest { viewModel = createViewModel() - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isFalse() - get(ReviewersState::isError).isFalse() - get(ReviewersState::reviewers).isEmpty() + expectThat(viewModel.state.data).isA().and { + get(Data.Loaded::reviewers).isEmpty() } } @@ -152,10 +147,8 @@ class ReviewersViewModelTest { runTest { viewModel = createViewModel() - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isFalse() - get(ReviewersState::isError).isFalse() - get(ReviewersState::reviewers).containsExactly( + expectThat(viewModel.state.data).isA().and { + get(Data.Loaded::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), @@ -171,12 +164,13 @@ class ReviewersViewModelTest { ) viewModel = createViewModel() - expectThat(viewModel.state) - .get(ReviewersState::reviewers) - .containsExactly( - Reviewer(2, "user2", false, 7, null), - Reviewer(3, "user3", false, 7, null), - ) + expectThat(viewModel.state.data).isA().and { + get(Data.Loaded::reviewers) + .containsExactly( + Reviewer(2, "user2", false, 7, null), + Reviewer(3, "user3", false, 7, null), + ) + } } @Test @@ -191,11 +185,11 @@ class ReviewersViewModelTest { } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() - expectThat(viewModel.state) - .get(ReviewersState::reviewers) - .containsExactly( + expectThat(viewModel.state.data).isA().and { + get(Data.Loaded::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), ) + } } @Test @@ -213,11 +207,7 @@ class ReviewersViewModelTest { } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isFalse() - get(ReviewersState::isError).isTrue() - get(ReviewersState::reviewers).isEmpty() - } + expectThat(viewModel.state.data).isA() } @Test @@ -232,11 +222,7 @@ class ReviewersViewModelTest { } returns Result.failure(WebException.NetworkError()) viewModel = createViewModel() - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isFalse() - get(ReviewersState::isError).isTrue() - get(ReviewersState::reviewers).isEmpty() - } + expectThat(viewModel.state.data).isA() } @Test @@ -247,11 +233,7 @@ class ReviewersViewModelTest { ) viewModel = createViewModel() - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isFalse() - get(ReviewersState::isError).isTrue() - get(ReviewersState::reviewers).isEmpty() - } + expectThat(viewModel.state.data).isA() } } @@ -283,15 +265,15 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("user1")) - expectThat(viewModel.state) { + expectThat(viewModel.state.data).isA().and { // Clicked item should have loading indicator - get(ReviewersState::reviewers) + get(Data.Loaded::reviewers) .first { it.login == "user1" } .get(Reviewer::isLoading) .isTrue() // Other items should NOT have loading indicator - get(ReviewersState::reviewers) + get(Data.Loaded::reviewers) .filterNot { it.login == "user1" } .all { get(Reviewer::isLoading).isFalse() } } @@ -340,10 +322,8 @@ class ReviewersViewModelTest { clearMocks(repository) viewModel.onAction(ReviewersAction.OnTryAgain) - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isFalse() - get(ReviewersState::isError).isFalse() - get(ReviewersState::reviewers).containsExactly( + expectThat(viewModel.state.data).isA().and { + get(Data.Loaded::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), @@ -364,11 +344,7 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.OnTryAgain) - expectThat(viewModel.state) { - get(ReviewersState::isLoading).isFalse() - get(ReviewersState::isError).isTrue() - get(ReviewersState::reviewers).isEmpty() - } + expectThat(viewModel.state.data).isA() } } } From a3cfa9225191134c80c7e9d2616d5d1ebee33431 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 20 Apr 2023 10:21:13 +0200 Subject: [PATCH 365/526] Rename Data.Loaded to Data.Success in ReviewersViewModel. --- .../loudius/ui/reviewers/ReviewersScreen.kt | 8 ++--- .../ui/reviewers/ReviewersViewModel.kt | 30 +++++++++---------- .../ui/reviewers/ReviewersViewModelTest.kt | 26 ++++++++-------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 52acd2ba4..9afd2afd3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -127,7 +127,7 @@ private fun ReviewersScreenStateless( ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Loaded -> ReviewersScreenContent(data, padding, onAction) + is Data.Success -> ReviewersScreenContent(data, padding, onAction) } }, ) @@ -135,7 +135,7 @@ private fun ReviewersScreenStateless( @Composable private fun ReviewersScreenContent( - data: Data.Loaded, + data: Data.Success, padding: PaddingValues, onAction: (ReviewersAction) -> Unit ) { @@ -272,7 +272,7 @@ fun DetailsScreenPreview() { LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - data = Data.Loaded(reviewers), + data = Data.Success(reviewers), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, @@ -286,7 +286,7 @@ fun DetailsScreenNoReviewsPreview() { LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - data = Data.Loaded(emptyList()), + data = Data.Success(emptyList()), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 6f3a05cbf..bc7da07e4 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -52,7 +52,7 @@ data class ReviewersState( sealed class Data { object Loading : Data() object Error : Data() - data class Loaded(val reviewers: List = emptyList()) : Data() + data class Success(val reviewers: List = emptyList()) : Data() } enum class ReviewersSnackbarType { @@ -81,7 +81,7 @@ class ReviewersViewModel @Inject constructor( getMergedData() .onSuccess { state = state.copy( - data = Data.Loaded(reviewers = it), + data = Data.Success(reviewers = it), pullRequestNumber = initialValues.pullRequestNumber ) } @@ -169,48 +169,48 @@ class ReviewersViewModel @Inject constructor( private fun notifyUser(userLogin: String) { val (owner, repo, pullRequestNumber) = initialValues - val loadedData = state.data as? Data.Loaded ?: return + val successData = state.data as? Data.Success ?: return viewModelScope.launch { - setUserItemLoading(loadedData, userLogin) + setUserItemLoading(successData, userLogin) repository.notify(owner, repo, pullRequestNumber, "@$userLogin") - .onSuccess { onNotifyUserSuccess(loadedData, userLogin) } - .onFailure { onNotifyUserFailure(loadedData, userLogin) } + .onSuccess { onNotifyUserSuccess(successData, userLogin) } + .onFailure { onNotifyUserFailure(successData, userLogin) } } } private fun setUserItemLoading( - loadedData: Data.Loaded, + successData: Data.Success, userLogin: String ) { state = state.copy( - data = Data.Loaded( - reviewers = loadedData.reviewers.updateLoadingState(userLogin, true) + data = Data.Success( + reviewers = successData.reviewers.updateLoadingState(userLogin, true) ) ) } private fun onNotifyUserFailure( - loadedData: Data.Loaded, + successData: Data.Success, userLogin: String ) { state = state.copy( snackbarTypeShown = FAILURE, - data = Data.Loaded( - reviewers = loadedData.reviewers.updateLoadingState(userLogin, false), + data = Data.Success( + reviewers = successData.reviewers.updateLoadingState(userLogin, false), ) ) } private fun onNotifyUserSuccess( - loadedData: Data.Loaded, + successData: Data.Success, userLogin: String ) { state = state.copy( snackbarTypeShown = SUCCESS, - data = Data.Loaded( - loadedData.reviewers.updateLoadingState(userLogin, false), + data = Data.Success( + successData.reviewers.updateLoadingState(userLogin, false), ) ) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 66de777af..59c056962 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -137,8 +137,8 @@ class ReviewersViewModelTest { viewModel = createViewModel() - expectThat(viewModel.state.data).isA().and { - get(Data.Loaded::reviewers).isEmpty() + expectThat(viewModel.state.data).isA().and { + get(Data.Success::reviewers).isEmpty() } } @@ -147,8 +147,8 @@ class ReviewersViewModelTest { runTest { viewModel = createViewModel() - expectThat(viewModel.state.data).isA().and { - get(Data.Loaded::reviewers).containsExactly( + expectThat(viewModel.state.data).isA().and { + get(Data.Success::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), @@ -164,8 +164,8 @@ class ReviewersViewModelTest { ) viewModel = createViewModel() - expectThat(viewModel.state.data).isA().and { - get(Data.Loaded::reviewers) + expectThat(viewModel.state.data).isA().and { + get(Data.Success::reviewers) .containsExactly( Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), @@ -185,8 +185,8 @@ class ReviewersViewModelTest { } returns Result.success(RequestedReviewersResponse(emptyList())) viewModel = createViewModel() - expectThat(viewModel.state.data).isA().and { - get(Data.Loaded::reviewers).containsExactly( + expectThat(viewModel.state.data).isA().and { + get(Data.Success::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), ) } @@ -265,15 +265,15 @@ class ReviewersViewModelTest { viewModel.onAction(ReviewersAction.Notify("user1")) - expectThat(viewModel.state.data).isA().and { + expectThat(viewModel.state.data).isA().and { // Clicked item should have loading indicator - get(Data.Loaded::reviewers) + get(Data.Success::reviewers) .first { it.login == "user1" } .get(Reviewer::isLoading) .isTrue() // Other items should NOT have loading indicator - get(Data.Loaded::reviewers) + get(Data.Success::reviewers) .filterNot { it.login == "user1" } .all { get(Reviewer::isLoading).isFalse() } } @@ -322,8 +322,8 @@ class ReviewersViewModelTest { clearMocks(repository) viewModel.onAction(ReviewersAction.OnTryAgain) - expectThat(viewModel.state.data).isA().and { - get(Data.Loaded::reviewers).containsExactly( + expectThat(viewModel.state.data).isA().and { + get(Data.Success::reviewers).containsExactly( Reviewer(1, "user1", true, 7, 5), Reviewer(2, "user2", false, 7, null), Reviewer(3, "user3", false, 7, null), From 62bb982f176496deeeaf317418edc0c096c863b7 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 20 Apr 2023 10:21:47 +0200 Subject: [PATCH 366/526] Rename Data.Loaded to Data.Success in PullRequestsViewModel. --- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 8 ++++---- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 12 ++++++------ .../ui/pullrequests/PullRequestsViewModelTest.kt | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index b551c7aee..dde6a3e90 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -98,7 +98,7 @@ private fun PullRequestsScreenStateless( onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Loaded -> PullRequestContent(state.data, padding, onAction) + is Data.Success -> PullRequestContent(state.data, padding, onAction) } }, ) @@ -106,7 +106,7 @@ private fun PullRequestsScreenStateless( @Composable private fun PullRequestContent( - state: Data.Loaded, + state: Data.Success, padding: PaddingValues, onAction: (PulLRequestsAction) -> Unit, ) { @@ -198,7 +198,7 @@ fun PullRequestsScreenPreview() { LoudiusTheme { PullRequestsScreenStateless( state = PullRequestState( - Data.Loaded( + Data.Success( listOf( PullRequest( id = 0, @@ -245,7 +245,7 @@ fun PullRequestsScreenPreview() { fun PullRequestsScreenEmptyListPreview() { LoudiusTheme { PullRequestsScreenStateless( - PullRequestState(Data.Loaded(emptyList())), + PullRequestState(Data.Success(emptyList())), onAction = {}, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 5a300f4d7..e9054918b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch import javax.inject.Inject +import kotlinx.coroutines.launch sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() @@ -36,7 +36,7 @@ sealed class PulLRequestsAction { sealed class Data { object Loading : Data() object Error : Data() - data class Loaded(val pullRequests: List) : Data() + data class Success(val pullRequests: List) : Data() } data class PullRequestState( @@ -67,7 +67,7 @@ class PullRequestsViewModel @Inject constructor( state = PullRequestState() pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { - state = state.copy(data = Data.Loaded(it.items)) + state = state.copy(data = Data.Success(it.items)) }.onFailure { state = state.copy(data = Data.Error) } @@ -81,9 +81,9 @@ class PullRequestsViewModel @Inject constructor( } private fun navigateToReviewers(itemClickedId: Int) { - val loadedData = state.data as? Data.Loaded ?: return - val index = loadedData.pullRequests.indexOfFirst { it.id == itemClickedId } - val itemClickedData = loadedData.pullRequests[index] + val successData = state.data as? Data.Success ?: return + val index = successData.pullRequests.indexOfFirst { it.id == itemClickedId } + val itemClickedData = successData.pullRequests[index] state = state.copy( navigateToReviewers = NavigationPayload( itemClickedData.owner, diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 49b2aea33..6f024c500 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -53,8 +53,8 @@ class PullRequestsViewModelTest { fun `WHEN init THEN display pull requests list`() = runTest { val viewModel = createViewModel() - expectThat(viewModel.state.data).isA().and { - get(Data.Loaded::pullRequests).single().isEqualTo(Defaults.pullRequest()) + expectThat(viewModel.state.data).isA().and { + get(Data.Success::pullRequests).single().isEqualTo(Defaults.pullRequest()) } } @@ -82,8 +82,8 @@ class PullRequestsViewModelTest { clearMocks(pullRequestRepository) viewModel.onAction(PulLRequestsAction.RetryClick) - expectThat(viewModel.state.data).isA().and { - get(Data.Loaded::pullRequests).single().isEqualTo(Defaults.pullRequest()) + expectThat(viewModel.state.data).isA().and { + get(Data.Success::pullRequests).single().isEqualTo(Defaults.pullRequest()) } } From 53a2b110500564233c8cebe022b5e36cda02e5cd Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 20 Apr 2023 10:55:58 +0200 Subject: [PATCH 367/526] chore: better way of scalling --- .../ui/components/LoudiusFullScreenError.kt | 134 +++++++++--------- .../ui/components/utils/ReferenceDevices.kt | 10 ++ 2 files changed, 78 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index 1eee11aad..0a9fa7056 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -17,14 +17,18 @@ package com.appunite.loudius.ui.components import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource @@ -32,10 +36,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R +import com.appunite.loudius.ui.components.utils.ReferenceDevices import com.appunite.loudius.ui.theme.LoudiusTheme -private const val minHeightToFitSpacer = 1856 - @Composable fun LoudiusFullScreenError( modifier: Modifier = Modifier, @@ -43,25 +46,12 @@ fun LoudiusFullScreenError( buttonText: String = stringResource(id = R.string.try_again), onButtonClick: () -> Unit, ) { - val density = LocalDensity.current - val configuration = LocalConfiguration.current - val screenHeight = with(density) { configuration.screenHeightDp.dp.roundToPx() } - - if (screenHeight >= minHeightToFitSpacer) { ScreenErrorWithSpacers( modifier = modifier, errorText = errorText, buttonText = buttonText, onButtonClick = onButtonClick, ) - } else { - ScreenErrorWithoutSpacers( - modifier = modifier, - errorText = errorText, - buttonText = buttonText, - onButtonClick = onButtonClick, - ) - } } @Composable @@ -72,64 +62,28 @@ fun ScreenErrorWithSpacers( onButtonClick: () -> Unit, ) { Column( - modifier = Modifier.fillMaxSize(), - ) { - Spacer(modifier = Modifier.weight(weight = 0.15f)) - Column( - modifier = modifier - .weight(weight = 0.6f) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, - ) { - ErrorImage() - ErrorText(text = errorText) - LoudiusOutlinedButton( - onClick = onButtonClick, - text = buttonText, - ) - } - Spacer(modifier = Modifier.weight(weight = 0.25f)) - } -} - -@Composable -fun ScreenErrorWithoutSpacers( - modifier: Modifier, - errorText: String, - buttonText: String, - onButtonClick: () -> Unit, -) { - Column( - modifier = modifier.fillMaxSize(), + modifier = modifier.padding(32.dp).fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceEvenly, ) { - ScreenErrorContent( - errorText = errorText, - buttonText = buttonText, - onButtonClick = onButtonClick, + Spacer(modifier = Modifier.weight(weight = 0.15f)) + ErrorImage(modifier = Modifier.weight(weight = .35f).sizeIn(maxWidth =400.dp, maxHeight = 400.dp).fillMaxWidth()) + Spacer(modifier = Modifier.weight(weight = 0.05f)) + ErrorText(text = errorText) + LoudiusOutlinedButton( + modifier = Modifier.padding(vertical = 16.dp), + onClick = onButtonClick, + text = buttonText, ) + Spacer(modifier = Modifier.weight(weight = 0.25f)) } } @Composable -fun ScreenErrorContent( - errorText: String, - buttonText: String, - onButtonClick: () -> Unit, +private fun ErrorImage( + modifier: Modifier = Modifier, ) { - ErrorImage() - ErrorText(text = errorText) - LoudiusOutlinedButton( - onClick = onButtonClick, - text = buttonText, - ) -} - -@Composable -private fun ErrorImage() { Image( + modifier = modifier, painter = painterResource(id = R.drawable.error_image), contentDescription = stringResource(R.string.error_image_desc), ) @@ -139,7 +93,7 @@ private fun ErrorImage() { private fun ErrorText(text: String) { LoudiusText( text = text, - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), style = LoudiusTextStyle.ScreenContent, ) } @@ -155,3 +109,51 @@ fun LoudiusErrorScreenPreview() { ) } } + + +@Preview(showSystemUi = true, device = ReferenceDevices.SmallPhone) +@Composable +fun LoudiusErrorScreenPreviewSmallPhone() { + LoudiusTheme { + LoudiusFullScreenError( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(R.string.try_again), + onButtonClick = {}, + ) + } +} +@Preview(showSystemUi = true, device = ReferenceDevices.SmallPhoneLandscape) +@Composable +fun LoudiusErrorScreenPreviewSmallPhoneLandscape() { + LoudiusTheme { + LoudiusFullScreenError( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(R.string.try_again), + onButtonClick = {}, + ) + } +} + +@Preview(showSystemUi = true, device = ReferenceDevices.Tablet) +@Composable +fun LoudiusErrorScreenPreviewTablet() { + LoudiusTheme { + LoudiusFullScreenError( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(R.string.try_again), + onButtonClick = {}, + ) + } +} + +@Preview(showSystemUi = true, device = ReferenceDevices.TabletPortrait) +@Composable +fun LoudiusErrorScreenPreviewTabletPortrait() { + LoudiusTheme { + LoudiusFullScreenError( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(R.string.try_again), + onButtonClick = {}, + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt b/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt new file mode 100644 index 000000000..30c33c0e6 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt @@ -0,0 +1,10 @@ +package com.appunite.loudius.ui.components.utils + +import androidx.compose.ui.tooling.preview.Devices + +object ReferenceDevices { + const val SmallPhone = "spec:id=reference_phone,shape=Normal,width=350,height=600,unit=dp,dpi=420" + const val SmallPhoneLandscape = "spec:id=reference_phone,shape=Normal,width=600,height=350,unit=dp,dpi=420" + const val Tablet = Devices.TABLET + const val TabletPortrait = "spec:shape=Normal,width=800,height=1280,unit=dp,dpi=420" +} \ No newline at end of file From 402710ac21412b050fe3a30a9929f1e46930c161 Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Thu, 20 Apr 2023 09:15:33 +0000 Subject: [PATCH 368/526] [MegaLinter] Apply linters fixes --- .../ui/components/LoudiusFullScreenError.kt | 21 +++++++------------ .../ui/components/utils/ReferenceDevices.kt | 2 +- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index 0a9fa7056..dc982a724 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -21,16 +21,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -46,12 +41,12 @@ fun LoudiusFullScreenError( buttonText: String = stringResource(id = R.string.try_again), onButtonClick: () -> Unit, ) { - ScreenErrorWithSpacers( - modifier = modifier, - errorText = errorText, - buttonText = buttonText, - onButtonClick = onButtonClick, - ) + ScreenErrorWithSpacers( + modifier = modifier, + errorText = errorText, + buttonText = buttonText, + onButtonClick = onButtonClick, + ) } @Composable @@ -66,7 +61,7 @@ fun ScreenErrorWithSpacers( horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.weight(weight = 0.15f)) - ErrorImage(modifier = Modifier.weight(weight = .35f).sizeIn(maxWidth =400.dp, maxHeight = 400.dp).fillMaxWidth()) + ErrorImage(modifier = Modifier.weight(weight = .35f).sizeIn(maxWidth = 400.dp, maxHeight = 400.dp).fillMaxWidth()) Spacer(modifier = Modifier.weight(weight = 0.05f)) ErrorText(text = errorText) LoudiusOutlinedButton( @@ -110,7 +105,6 @@ fun LoudiusErrorScreenPreview() { } } - @Preview(showSystemUi = true, device = ReferenceDevices.SmallPhone) @Composable fun LoudiusErrorScreenPreviewSmallPhone() { @@ -122,6 +116,7 @@ fun LoudiusErrorScreenPreviewSmallPhone() { ) } } + @Preview(showSystemUi = true, device = ReferenceDevices.SmallPhoneLandscape) @Composable fun LoudiusErrorScreenPreviewSmallPhoneLandscape() { diff --git a/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt b/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt index 30c33c0e6..b241cb8a2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt @@ -7,4 +7,4 @@ object ReferenceDevices { const val SmallPhoneLandscape = "spec:id=reference_phone,shape=Normal,width=600,height=350,unit=dp,dpi=420" const val Tablet = Devices.TABLET const val TabletPortrait = "spec:shape=Normal,width=800,height=1280,unit=dp,dpi=420" -} \ No newline at end of file +} From b056c71498fba607319cfad492a71dc3db2c2c9b Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 20 Apr 2023 12:13:19 +0200 Subject: [PATCH 369/526] Perform few improvements in ReviewersViewModel. --- .../loudius/ui/reviewers/ReviewersViewModel.kt | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index bc7da07e4..669ba51ca 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -79,12 +79,7 @@ class ReviewersViewModel @Inject constructor( state = state.copy(data = Data.Loading) getMergedData() - .onSuccess { - state = state.copy( - data = Data.Success(reviewers = it), - pullRequestNumber = initialValues.pullRequestNumber - ) - } + .onSuccess { state = state.copy(data = Data.Success(reviewers = it)) } .onFailure { state = state.copy(data = Data.Error) } } } @@ -162,17 +157,17 @@ class ReviewersViewModel @Inject constructor( ChronoUnit.HOURS.between(submissionTime, LocalDateTime.now()) fun onAction(action: ReviewersAction) = when (action) { - is ReviewersAction.Notify -> notifyUser(action.userLogin) + is ReviewersAction.Notify -> notifyReviewer(action.userLogin) is ReviewersAction.OnSnackbarDismiss -> dismissSnackbar() is ReviewersAction.OnTryAgain -> fetchData() } - private fun notifyUser(userLogin: String) { + private fun notifyReviewer(userLogin: String) { val (owner, repo, pullRequestNumber) = initialValues val successData = state.data as? Data.Success ?: return viewModelScope.launch { - setUserItemLoading(successData, userLogin) + setReviewerToLoading(successData, userLogin) repository.notify(owner, repo, pullRequestNumber, "@$userLogin") .onSuccess { onNotifyUserSuccess(successData, userLogin) } @@ -180,7 +175,7 @@ class ReviewersViewModel @Inject constructor( } } - private fun setUserItemLoading( + private fun setReviewerToLoading( successData: Data.Success, userLogin: String ) { From a5f786bbd439db743cc70f84df3f3ffa8a810217 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 20 Apr 2023 10:17:24 +0000 Subject: [PATCH 370/526] [MegaLinter] Apply linters fixes --- .../ui/pullrequests/PullRequestsViewModel.kt | 2 +- .../loudius/ui/reviewers/ReviewersScreen.kt | 2 +- .../ui/reviewers/ReviewersViewModel.kt | 20 +++++++++---------- .../ui/reviewers/ReviewersViewModelTest.kt | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index e9054918b..3aad56aa2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject sealed class PulLRequestsAction { data class ItemClick(val id: Int) : PulLRequestsAction() diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 9afd2afd3..7ab8baf4e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -137,7 +137,7 @@ private fun ReviewersScreenStateless( private fun ReviewersScreenContent( data: Data.Success, padding: PaddingValues, - onAction: (ReviewersAction) -> Unit + onAction: (ReviewersAction) -> Unit, ) { if (data.reviewers.isNotEmpty()) { ReviewersList( diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 669ba51ca..05af0bcbf 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -30,12 +30,12 @@ import com.appunite.loudius.network.model.Review import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel -import java.time.LocalDateTime -import java.time.temporal.ChronoUnit -import javax.inject.Inject import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import javax.inject.Inject sealed class ReviewersAction { data class Notify(val userLogin: String) : ReviewersAction() @@ -177,36 +177,36 @@ class ReviewersViewModel @Inject constructor( private fun setReviewerToLoading( successData: Data.Success, - userLogin: String + userLogin: String, ) { state = state.copy( data = Data.Success( - reviewers = successData.reviewers.updateLoadingState(userLogin, true) - ) + reviewers = successData.reviewers.updateLoadingState(userLogin, true), + ), ) } private fun onNotifyUserFailure( successData: Data.Success, - userLogin: String + userLogin: String, ) { state = state.copy( snackbarTypeShown = FAILURE, data = Data.Success( reviewers = successData.reviewers.updateLoadingState(userLogin, false), - ) + ), ) } private fun onNotifyUserSuccess( successData: Data.Success, - userLogin: String + userLogin: String, ) { state = state.copy( snackbarTypeShown = SUCCESS, data = Data.Success( successData.reviewers.updateLoadingState(userLogin, false), - ) + ), ) } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index 59c056962..b39c2a01e 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -29,10 +29,6 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.spyk import io.mockk.verify -import java.time.Clock -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach @@ -51,6 +47,10 @@ import strikt.assertions.isEqualTo import strikt.assertions.isFalse import strikt.assertions.isNull import strikt.assertions.isTrue +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(MainDispatcherExtension::class) From b19eb0378a14ed3f2058193da3db632ad2412dee Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 24 Apr 2023 09:16:17 +0200 Subject: [PATCH 371/526] Add git LFS. --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..0542767ef --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text From 8851ac357ee260c2ada0137ff0367cf6e133d045 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 27 Apr 2023 15:36:34 +0200 Subject: [PATCH 372/526] Create new android library module for screenshots. --- app/build.gradle | 4 +- build.gradle | 2 + screenshots/build.gradle | 53 ++++++++++++++++++++++++ screenshots/src/main/AndroidManifest.xml | 4 ++ settings.gradle | 1 + 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 screenshots/build.gradle create mode 100644 screenshots/src/main/AndroidManifest.xml diff --git a/app/build.gradle b/app/build.gradle index 87c904f98..daa394963 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,6 @@ plugins { - id 'kotlin-kapt' id 'com.android.application' + id 'kotlin-kapt' id 'org.jetbrains.kotlin.android' id 'com.google.dagger.hilt.android' id 'org.jlleitschuh.gradle.ktlint' version '11.2.0' @@ -8,7 +8,7 @@ plugins { android { namespace 'com.appunite.loudius' - compileSdk 33 + compileSdkVersion 33 defaultConfig { applicationId "com.appunite.loudius" diff --git a/build.gradle b/build.gradle index 6ad567c7c..b2158b943 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,10 @@ buildscript { ext { compose_version = '1.3.3' } + }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { + id 'app.cash.paparazzi' version '1.2.0' apply false id 'com.android.application' version '7.4.1' apply false id 'com.android.library' version '7.4.1' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false diff --git a/screenshots/build.gradle b/screenshots/build.gradle new file mode 100644 index 000000000..e83dc6c32 --- /dev/null +++ b/screenshots/build.gradle @@ -0,0 +1,53 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'app.cash.paparazzi' + id 'kotlin-kapt' +} + +android { + compileSdkVersion 33 + + defaultConfig { + minSdk 24 + targetSdk 33 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.4.2' + } +} + +dependencies { + implementation project(":app") + + def composeBom = platform('androidx.compose:compose-bom:2023.01.00') + implementation composeBom + implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-tooling-preview' + + implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + + testImplementation "com.airbnb.android:showkase-screenshot-testing:1.0.0-beta18" + testImplementation "com.google.testparameterinjector:test-parameter-injector:1.8" + implementation "com.airbnb.android:showkase:1.0.0-beta18" + kapt "com.airbnb.android:showkase-processor:1.0.0-beta18" +} diff --git a/screenshots/src/main/AndroidManifest.xml b/screenshots/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c6dcb2bbd --- /dev/null +++ b/screenshots/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/settings.gradle b/settings.gradle index 7565b8f60..ede55b04a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,3 +15,4 @@ dependencyResolutionManagement { rootProject.name = "Loudius" include ':app' include ':custom-ktlint-rules' +include ':screenshots' From 5db7b3f4222d568ed9e3e17114fea51641c2b231 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 28 Apr 2023 13:45:35 +0200 Subject: [PATCH 373/526] Move some ui components to the screenshots module to serve as an example. --- app/build.gradle | 2 ++ .../com/appunite/loudius/LoginScreenTest.kt | 2 +- .../java/com/appunite/loudius/MainActivity.kt | 2 +- .../ui/authenticating/AuthenticatingScreen.kt | 4 +-- .../loudius/ui/components/LoudiusListItem.kt | 3 ++ .../ui/components/LoudiusLoadingIndicator.kt | 2 +- .../ui/components/LoudiusPlaceholderText.kt | 4 ++- .../loudius/ui/components/LoudiusTopAppBar.kt | 4 ++- .../appunite/loudius/ui/login/LoginScreen.kt | 16 +++++---- .../ui/pullrequests/PullRequestsScreen.kt | 8 ++--- .../loudius/ui/reviewers/ReviewersScreen.kt | 10 +++--- screenshots/build.gradle | 3 -- .../screenshots}/components/LoudiusDialog.kt | 6 ++-- .../components/LoudiusErrorDialog.kt | 6 ++-- .../components/LoudiusFullScreenError.kt | 6 ++-- .../components/LoudiusOutlinedButton.kt | 6 ++-- .../screenshots}/components/LoudiusText.kt | 4 +-- .../loudius/screenshots}/theme/Color.kt | 2 +- .../loudius/screenshots}/theme/Theme.kt | 2 +- .../loudius/screenshots}/theme/Type.kt | 2 +- .../src/main/res/drawable}/error_image.png | Bin .../src/main/res/drawable/ic_github.xml | 0 screenshots/src/main/res/values/strings.xml | 32 ++++++++++++++++++ 23 files changed, 83 insertions(+), 43 deletions(-) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusDialog.kt (92%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusErrorDialog.kt (92%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusFullScreenError.kt (96%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusOutlinedButton.kt (96%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusText.kt (95%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/theme/Color.kt (95%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/theme/Theme.kt (98%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/theme/Type.kt (97%) rename {app/src/main/res/drawable-xhdpi => screenshots/src/main/res/drawable}/error_image.png (100%) rename {app => screenshots}/src/main/res/drawable/ic_github.xml (100%) create mode 100644 screenshots/src/main/res/values/strings.xml diff --git a/app/build.gradle b/app/build.gradle index daa394963..1b37482ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,6 +55,8 @@ android { } dependencies { + implementation project(":screenshots") + //Desugaring for use of java.time in api lower then 26 coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 5ae5694b0..260c3c482 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.login.LoginScreen -import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 83a0c46c4..0653d8df7 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -31,12 +31,12 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.appunite.loudius.common.Screen +import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.MainViewModel import com.appunite.loudius.ui.authenticating.AuthenticatingScreen import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.reviewers.ReviewersScreen -import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 22d50bf27..9235adfde 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.ui.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.components.LoudiusLoadingIndicator -import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun AuthenticatingScreen( diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt index 0bfabf07f..bf34a2ef7 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt @@ -33,6 +33,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton +import com.appunite.loudius.screenshots.components.LoudiusText +import com.appunite.loudius.screenshots.components.LoudiusTextStyle import com.appunite.loudius.ui.components.utils.bottomBorder @Composable diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index f89baabc5..8f69f7d4d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -31,7 +31,7 @@ import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.appunite.loudius.R -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.theme.LoudiusTheme @Composable fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt index 06bb70c3b..955949a04 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt @@ -27,7 +27,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.components.LoudiusText +import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.theme.LoudiusTheme @Composable fun LoudiusPlaceholderText(@StringRes textId: Int) { diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 279224df7..c809a20c3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -27,7 +27,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.appunite.loudius.R -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.components.LoudiusText +import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.theme.LoudiusTheme @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index f5d1d679e..b15b16b60 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -35,12 +35,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTHORIZATION_URL -import com.appunite.loudius.ui.components.LoudiusDialog -import com.appunite.loudius.ui.components.LoudiusOutlinedButton -import com.appunite.loudius.ui.components.LoudiusOutlinedButtonIcon -import com.appunite.loudius.ui.components.LoudiusOutlinedButtonStyle -import com.appunite.loudius.ui.components.LoudiusText -import com.appunite.loudius.ui.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.components.LoudiusDialog +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonIcon +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonStyle +import com.appunite.loudius.screenshots.components.LoudiusText +import com.appunite.loudius.screenshots.components.LoudiusTextStyle @Composable fun LoginScreen( @@ -56,9 +56,11 @@ fun LoginScreen( ) viewModel.onAction(LoginAction.ClearNavigation) } + LoginNavigateTo.OpenXiaomiPermissionManager -> { context.startActivity(GithubHelper.xiaomiPermissionManagerForGithub()) } + null -> Unit } } @@ -88,7 +90,7 @@ fun LoginScreenStateless( style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = R.drawable.ic_github), + painter = painterResource(id = com.appunite.loudius.screenshots.R.drawable.ic_github), contentDescription = stringResource(R.string.github_icon), ) }, diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index dde6a3e90..d25a0629c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -38,15 +38,15 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest -import com.appunite.loudius.ui.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusText +import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.components.LoudiusListIcon import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusPlaceholderText -import com.appunite.loudius.ui.components.LoudiusText -import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar -import com.appunite.loudius.ui.theme.LoudiusTheme import java.time.LocalDateTime typealias NavigateToReviewers = (String, String, String, String) -> Unit diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 7ab8baf4e..b26d913c4 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -42,18 +42,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.ui.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton +import com.appunite.loudius.screenshots.components.LoudiusText +import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.components.LoudiusListIcon import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator -import com.appunite.loudius.ui.components.LoudiusOutlinedButton import com.appunite.loudius.ui.components.LoudiusPlaceholderText -import com.appunite.loudius.ui.components.LoudiusText -import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS -import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun ReviewersScreen( diff --git a/screenshots/build.gradle b/screenshots/build.gradle index e83dc6c32..70457943c 100644 --- a/screenshots/build.gradle +++ b/screenshots/build.gradle @@ -30,8 +30,6 @@ android { } dependencies { - implementation project(":app") - def composeBom = platform('androidx.compose:compose-bom:2023.01.00') implementation composeBom implementation 'androidx.compose.material3:material3' @@ -40,7 +38,6 @@ dependencies { implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.8.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusDialog.kt similarity index 92% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusDialog.kt index a09538499..23d32f157 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusDialog.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusDialog.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.theme.LoudiusTheme @Composable fun LoudiusDialog( @@ -30,7 +30,7 @@ fun LoudiusDialog( title: String, dismissButton: @Composable (() -> Unit)? = null, /** - * For text [com.appunite.loudius.ui.components.LoudiusTextStyle.ScreenContent] should be used + * For text [LoudiusTextStyle.ScreenContent] should be used */ text: @Composable (() -> Unit)? = null, diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusErrorDialog.kt similarity index 92% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusErrorDialog.kt index a4914802f..fe61d300e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusErrorDialog.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -23,8 +23,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.R -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.R +import com.appunite.loudius.screenshots.theme.LoudiusTheme @Composable fun LoudiusErrorDialog( diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt similarity index 96% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt index 1eee11aad..abf5874d3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement @@ -31,8 +31,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.R -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.R +import com.appunite.loudius.screenshots.theme.LoudiusTheme private const val minHeightToFitSpacer = 1856 diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusOutlinedButton.kt similarity index 96% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusOutlinedButton.kt index 8ae80bacf..373e50d24 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusOutlinedButton.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusOutlinedButton.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon @@ -26,8 +26,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.R -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.R +import com.appunite.loudius.screenshots.theme.LoudiusTheme enum class LoudiusOutlinedButtonStyle { Large, diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusText.kt similarity index 95% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusText.kt index 4bfa12c8f..b7f7ad50c 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusText.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusText.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme @@ -23,7 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.screenshots.theme.LoudiusTheme enum class LoudiusTextStyle { ListHeader, diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Color.kt similarity index 95% rename from app/src/main/java/com/appunite/loudius/ui/theme/Color.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Color.kt index a9deacbf3..6f1751ec7 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Color.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Color.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.theme +package com.appunite.loudius.screenshots.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt similarity index 98% rename from app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt index d29d43031..af76cad61 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Theme.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.theme +package com.appunite.loudius.screenshots.theme import android.app.Activity import android.os.Build diff --git a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Type.kt similarity index 97% rename from app/src/main/java/com/appunite/loudius/ui/theme/Type.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Type.kt index 05075b5e9..df5673ab7 100644 --- a/app/src/main/java/com/appunite/loudius/ui/theme/Type.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Type.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.theme +package com.appunite.loudius.screenshots.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/res/drawable-xhdpi/error_image.png b/screenshots/src/main/res/drawable/error_image.png similarity index 100% rename from app/src/main/res/drawable-xhdpi/error_image.png rename to screenshots/src/main/res/drawable/error_image.png diff --git a/app/src/main/res/drawable/ic_github.xml b/screenshots/src/main/res/drawable/ic_github.xml similarity index 100% rename from app/src/main/res/drawable/ic_github.xml rename to screenshots/src/main/res/drawable/ic_github.xml diff --git a/screenshots/src/main/res/values/strings.xml b/screenshots/src/main/res/values/strings.xml new file mode 100644 index 000000000..8cfe38d44 --- /dev/null +++ b/screenshots/src/main/res/values/strings.xml @@ -0,0 +1,32 @@ + + Loudius + Back button + Pull request + User image + Notify + Reviewed %d h ago. + Not reviewed for %d h. + Pull request # %s + Github icon + Error + Something went wrong… + OK + error image + Try again + Something went wrong…\nYou need to log in again. + Take me to login + Awesome! Your collaborator have been pinged for some serious code review action! 🎉 + Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! + Unauthorized collaborator detected! Please login again. + Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 + Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 + + + Log in + Loudius logo + Grant permission + You\'re using a Xiaomi device and you have Github App installed, please note that there\'s a known bug that requires you to grant the \"Display pup-up windows while running in the background\" permission. This will allow you to continue using the app without any interruptions. + Necessary permissions + I\'ve already granted + + From 53f134bb4bd2cf46a395494607f23d99503ca6b0 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 28 Apr 2023 15:16:09 +0200 Subject: [PATCH 374/526] Add first golden tests. --- .../loudius/screenshots/theme/Theme.kt | 17 ++--- .../loudius/screenshots/LoudiusDialogTest.kt | 70 +++++++++++++++++++ ...ts_LoudiusDialogTest_loudiusDialogTest.png | 3 + ...udiusDialogTest_loudiusErrorDialogTest.png | 3 + 4 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt create mode 100644 screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png create mode 100644 screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt index af76cad61..6a3e715f2 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt @@ -16,7 +16,6 @@ package com.appunite.loudius.screenshots.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -25,11 +24,8 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView -import androidx.core.view.ViewCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -69,16 +65,17 @@ fun LoudiusTheme( val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } + darkTheme -> DarkColorScheme else -> LightColorScheme } val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() - ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme - } - } +// if (!view.isInEditMode) { +// SideEffect { +// (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() +// ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme +// } +// } MaterialTheme( colorScheme = colorScheme, diff --git a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt new file mode 100644 index 000000000..309534eac --- /dev/null +++ b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.screenshots + +import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 +import app.cash.paparazzi.Paparazzi +import com.appunite.loudius.screenshots.components.LoudiusDialog +import com.appunite.loudius.screenshots.components.LoudiusErrorDialog +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton +import com.appunite.loudius.screenshots.components.LoudiusText +import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.theme.LoudiusTheme +import org.junit.Rule +import org.junit.Test + +class LoudiusDialogTest { + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = PIXEL_5, + ) + + @Test + fun loudiusDialogTest() { + paparazzi.snapshot { + LoudiusTheme { + LoudiusDialog( + onDismissRequest = { }, + title = "Title", + text = { + LoudiusText( + style = LoudiusTextStyle.ScreenContent, + text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse dapibus elit justo, at pharetra nulla mattis vel. Integer gravida tortor sed fringilla viverra. Duis scelerisque ante neque, a pretium eros.", + ) + }, + confirmButton = { + LoudiusOutlinedButton(text = "Confirm") {} + }, + dismissButton = { + LoudiusOutlinedButton(text = "Dismiss") {} + }, + ) + } + } + } + + @Test + fun loudiusErrorDialogTest() { + paparazzi.snapshot { + LoudiusTheme { + LoudiusErrorDialog(onConfirmButtonClick = {}) + } + } + } + + +} diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png new file mode 100644 index 000000000..14a627316 --- /dev/null +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:370b4bf51455a9feeb1e8a2ddfaf3b4f2d49798ae17ffed820002f871e4bbd3b +size 44563 diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png new file mode 100644 index 000000000..c3f094cb4 --- /dev/null +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e10f4506e0c84a7a1c1ce16a1848fd483649e3cb03f84520fa66c7bb7432bc70 +size 15496 From b7a6bb62004d19277bd44277bdaf52afdefb95f9 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Tue, 2 May 2023 13:37:30 +0200 Subject: [PATCH 375/526] chore: mock web server fun --- app/build.gradle | 12 +- .../com/appunite/loudius/LoginScreenTest.kt | 7 + .../loudius/PullRequestsScreenTest.kt | 164 ++++++++++++++++++ .../loudius/util/MockWebServerRule.kt | 94 ++++++++++ app/src/debug/AndroidManifest.xml | 2 +- .../res/xml/debug_network_security_config.xml | 6 + .../com/appunite/loudius/di/NetworkModule.kt | 2 + .../appunite/loudius/di/TestInterceptor.kt | 17 ++ 8 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt create mode 100644 app/src/debug/res/xml/debug_network_security_config.xml create mode 100644 app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt diff --git a/app/build.gradle b/app/build.gradle index 87c904f98..6b0305882 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,6 +50,15 @@ android { packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' + excludes += '/META-INF/LICENSE.md' + excludes += '/META-INF/LICENSE-notice.md' + } + } + testOptions { + packagingOptions { + jniLibs { + useLegacyPackaging true + } } } } @@ -74,7 +83,6 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' - androidTestImplementation 'androidx.compose.ui:ui-test-junit4' //Lottie - Compose implementation ("com.airbnb.android:lottie-compose:5.2.0") @@ -120,6 +128,8 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version") + androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") + androidTestImplementation("io.mockk:mockk-android:1.13.3") // ktlint ktlintRuleset project(":custom-ktlint-rules") diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 5ae5694b0..19d07ffcb 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -16,18 +16,25 @@ package com.appunite.loudius + import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.MockWebServerRule +import com.appunite.loudius.util.jsonResponse +import com.appunite.loudius.util.matchArg +import com.appunite.loudius.util.path import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import strikt.assertions.startsWith @RunWith(AndroidJUnit4::class) @HiltAndroidTest diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt new file mode 100644 index 000000000..606b928ec --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius + + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.ui.login.LoginScreen +import com.appunite.loudius.ui.pullrequests.PullRequestsScreen +import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.MockWebServerRule +import com.appunite.loudius.util.jsonResponse +import com.appunite.loudius.util.matchArg +import com.appunite.loudius.util.path +import com.appunite.loudius.util.url +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.every +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import strikt.assertions.isEqualTo +import strikt.assertions.startsWith + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +class PullRequestsScreenTest { + + @get:Rule(order = 0) + val hiltRule = HiltAndroidRule(this) + + @get:Rule + var mockWebServer: MockWebServerRule = MockWebServerRule() + + + @get:Rule(order = 1) + val composeTestRule = createAndroidComposeRule() + + @Before + fun setUp() { + hiltRule.inject() + } + + + + @Test + fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { + + every { mockWebServer.dispatcher.dispatch(matchArg { path.isEqualTo("/user") }) } returns + jsonResponse("{\"id\": 1, \"login\": \"jacek\"}") + + val jsonResponse = """ + { + "total_count":1, + "incomplete_results":false, + "items":[ + { + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", + "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", + "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", + "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", + "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "id":1, + "node_id":"example_node_id", + "number":1, + "title":"example title", + "user":{ + "login":"exampleUser", + "id":1, + "node_id":"example_user_node_id", + "avatar_url":"https://avatars.githubusercontent.com/u/1", + "gravatar_id":"", + "url":"https://api.github.com/users/exampleUser", + "html_url":"https://github.com/exampleUser", + "followers_url":"https://api.github.com/users/exampleUser/followers", + "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", + "organizations_url":"https://api.github.com/users/exampleUser/orgs", + "repos_url":"https://api.github.com/users/exampleUser/repos", + "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url":"https://api.github.com/users/exampleUser/received_events", + "type":"User", + "site_admin":false + }, + "labels":[ + + ], + "state":"open", + "locked":false, + "assignee":null, + "assignees":[ + + ], + "milestone":null, + "comments":1, + "created_at":"2023-03-07T09:21:45Z", + "updated_at":"2023-03-07T09:24:24Z", + "closed_at":null, + "author_association":"COLLABORATOR", + "active_lock_reason":null, + "draft":false, + "pull_request":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", + "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", + "merged_at":null + }, + "body":"pr only for demonstration purposes . . . .", + "reactions":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", + "total_count":0, + "+1":0, + "-1":0, + "laugh":0, + "hooray":0, + "confused":0, + "heart":0, + "rocket":0, + "eyes":0 + }, + "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", + "performed_via_github_app":null, + "state_reason":null, + "score":1.0 + } + ] + } + """.trimIndent() + + every { mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) } returns + jsonResponse(jsonResponse) + + composeTestRule.setContent { + LoudiusTheme { + PullRequestsScreen { owner, repo, pullRequestNumber, submissionTime -> Unit } + } + } + + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText("example title").assertIsDisplayed() + } +} diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt new file mode 100644 index 000000000..4e5cde60c --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -0,0 +1,94 @@ +package com.appunite.loudius.util + +import android.util.Log +import com.appunite.loudius.di.TestInterceptor +import io.mockk.MockKMatcherScope +import io.mockk.every +import io.mockk.mockk +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.Response +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import okhttp3.mockwebserver.SocketPolicy +import org.intellij.lang.annotations.Language +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import strikt.api.Assertion +import strikt.api.expectThat +import strikt.assertions.isNotNull + +private const val TAG = "MockWebServerRule" + +class MockWebServerRule : TestRule { + + val dispatcher: Dispatcher = mockk { + every { shutdown() } returns Unit + every { peek() } returns MockResponse().apply { this.socketPolicy = SocketPolicy.KEEP_OPEN } + every { dispatch(any()) } answers { + val request = it.invocation.args[0] as RecordedRequest + Log.w(TAG, "Request is not mocked: ${request.method} ${request.path}") + MockResponse().setResponseCode(404) + } + } + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + MockWebServer().use { server -> + server.dispatcher = dispatcher + TestInterceptor.testInterceptor = UrlOverrideInterceptor(server.url("/")) + Log.v(TAG, "TestInterceptor installed") + try { + + } finally { + base.evaluate() + Log.v(TAG, "TestInterceptor uninstalled") + TestInterceptor.testInterceptor = null + } + } + } + + } + } +} + +class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val newUrl = request.url.newBuilder() + .host(baseUrl.host) + .scheme(baseUrl.scheme) + .port(baseUrl.port) + .build() + Log.w(TAG, "Overriding url, from: ${request.url} to: $newUrl") + return chain.proceed(request.newBuilder().url(newUrl) + .addHeader("X-Test-Original-Url", request.url.toString()).build() + ) + } + +} + +fun jsonResponse(@Language("JSON") json: String): MockResponse = MockResponse() + .addHeader("Content-Type", "application/json") + .setBody(json) + +inline fun MockKMatcherScope.matchArg(noinline block: Assertion.Builder.() -> Unit): T { + return match { + try { + expectThat(it, block) + true + } catch (e: AssertionError) { + false + } + } +} + +@get:JvmName("recordedRequestPath") +inline val Assertion.Builder.path: Assertion.Builder get() = get(RecordedRequest::path).isNotNull() +inline val Assertion.Builder.url: Assertion.Builder get() = get(RecordedRequest::requestUrl).isNotNull() +@get:JvmName("httpUrlPath") +inline val Assertion.Builder.path: Assertion.Builder get() = get("path") { encodedPath } \ No newline at end of file diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index a27e8c399..a6615e09c 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/debug/res/xml/debug_network_security_config.xml b/app/src/debug/res/xml/debug_network_security_config.xml new file mode 100644 index 000000000..5e4ba9c97 --- /dev/null +++ b/app/src/debug/res/xml/debug_network_security_config.xml @@ -0,0 +1,6 @@ + + + + localhost + + \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt index c0fca4070..2a222f95f 100644 --- a/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt +++ b/app/src/main/java/com/appunite/loudius/di/NetworkModule.kt @@ -61,6 +61,7 @@ object NetworkModule { loggingInterceptor: HttpLoggingInterceptor, ): Retrofit { val okHttpClient = OkHttpClient.Builder() + .addInterceptor(TestInterceptor) .addInterceptor(loggingInterceptor) .build() @@ -83,6 +84,7 @@ object NetworkModule { ): Retrofit { val okHttpClient = OkHttpClient.Builder() .addInterceptor(authInterceptor) + .addInterceptor(TestInterceptor) .addInterceptor(AuthFailureInterceptor(authFailureHandler)) .addInterceptor(loggingInterceptor) .build() diff --git a/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt b/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt new file mode 100644 index 000000000..8f58d1220 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt @@ -0,0 +1,17 @@ +package com.appunite.loudius.di + +import okhttp3.Interceptor +import okhttp3.Response + +object TestInterceptor : Interceptor { + var testInterceptor: Interceptor? = null + override fun intercept(chain: Interceptor.Chain): Response { + val interceptor = testInterceptor ?: DoNothingInterceptor + return interceptor.intercept(chain) + } +} + +private object DoNothingInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) + +} \ No newline at end of file From bb2e5f0984f77b98d4aaf8c25734555c2e46e26e Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Tue, 2 May 2023 12:52:39 +0000 Subject: [PATCH 376/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/LoginScreenTest.kt | 7 ------- .../com/appunite/loudius/PullRequestsScreenTest.kt | 13 +++---------- .../com/appunite/loudius/util/MockWebServerRule.kt | 11 +++++------ .../java/com/appunite/loudius/di/TestInterceptor.kt | 3 +-- 4 files changed, 9 insertions(+), 25 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 19d07ffcb..5ae5694b0 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -16,25 +16,18 @@ package com.appunite.loudius - import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.theme.LoudiusTheme -import com.appunite.loudius.util.MockWebServerRule -import com.appunite.loudius.util.jsonResponse -import com.appunite.loudius.util.matchArg -import com.appunite.loudius.util.path import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.every import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import strikt.assertions.startsWith @RunWith(AndroidJUnit4::class) @HiltAndroidTest diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 606b928ec..1e4eeceb3 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -16,19 +16,16 @@ package com.appunite.loudius - import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.MockWebServerRule import com.appunite.loudius.util.jsonResponse import com.appunite.loudius.util.matchArg import com.appunite.loudius.util.path -import com.appunite.loudius.util.url import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import io.mockk.every @@ -49,7 +46,6 @@ class PullRequestsScreenTest { @get:Rule var mockWebServer: MockWebServerRule = MockWebServerRule() - @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() @@ -58,13 +54,10 @@ class PullRequestsScreenTest { hiltRule.inject() } - - @Test fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { - every { mockWebServer.dispatcher.dispatch(matchArg { path.isEqualTo("/user") }) } returns - jsonResponse("{\"id\": 1, \"login\": \"jacek\"}") + jsonResponse("{\"id\": 1, \"login\": \"jacek\"}") val jsonResponse = """ { @@ -146,10 +139,10 @@ class PullRequestsScreenTest { } ] } - """.trimIndent() + """.trimIndent() every { mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) } returns - jsonResponse(jsonResponse) + jsonResponse(jsonResponse) composeTestRule.setContent { LoudiusTheme { diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt index 4e5cde60c..363af0fd1 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -43,7 +43,6 @@ class MockWebServerRule : TestRule { TestInterceptor.testInterceptor = UrlOverrideInterceptor(server.url("/")) Log.v(TAG, "TestInterceptor installed") try { - } finally { base.evaluate() Log.v(TAG, "TestInterceptor uninstalled") @@ -51,7 +50,6 @@ class MockWebServerRule : TestRule { } } } - } } } @@ -65,11 +63,11 @@ class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor { .port(baseUrl.port) .build() Log.w(TAG, "Overriding url, from: ${request.url} to: $newUrl") - return chain.proceed(request.newBuilder().url(newUrl) - .addHeader("X-Test-Original-Url", request.url.toString()).build() + return chain.proceed( + request.newBuilder().url(newUrl) + .addHeader("X-Test-Original-Url", request.url.toString()).build(), ) } - } fun jsonResponse(@Language("JSON") json: String): MockResponse = MockResponse() @@ -90,5 +88,6 @@ inline fun MockKMatcherScope.matchArg(noinline block: Assertio @get:JvmName("recordedRequestPath") inline val Assertion.Builder.path: Assertion.Builder get() = get(RecordedRequest::path).isNotNull() inline val Assertion.Builder.url: Assertion.Builder get() = get(RecordedRequest::requestUrl).isNotNull() + @get:JvmName("httpUrlPath") -inline val Assertion.Builder.path: Assertion.Builder get() = get("path") { encodedPath } \ No newline at end of file +inline val Assertion.Builder.path: Assertion.Builder get() = get("path") { encodedPath } diff --git a/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt b/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt index 8f58d1220..c57ebf2fd 100644 --- a/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt @@ -13,5 +13,4 @@ object TestInterceptor : Interceptor { private object DoNothingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()) - -} \ No newline at end of file +} From 5b90408b9de8e4685b4b56f9d4dc2b25831ce3cf Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 8 May 2023 09:14:47 +0200 Subject: [PATCH 377/526] mock web server test --- .../loudius/PullRequestsScreenTest.kt | 30 ++++++++++--------- .../com/appunite/loudius/common/TestTags.kt | 22 ++++++++++++++ .../ui/pullrequests/PullRequestsScreen.kt | 6 +++- 3 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/common/TestTags.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 1e4eeceb3..a7879e147 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -18,8 +18,9 @@ package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onNodeWithTag import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.common.TestTags import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.MockWebServerRule @@ -35,6 +36,7 @@ import org.junit.Test import org.junit.runner.RunWith import strikt.assertions.isEqualTo import strikt.assertions.startsWith +import java.lang.Thread.sleep @RunWith(AndroidJUnit4::class) @HiltAndroidTest @@ -52,12 +54,18 @@ class PullRequestsScreenTest { @Before fun setUp() { hiltRule.inject() + composeTestRule.setContent { + LoudiusTheme { + PullRequestsScreen { _, _, _, _ -> } + } + } } @Test fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { - every { mockWebServer.dispatcher.dispatch(matchArg { path.isEqualTo("/user") }) } returns - jsonResponse("{\"id\": 1, \"login\": \"jacek\"}") + every { + mockWebServer.dispatcher.dispatch(matchArg { path.isEqualTo("/user") }) + } returns jsonResponse("{\"id\": 1, \"login\": \"user\"}") val jsonResponse = """ { @@ -141,17 +149,11 @@ class PullRequestsScreenTest { } """.trimIndent() - every { mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) } returns - jsonResponse(jsonResponse) - - composeTestRule.setContent { - LoudiusTheme { - PullRequestsScreen { owner, repo, pullRequestNumber, submissionTime -> Unit } - } - } - - composeTestRule.waitForIdle() + every { + mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) + } returns jsonResponse(jsonResponse) - composeTestRule.onNodeWithText("example title").assertIsDisplayed() + sleep(3000) // Temporary solution + composeTestRule.onNodeWithTag(TestTags.PULL_REQUEST_ITEM).assertIsDisplayed() } } diff --git a/app/src/main/java/com/appunite/loudius/common/TestTags.kt b/app/src/main/java/com/appunite/loudius/common/TestTags.kt new file mode 100644 index 000000000..6066e3958 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/common/TestTags.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.common + +object TestTags { + + const val PULL_REQUEST_ITEM = "PULL_REQUEST_ITEM" +} diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index dde6a3e90..aac19e81b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -31,12 +31,14 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants +import com.appunite.loudius.common.TestTags import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon @@ -148,7 +150,9 @@ private fun PullRequestItem( ) { LoudiusListItem( index = index, - modifier = Modifier.clickable { onClick(PulLRequestsAction.ItemClick(data.id)) }, + modifier = Modifier + .testTag(TestTags.PULL_REQUEST_ITEM) + .clickable { onClick(PulLRequestsAction.ItemClick(data.id)) }, icon = { modifier -> PullRequestIcon(modifier) }, content = { modifier -> RepoDetails( From 3bf1a2e2ce1e3b045c837a74ca7741fed4420277 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 9 May 2023 10:29:09 +0200 Subject: [PATCH 378/526] add body to 404 response --- .../java/com/appunite/loudius/PullRequestsScreenTest.kt | 2 +- .../java/com/appunite/loudius/util/MockWebServerRule.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index a7879e147..269e66f94 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -62,7 +62,7 @@ class PullRequestsScreenTest { } @Test - fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { + fun whenResponseIsCorrectThenPullRequestItemIsVisible() { every { mockWebServer.dispatcher.dispatch(matchArg { path.isEqualTo("/user") }) } returns jsonResponse("{\"id\": 1, \"login\": \"user\"}") diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt index 363af0fd1..6d1120e37 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -31,7 +31,7 @@ class MockWebServerRule : TestRule { every { dispatch(any()) } answers { val request = it.invocation.args[0] as RecordedRequest Log.w(TAG, "Request is not mocked: ${request.method} ${request.path}") - MockResponse().setResponseCode(404) + MockResponse().setResponseCode(404).setBody("{'error': 'Page not found'}") } } From a2236c1b81ff4f1539fe1563cb28b94fb56a7b47 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 9 May 2023 16:25:20 +0200 Subject: [PATCH 379/526] add base.evaluate() to try block --- .../java/com/appunite/loudius/util/MockWebServerRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt index 6d1120e37..cc27c98a4 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -43,8 +43,8 @@ class MockWebServerRule : TestRule { TestInterceptor.testInterceptor = UrlOverrideInterceptor(server.url("/")) Log.v(TAG, "TestInterceptor installed") try { - } finally { base.evaluate() + } finally { Log.v(TAG, "TestInterceptor uninstalled") TestInterceptor.testInterceptor = null } From ddb6015d588d241d3fec7b9db02cc9f3f984b780 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 10 May 2023 13:02:50 +0200 Subject: [PATCH 380/526] add license, remove test tags --- .../loudius/PullRequestsScreenTest.kt | 7 +++--- .../loudius/util/MockWebServerRule.kt | 16 ++++++++++++++ .../com/appunite/loudius/common/TestTags.kt | 22 ------------------- .../appunite/loudius/di/TestInterceptor.kt | 16 ++++++++++++++ .../ui/pullrequests/PullRequestsScreen.kt | 3 --- 5 files changed, 35 insertions(+), 29 deletions(-) delete mode 100644 app/src/main/java/com/appunite/loudius/common/TestTags.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 269e66f94..4646ac44f 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -18,9 +18,8 @@ package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.common.TestTags import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.MockWebServerRule @@ -82,7 +81,7 @@ class PullRequestsScreenTest { "id":1, "node_id":"example_node_id", "number":1, - "title":"example title", + "title":"First Pull-Request title", "user":{ "login":"exampleUser", "id":1, @@ -154,6 +153,6 @@ class PullRequestsScreenTest { } returns jsonResponse(jsonResponse) sleep(3000) // Temporary solution - composeTestRule.onNodeWithTag(TestTags.PULL_REQUEST_ITEM).assertIsDisplayed() + composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt index cc27c98a4..ff5a0d01b 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.util import android.util.Log diff --git a/app/src/main/java/com/appunite/loudius/common/TestTags.kt b/app/src/main/java/com/appunite/loudius/common/TestTags.kt deleted file mode 100644 index 6066e3958..000000000 --- a/app/src/main/java/com/appunite/loudius/common/TestTags.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.common - -object TestTags { - - const val PULL_REQUEST_ITEM = "PULL_REQUEST_ITEM" -} diff --git a/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt b/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt index c57ebf2fd..0a59ea524 100644 --- a/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt +++ b/app/src/main/java/com/appunite/loudius/di/TestInterceptor.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.di import okhttp3.Interceptor diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index aac19e81b..a173f6924 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -31,14 +31,12 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants -import com.appunite.loudius.common.TestTags import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon @@ -151,7 +149,6 @@ private fun PullRequestItem( LoudiusListItem( index = index, modifier = Modifier - .testTag(TestTags.PULL_REQUEST_ITEM) .clickable { onClick(PulLRequestsAction.ItemClick(data.id)) }, icon = { modifier -> PullRequestIcon(modifier) }, content = { modifier -> From f2edce61b3da47fdc8345ae9ece58e8edfcf59c9 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 11 May 2023 12:19:11 +0200 Subject: [PATCH 381/526] chore: play with idling resources --- .../loudius/PullRequestsScreenTest.kt | 37 +++++-- .../model/error/DefaultErrorResponse.kt | 4 +- .../loudius/network/utils/ApiRequester.kt | 4 +- .../ui/components/LoudiusLoadingIndicator.kt | 101 +++++++++++++++--- 4 files changed, 119 insertions(+), 27 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 4646ac44f..1c7d516d8 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -16,10 +16,14 @@ package com.appunite.loudius +import android.util.Log +import androidx.compose.ui.test.IdlingResource import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.ui.components.CountingIdlingResource +import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.MockWebServerRule @@ -35,8 +39,13 @@ import org.junit.Test import org.junit.runner.RunWith import strikt.assertions.isEqualTo import strikt.assertions.startsWith -import java.lang.Thread.sleep +//class MockWebServerRule : TestRule { +// override fun apply(base: Statement, description: Description): Statement { +// TODO("Not yet implemented") +// } +// +//} @RunWith(AndroidJUnit4::class) @HiltAndroidTest class PullRequestsScreenTest { @@ -52,12 +61,8 @@ class PullRequestsScreenTest { @Before fun setUp() { + composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) hiltRule.inject() - composeTestRule.setContent { - LoudiusTheme { - PullRequestsScreen { _, _, _, _ -> } - } - } } @Test @@ -150,9 +155,25 @@ class PullRequestsScreenTest { every { mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) - } returns jsonResponse(jsonResponse) + } answers { + Thread.sleep(7000) + jsonResponse(jsonResponse) + } + + composeTestRule.setContent { + LoudiusTheme { + PullRequestsScreen { _, _, _, _ -> } + } + } - sleep(3000) // Temporary solution composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() } } + +// move to some helpers class +private fun CountingIdlingResource.toIdlingResource(): IdlingResource = object : IdlingResource { + override val isIdleNow: Boolean + get() = this@toIdlingResource.isIdleNow + + override fun getDiagnosticMessageIfBusy(): String = this@toIdlingResource.getDiagnosticMessageIfBusy() +} \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt index 8d1764fb2..a7019d375 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt @@ -17,6 +17,6 @@ package com.appunite.loudius.network.model.error data class DefaultErrorResponse( - val message: String, - val documentationUrl: String, + val message: String?, + val documentationUrl: String?, ) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt index a4d0a254b..957f5739a 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt @@ -44,11 +44,11 @@ class ApiRequester @Inject constructor(private val gson: Gson) { } private fun getApiErrorMessageIfExist(throwable: HttpException) = try { - val errorResponse = gson.fromJson( + val errorResponse: DefaultErrorResponse? = gson.fromJson( throwable.response()?.errorBody()?.charStream(), DefaultErrorResponse::class.java, ) - errorResponse.message + errorResponse?.message } catch (throwable: JSONException) { null } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index f89baabc5..bd45d53c3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -16,13 +16,20 @@ package com.appunite.loudius.ui.components +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.provider.Settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.airbnb.lottie.compose.LottieAnimation @@ -32,25 +39,60 @@ import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme +import java.util.concurrent.atomic.AtomicInteger + @Composable -fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) - val progress by animateLottieCompositionAsState( - composition = composition, - iterations = LottieConstants.IterateForever, - ) - Box( - modifier = modifier.fillMaxSize(), - ) { - LottieAnimation( - composition = composition, - progress = { progress }, - modifier = Modifier - .align(Alignment.Center) - .size(96.dp), +fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { + IdlingResourceWrapper { + Box( + modifier = modifier.fillMaxSize(), + ) { + if (LocalContext.current.areSystemAnimationsEnabled()) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever, + ) + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier + .align(Alignment.Center) + .size(96.dp), + ) + } else { + // TODO make it centered + LoudiusText(text = "Loading...") + } + } + } +} + +@SuppressLint("ObsoleteSdkInt", "Deprecated") +private fun Context.areSystemAnimationsEnabled(): Boolean { + val duration: Float + val transition: Float + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + duration = Settings.Global.getFloat( + contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f + ) + transition = Settings.Global.getFloat( + contentResolver, + Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f + ) + } else { + duration = Settings.System.getFloat( + contentResolver, + Settings.System.ANIMATOR_DURATION_SCALE, 1.0f + ) + transition = Settings.System.getFloat( + contentResolver, + Settings.System.TRANSITION_ANIMATION_SCALE, 1.0f ) } + return duration != 0f && transition != 0f } @Preview @@ -60,3 +102,32 @@ fun LoudiusLoadingIndicatorPreview() { LoudiusLoadingIndicator() } } + +// move to some helpers class +class CountingIdlingResource(val name: String) { + private var counter = AtomicInteger(0) + val isIdleNow: Boolean get() = counter.get() == 0 + fun getDiagnosticMessageIfBusy(): String = "$name is busy" + + fun increment() { + counter.incrementAndGet() + } + fun decrement() { + counter.decrementAndGet() + } +} + + +// those two move to IdlingResourceWrapper.kt file: +val countingResource = CountingIdlingResource("IdlingResourceWrapper"); + +@Composable +fun IdlingResourceWrapper(content: @Composable () -> Unit) { + DisposableEffect(Unit) { + countingResource.increment() + onDispose { + countingResource.decrement() + } + } + content() +} \ No newline at end of file From b10e2f3f7ad942a8dadcfa9e4a11d209a845b658 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 11 May 2023 14:25:39 +0200 Subject: [PATCH 382/526] chore: add device references --- .../ui/components/LoudiusFullScreenError.kt | 52 +------------------ .../ui/components/utils/ReferenceDevices.kt | 40 +++++++++++++- 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index dc982a724..0c96df8b3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R -import com.appunite.loudius.ui.components.utils.ReferenceDevices +import com.appunite.loudius.ui.components.utils.MultiScreenPreviews import com.appunite.loudius.ui.theme.LoudiusTheme @Composable @@ -93,7 +93,7 @@ private fun ErrorText(text: String) { ) } -@Preview(showSystemUi = true) +@MultiScreenPreviews @Composable fun LoudiusErrorScreenPreview() { LoudiusTheme { @@ -104,51 +104,3 @@ fun LoudiusErrorScreenPreview() { ) } } - -@Preview(showSystemUi = true, device = ReferenceDevices.SmallPhone) -@Composable -fun LoudiusErrorScreenPreviewSmallPhone() { - LoudiusTheme { - LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), - onButtonClick = {}, - ) - } -} - -@Preview(showSystemUi = true, device = ReferenceDevices.SmallPhoneLandscape) -@Composable -fun LoudiusErrorScreenPreviewSmallPhoneLandscape() { - LoudiusTheme { - LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), - onButtonClick = {}, - ) - } -} - -@Preview(showSystemUi = true, device = ReferenceDevices.Tablet) -@Composable -fun LoudiusErrorScreenPreviewTablet() { - LoudiusTheme { - LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), - onButtonClick = {}, - ) - } -} - -@Preview(showSystemUi = true, device = ReferenceDevices.TabletPortrait) -@Composable -fun LoudiusErrorScreenPreviewTabletPortrait() { - LoudiusTheme { - LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), - onButtonClick = {}, - ) - } -} diff --git a/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt b/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt index b241cb8a2..fff86f17b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt @@ -1,10 +1,46 @@ package com.appunite.loudius.ui.components.utils import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview object ReferenceDevices { - const val SmallPhone = "spec:id=reference_phone,shape=Normal,width=350,height=600,unit=dp,dpi=420" - const val SmallPhoneLandscape = "spec:id=reference_phone,shape=Normal,width=600,height=350,unit=dp,dpi=420" + const val SmallPhone = + "spec:id=reference_phone,shape=Normal,width=350,height=600,unit=dp,dpi=420" + const val SmallPhoneLandscape = + "spec:id=reference_phone,shape=Normal,width=600,height=350,unit=dp,dpi=420" + const val Default = Devices.DEFAULT const val Tablet = Devices.TABLET const val TabletPortrait = "spec:shape=Normal,width=800,height=1280,unit=dp,dpi=420" } + +@Preview( + showSystemUi = true, + name = "small phone - portrait", + group = "multi screen", + device = ReferenceDevices.SmallPhone, +) +@Preview( + showSystemUi = true, + name = "small phone - landscape", + group = "multi screen", + device = ReferenceDevices.SmallPhoneLandscape, +) +@Preview( + showSystemUi = true, + name = "default phone", + group = "multi screen", + device = ReferenceDevices.Default, +) +@Preview( + showSystemUi = true, + name = "tablet - landscape", + group = "multi screen", + device = ReferenceDevices.Tablet, +) +@Preview( + showSystemUi = true, + name = "tablet - portrait", + group = "multi screen", + device = ReferenceDevices.TabletPortrait, +) +annotation class MultiScreenPreviews From 56ad1521d69dd9e3f05bdda784c40f66b16111cd Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Thu, 11 May 2023 12:29:51 +0000 Subject: [PATCH 383/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/ui/components/LoudiusFullScreenError.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index 0c96df8b3..cf16d05a6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.ui.components.utils.MultiScreenPreviews From 90c52fc4187f1344758a438e5c3908e4ed193d76 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 11 May 2023 14:35:06 +0200 Subject: [PATCH 384/526] idling resources --- .../loudius/PullRequestsScreenTest.kt | 18 +--- .../loudius/util/IdlingResourceExtensions.kt | 32 +++++++ .../loudius/common/CountingIdlingResource.kt | 33 +++++++ .../model/error/DefaultErrorResponse.kt | 4 +- .../loudius/network/utils/ApiRequester.kt | 4 +- .../ui/components/IdlingResourceWrapper.kt | 34 +++++++ .../ui/components/LoudiusLoadingIndicator.kt | 94 +++---------------- 7 files changed, 117 insertions(+), 102 deletions(-) create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt create mode 100644 app/src/main/java/com/appunite/loudius/common/CountingIdlingResource.kt create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/IdlingResourceWrapper.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 1c7d516d8..16830da35 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -16,16 +16,14 @@ package com.appunite.loudius -import android.util.Log -import androidx.compose.ui.test.IdlingResource import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.ui.components.CountingIdlingResource import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import com.appunite.loudius.util.MockWebServerRule import com.appunite.loudius.util.jsonResponse import com.appunite.loudius.util.matchArg @@ -40,12 +38,6 @@ import org.junit.runner.RunWith import strikt.assertions.isEqualTo import strikt.assertions.startsWith -//class MockWebServerRule : TestRule { -// override fun apply(base: Statement, description: Description): Statement { -// TODO("Not yet implemented") -// } -// -//} @RunWith(AndroidJUnit4::class) @HiltAndroidTest class PullRequestsScreenTest { @@ -169,11 +161,3 @@ class PullRequestsScreenTest { composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() } } - -// move to some helpers class -private fun CountingIdlingResource.toIdlingResource(): IdlingResource = object : IdlingResource { - override val isIdleNow: Boolean - get() = this@toIdlingResource.isIdleNow - - override fun getDiagnosticMessageIfBusy(): String = this@toIdlingResource.getDiagnosticMessageIfBusy() -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt b/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt new file mode 100644 index 000000000..5bcb1a1a5 --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.util + +import androidx.compose.ui.test.IdlingResource +import com.appunite.loudius.common.CountingIdlingResource + +object IdlingResourceExtensions { + + fun CountingIdlingResource.toIdlingResource(): IdlingResource = object : + IdlingResource { + override val isIdleNow: Boolean + get() = this@toIdlingResource.isIdleNow + + override fun getDiagnosticMessageIfBusy(): String = + this@toIdlingResource.getDiagnosticMessageIfBusy() + } +} diff --git a/app/src/main/java/com/appunite/loudius/common/CountingIdlingResource.kt b/app/src/main/java/com/appunite/loudius/common/CountingIdlingResource.kt new file mode 100644 index 000000000..2e12d6777 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/common/CountingIdlingResource.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.common + +import java.util.concurrent.atomic.AtomicInteger + +class CountingIdlingResource(val name: String) { + private var counter = AtomicInteger(0) + val isIdleNow: Boolean get() = counter.get() == 0 + fun getDiagnosticMessageIfBusy(): String = "$name is busy" + + fun increment() { + counter.incrementAndGet() + } + + fun decrement() { + counter.decrementAndGet() + } +} diff --git a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt index a7019d375..8d1764fb2 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt @@ -17,6 +17,6 @@ package com.appunite.loudius.network.model.error data class DefaultErrorResponse( - val message: String?, - val documentationUrl: String?, + val message: String, + val documentationUrl: String, ) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt index 957f5739a..a4d0a254b 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt @@ -44,11 +44,11 @@ class ApiRequester @Inject constructor(private val gson: Gson) { } private fun getApiErrorMessageIfExist(throwable: HttpException) = try { - val errorResponse: DefaultErrorResponse? = gson.fromJson( + val errorResponse = gson.fromJson( throwable.response()?.errorBody()?.charStream(), DefaultErrorResponse::class.java, ) - errorResponse?.message + errorResponse.message } catch (throwable: JSONException) { null } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/IdlingResourceWrapper.kt b/app/src/main/java/com/appunite/loudius/ui/components/IdlingResourceWrapper.kt new file mode 100644 index 000000000..35b4b6d73 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/IdlingResourceWrapper.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import com.appunite.loudius.common.CountingIdlingResource + +val countingResource = CountingIdlingResource("IdlingResourceWrapper") + +@Composable +fun IdlingResourceWrapper(content: @Composable () -> Unit) { + DisposableEffect(Unit) { + countingResource.increment() + onDispose { + countingResource.decrement() + } + } + content() +} diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index bd45d53c3..56df14604 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -16,20 +16,13 @@ package com.appunite.loudius.ui.components -import android.annotation.SuppressLint -import android.content.Context -import android.os.Build -import android.provider.Settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.airbnb.lottie.compose.LottieAnimation @@ -39,62 +32,30 @@ import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme -import java.util.concurrent.atomic.AtomicInteger @Composable -fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { +fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { IdlingResourceWrapper { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever, + ) Box( modifier = modifier.fillMaxSize(), ) { - if (LocalContext.current.areSystemAnimationsEnabled()) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.loading_indicator)) - val progress by animateLottieCompositionAsState( - composition = composition, - iterations = LottieConstants.IterateForever, - ) - LottieAnimation( - composition = composition, - progress = { progress }, - modifier = Modifier - .align(Alignment.Center) - .size(96.dp), - ) - } else { - // TODO make it centered - LoudiusText(text = "Loading...") - } + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier + .align(Alignment.Center) + .size(96.dp), + ) } } } -@SuppressLint("ObsoleteSdkInt", "Deprecated") -private fun Context.areSystemAnimationsEnabled(): Boolean { - val duration: Float - val transition: Float - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - duration = Settings.Global.getFloat( - contentResolver, - Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f - ) - transition = Settings.Global.getFloat( - contentResolver, - Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f - ) - } else { - duration = Settings.System.getFloat( - contentResolver, - Settings.System.ANIMATOR_DURATION_SCALE, 1.0f - ) - transition = Settings.System.getFloat( - contentResolver, - Settings.System.TRANSITION_ANIMATION_SCALE, 1.0f - ) - } - return duration != 0f && transition != 0f -} - @Preview @Composable fun LoudiusLoadingIndicatorPreview() { @@ -102,32 +63,3 @@ fun LoudiusLoadingIndicatorPreview() { LoudiusLoadingIndicator() } } - -// move to some helpers class -class CountingIdlingResource(val name: String) { - private var counter = AtomicInteger(0) - val isIdleNow: Boolean get() = counter.get() == 0 - fun getDiagnosticMessageIfBusy(): String = "$name is busy" - - fun increment() { - counter.incrementAndGet() - } - fun decrement() { - counter.decrementAndGet() - } -} - - -// those two move to IdlingResourceWrapper.kt file: -val countingResource = CountingIdlingResource("IdlingResourceWrapper"); - -@Composable -fun IdlingResourceWrapper(content: @Composable () -> Unit) { - DisposableEffect(Unit) { - countingResource.increment() - onDispose { - countingResource.decrement() - } - } - content() -} \ No newline at end of file From 70108bc455c6d995c0257379ea775d2646bdd6d9 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 11 May 2023 15:13:16 +0200 Subject: [PATCH 385/526] remove Thread.sleep() --- .../java/com/appunite/loudius/PullRequestsScreenTest.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 16830da35..95e6167dd 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -147,10 +147,7 @@ class PullRequestsScreenTest { every { mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) - } answers { - Thread.sleep(7000) - jsonResponse(jsonResponse) - } + } answers { jsonResponse(jsonResponse) } composeTestRule.setContent { LoudiusTheme { From fcc00574ba7f6cb1992c322efc8421cc84590eb5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 11 May 2023 15:16:25 +0200 Subject: [PATCH 386/526] answers to returns --- .../java/com/appunite/loudius/PullRequestsScreenTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 95e6167dd..f992c9982 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -147,7 +147,7 @@ class PullRequestsScreenTest { every { mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) - } answers { jsonResponse(jsonResponse) } + } returns jsonResponse(jsonResponse) composeTestRule.setContent { LoudiusTheme { From db6b8728c9aad1f1d1a3b31f8e55fed1b8f6aecd Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 11 May 2023 13:20:25 +0000 Subject: [PATCH 387/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/ui/components/LoudiusLoadingIndicator.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt index 56df14604..ae73a345a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt @@ -33,7 +33,6 @@ import com.airbnb.lottie.compose.rememberLottieComposition import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme - @Composable fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { IdlingResourceWrapper { From 3b7f97e751496bee45181e9f4fa0a1ee8f03086d Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Fri, 12 May 2023 14:13:12 +0200 Subject: [PATCH 388/526] chore: better mocking web-server --- app/build.gradle | 1 + .../loudius/PullRequestsScreenTest.kt | 204 ++++++------ .../loudius/util/MockWebServerRule.kt | 147 +++++++-- .../loudius/util/MockWebServerRuleTest.kt | 293 ++++++++++++++++++ .../com/appunite/loudius/util/assertions.kt | 29 ++ .../model/error/DefaultErrorResponse.kt | 4 +- .../loudius/network/utils/ApiRequester.kt | 4 +- 7 files changed, 550 insertions(+), 132 deletions(-) create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/assertions.kt diff --git a/app/build.gradle b/app/build.gradle index 6b0305882..ddb54f1fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,6 +119,7 @@ dependencies { testImplementation 'io.strikt:strikt-core:0.33.0' testImplementation 'io.strikt:strikt-mockk:0.33.0' androidTestImplementation 'io.strikt:strikt-core:0.33.0' + androidTestImplementation 'io.strikt:strikt-mockk:0.33.0' //testing testImplementation "io.mockk:mockk:1.13.3" diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 4646ac44f..921abec23 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -24,133 +24,145 @@ import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.MockWebServerRule import com.appunite.loudius.util.jsonResponse -import com.appunite.loudius.util.matchArg import com.appunite.loudius.util.path +import com.appunite.loudius.util.queryParameter +import com.appunite.loudius.util.url import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.every import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import strikt.api.expectThat import strikt.assertions.isEqualTo -import strikt.assertions.startsWith import java.lang.Thread.sleep @RunWith(AndroidJUnit4::class) @HiltAndroidTest class PullRequestsScreenTest { - @get:Rule(order = 0) + @get:Rule(order = 1) val hiltRule = HiltAndroidRule(this) - @get:Rule + @get:Rule(order = 0) var mockWebServer: MockWebServerRule = MockWebServerRule() - @get:Rule(order = 1) + @get:Rule(order = 2) val composeTestRule = createAndroidComposeRule() @Before fun setUp() { hiltRule.inject() - composeTestRule.setContent { - LoudiusTheme { - PullRequestsScreen { _, _, _, _ -> } - } - } } @Test fun whenResponseIsCorrectThenPullRequestItemIsVisible() { - every { - mockWebServer.dispatcher.dispatch(matchArg { path.isEqualTo("/user") }) - } returns jsonResponse("{\"id\": 1, \"login\": \"user\"}") - val jsonResponse = """ - { - "total_count":1, - "incomplete_results":false, - "items":[ + mockWebServer.register { + expectThat(it).url.path.isEqualTo("/user") + + jsonResponse("""{"id": 1, "login": "jacek"}""") + } + + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/search/issues") + queryParameter("q").isEqualTo("author:jacek type:pr state:open") + queryParameter("page").isEqualTo("0") + queryParameter("per_page").isEqualTo("100") + } + + jsonResponse( + """ { - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", - "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", - "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", - "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", - "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", - "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", - "id":1, - "node_id":"example_node_id", - "number":1, - "title":"First Pull-Request title", - "user":{ - "login":"exampleUser", - "id":1, - "node_id":"example_user_node_id", - "avatar_url":"https://avatars.githubusercontent.com/u/1", - "gravatar_id":"", - "url":"https://api.github.com/users/exampleUser", - "html_url":"https://github.com/exampleUser", - "followers_url":"https://api.github.com/users/exampleUser/followers", - "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", - "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", - "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", - "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", - "organizations_url":"https://api.github.com/users/exampleUser/orgs", - "repos_url":"https://api.github.com/users/exampleUser/repos", - "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", - "received_events_url":"https://api.github.com/users/exampleUser/received_events", - "type":"User", - "site_admin":false - }, - "labels":[ - - ], - "state":"open", - "locked":false, - "assignee":null, - "assignees":[ - - ], - "milestone":null, - "comments":1, - "created_at":"2023-03-07T09:21:45Z", - "updated_at":"2023-03-07T09:24:24Z", - "closed_at":null, - "author_association":"COLLABORATOR", - "active_lock_reason":null, - "draft":false, - "pull_request":{ - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", - "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", - "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", - "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", - "merged_at":null - }, - "body":"pr only for demonstration purposes . . . .", - "reactions":{ - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", - "total_count":0, - "+1":0, - "-1":0, - "laugh":0, - "hooray":0, - "confused":0, - "heart":0, - "rocket":0, - "eyes":0 - }, - "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", - "performed_via_github_app":null, - "state_reason":null, - "score":1.0 + "total_count":1, + "incomplete_results":false, + "items":[ + { + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", + "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", + "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", + "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", + "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "id":1, + "node_id":"example_node_id", + "number":1, + "title":"First Pull-Request title", + "user":{ + "login":"exampleUser", + "id":1, + "node_id":"example_user_node_id", + "avatar_url":"https://avatars.githubusercontent.com/u/1", + "gravatar_id":"", + "url":"https://api.github.com/users/exampleUser", + "html_url":"https://github.com/exampleUser", + "followers_url":"https://api.github.com/users/exampleUser/followers", + "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", + "organizations_url":"https://api.github.com/users/exampleUser/orgs", + "repos_url":"https://api.github.com/users/exampleUser/repos", + "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url":"https://api.github.com/users/exampleUser/received_events", + "type":"User", + "site_admin":false + }, + "labels":[ + + ], + "state":"open", + "locked":false, + "assignee":null, + "assignees":[ + + ], + "milestone":null, + "comments":1, + "created_at":"2023-03-07T09:21:45Z", + "updated_at":"2023-03-07T09:24:24Z", + "closed_at":null, + "author_association":"COLLABORATOR", + "active_lock_reason":null, + "draft":false, + "pull_request":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", + "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", + "merged_at":null + }, + "body":"pr only for demonstration purposes . . . .", + "reactions":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", + "total_count":0, + "+1":0, + "-1":0, + "laugh":0, + "hooray":0, + "confused":0, + "heart":0, + "rocket":0, + "eyes":0 + }, + "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", + "performed_via_github_app":null, + "state_reason":null, + "score":1.0 + } + ] } - ] - } - """.trimIndent() + """ + ) + } - every { - mockWebServer.dispatcher.dispatch(matchArg { path.startsWith("/search/issues") }) - } returns jsonResponse(jsonResponse) + composeTestRule.setContent { + LoudiusTheme { + PullRequestsScreen { _, _, _, _ -> } + } + } sleep(3000) // Temporary solution composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt index ff5a0d01b..f559555ab 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -18,38 +18,43 @@ package com.appunite.loudius.util import android.util.Log import com.appunite.loudius.di.TestInterceptor -import io.mockk.MockKMatcherScope -import io.mockk.every -import io.mockk.mockk +import okhttp3.Headers import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.Response import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest -import okhttp3.mockwebserver.SocketPolicy +import okio.Buffer import org.intellij.lang.annotations.Language import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import strikt.api.Assertion -import strikt.api.expectThat -import strikt.assertions.isNotNull private const val TAG = "MockWebServerRule" + +class Request( + val headers: Headers, + val method: String, + val url: HttpUrl, + val body: Buffer, +) { + override fun toString(): String = + "Request(method=$method, url=$url, headers=${headers.joinToString(separator = ",") { (key, value) -> "$key: $value" }})" +} + +typealias ResponseGenerator = (Request) -> MockResponse + class MockWebServerRule : TestRule { - val dispatcher: Dispatcher = mockk { - every { shutdown() } returns Unit - every { peek() } returns MockResponse().apply { this.socketPolicy = SocketPolicy.KEEP_OPEN } - every { dispatch(any()) } answers { - val request = it.invocation.args[0] as RecordedRequest - Log.w(TAG, "Request is not mocked: ${request.method} ${request.path}") - MockResponse().setResponseCode(404).setBody("{'error': 'Page not found'}") - } - } + private val dispatcher: MockDispatcher = MockDispatcher() + + fun register(response: ResponseGenerator) = dispatcher.register(response) + + fun clear() = dispatcher.clear() override fun apply(base: Statement, description: Description): Statement { return object : Statement() { @@ -60,6 +65,17 @@ class MockWebServerRule : TestRule { Log.v(TAG, "TestInterceptor installed") try { base.evaluate() + } catch (e: Throwable) { + if (dispatcher.errors.isEmpty()) { + throw e + } else { + throw MultipleFailuresError( + "An test exception occurred, but we also found some not mocked requests", + buildList { + add(e) + addAll(dispatcher.errors) + }) + } } finally { Log.v(TAG, "TestInterceptor uninstalled") TestInterceptor.testInterceptor = null @@ -70,7 +86,12 @@ class MockWebServerRule : TestRule { } } -class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor { +fun jsonResponse(@Language("JSON") json: String): MockResponse = MockResponse() + .addHeader("Content-Type", "application/json") + .setBody(json.trimIndent()) + + +private class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val newUrl = request.url.newBuilder() @@ -78,7 +99,6 @@ class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor { .scheme(baseUrl.scheme) .port(baseUrl.port) .build() - Log.w(TAG, "Overriding url, from: ${request.url} to: $newUrl") return chain.proceed( request.newBuilder().url(newUrl) .addHeader("X-Test-Original-Url", request.url.toString()).build(), @@ -86,24 +106,87 @@ class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor { } } -fun jsonResponse(@Language("JSON") json: String): MockResponse = MockResponse() - .addHeader("Content-Type", "application/json") - .setBody(json) +private class MockDispatcher : Dispatcher() { + + data class Mock(val response: ResponseGenerator) + + private val mocks: MutableList = mutableListOf() + val errors: MutableList = mutableListOf() + + fun register(response: ResponseGenerator) { + mocks.add(Mock(response)) + } -inline fun MockKMatcherScope.matchArg(noinline block: Assertion.Builder.() -> Unit): T { - return match { + fun clear() { + mocks.clear() + } + + override fun dispatch(request: RecordedRequest): MockResponse { try { - expectThat(it, block) - true - } catch (e: AssertionError) { - false + val mockRequest = try { + Request( + url = (request.getHeader("X-Test-Original-Url") + ?: throw Exception("No X-Test-Original-Url header, problem with mocker")).toHttpUrl(), + headers = request.headers.newBuilder().removeAll("X-Test-Original-Url").build(), + method = request.method ?: throw Exception("Nullable method in the request"), + body = request.body, + ) + } catch (e: Exception) { + throw Exception("Request: $request, is incorrect", e) + } + return runMocks(mockRequest) + } catch (e: Throwable) { + errors.add(e) + Log.w(TAG, e.message!!) + return MockResponse().setResponseCode(404) } } + + private fun runMocks(mockRequest: Request): MockResponse { + val assertionErrors = buildList { + mocks.forEach { + try { + return it.response(mockRequest) + } catch (e: AssertionError) { + add(e) + } + } + } + throw MultipleFailuresError( + "Request: ${mockRequest.method} ${mockRequest.url}, " + if (assertionErrors.isEmpty()) "there are no mocks" else "no mock is matching the request", + assertionErrors + ) + } } -@get:JvmName("recordedRequestPath") -inline val Assertion.Builder.path: Assertion.Builder get() = get(RecordedRequest::path).isNotNull() -inline val Assertion.Builder.url: Assertion.Builder get() = get(RecordedRequest::requestUrl).isNotNull() +class MultipleFailuresError(val heading: String, val failures: List) : + AssertionError(heading, failures.getOrNull(0)) { + init { + require(heading.isNotBlank()) { "Heading should not be blank" } + } + + override val message: String + get() = buildString { + append(heading) + append(" (") + append(failures.size).append(" ") + append( + when (failures.size) { + 0 -> "no failures" + 1 -> "failure" + else -> "failures" + } + ) + append(")") + append("\n") -@get:JvmName("httpUrlPath") -inline val Assertion.Builder.path: Assertion.Builder get() = get("path") { encodedPath } + failures.joinTo(this, separator = "\n") { + nullSafeMessage(it).lines().joinToString(separator = "\n") { "\t$it" } + } + } + + + private fun nullSafeMessage(failure: Throwable): String = + failure.javaClass.name + ": " + failure.message.orEmpty().ifBlank { "" } + +} diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt new file mode 100644 index 000000000..a2f4830fe --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt @@ -0,0 +1,293 @@ +package com.appunite.loudius.util + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.di.TestInterceptor +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import okhttp3.OkHttpClient +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.mockwebserver.MockResponse +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import strikt.api.expectThat +import strikt.assertions.contains +import strikt.assertions.first +import strikt.assertions.isA +import strikt.assertions.isEmpty +import strikt.assertions.isEqualTo +import strikt.assertions.isNotNull +import strikt.assertions.last +import strikt.assertions.message +import strikt.assertions.single +import strikt.mockk.captured + +@RunWith(AndroidJUnit4::class) +class MockWebServerRuleTest { + + @Suppress("DEPRECATION") + @get:Rule(order = 0) + val expectedException: ExpectedException = ExpectedException.none() + + @get:Rule(order = 1) + val mockWebServerRule: MockWebServerRule = MockWebServerRule() + + @Test + fun testWhenRequestIsMocked_requestReturnsItsValue() { + mockWebServerRule.register { _ -> + MockResponse().setBody("Hello, World!") + } + + val response = executeRequest("https://example.com/user") + + expectThat(response).and { + code.isEqualTo(200) + bodyString.isEqualTo("Hello, World!") + } + } + + @Test + fun testWhenJsonIsMocked_requestReturnsItsValueAndCorrectHeaders() { + mockWebServerRule.register { _ -> + jsonResponse("""{"krowa": "pies"}""") + } + + val response = executeRequest("https://example.com/user") + + expectThat(response).and { + code.isEqualTo(200) + headers.header("Content-Type").isEqualTo("application/json") + bodyString.isEqualTo("""{"krowa": "pies"}""") + } + } + + @Test + fun testWhenMockIsCleared_requestGetsDefault404() { + mockWebServerRule.register { _ -> + MockResponse().setBody("Hello, World!") + } + + mockWebServerRule.clear() + + val response = executeRequest("https://example.com/user") + expectThat(response).code.isEqualTo(404) + } + + @Test + fun testWhenNoRequestIsMocked_throw404() { + val response = executeRequest("https://example.com/not_mocked") + + expectThat(response).code.isEqualTo(404) + } + + @Test + fun testTestFailsAndThereIsSomeMockingIssue_theRootOfTheFailureIsOnTheFirstPlace() { + expectedException.expect(matcher { + expectThat(it).isA().message.isNotNull() + .contains("An test exception occurred, but we also found some not mocked requests") + expectThat(it).isA().get(MultipleFailuresError::failures) + .first().isA() + }) + + executeRequest("https://example.com/not_mocked") + + throw CustomException + } + + + @Test + fun testTestFailsAndThereIsSomeMockingIssue_returnInformationAboutExceptionAndNotMockedResponses() { + expectedException.expect(matcher { + expectThat(it).isA().message.isNotNull() + .contains("Request: GET https://example.com/not_mocked, there are no mocks") + + expectThat(it) + .isA() + .get(MultipleFailuresError::failures) + .last() + .isA() + .and { + message.isNotNull() + .contains("Request: GET https://example.com/not_mocked, there are no mocks") + get(MultipleFailuresError::failures).isEmpty() + } + }) + + executeRequest("https://example.com/not_mocked") + + throw CustomException + } + + @Test + fun testTestFailsAndThereIsSomeMockingIssue_listAllUnmatchedRequests() { + mockWebServerRule.register { + expectThat(it).url.path.isEqualTo("/different_url") + + MockResponse().setBody("Hello, World!") + } + expectedException.expect(matcher { + expectThat(it).isA().message.isNotNull().and { + contains("Request: GET https://example.com/not_mocked, no mock is matching the request") + contains("✗ is equal to \"/different_url\"") + contains("found \"/not_mocked\"") + } + + expectThat(it) + .isA() + .get(MultipleFailuresError::failures) + .last() + .isA() + .and { + message.isNotNull() + .contains("Request: GET https://example.com/not_mocked, no mock is matching the request") + get(MultipleFailuresError::failures) + .single() + .isA().and { + message.isNotNull().contains("✗ is equal to \"/different_url\"") + message.isNotNull().contains("found \"/not_mocked\"") + + } + } + }) + + executeRequest("https://example.com/not_mocked") + + throw CustomException + } + + + @Test + fun testWhenRequestUrlIsDifferentThenMocked_throw404() { + mockWebServerRule.register { + expectThat(it).url.path.isEqualTo("/different_url") + + MockResponse().setBody("Hello, World!") + } + + val response = executeRequest("https://example.com/not_mocked") + + expectThat(response).code.isEqualTo(404) + } + + @Test + fun testWhenRequestIsMade_passedUrlAndMethodAreCorrect() { + val slot = slot() + val mock = mockk { + every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") + } + mockWebServerRule.register(mock) + + executeRequest("https://example.com/url") { it.addHeader("Accept", "application/json") } + + expectThat(slot).captured.and { + url.path.isEqualTo("/url") + url.host.isEqualTo("example.com") + method.isEqualTo("GET") + } + } + + @Test + fun testWhenRequestIsMadeWithHeader_passedRequestHasHeaderSet() { + val slot = slot() + val mock = mockk { + every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") + } + mockWebServerRule.register(mock) + + executeRequest("https://example.com/url") { it.addHeader("Accept", "application/json") } + + expectThat(slot).captured.headers.header("Accept").isEqualTo("application/json") + } + + @Test + fun testWhenGetRequestIsMade_bodyIsEmpty() { + val slot = slot() + val mock = mockk { + every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") + } + mockWebServerRule.register(mock) + + executeRequest("https://example.com/url") + + expectThat(slot).captured.and { + body.utf8.isEmpty() + method.isEqualTo("GET") + } + } + + @Test + fun testWhenPostRequestIsMade_bodyIsSet() { + val slot = slot() + val mock = mockk { + every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") + } + mockWebServerRule.register(mock) + + executeRequest("https://example.com/url") { + it.method( + "POST", + "Request body".toRequestBody() + ) + } + + expectThat(slot).captured.and { + body.utf8.isEqualTo("Request body") + method.isEqualTo("POST") + } + } + + @Test + fun whenExceptionIsThrown_passIt() { + expectedException.expect(matcher { + expectThat(it).isA() + }) + + throw CustomException + } + + private fun executeRequest( + url: String, + builder: (okhttp3.Request.Builder) -> okhttp3.Request.Builder = { it } + ): Response { + val client = OkHttpClient.Builder() + .addInterceptor(TestInterceptor) + .build() + val request = builder( + okhttp3.Request.Builder() + .url(url) + ) + .build() + return client.newCall(request).execute() + } +} + +private object CustomException : Exception("Custom exception") + +private fun matcher(check: (T) -> Unit): Matcher = object : TypeSafeMatcher() { + override fun describeTo(description: Description) {} + + override fun describeMismatchSafely(item: T, description: Description) { + try { + check(item) + } catch (e: AssertionError) { + description.appendText(e.message) + return + } + throw RuntimeException("Something went wrong... exception should happen") + } + + override fun matchesSafely(item: T): Boolean { + return try { + check(item) + true + } catch (e: AssertionError) { + false + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appunite/loudius/util/assertions.kt b/app/src/androidTest/java/com/appunite/loudius/util/assertions.kt new file mode 100644 index 000000000..bbace355e --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/assertions.kt @@ -0,0 +1,29 @@ +package com.appunite.loudius.util + +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.Response +import okhttp3.ResponseBody +import okio.Buffer +import strikt.api.Assertion +import strikt.assertions.isNotNull + +inline val Assertion.Builder.url: Assertion.Builder get() = get(Request::url) +inline val Assertion.Builder.method: Assertion.Builder get() = get(Request::method) +@get:JvmName("requestBody") +inline val Assertion.Builder.body: Assertion.Builder get() = get(Request::body) +inline val Assertion.Builder.utf8: Assertion.Builder get() = get("utf8 string") { readUtf8() } +@get:JvmName("requestHeaders") +inline val Assertion.Builder.headers: Assertion.Builder get() = get(Request::headers) +inline val Assertion.Builder.code: Assertion.Builder get() = get("code") { code } +@get:JvmName("responseBody") +inline val Assertion.Builder.body: Assertion.Builder get() = get("body") { body } +inline val Assertion.Builder.bodyString: Assertion.Builder get() = body.isNotNull().get("utf8") { string() } +@get:JvmName("responseHeaders") +inline val Assertion.Builder.headers: Assertion.Builder get() = get("headers") { headers } +inline fun Assertion.Builder.header(name: String): Assertion.Builder = get("header $name") { get(name) } +inline val Assertion.Builder.path: Assertion.Builder get() = get("path") { this.encodedPath } +inline val Assertion.Builder.host: Assertion.Builder get() = get("host") { this.host } + +@Suppress("NOTHING_TO_INLINE") +inline fun Assertion.Builder.queryParameter(name: String): Assertion.Builder = get("query parameter \"$name\"") { this.queryParameter(name) } \ No newline at end of file diff --git a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt index 8d1764fb2..a7019d375 100644 --- a/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt +++ b/app/src/main/java/com/appunite/loudius/network/model/error/DefaultErrorResponse.kt @@ -17,6 +17,6 @@ package com.appunite.loudius.network.model.error data class DefaultErrorResponse( - val message: String, - val documentationUrl: String, + val message: String?, + val documentationUrl: String?, ) diff --git a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt index a4d0a254b..957f5739a 100644 --- a/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt +++ b/app/src/main/java/com/appunite/loudius/network/utils/ApiRequester.kt @@ -44,11 +44,11 @@ class ApiRequester @Inject constructor(private val gson: Gson) { } private fun getApiErrorMessageIfExist(throwable: HttpException) = try { - val errorResponse = gson.fromJson( + val errorResponse: DefaultErrorResponse? = gson.fromJson( throwable.response()?.errorBody()?.charStream(), DefaultErrorResponse::class.java, ) - errorResponse.message + errorResponse?.message } catch (throwable: JSONException) { null } From e7516097c8de433e75b3a6caea5c559f97442068 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Fri, 12 May 2023 15:56:01 +0200 Subject: [PATCH 389/526] chore: extract common methods --- .../loudius/util/MockWebServerRuleTest.kt | 46 ++++++++----------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt index a2f4830fe..8ee17ed07 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt @@ -2,6 +2,7 @@ package com.appunite.loudius.util import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.di.TestInterceptor +import io.mockk.CapturingSlot import io.mockk.every import io.mockk.mockk import io.mockk.slot @@ -177,13 +178,7 @@ class MockWebServerRuleTest { @Test fun testWhenRequestIsMade_passedUrlAndMethodAreCorrect() { - val slot = slot() - val mock = mockk { - every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") - } - mockWebServerRule.register(mock) - - executeRequest("https://example.com/url") { it.addHeader("Accept", "application/json") } + val slot = executeRequestAndGetMockedArgumentRequest() expectThat(slot).captured.and { url.path.isEqualTo("/url") @@ -194,26 +189,16 @@ class MockWebServerRuleTest { @Test fun testWhenRequestIsMadeWithHeader_passedRequestHasHeaderSet() { - val slot = slot() - val mock = mockk { - every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") + val slot = executeRequestAndGetMockedArgumentRequest { + it.addHeader("Accept", "application/json") } - mockWebServerRule.register(mock) - - executeRequest("https://example.com/url") { it.addHeader("Accept", "application/json") } expectThat(slot).captured.headers.header("Accept").isEqualTo("application/json") } @Test fun testWhenGetRequestIsMade_bodyIsEmpty() { - val slot = slot() - val mock = mockk { - every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") - } - mockWebServerRule.register(mock) - - executeRequest("https://example.com/url") + val slot = executeRequestAndGetMockedArgumentRequest() expectThat(slot).captured.and { body.utf8.isEmpty() @@ -223,13 +208,7 @@ class MockWebServerRuleTest { @Test fun testWhenPostRequestIsMade_bodyIsSet() { - val slot = slot() - val mock = mockk { - every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") - } - mockWebServerRule.register(mock) - - executeRequest("https://example.com/url") { + val slot = executeRequestAndGetMockedArgumentRequest { it.method( "POST", "Request body".toRequestBody() @@ -265,6 +244,19 @@ class MockWebServerRuleTest { .build() return client.newCall(request).execute() } + + private fun executeRequestAndGetMockedArgumentRequest( + requestBuilder: (okhttp3.Request.Builder) -> okhttp3.Request.Builder = { it } + ): CapturingSlot { + val slot = slot() + val mock = mockk { + every { this@mockk.invoke(capture(slot)) } returns MockResponse().setBody("Hello, World!") + } + mockWebServerRule.register(mock) + + executeRequest("https://example.com/url", requestBuilder) + return slot + } } private object CustomException : Exception("Custom exception") From bc17561d5faf376082884a94782da6082c99acf3 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Fri, 12 May 2023 16:02:23 +0200 Subject: [PATCH 390/526] chore: lint fixes --- .../loudius/PullRequestsScreenTest.kt | 1 - .../util/{assertions.kt => Assertions.kt} | 6 +- .../loudius/util/MockWebServerRule.kt | 19 ++-- .../loudius/util/MockWebServerRuleTest.kt | 105 +++++++++--------- 4 files changed, 69 insertions(+), 62 deletions(-) rename app/src/androidTest/java/com/appunite/loudius/util/{assertions.kt => Assertions.kt} (98%) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index b84868221..bdcbc5eb6 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -59,7 +59,6 @@ class PullRequestsScreenTest { @Test fun whenResponseIsCorrectThenPullRequestItemIsVisible() { - mockWebServer.register { expectThat(it).url.path.isEqualTo("/user") diff --git a/app/src/androidTest/java/com/appunite/loudius/util/assertions.kt b/app/src/androidTest/java/com/appunite/loudius/util/Assertions.kt similarity index 98% rename from app/src/androidTest/java/com/appunite/loudius/util/assertions.kt rename to app/src/androidTest/java/com/appunite/loudius/util/Assertions.kt index bbace355e..393883110 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/assertions.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/Assertions.kt @@ -10,15 +10,19 @@ import strikt.assertions.isNotNull inline val Assertion.Builder.url: Assertion.Builder get() = get(Request::url) inline val Assertion.Builder.method: Assertion.Builder get() = get(Request::method) + @get:JvmName("requestBody") inline val Assertion.Builder.body: Assertion.Builder get() = get(Request::body) inline val Assertion.Builder.utf8: Assertion.Builder get() = get("utf8 string") { readUtf8() } + @get:JvmName("requestHeaders") inline val Assertion.Builder.headers: Assertion.Builder get() = get(Request::headers) inline val Assertion.Builder.code: Assertion.Builder get() = get("code") { code } + @get:JvmName("responseBody") inline val Assertion.Builder.body: Assertion.Builder get() = get("body") { body } inline val Assertion.Builder.bodyString: Assertion.Builder get() = body.isNotNull().get("utf8") { string() } + @get:JvmName("responseHeaders") inline val Assertion.Builder.headers: Assertion.Builder get() = get("headers") { headers } inline fun Assertion.Builder.header(name: String): Assertion.Builder = get("header $name") { get(name) } @@ -26,4 +30,4 @@ inline val Assertion.Builder.path: Assertion.Builder get() = ge inline val Assertion.Builder.host: Assertion.Builder get() = get("host") { this.host } @Suppress("NOTHING_TO_INLINE") -inline fun Assertion.Builder.queryParameter(name: String): Assertion.Builder = get("query parameter \"$name\"") { this.queryParameter(name) } \ No newline at end of file +inline fun Assertion.Builder.queryParameter(name: String): Assertion.Builder = get("query parameter \"$name\"") { this.queryParameter(name) } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt index f559555ab..317b49787 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -35,12 +35,11 @@ import org.junit.runners.model.Statement private const val TAG = "MockWebServerRule" - class Request( val headers: Headers, val method: String, val url: HttpUrl, - val body: Buffer, + val body: Buffer ) { override fun toString(): String = "Request(method=$method, url=$url, headers=${headers.joinToString(separator = ",") { (key, value) -> "$key: $value" }})" @@ -74,7 +73,8 @@ class MockWebServerRule : TestRule { buildList { add(e) addAll(dispatcher.errors) - }) + } + ) } } finally { Log.v(TAG, "TestInterceptor uninstalled") @@ -90,7 +90,6 @@ fun jsonResponse(@Language("JSON") json: String): MockResponse = MockResponse() .addHeader("Content-Type", "application/json") .setBody(json.trimIndent()) - private class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() @@ -101,7 +100,7 @@ private class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor .build() return chain.proceed( request.newBuilder().url(newUrl) - .addHeader("X-Test-Original-Url", request.url.toString()).build(), + .addHeader("X-Test-Original-Url", request.url.toString()).build() ) } } @@ -125,11 +124,13 @@ private class MockDispatcher : Dispatcher() { try { val mockRequest = try { Request( - url = (request.getHeader("X-Test-Original-Url") - ?: throw Exception("No X-Test-Original-Url header, problem with mocker")).toHttpUrl(), + url = ( + request.getHeader("X-Test-Original-Url") + ?: throw Exception("No X-Test-Original-Url header, problem with mocker") + ).toHttpUrl(), headers = request.headers.newBuilder().removeAll("X-Test-Original-Url").build(), method = request.method ?: throw Exception("Nullable method in the request"), - body = request.body, + body = request.body ) } catch (e: Exception) { throw Exception("Request: $request, is incorrect", e) @@ -185,8 +186,6 @@ class MultipleFailuresError(val heading: String, val failures: List) } } - private fun nullSafeMessage(failure: Throwable): String = failure.javaClass.name + ": " + failure.message.orEmpty().ifBlank { "" } - } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt index 8ee17ed07..5240e4e06 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt @@ -89,36 +89,39 @@ class MockWebServerRuleTest { @Test fun testTestFailsAndThereIsSomeMockingIssue_theRootOfTheFailureIsOnTheFirstPlace() { - expectedException.expect(matcher { - expectThat(it).isA().message.isNotNull() - .contains("An test exception occurred, but we also found some not mocked requests") - expectThat(it).isA().get(MultipleFailuresError::failures) - .first().isA() - }) + expectedException.expect( + matcher { + expectThat(it).isA().message.isNotNull() + .contains("An test exception occurred, but we also found some not mocked requests") + expectThat(it).isA().get(MultipleFailuresError::failures) + .first().isA() + } + ) executeRequest("https://example.com/not_mocked") throw CustomException } - @Test fun testTestFailsAndThereIsSomeMockingIssue_returnInformationAboutExceptionAndNotMockedResponses() { - expectedException.expect(matcher { - expectThat(it).isA().message.isNotNull() - .contains("Request: GET https://example.com/not_mocked, there are no mocks") - - expectThat(it) - .isA() - .get(MultipleFailuresError::failures) - .last() - .isA() - .and { - message.isNotNull() - .contains("Request: GET https://example.com/not_mocked, there are no mocks") - get(MultipleFailuresError::failures).isEmpty() - } - }) + expectedException.expect( + matcher { + expectThat(it).isA().message.isNotNull() + .contains("Request: GET https://example.com/not_mocked, there are no mocks") + + expectThat(it) + .isA() + .get(MultipleFailuresError::failures) + .last() + .isA() + .and { + message.isNotNull() + .contains("Request: GET https://example.com/not_mocked, there are no mocks") + get(MultipleFailuresError::failures).isEmpty() + } + } + ) executeRequest("https://example.com/not_mocked") @@ -132,37 +135,37 @@ class MockWebServerRuleTest { MockResponse().setBody("Hello, World!") } - expectedException.expect(matcher { - expectThat(it).isA().message.isNotNull().and { - contains("Request: GET https://example.com/not_mocked, no mock is matching the request") - contains("✗ is equal to \"/different_url\"") - contains("found \"/not_mocked\"") - } - - expectThat(it) - .isA() - .get(MultipleFailuresError::failures) - .last() - .isA() - .and { - message.isNotNull() - .contains("Request: GET https://example.com/not_mocked, no mock is matching the request") - get(MultipleFailuresError::failures) - .single() - .isA().and { - message.isNotNull().contains("✗ is equal to \"/different_url\"") - message.isNotNull().contains("found \"/not_mocked\"") - - } + expectedException.expect( + matcher { + expectThat(it).isA().message.isNotNull().and { + contains("Request: GET https://example.com/not_mocked, no mock is matching the request") + contains("✗ is equal to \"/different_url\"") + contains("found \"/not_mocked\"") } - }) + + expectThat(it) + .isA() + .get(MultipleFailuresError::failures) + .last() + .isA() + .and { + message.isNotNull() + .contains("Request: GET https://example.com/not_mocked, no mock is matching the request") + get(MultipleFailuresError::failures) + .single() + .isA().and { + message.isNotNull().contains("✗ is equal to \"/different_url\"") + message.isNotNull().contains("found \"/not_mocked\"") + } + } + } + ) executeRequest("https://example.com/not_mocked") throw CustomException } - @Test fun testWhenRequestUrlIsDifferentThenMocked_throw404() { mockWebServerRule.register { @@ -223,9 +226,11 @@ class MockWebServerRuleTest { @Test fun whenExceptionIsThrown_passIt() { - expectedException.expect(matcher { - expectThat(it).isA() - }) + expectedException.expect( + matcher { + expectThat(it).isA() + } + ) throw CustomException } @@ -282,4 +287,4 @@ private fun matcher(check: (T) -> Unit): Matcher = object : TypeSafeMatch false } } -} \ No newline at end of file +} From a89104a0fc406b5728fb6ee12f8f905db8ddbba5 Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Fri, 12 May 2023 14:07:06 +0000 Subject: [PATCH 391/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/PullRequestsScreenTest.kt | 2 +- .../appunite/loudius/util/MockWebServerRule.kt | 12 ++++++------ .../loudius/util/MockWebServerRuleTest.kt | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index bdcbc5eb6..e9552abfb 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -155,7 +155,7 @@ class PullRequestsScreenTest { } ] } - """ + """, ) } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt index 317b49787..3ee4a4446 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRule.kt @@ -39,7 +39,7 @@ class Request( val headers: Headers, val method: String, val url: HttpUrl, - val body: Buffer + val body: Buffer, ) { override fun toString(): String = "Request(method=$method, url=$url, headers=${headers.joinToString(separator = ",") { (key, value) -> "$key: $value" }})" @@ -73,7 +73,7 @@ class MockWebServerRule : TestRule { buildList { add(e) addAll(dispatcher.errors) - } + }, ) } } finally { @@ -100,7 +100,7 @@ private class UrlOverrideInterceptor(private val baseUrl: HttpUrl) : Interceptor .build() return chain.proceed( request.newBuilder().url(newUrl) - .addHeader("X-Test-Original-Url", request.url.toString()).build() + .addHeader("X-Test-Original-Url", request.url.toString()).build(), ) } } @@ -130,7 +130,7 @@ private class MockDispatcher : Dispatcher() { ).toHttpUrl(), headers = request.headers.newBuilder().removeAll("X-Test-Original-Url").build(), method = request.method ?: throw Exception("Nullable method in the request"), - body = request.body + body = request.body, ) } catch (e: Exception) { throw Exception("Request: $request, is incorrect", e) @@ -155,7 +155,7 @@ private class MockDispatcher : Dispatcher() { } throw MultipleFailuresError( "Request: ${mockRequest.method} ${mockRequest.url}, " + if (assertionErrors.isEmpty()) "there are no mocks" else "no mock is matching the request", - assertionErrors + assertionErrors, ) } } @@ -176,7 +176,7 @@ class MultipleFailuresError(val heading: String, val failures: List) 0 -> "no failures" 1 -> "failure" else -> "failures" - } + }, ) append(")") append("\n") diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt index 5240e4e06..de6a6b859 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt @@ -95,7 +95,7 @@ class MockWebServerRuleTest { .contains("An test exception occurred, but we also found some not mocked requests") expectThat(it).isA().get(MultipleFailuresError::failures) .first().isA() - } + }, ) executeRequest("https://example.com/not_mocked") @@ -120,7 +120,7 @@ class MockWebServerRuleTest { .contains("Request: GET https://example.com/not_mocked, there are no mocks") get(MultipleFailuresError::failures).isEmpty() } - } + }, ) executeRequest("https://example.com/not_mocked") @@ -158,7 +158,7 @@ class MockWebServerRuleTest { message.isNotNull().contains("found \"/not_mocked\"") } } - } + }, ) executeRequest("https://example.com/not_mocked") @@ -214,7 +214,7 @@ class MockWebServerRuleTest { val slot = executeRequestAndGetMockedArgumentRequest { it.method( "POST", - "Request body".toRequestBody() + "Request body".toRequestBody(), ) } @@ -229,7 +229,7 @@ class MockWebServerRuleTest { expectedException.expect( matcher { expectThat(it).isA() - } + }, ) throw CustomException @@ -237,21 +237,21 @@ class MockWebServerRuleTest { private fun executeRequest( url: String, - builder: (okhttp3.Request.Builder) -> okhttp3.Request.Builder = { it } + builder: (okhttp3.Request.Builder) -> okhttp3.Request.Builder = { it }, ): Response { val client = OkHttpClient.Builder() .addInterceptor(TestInterceptor) .build() val request = builder( okhttp3.Request.Builder() - .url(url) + .url(url), ) .build() return client.newCall(request).execute() } private fun executeRequestAndGetMockedArgumentRequest( - requestBuilder: (okhttp3.Request.Builder) -> okhttp3.Request.Builder = { it } + requestBuilder: (okhttp3.Request.Builder) -> okhttp3.Request.Builder = { it }, ): CapturingSlot { val slot = slot() val mock = mockk { From 5bd50e9de2c0ea0b4c05afff97a5e8d9db646f7d Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Fri, 12 May 2023 16:43:06 +0200 Subject: [PATCH 392/526] chore: added license header --- .../java/com/appunite/loudius/util/Assertions.kt | 16 ++++++++++++++++ .../loudius/util/MockWebServerRuleTest.kt | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/Assertions.kt b/app/src/androidTest/java/com/appunite/loudius/util/Assertions.kt index 393883110..cbec23e6e 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/Assertions.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/Assertions.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.util import okhttp3.Headers diff --git a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt index de6a6b859..b583cc3c1 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/MockWebServerRuleTest.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.appunite.loudius.util import androidx.test.ext.junit.runners.AndroidJUnit4 From 17ab8db34fb8e31c112dc14c15fcf525a9c33f21 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 12:42:20 +0200 Subject: [PATCH 393/526] Move ReferenceDevices.kt to the screenshots module. --- .../screenshots/components/LoudiusFullScreenError.kt | 11 ++++++++--- .../screenshots}/components/utils/ReferenceDevices.kt | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/utils/ReferenceDevices.kt (95%) diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt index 1cbfd030d..91f1d0661 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.appunite.loudius.screenshots.R +import com.appunite.loudius.screenshots.components.utils.MultiScreenPreviews import com.appunite.loudius.screenshots.theme.LoudiusTheme -import com.appunite.loudius.ui.components.utils.MultiScreenPreviews @Composable @@ -57,11 +57,16 @@ fun ScreenErrorWithSpacers( onButtonClick: () -> Unit, ) { Column( - modifier = modifier.padding(32.dp).fillMaxSize(), + modifier = modifier + .padding(32.dp) + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.weight(weight = 0.15f)) - ErrorImage(modifier = Modifier.weight(weight = .35f).sizeIn(maxWidth = 400.dp, maxHeight = 400.dp).fillMaxWidth()) + ErrorImage(modifier = Modifier + .weight(weight = .35f) + .sizeIn(maxWidth = 400.dp, maxHeight = 400.dp) + .fillMaxWidth()) Spacer(modifier = Modifier.weight(weight = 0.05f)) ErrorText(text = errorText) LoudiusOutlinedButton( diff --git a/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/ReferenceDevices.kt similarity index 95% rename from app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/ReferenceDevices.kt index fff86f17b..1819d77fa 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/utils/ReferenceDevices.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/ReferenceDevices.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.ui.components.utils +package com.appunite.loudius.screenshots.components.utils import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview From f8e6cc1898ca6915035df68d79729f0326ff8268 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 13:16:59 +0200 Subject: [PATCH 394/526] Add additional screenshot tests. --- .../loudius/screenshots/LoudiusButtonTests.kt | 63 +++++++++++++++++++ .../loudius/screenshots/LoudiusDialogTest.kt | 18 +++++- ...ts_LoudiusDialogTest_loudiusDialogTest.png | 4 +- ...udiusDialogTest_loudiusErrorDialogTest.png | 4 +- ...sDialogTest_loudiusFullScreenErrorTest.png | 3 + ...LoudiusDialogTest_loudiusOulinedButton.png | 3 + ...oudiusDialogTest_loudiusOutlinedButton.png | 3 + 7 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt create mode 100644 screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png create mode 100644 screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png create mode 100644 screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOutlinedButton.png diff --git a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt new file mode 100644 index 000000000..33e867d9c --- /dev/null +++ b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.screenshots + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonIcon +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonStyle +import com.appunite.loudius.screenshots.theme.LoudiusTheme +import org.junit.Rule +import org.junit.Test + +class LoudiusButtonTests { + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_5, + renderingMode = SessionParams.RenderingMode.V_SCROLL, + showSystemUi = false, + ) + + @Test + fun loudiusOutlinedButton() { + paparazzi.snapshot { + Box(modifier = Modifier.background(Color.White)) { + LoudiusTheme(darkTheme = false) { + LoudiusOutlinedButton( + onClick = { }, + text = "Log In", + style = LoudiusOutlinedButtonStyle.Large, + icon = { + LoudiusOutlinedButtonIcon( + painter = painterResource(id = R.drawable.ic_github), + "Github Icon", + ) + }, + ) + } + } + } + } +} diff --git a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt index 309534eac..dcccd3387 100644 --- a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt +++ b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt @@ -16,10 +16,13 @@ package com.appunite.loudius.screenshots +import androidx.compose.ui.res.stringResource import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 import app.cash.paparazzi.Paparazzi +import com.android.ide.common.rendering.api.SessionParams import com.appunite.loudius.screenshots.components.LoudiusDialog import com.appunite.loudius.screenshots.components.LoudiusErrorDialog +import com.appunite.loudius.screenshots.components.LoudiusFullScreenError import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton import com.appunite.loudius.screenshots.components.LoudiusText import com.appunite.loudius.screenshots.components.LoudiusTextStyle @@ -31,6 +34,8 @@ class LoudiusDialogTest { @get:Rule val paparazzi = Paparazzi( deviceConfig = PIXEL_5, + renderingMode = SessionParams.RenderingMode.V_SCROLL, + showSystemUi = false, ) @Test @@ -66,5 +71,16 @@ class LoudiusDialogTest { } } - + @Test + fun loudiusFullScreenErrorTest() { + paparazzi.snapshot { + LoudiusTheme(darkTheme = false) { + LoudiusFullScreenError( + errorText = stringResource(id = R.string.error_dialog_text), + buttonText = stringResource(R.string.try_again), + onButtonClick = {}, + ) + } + } + } } diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png index 14a627316..23b70e73a 100644 --- a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:370b4bf51455a9feeb1e8a2ddfaf3b4f2d49798ae17ffed820002f871e4bbd3b -size 44563 +oid sha256:4bd222e8ebc495897cd5814b1e5e27bbb9c72745be658d4e5d337704ced9e767 +size 43084 diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png index c3f094cb4..12f7986b6 100644 --- a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e10f4506e0c84a7a1c1ce16a1848fd483649e3cb03f84520fa66c7bb7432bc70 -size 15496 +oid sha256:84dff7ee7c10a4cde15c3e8021f754cc634c1884cd51df8386c25db9d48de5e6 +size 13930 diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png new file mode 100644 index 000000000..3e33a5388 --- /dev/null +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fc6324ad4c8908a9c7697621ef4a95783558b37a2b5932b5a4cadc8c377cd6b +size 55745 diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png new file mode 100644 index 000000000..b14a6390a --- /dev/null +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82d235e2729960ddf8ebdc674a11ba1abaac9ce7b9c03da7727cd051fa2ae43f +size 19447 diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOutlinedButton.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOutlinedButton.png new file mode 100644 index 000000000..d4445452f --- /dev/null +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOutlinedButton.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c98321726f8315622a8df1d6382c77a2f46a3ab60c7db99a0d133a969672c3ec +size 8106 From e2cdc358d10cee80a066f4fa186d0e265855e493 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 13:17:20 +0200 Subject: [PATCH 395/526] Add the background for the LoudiusFullScreenError --- .../loudius/screenshots/components/LoudiusFullScreenError.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt index 91f1d0661..265556734 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt @@ -17,6 +17,7 @@ package com.appunite.loudius.screenshots.components import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -26,6 +27,7 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -58,6 +60,7 @@ fun ScreenErrorWithSpacers( ) { Column( modifier = modifier + .background(color = Color.White) .padding(32.dp) .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, From 5e216068383c75f3a3417c90e19f507bdbdbe527 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 13:20:01 +0200 Subject: [PATCH 396/526] Remove stale screenshot images. --- ...s.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png} | 0 ...dius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png | 3 --- 2 files changed, 3 deletions(-) rename screenshots/src/test/snapshots/images/{com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOutlinedButton.png => com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png} (100%) delete mode 100644 screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOutlinedButton.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png similarity index 100% rename from screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOutlinedButton.png rename to screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png deleted file mode 100644 index b14a6390a..000000000 --- a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusOulinedButton.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:82d235e2729960ddf8ebdc674a11ba1abaac9ce7b9c03da7727cd051fa2ae43f -size 19447 From d7ff6063631057bdba4eda8200321e52f26b5f60 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 13:53:59 +0200 Subject: [PATCH 397/526] Use composable previews in tests. --- .../loudius/screenshots/LoudiusButtonTests.kt | 32 ++++++-------- .../loudius/screenshots/LoudiusDialogTest.kt | 42 +++---------------- ...udiusButtonTests_loudiusOutlinedButton.png | 4 +- 3 files changed, 20 insertions(+), 58 deletions(-) diff --git a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt index 33e867d9c..4e02c6413 100644 --- a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt +++ b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt @@ -17,17 +17,17 @@ package com.appunite.loudius.screenshots import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import com.android.ide.common.rendering.api.SessionParams -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonIcon -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonStyle -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonDisabledPreview +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonLargePreview +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonPreview +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonWithIconLargePreview +import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonWithIconPreview import org.junit.Rule import org.junit.Test @@ -43,20 +43,12 @@ class LoudiusButtonTests { @Test fun loudiusOutlinedButton() { paparazzi.snapshot { - Box(modifier = Modifier.background(Color.White)) { - LoudiusTheme(darkTheme = false) { - LoudiusOutlinedButton( - onClick = { }, - text = "Log In", - style = LoudiusOutlinedButtonStyle.Large, - icon = { - LoudiusOutlinedButtonIcon( - painter = painterResource(id = R.drawable.ic_github), - "Github Icon", - ) - }, - ) - } + Column(Modifier.background(Color.White)) { + LoudiusOutlinedButtonWithIconLargePreview() + LoudiusOutlinedButtonDisabledPreview() + LoudiusOutlinedButtonWithIconPreview() + LoudiusOutlinedButtonLargePreview() + LoudiusOutlinedButtonPreview() } } } diff --git a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt index dcccd3387..ab3200a0e 100644 --- a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt +++ b/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt @@ -16,17 +16,12 @@ package com.appunite.loudius.screenshots -import androidx.compose.ui.res.stringResource import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 import app.cash.paparazzi.Paparazzi import com.android.ide.common.rendering.api.SessionParams -import com.appunite.loudius.screenshots.components.LoudiusDialog -import com.appunite.loudius.screenshots.components.LoudiusErrorDialog -import com.appunite.loudius.screenshots.components.LoudiusFullScreenError -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton -import com.appunite.loudius.screenshots.components.LoudiusText -import com.appunite.loudius.screenshots.components.LoudiusTextStyle -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.screenshots.components.LoudiusDialogAdvancedPreview +import com.appunite.loudius.screenshots.components.LoudiusErrorDialogPreview +import com.appunite.loudius.screenshots.components.LoudiusErrorScreenPreview import org.junit.Rule import org.junit.Test @@ -41,46 +36,21 @@ class LoudiusDialogTest { @Test fun loudiusDialogTest() { paparazzi.snapshot { - LoudiusTheme { - LoudiusDialog( - onDismissRequest = { }, - title = "Title", - text = { - LoudiusText( - style = LoudiusTextStyle.ScreenContent, - text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse dapibus elit justo, at pharetra nulla mattis vel. Integer gravida tortor sed fringilla viverra. Duis scelerisque ante neque, a pretium eros.", - ) - }, - confirmButton = { - LoudiusOutlinedButton(text = "Confirm") {} - }, - dismissButton = { - LoudiusOutlinedButton(text = "Dismiss") {} - }, - ) - } + LoudiusDialogAdvancedPreview() } } @Test fun loudiusErrorDialogTest() { paparazzi.snapshot { - LoudiusTheme { - LoudiusErrorDialog(onConfirmButtonClick = {}) - } + LoudiusErrorDialogPreview() } } @Test fun loudiusFullScreenErrorTest() { paparazzi.snapshot { - LoudiusTheme(darkTheme = false) { - LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), - onButtonClick = {}, - ) - } + LoudiusErrorScreenPreview() } } } diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png index d4445452f..0e5c42055 100644 --- a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png +++ b/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c98321726f8315622a8df1d6382c77a2f46a3ab60c7db99a0d133a969672c3ec -size 8106 +oid sha256:4b5e9a594d9e3abdd88b4384b98e0157e4528c676a6be8a1db90f65e404edaf6 +size 23354 From 8b610ab1ed1c3ba905f7cb92a6e4ff75ddffefa3 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 14:56:23 +0200 Subject: [PATCH 398/526] Fix bug with context not being activity during paparazzi tests. --- .../appunite/loudius/screenshots/theme/Theme.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt index 6a3e715f2..366540009 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt @@ -16,6 +16,7 @@ package com.appunite.loudius.screenshots.theme +import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -24,8 +25,11 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -70,12 +74,12 @@ fun LoudiusTheme( else -> LightColorScheme } val view = LocalView.current -// if (!view.isInEditMode) { -// SideEffect { -// (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() -// ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme -// } -// } + if (!view.isInEditMode) { + SideEffect { + (view.context as? Activity)?.window?.statusBarColor = colorScheme.primary.toArgb() + ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme + } + } MaterialTheme( colorScheme = colorScheme, From 160266eae3233753713ef0a47953ecdffceed066 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 15:09:57 +0200 Subject: [PATCH 399/526] Remove not needed dependencies from screenshots module. --- screenshots/build.gradle | 7 ------- 1 file changed, 7 deletions(-) diff --git a/screenshots/build.gradle b/screenshots/build.gradle index 70457943c..3b7e30109 100644 --- a/screenshots/build.gradle +++ b/screenshots/build.gradle @@ -40,11 +40,4 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - - - testImplementation "com.airbnb.android:showkase-screenshot-testing:1.0.0-beta18" - testImplementation "com.google.testparameterinjector:test-parameter-injector:1.8" - implementation "com.airbnb.android:showkase:1.0.0-beta18" - kapt "com.airbnb.android:showkase-processor:1.0.0-beta18" } From 8da0859813c6edb77c0e93ebb9b8a192fca6d5ad Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 15:22:47 +0200 Subject: [PATCH 400/526] Remove redundant strings from the screenshot module. --- .../ui/authenticating/AuthenticatingScreen.kt | 3 ++- .../appunite/loudius/ui/login/LoginScreen.kt | 3 ++- app/src/main/res/values/strings.xml | 6 ----- screenshots/src/main/res/values/strings.xml | 24 ------------------- 4 files changed, 4 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 9235adfde..34b7ea22d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R +import com.appunite.loudius.screenshots.R.string import com.appunite.loudius.screenshots.components.LoudiusFullScreenError import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.components.LoudiusLoadingIndicator @@ -68,7 +69,7 @@ private fun ShowLoudiusLoginErrorScreen( navigateToLogin: () -> Unit, ) { LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_login_text), + errorText = stringResource(id = string.error_login_text), buttonText = stringResource(id = R.string.go_to_login), onButtonClick = navigateToLogin, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index b15b16b60..797ac5f55 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTHORIZATION_URL +import com.appunite.loudius.screenshots.R.drawable import com.appunite.loudius.screenshots.components.LoudiusDialog import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonIcon @@ -90,7 +91,7 @@ fun LoginScreenStateless( style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = com.appunite.loudius.screenshots.R.drawable.ic_github), + painter = painterResource(id = drawable.ic_github), contentDescription = stringResource(R.string.github_icon), ) }, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cfe38d44..b7714e67d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,12 +8,6 @@ Not reviewed for %d h. Pull request # %s Github icon - Error - Something went wrong… - OK - error image - Try again - Something went wrong…\nYou need to log in again. Take me to login Awesome! Your collaborator have been pinged for some serious code review action! 🎉 Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! diff --git a/screenshots/src/main/res/values/strings.xml b/screenshots/src/main/res/values/strings.xml index 8cfe38d44..22440dd75 100644 --- a/screenshots/src/main/res/values/strings.xml +++ b/screenshots/src/main/res/values/strings.xml @@ -1,32 +1,8 @@ - Loudius - Back button - Pull request - User image - Notify - Reviewed %d h ago. - Not reviewed for %d h. - Pull request # %s - Github icon Error Something went wrong… OK error image Try again Something went wrong…\nYou need to log in again. - Take me to login - Awesome! Your collaborator have been pinged for some serious code review action! 🎉 - Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! - Unauthorized collaborator detected! Please login again. - Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 - Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 - - - Log in - Loudius logo - Grant permission - You\'re using a Xiaomi device and you have Github App installed, please note that there\'s a known bug that requires you to grant the \"Display pup-up windows while running in the background\" permission. This will allow you to continue using the app without any interruptions. - Necessary permissions - I\'ve already granted - From f698920ee92ae459dde72f5b412dcb1f9e1e3cf8 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 15 May 2023 16:44:21 +0200 Subject: [PATCH 401/526] Fix imports at android tests. --- .../java/com/appunite/loudius/PullRequestsScreenTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index f992c9982..aa24903ca 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -20,9 +20,9 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.pullrequests.PullRequestsScreen -import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import com.appunite.loudius.util.MockWebServerRule import com.appunite.loudius.util.jsonResponse From 30ffa043eb3dbd0a918d2d6347b39a069482442e Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 16 May 2023 10:56:08 +0200 Subject: [PATCH 402/526] trying to mock anything... --- .../appunite/loudius/ReviewersScreenTest.kt | 312 ++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt new file mode 100644 index 000000000..a3b181adc --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.lifecycle.SavedStateHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.domain.repository.PullRequestRepository +import com.appunite.loudius.ui.components.countingResource +import com.appunite.loudius.ui.reviewers.ReviewersScreen +import com.appunite.loudius.ui.reviewers.ReviewersViewModel +import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource +import com.appunite.loudius.util.MockWebServerRule +import com.appunite.loudius.util.jsonResponse +import com.appunite.loudius.util.path +import com.appunite.loudius.util.url +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.mockk +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import strikt.api.expectThat +import strikt.assertions.isEqualTo + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +class ReviewersScreenTest { + + companion object { + private const val OWNER = "owner" + private const val REPO = "repo" + private const val DATE = "2022-01-29T08:00:00" + private const val PR_NUMBER = "1" + } + + @get:Rule(order = 1) + val hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 0) + var mockWebServer: MockWebServerRule = MockWebServerRule() + + @get:Rule(order = 2) + val composeTestRule = createAndroidComposeRule() + + //@Inject + //lateinit var repository: PullRequestRepository + + // private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") +// private val systemClockFixed = +// Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) +// +// private val repository: PullRequestRepository = mockk(relaxed = true) +// private val savedStateHandle: SavedStateHandle = mockk { +// every { get("owner") } returns "exampleOwner" +// every { get("repo") } returns "repo" +// every { get("submission_date") } returns "2022-01-29T08:00:00" +// every { get("pull_request_number") } returns "correctPullRequestNumber" +// } + private var savedStateHandle: SavedStateHandle = SavedStateHandle( + mapOf( + "owner" to OWNER, + "repo" to REPO, + "submission_date" to DATE, + "pull_request_number" to PR_NUMBER + ) + ) +// private lateinit var viewModel: ReviewersViewModel +// +// private fun createViewModel() = ReviewersViewModel(repository, savedStateHandle) + + @Before + fun setUp() { + composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) + hiltRule.inject() + val repositoryMock = mockk() + val viewModel = ReviewersViewModel(repositoryMock, savedStateHandle) + +// MockKAnnotations.init(this) +// mockkStatic(Clock::class) +// every { Clock.systemDefaultZone() } returns systemClockFixed + } + + @Test + fun whenResponseIsCorrectThenReviewersAreVisible() { + + mockWebServer.register { + expectThat(it).url.path.isEqualTo("/user") + + jsonResponse("""{"id": 1, "login": "user"}""") + } + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/repos/appunite/Loudius/pulls/41/requested_reviewers") + } + + jsonResponse( + """ + { + "users": [ + { + "login": "kezc", + "id": 1, + "node_id": "1", + "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kezc", + "html_url": "https://github.com/kezc", + "followers_url": "https://api.github.com/users/kezc/followers", + "following_url": "https://api.github.com/users/kezc/following{/other_user}", + "gists_url": "https://api.github.com/users/kezc/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kezc/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kezc/subscriptions", + "organizations_url": "https://api.github.com/users/kezc/orgs", + "repos_url": "https://api.github.com/users/kezc/repos", + "events_url": "https://api.github.com/users/kezc/events{/privacy}", + "received_events_url": "https://api.github.com/users/kezc/received_events", + "type": "User", + "site_admin": false + }, + { + "login": "Krzysiudan", + "id": 1, + "node_id": "1", + "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Krzysiudan", + "html_url": "https://github.com/Krzysiudan", + "followers_url": "https://api.github.com/users/Krzysiudan/followers", + "following_url": "https://api.github.com/users/Krzysiudan/following{/other_user}", + "gists_url": "https://api.github.com/users/Krzysiudan/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Krzysiudan/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Krzysiudan/subscriptions", + "organizations_url": "https://api.github.com/users/Krzysiudan/orgs", + "repos_url": "https://api.github.com/users/Krzysiudan/repos", + "events_url": "https://api.github.com/users/Krzysiudan/events{/privacy}", + "received_events_url": "https://api.github.com/users/Krzysiudan/received_events", + "type": "User", + "site_admin": false + } + ], + "teams": [] + } + """, + ) + } + + composeTestRule.setContent { + LoudiusTheme { + ReviewersScreen { } + } + } + + composeTestRule.onRoot() + composeTestRule.onNodeWithText("kezc").assertIsDisplayed() + } + + @Test + fun whenClickOnNotifyThenNotifyReviewer() { + + } +} + +private val reviewersJsonResponse = """ + { + "users": [ + { + "login": "kezc", + "id": 1, + "node_id": "1", + "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/kezc", + "html_url": "https://github.com/kezc", + "followers_url": "https://api.github.com/users/kezc/followers", + "following_url": "https://api.github.com/users/kezc/following{/other_user}", + "gists_url": "https://api.github.com/users/kezc/gists{/gist_id}", + "starred_url": "https://api.github.com/users/kezc/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/kezc/subscriptions", + "organizations_url": "https://api.github.com/users/kezc/orgs", + "repos_url": "https://api.github.com/users/kezc/repos", + "events_url": "https://api.github.com/users/kezc/events{/privacy}", + "received_events_url": "https://api.github.com/users/kezc/received_events", + "type": "User", + "site_admin": false + }, + { + "login": "Krzysiudan", + "id": 1, + "node_id": "1", + "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Krzysiudan", + "html_url": "https://github.com/Krzysiudan", + "followers_url": "https://api.github.com/users/Krzysiudan/followers", + "following_url": "https://api.github.com/users/Krzysiudan/following{/other_user}", + "gists_url": "https://api.github.com/users/Krzysiudan/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Krzysiudan/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Krzysiudan/subscriptions", + "organizations_url": "https://api.github.com/users/Krzysiudan/orgs", + "repos_url": "https://api.github.com/users/Krzysiudan/repos", + "events_url": "https://api.github.com/users/Krzysiudan/events{/privacy}", + "received_events_url": "https://api.github.com/users/Krzysiudan/received_events", + "type": "User", + "site_admin": false + } + ], + "teams": [] + } + """.trimIndent() + +private val prsJsonResponse = """ + { + "total_count":1, + "incomplete_results":false, + "items":[ + { + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", + "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", + "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", + "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", + "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "id":1, + "node_id":"example_node_id", + "number":1, + "title":"First Pull-Request title", + "user":{ + "login":"exampleUser", + "id":1, + "node_id":"example_user_node_id", + "avatar_url":"https://avatars.githubusercontent.com/u/1", + "gravatar_id":"", + "url":"https://api.github.com/users/exampleUser", + "html_url":"https://github.com/exampleUser", + "followers_url":"https://api.github.com/users/exampleUser/followers", + "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", + "organizations_url":"https://api.github.com/users/exampleUser/orgs", + "repos_url":"https://api.github.com/users/exampleUser/repos", + "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url":"https://api.github.com/users/exampleUser/received_events", + "type":"User", + "site_admin":false + }, + "labels":[ + + ], + "state":"open", + "locked":false, + "assignee":null, + "assignees":[ + + ], + "milestone":null, + "comments":1, + "created_at":"2023-03-07T09:21:45Z", + "updated_at":"2023-03-07T09:24:24Z", + "closed_at":null, + "author_association":"COLLABORATOR", + "active_lock_reason":null, + "draft":false, + "pull_request":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", + "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", + "merged_at":null + }, + "body":"pr only for demonstration purposes . . . .", + "reactions":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", + "total_count":0, + "+1":0, + "-1":0, + "laugh":0, + "hooray":0, + "confused":0, + "heart":0, + "rocket":0, + "eyes":0 + }, + "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", + "performed_via_github_app":null, + "state_reason":null, + "score":1.0 + } + ] + } + """.trimIndent() From 37ffb80a67d983c710276336e452b5762b98794e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 16 May 2023 12:59:57 +0200 Subject: [PATCH 403/526] Remove not needed composeBom variable from build.gradle. --- screenshots/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/screenshots/build.gradle b/screenshots/build.gradle index 3b7e30109..fe13ce231 100644 --- a/screenshots/build.gradle +++ b/screenshots/build.gradle @@ -30,8 +30,7 @@ android { } dependencies { - def composeBom = platform('androidx.compose:compose-bom:2023.01.00') - implementation composeBom + implementation platform('androidx.compose:compose-bom:2023.01.00') implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling-preview' From bf13707a9f2a7b7159a697cd56cef8f09bd4003f Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 16 May 2023 13:05:10 +0200 Subject: [PATCH 404/526] Import screenshots resources with alias screenshotR. --- .../loudius/ui/authenticating/AuthenticatingScreen.kt | 4 ++-- .../main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 34b7ea22d..f92e113ea 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -22,10 +22,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.screenshots.R.string import com.appunite.loudius.screenshots.components.LoudiusFullScreenError import com.appunite.loudius.screenshots.theme.LoudiusTheme import com.appunite.loudius.ui.components.LoudiusLoadingIndicator +import com.appunite.loudius.screenshots.R as screenshotsR @Composable fun AuthenticatingScreen( @@ -69,7 +69,7 @@ private fun ShowLoudiusLoginErrorScreen( navigateToLogin: () -> Unit, ) { LoudiusFullScreenError( - errorText = stringResource(id = string.error_login_text), + errorText = stringResource(id = screenshotsR.string.error_login_text), buttonText = stringResource(id = R.string.go_to_login), onButtonClick = navigateToLogin, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 797ac5f55..1c0c57255 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -35,13 +35,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTHORIZATION_URL -import com.appunite.loudius.screenshots.R.drawable import com.appunite.loudius.screenshots.components.LoudiusDialog import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonIcon import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonStyle import com.appunite.loudius.screenshots.components.LoudiusText import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.R as screenshotsR @Composable fun LoginScreen( @@ -91,7 +91,7 @@ fun LoginScreenStateless( style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = drawable.ic_github), + painter = painterResource(id = screenshotsR.drawable.ic_github), contentDescription = stringResource(R.string.github_icon), ) }, From d985528a73b4129352755e52ece887c4335d5be6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 16 May 2023 13:33:12 +0200 Subject: [PATCH 405/526] Fix not showing ui previews at the screenshots module. --- app/build.gradle | 1 - screenshots/build.gradle | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index be498264b..e77f81b95 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,7 +83,6 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview' implementation "androidx.navigation:navigation-compose:2.5.3" debugImplementation 'androidx.compose.ui:ui-tooling' - debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' //Lottie - Compose diff --git a/screenshots/build.gradle b/screenshots/build.gradle index fe13ce231..e6acc35cb 100644 --- a/screenshots/build.gradle +++ b/screenshots/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.6.1' From 72b771bfb0258151dec7ceabe66644b129b2a671 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 16 May 2023 13:46:07 +0200 Subject: [PATCH 406/526] Move rest of the ui components to the screenshot module. --- app/build.gradle | 3 --- .../com/appunite/loudius/PullRequestsScreenTest.kt | 2 +- .../loudius/util/IdlingResourceExtensions.kt | 2 +- .../ui/authenticating/AuthenticatingScreen.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 13 +++++++------ .../loudius/ui/reviewers/ReviewersScreen.kt | 13 +++++++------ app/src/main/res/values/strings.xml | 2 -- screenshots/build.gradle | 3 +++ .../screenshots}/common/CountingIdlingResource.kt | 2 +- .../components/IdlingResourceWrapper.kt | 4 ++-- .../screenshots}/components/LoudiusListItem.kt | 13 +++++-------- .../components/LoudiusLoadingIndicator.kt | 4 ++-- .../components/LoudiusPlaceholderText.kt | 6 ++---- .../screenshots}/components/LoudiusTopAppBar.kt | 6 ++---- .../components/utils/BottomBorderModifier.kt | 2 +- .../src/main/res/drawable/arrow_back.xml | 0 .../src/main/res/drawable/person_outline_24px.xml | 0 .../src/main/res/raw/loading_indicator.json | 0 screenshots/src/main/res/values/strings.xml | 6 ++++-- 19 files changed, 39 insertions(+), 44 deletions(-) rename {app/src/main/java/com/appunite/loudius => screenshots/src/main/java/com/appunite/loudius/screenshots}/common/CountingIdlingResource.kt (95%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/IdlingResourceWrapper.kt (89%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusListItem.kt (91%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusLoadingIndicator.kt (95%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusPlaceholderText.kt (88%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/LoudiusTopAppBar.kt (91%) rename {app/src/main/java/com/appunite/loudius/ui => screenshots/src/main/java/com/appunite/loudius/screenshots}/components/utils/BottomBorderModifier.kt (96%) rename {app => screenshots}/src/main/res/drawable/arrow_back.xml (100%) rename {app => screenshots}/src/main/res/drawable/person_outline_24px.xml (100%) rename {app => screenshots}/src/main/res/raw/loading_indicator.json (100%) diff --git a/app/build.gradle b/app/build.gradle index e77f81b95..b201ca088 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,9 +85,6 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' - //Lottie - Compose - implementation ("com.airbnb.android:lottie-compose:5.2.0") - //DI - Hilt implementation "com.google.dagger:hilt-android:2.45" kapt "com.google.dagger:hilt-compiler:2.45" diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index aa24903ca..01363abec 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.screenshots.components.countingResource import com.appunite.loudius.screenshots.theme.LoudiusTheme -import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import com.appunite.loudius.util.MockWebServerRule diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt b/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt index 5bcb1a1a5..8147449a4 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt @@ -17,7 +17,7 @@ package com.appunite.loudius.util import androidx.compose.ui.test.IdlingResource -import com.appunite.loudius.common.CountingIdlingResource +import com.appunite.loudius.screenshots.common.CountingIdlingResource object IdlingResourceExtensions { diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index f92e113ea..e6043633a 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.screenshots.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusLoadingIndicator import com.appunite.loudius.screenshots.theme.LoudiusTheme -import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.screenshots.R as screenshotsR @Composable diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index fd9380d3d..47b42a7b8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -39,15 +39,16 @@ import com.appunite.loudius.R import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.screenshots.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusListIcon +import com.appunite.loudius.screenshots.components.LoudiusListItem +import com.appunite.loudius.screenshots.components.LoudiusLoadingIndicator +import com.appunite.loudius.screenshots.components.LoudiusPlaceholderText import com.appunite.loudius.screenshots.components.LoudiusText import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.components.LoudiusTopAppBar import com.appunite.loudius.screenshots.theme.LoudiusTheme -import com.appunite.loudius.ui.components.LoudiusListIcon -import com.appunite.loudius.ui.components.LoudiusListItem -import com.appunite.loudius.ui.components.LoudiusLoadingIndicator -import com.appunite.loudius.ui.components.LoudiusPlaceholderText -import com.appunite.loudius.ui.components.LoudiusTopAppBar import java.time.LocalDateTime +import com.appunite.loudius.screenshots.R as screenshotsR typealias NavigateToReviewers = (String, String, String, String) -> Unit @@ -188,7 +189,7 @@ private fun RepoDetails(modifier: Modifier, pullRequestTitle: String, repository private fun EmptyListPlaceholder(padding: PaddingValues) { Box(modifier = Modifier.padding(padding)) { LoudiusPlaceholderText( - textId = R.string.you_dont_have_any_pull_request, + textId = screenshotsR.string.you_dont_have_any_pull_request, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index b26d913c4..f4b469c8f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -43,17 +43,18 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.screenshots.components.LoudiusFullScreenError +import com.appunite.loudius.screenshots.components.LoudiusListIcon +import com.appunite.loudius.screenshots.components.LoudiusListItem +import com.appunite.loudius.screenshots.components.LoudiusLoadingIndicator import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton +import com.appunite.loudius.screenshots.components.LoudiusPlaceholderText import com.appunite.loudius.screenshots.components.LoudiusText import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.components.LoudiusTopAppBar import com.appunite.loudius.screenshots.theme.LoudiusTheme -import com.appunite.loudius.ui.components.LoudiusListIcon -import com.appunite.loudius.ui.components.LoudiusListItem -import com.appunite.loudius.ui.components.LoudiusLoadingIndicator -import com.appunite.loudius.ui.components.LoudiusPlaceholderText -import com.appunite.loudius.ui.components.LoudiusTopAppBar import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS +import com.appunite.loudius.screenshots.R as screenshotsR @Composable fun ReviewersScreen( @@ -210,7 +211,7 @@ private fun NotifyButtonOrLoadingIndicator( @Composable private fun ReviewerAvatarView(modifier: Modifier = Modifier) { LoudiusListIcon( - painter = painterResource(id = R.drawable.person_outline_24px), + painter = painterResource(id = screenshotsR.drawable.person_outline_24px), contentDescription = stringResource( R.string.details_screen_user_image_description, ), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b7714e67d..84d0d3809 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ Loudius - Back button Pull request User image Notify @@ -12,7 +11,6 @@ Awesome! Your collaborator have been pinged for some serious code review action! 🎉 Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! Unauthorized collaborator detected! Please login again. - Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 diff --git a/screenshots/build.gradle b/screenshots/build.gradle index e6acc35cb..ac71e6c21 100644 --- a/screenshots/build.gradle +++ b/screenshots/build.gradle @@ -36,6 +36,9 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' + //Lottie - Compose + implementation("com.airbnb.android:lottie-compose:5.2.0") + implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.6.1' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/com/appunite/loudius/common/CountingIdlingResource.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/common/CountingIdlingResource.kt similarity index 95% rename from app/src/main/java/com/appunite/loudius/common/CountingIdlingResource.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/common/CountingIdlingResource.kt index 2e12d6777..b2bb34558 100644 --- a/app/src/main/java/com/appunite/loudius/common/CountingIdlingResource.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/common/CountingIdlingResource.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.common +package com.appunite.loudius.screenshots.common import java.util.concurrent.atomic.AtomicInteger diff --git a/app/src/main/java/com/appunite/loudius/ui/components/IdlingResourceWrapper.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/IdlingResourceWrapper.kt similarity index 89% rename from app/src/main/java/com/appunite/loudius/ui/components/IdlingResourceWrapper.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/IdlingResourceWrapper.kt index 35b4b6d73..ea4fc7810 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/IdlingResourceWrapper.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/IdlingResourceWrapper.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import com.appunite.loudius.common.CountingIdlingResource +import com.appunite.loudius.screenshots.common.CountingIdlingResource val countingResource = CountingIdlingResource("IdlingResourceWrapper") diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusListItem.kt similarity index 91% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusListItem.kt index bf34a2ef7..3e10eb609 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusListItem.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusListItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -32,11 +32,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.R -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton -import com.appunite.loudius.screenshots.components.LoudiusText -import com.appunite.loudius.screenshots.components.LoudiusTextStyle -import com.appunite.loudius.ui.components.utils.bottomBorder +import com.appunite.loudius.screenshots.components.utils.bottomBorder +import com.appunite.loudius.screenshots.R as screenshotsR @Composable fun resolveListItemBackgroundColor(index: Int): Color = @@ -168,7 +165,7 @@ fun LoudiusListItemContentAndIconPreview() { index = 0, icon = { modifier -> LoudiusListIcon( - painter = painterResource(id = R.drawable.person_outline_24px), + painter = painterResource(id = screenshotsR.drawable.person_outline_24px), contentDescription = "Test", modifier = modifier, ) @@ -190,7 +187,7 @@ private fun LoudiusListItemExample(index: Int) { icon = { modifier -> LoudiusListIcon( modifier = modifier, - painter = painterResource(id = R.drawable.person_outline_24px), + painter = painterResource(id = screenshotsR.drawable.person_outline_24px), contentDescription = "Test", ) }, diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusLoadingIndicator.kt similarity index 95% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusLoadingIndicator.kt index 78c6cd4c7..26bed0406 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusLoadingIndicator.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusLoadingIndicator.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -30,7 +30,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition -import com.appunite.loudius.R +import com.appunite.loudius.screenshots.R import com.appunite.loudius.screenshots.theme.LoudiusTheme @Composable diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusPlaceholderText.kt similarity index 88% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusPlaceholderText.kt index 955949a04..1e96394e4 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusPlaceholderText.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box @@ -26,9 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.R -import com.appunite.loudius.screenshots.components.LoudiusText -import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.R import com.appunite.loudius.screenshots.theme.LoudiusTheme @Composable diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusTopAppBar.kt similarity index 91% rename from app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusTopAppBar.kt index c809a20c3..d85473013 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusTopAppBar.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components +package com.appunite.loudius.screenshots.components import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -26,9 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.R -import com.appunite.loudius.screenshots.components.LoudiusText -import com.appunite.loudius.screenshots.components.LoudiusTextStyle +import com.appunite.loudius.screenshots.R import com.appunite.loudius.screenshots.theme.LoudiusTheme @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/utils/BottomBorderModifier.kt b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/BottomBorderModifier.kt similarity index 96% rename from app/src/main/java/com/appunite/loudius/ui/components/utils/BottomBorderModifier.kt rename to screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/BottomBorderModifier.kt index b2e6bcd3a..5930ac98d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/utils/BottomBorderModifier.kt +++ b/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/BottomBorderModifier.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.ui.components.utils +package com.appunite.loudius.screenshots.components.utils import androidx.compose.ui.Modifier import androidx.compose.ui.composed diff --git a/app/src/main/res/drawable/arrow_back.xml b/screenshots/src/main/res/drawable/arrow_back.xml similarity index 100% rename from app/src/main/res/drawable/arrow_back.xml rename to screenshots/src/main/res/drawable/arrow_back.xml diff --git a/app/src/main/res/drawable/person_outline_24px.xml b/screenshots/src/main/res/drawable/person_outline_24px.xml similarity index 100% rename from app/src/main/res/drawable/person_outline_24px.xml rename to screenshots/src/main/res/drawable/person_outline_24px.xml diff --git a/app/src/main/res/raw/loading_indicator.json b/screenshots/src/main/res/raw/loading_indicator.json similarity index 100% rename from app/src/main/res/raw/loading_indicator.json rename to screenshots/src/main/res/raw/loading_indicator.json diff --git a/screenshots/src/main/res/values/strings.xml b/screenshots/src/main/res/values/strings.xml index 22440dd75..b32f6f07c 100644 --- a/screenshots/src/main/res/values/strings.xml +++ b/screenshots/src/main/res/values/strings.xml @@ -1,8 +1,10 @@ + Back button Error Something went wrong… - OK error image - Try again Something went wrong…\nYou need to log in again. + OK + Try again + Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 From f74314802c70af15c5fdb886e62ab975ad1611f8 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 16 May 2023 14:26:07 +0200 Subject: [PATCH 407/526] Rename screenshot module into components module. --- app/build.gradle | 3 +-- .../com/appunite/loudius/LoginScreenTest.kt | 2 +- .../loudius/PullRequestsScreenTest.kt | 4 +-- .../loudius/util/IdlingResourceExtensions.kt | 2 +- .../java/com/appunite/loudius/MainActivity.kt | 2 +- .../ui/authenticating/AuthenticatingScreen.kt | 10 ++++---- .../appunite/loudius/ui/login/LoginScreen.kt | 16 ++++++------ .../ui/pullrequests/PullRequestsScreen.kt | 22 ++++++++-------- .../loudius/ui/reviewers/ReviewersScreen.kt | 24 +++++++++--------- {screenshots => components}/build.gradle | 0 components/src/main/AndroidManifest.xml | 4 +++ .../common/CountingIdlingResource.kt | 2 +- .../components/IdlingResourceWrapper.kt | 4 +-- .../components}/components/LoudiusDialog.kt | 4 +-- .../components/LoudiusErrorDialog.kt | 6 ++--- .../components/LoudiusFullScreenError.kt | 8 +++--- .../components}/components/LoudiusListItem.kt | 10 ++++---- .../components/LoudiusLoadingIndicator.kt | 6 ++--- .../components/LoudiusOutlinedButton.kt | 6 ++--- .../components/LoudiusPlaceholderText.kt | 6 ++--- .../components}/components/LoudiusText.kt | 4 +-- .../components/LoudiusTopAppBar.kt | 6 ++--- .../components/utils/BottomBorderModifier.kt | 2 +- .../components/utils/ReferenceDevices.kt | 2 +- .../loudius/components}/theme/Color.kt | 2 +- .../loudius/components}/theme/Theme.kt | 2 +- .../loudius/components}/theme/Type.kt | 2 +- .../src/main/res/drawable/arrow_back.xml | 0 .../src/main/res/drawable/error_image.png | Bin .../src/main/res/drawable/ic_github.xml | 0 .../main/res/drawable/person_outline_24px.xml | 0 .../src/main/res/raw/loading_indicator.json | 0 .../src/main/res/values/strings.xml | 0 .../loudius/components}/LoudiusButtonTests.kt | 12 ++++----- .../loudius/components}/LoudiusDialogTest.kt | 8 +++--- ...udiusButtonTests_loudiusOutlinedButton.png | 0 ...ts_LoudiusDialogTest_loudiusDialogTest.png | 0 ...udiusDialogTest_loudiusErrorDialogTest.png | 0 ...sDialogTest_loudiusFullScreenErrorTest.png | 0 screenshots/src/main/AndroidManifest.xml | 4 --- settings.gradle | 2 +- 41 files changed, 93 insertions(+), 94 deletions(-) rename {screenshots => components}/build.gradle (100%) create mode 100644 components/src/main/AndroidManifest.xml rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/common/CountingIdlingResource.kt (95%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/IdlingResourceWrapper.kt (89%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusDialog.kt (95%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusErrorDialog.kt (92%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusFullScreenError.kt (93%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusListItem.kt (93%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusLoadingIndicator.kt (93%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusOutlinedButton.kt (96%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusPlaceholderText.kt (91%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusText.kt (95%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/LoudiusTopAppBar.kt (93%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/utils/BottomBorderModifier.kt (96%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/components/utils/ReferenceDevices.kt (95%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/theme/Color.kt (95%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/theme/Theme.kt (98%) rename {screenshots/src/main/java/com/appunite/loudius/screenshots => components/src/main/java/com/appunite/loudius/components}/theme/Type.kt (97%) rename {screenshots => components}/src/main/res/drawable/arrow_back.xml (100%) rename {screenshots => components}/src/main/res/drawable/error_image.png (100%) rename {screenshots => components}/src/main/res/drawable/ic_github.xml (100%) rename {screenshots => components}/src/main/res/drawable/person_outline_24px.xml (100%) rename {screenshots => components}/src/main/res/raw/loading_indicator.json (100%) rename {screenshots => components}/src/main/res/values/strings.xml (100%) rename {screenshots/src/test/java/com/appunite/loudius/screenshots => components/src/test/java/com/appunite/loudius/components}/LoudiusButtonTests.kt (77%) rename {screenshots/src/test/java/com/appunite/loudius/screenshots => components/src/test/java/com/appunite/loudius/components}/LoudiusDialogTest.kt (83%) rename {screenshots => components}/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png (100%) rename {screenshots => components}/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png (100%) rename {screenshots => components}/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png (100%) rename {screenshots => components}/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png (100%) delete mode 100644 screenshots/src/main/AndroidManifest.xml diff --git a/app/build.gradle b/app/build.gradle index b201ca088..d4b04a5a6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,8 +64,7 @@ android { } dependencies { - implementation project(":screenshots") - + implementation project(':components') //Desugaring for use of java.time in api lower then 26 coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 260c3c482..2e7cfd708 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.ui.login.LoginScreen import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 01363abec..ec0f9c4fa 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.screenshots.components.countingResource -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.components.countingResource +import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import com.appunite.loudius.util.MockWebServerRule diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt b/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt index 8147449a4..002569abe 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IdlingResourceExtensions.kt @@ -17,7 +17,7 @@ package com.appunite.loudius.util import androidx.compose.ui.test.IdlingResource -import com.appunite.loudius.screenshots.common.CountingIdlingResource +import com.appunite.loudius.components.common.CountingIdlingResource object IdlingResourceExtensions { diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 0653d8df7..564c1d030 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -31,7 +31,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.appunite.loudius.common.Screen -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.ui.MainViewModel import com.appunite.loudius.ui.authenticating.AuthenticatingScreen import com.appunite.loudius.ui.login.LoginScreen diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index e6043633a..92dd9cae9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -22,10 +22,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.screenshots.components.LoudiusFullScreenError -import com.appunite.loudius.screenshots.components.LoudiusLoadingIndicator -import com.appunite.loudius.screenshots.theme.LoudiusTheme -import com.appunite.loudius.screenshots.R as screenshotsR +import com.appunite.loudius.components.components.LoudiusFullScreenError +import com.appunite.loudius.components.components.LoudiusLoadingIndicator +import com.appunite.loudius.components.theme.LoudiusTheme +import com.appunite.loudius.components.R as componentsR @Composable fun AuthenticatingScreen( @@ -69,7 +69,7 @@ private fun ShowLoudiusLoginErrorScreen( navigateToLogin: () -> Unit, ) { LoudiusFullScreenError( - errorText = stringResource(id = screenshotsR.string.error_login_text), + errorText = stringResource(id = componentsR.string.error_login_text), buttonText = stringResource(id = R.string.go_to_login), onButtonClick = navigateToLogin, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 1c0c57255..a766f1ef0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -35,13 +35,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTHORIZATION_URL -import com.appunite.loudius.screenshots.components.LoudiusDialog -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonIcon -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonStyle -import com.appunite.loudius.screenshots.components.LoudiusText -import com.appunite.loudius.screenshots.components.LoudiusTextStyle -import com.appunite.loudius.screenshots.R as screenshotsR +import com.appunite.loudius.components.components.LoudiusDialog +import com.appunite.loudius.components.components.LoudiusOutlinedButton +import com.appunite.loudius.components.components.LoudiusOutlinedButtonIcon +import com.appunite.loudius.components.components.LoudiusOutlinedButtonStyle +import com.appunite.loudius.components.components.LoudiusText +import com.appunite.loudius.components.components.LoudiusTextStyle +import com.appunite.loudius.components.R as componentsR @Composable fun LoginScreen( @@ -91,7 +91,7 @@ fun LoginScreenStateless( style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = screenshotsR.drawable.ic_github), + painter = painterResource(id = componentsR.drawable.ic_github), contentDescription = stringResource(R.string.github_icon), ) }, diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 47b42a7b8..c9a0e8a2b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -37,18 +37,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants +import com.appunite.loudius.components.components.LoudiusFullScreenError +import com.appunite.loudius.components.components.LoudiusListIcon +import com.appunite.loudius.components.components.LoudiusListItem +import com.appunite.loudius.components.components.LoudiusLoadingIndicator +import com.appunite.loudius.components.components.LoudiusPlaceholderText +import com.appunite.loudius.components.components.LoudiusText +import com.appunite.loudius.components.components.LoudiusTextStyle +import com.appunite.loudius.components.components.LoudiusTopAppBar +import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.network.model.PullRequest -import com.appunite.loudius.screenshots.components.LoudiusFullScreenError -import com.appunite.loudius.screenshots.components.LoudiusListIcon -import com.appunite.loudius.screenshots.components.LoudiusListItem -import com.appunite.loudius.screenshots.components.LoudiusLoadingIndicator -import com.appunite.loudius.screenshots.components.LoudiusPlaceholderText -import com.appunite.loudius.screenshots.components.LoudiusText -import com.appunite.loudius.screenshots.components.LoudiusTextStyle -import com.appunite.loudius.screenshots.components.LoudiusTopAppBar -import com.appunite.loudius.screenshots.theme.LoudiusTheme import java.time.LocalDateTime -import com.appunite.loudius.screenshots.R as screenshotsR +import com.appunite.loudius.components.R as componentsR typealias NavigateToReviewers = (String, String, String, String) -> Unit @@ -189,7 +189,7 @@ private fun RepoDetails(modifier: Modifier, pullRequestTitle: String, repository private fun EmptyListPlaceholder(padding: PaddingValues) { Box(modifier = Modifier.padding(padding)) { LoudiusPlaceholderText( - textId = screenshotsR.string.you_dont_have_any_pull_request, + textId = componentsR.string.you_dont_have_any_pull_request, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index f4b469c8f..d59e1ca04 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -42,19 +42,19 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.screenshots.components.LoudiusFullScreenError -import com.appunite.loudius.screenshots.components.LoudiusListIcon -import com.appunite.loudius.screenshots.components.LoudiusListItem -import com.appunite.loudius.screenshots.components.LoudiusLoadingIndicator -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButton -import com.appunite.loudius.screenshots.components.LoudiusPlaceholderText -import com.appunite.loudius.screenshots.components.LoudiusText -import com.appunite.loudius.screenshots.components.LoudiusTextStyle -import com.appunite.loudius.screenshots.components.LoudiusTopAppBar -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.components.LoudiusFullScreenError +import com.appunite.loudius.components.components.LoudiusListIcon +import com.appunite.loudius.components.components.LoudiusListItem +import com.appunite.loudius.components.components.LoudiusLoadingIndicator +import com.appunite.loudius.components.components.LoudiusOutlinedButton +import com.appunite.loudius.components.components.LoudiusPlaceholderText +import com.appunite.loudius.components.components.LoudiusText +import com.appunite.loudius.components.components.LoudiusTextStyle +import com.appunite.loudius.components.components.LoudiusTopAppBar +import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.FAILURE import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS -import com.appunite.loudius.screenshots.R as screenshotsR +import com.appunite.loudius.components.R as componentsR @Composable fun ReviewersScreen( @@ -211,7 +211,7 @@ private fun NotifyButtonOrLoadingIndicator( @Composable private fun ReviewerAvatarView(modifier: Modifier = Modifier) { LoudiusListIcon( - painter = painterResource(id = screenshotsR.drawable.person_outline_24px), + painter = painterResource(id = componentsR.drawable.person_outline_24px), contentDescription = stringResource( R.string.details_screen_user_image_description, ), diff --git a/screenshots/build.gradle b/components/build.gradle similarity index 100% rename from screenshots/build.gradle rename to components/build.gradle diff --git a/components/src/main/AndroidManifest.xml b/components/src/main/AndroidManifest.xml new file mode 100644 index 000000000..70340319c --- /dev/null +++ b/components/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/common/CountingIdlingResource.kt b/components/src/main/java/com/appunite/loudius/components/common/CountingIdlingResource.kt similarity index 95% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/common/CountingIdlingResource.kt rename to components/src/main/java/com/appunite/loudius/components/common/CountingIdlingResource.kt index b2bb34558..08012afaf 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/common/CountingIdlingResource.kt +++ b/components/src/main/java/com/appunite/loudius/components/common/CountingIdlingResource.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.common +package com.appunite.loudius.components.common import java.util.concurrent.atomic.AtomicInteger diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/IdlingResourceWrapper.kt b/components/src/main/java/com/appunite/loudius/components/components/IdlingResourceWrapper.kt similarity index 89% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/IdlingResourceWrapper.kt rename to components/src/main/java/com/appunite/loudius/components/components/IdlingResourceWrapper.kt index ea4fc7810..b28b407b2 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/IdlingResourceWrapper.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/IdlingResourceWrapper.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import com.appunite.loudius.screenshots.common.CountingIdlingResource +import com.appunite.loudius.components.common.CountingIdlingResource val countingResource = CountingIdlingResource("IdlingResourceWrapper") diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusDialog.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusDialog.kt similarity index 95% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusDialog.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusDialog.kt index 23d32f157..fbea48459 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusDialog.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusDialog.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.theme.LoudiusTheme @Composable fun LoudiusDialog( diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusErrorDialog.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusErrorDialog.kt similarity index 92% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusErrorDialog.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusErrorDialog.kt index fe61d300e..27be9d6b3 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusErrorDialog.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusErrorDialog.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -23,8 +23,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.screenshots.R -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.R +import com.appunite.loudius.components.theme.LoudiusTheme @Composable fun LoudiusErrorDialog( diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt similarity index 93% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt index 265556734..c2f352de5 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusFullScreenError.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -31,9 +31,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.appunite.loudius.screenshots.R -import com.appunite.loudius.screenshots.components.utils.MultiScreenPreviews -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.R +import com.appunite.loudius.components.components.utils.MultiScreenPreviews +import com.appunite.loudius.components.theme.LoudiusTheme @Composable diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusListItem.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusListItem.kt similarity index 93% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusListItem.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusListItem.kt index 3e10eb609..8150a4a1e 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusListItem.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusListItem.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -32,8 +32,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.screenshots.components.utils.bottomBorder -import com.appunite.loudius.screenshots.R as screenshotsR +import com.appunite.loudius.components.components.utils.bottomBorder +import com.appunite.loudius.components.R as componentsR @Composable fun resolveListItemBackgroundColor(index: Int): Color = @@ -165,7 +165,7 @@ fun LoudiusListItemContentAndIconPreview() { index = 0, icon = { modifier -> LoudiusListIcon( - painter = painterResource(id = screenshotsR.drawable.person_outline_24px), + painter = painterResource(id = componentsR.drawable.person_outline_24px), contentDescription = "Test", modifier = modifier, ) @@ -187,7 +187,7 @@ private fun LoudiusListItemExample(index: Int) { icon = { modifier -> LoudiusListIcon( modifier = modifier, - painter = painterResource(id = screenshotsR.drawable.person_outline_24px), + painter = painterResource(id = componentsR.drawable.person_outline_24px), contentDescription = "Test", ) }, diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusLoadingIndicator.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusLoadingIndicator.kt similarity index 93% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusLoadingIndicator.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusLoadingIndicator.kt index 26bed0406..46358857e 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusLoadingIndicator.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusLoadingIndicator.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -30,8 +30,8 @@ import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.LottieConstants import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition -import com.appunite.loudius.screenshots.R -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.R +import com.appunite.loudius.components.theme.LoudiusTheme @Composable fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusOutlinedButton.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt similarity index 96% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusOutlinedButton.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt index 373e50d24..929a57b21 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusOutlinedButton.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon @@ -26,8 +26,8 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.screenshots.R -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.R +import com.appunite.loudius.components.theme.LoudiusTheme enum class LoudiusOutlinedButtonStyle { Large, diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusPlaceholderText.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt similarity index 91% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusPlaceholderText.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt index 1e96394e4..d37191dc7 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusPlaceholderText.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box @@ -26,8 +26,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.screenshots.R -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.R +import com.appunite.loudius.components.theme.LoudiusTheme @Composable fun LoudiusPlaceholderText(@StringRes textId: Int) { diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusText.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusText.kt similarity index 95% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusText.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusText.kt index b7f7ad50c..34829ff4c 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusText.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusText.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme @@ -23,7 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.theme.LoudiusTheme enum class LoudiusTextStyle { ListHeader, diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusTopAppBar.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusTopAppBar.kt similarity index 93% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusTopAppBar.kt rename to components/src/main/java/com/appunite/loudius/components/components/LoudiusTopAppBar.kt index d85473013..fd5b183ef 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/LoudiusTopAppBar.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusTopAppBar.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components +package com.appunite.loudius.components.components import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.appunite.loudius.screenshots.R -import com.appunite.loudius.screenshots.theme.LoudiusTheme +import com.appunite.loudius.components.R +import com.appunite.loudius.components.theme.LoudiusTheme @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/BottomBorderModifier.kt b/components/src/main/java/com/appunite/loudius/components/components/utils/BottomBorderModifier.kt similarity index 96% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/BottomBorderModifier.kt rename to components/src/main/java/com/appunite/loudius/components/components/utils/BottomBorderModifier.kt index 5930ac98d..7a6fec8bb 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/BottomBorderModifier.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/utils/BottomBorderModifier.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.components.utils +package com.appunite.loudius.components.components.utils import androidx.compose.ui.Modifier import androidx.compose.ui.composed diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/ReferenceDevices.kt b/components/src/main/java/com/appunite/loudius/components/components/utils/ReferenceDevices.kt similarity index 95% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/ReferenceDevices.kt rename to components/src/main/java/com/appunite/loudius/components/components/utils/ReferenceDevices.kt index 1819d77fa..4615af329 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/components/utils/ReferenceDevices.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/utils/ReferenceDevices.kt @@ -1,4 +1,4 @@ -package com.appunite.loudius.screenshots.components.utils +package com.appunite.loudius.components.components.utils import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Color.kt b/components/src/main/java/com/appunite/loudius/components/theme/Color.kt similarity index 95% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Color.kt rename to components/src/main/java/com/appunite/loudius/components/theme/Color.kt index 6f1751ec7..a4c3ec6a0 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Color.kt +++ b/components/src/main/java/com/appunite/loudius/components/theme/Color.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.theme +package com.appunite.loudius.components.theme import androidx.compose.ui.graphics.Color diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt b/components/src/main/java/com/appunite/loudius/components/theme/Theme.kt similarity index 98% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt rename to components/src/main/java/com/appunite/loudius/components/theme/Theme.kt index 366540009..35399a09b 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Theme.kt +++ b/components/src/main/java/com/appunite/loudius/components/theme/Theme.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.theme +package com.appunite.loudius.components.theme import android.app.Activity import android.os.Build diff --git a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Type.kt b/components/src/main/java/com/appunite/loudius/components/theme/Type.kt similarity index 97% rename from screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Type.kt rename to components/src/main/java/com/appunite/loudius/components/theme/Type.kt index df5673ab7..736a01be9 100644 --- a/screenshots/src/main/java/com/appunite/loudius/screenshots/theme/Type.kt +++ b/components/src/main/java/com/appunite/loudius/components/theme/Type.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots.theme +package com.appunite.loudius.components.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/screenshots/src/main/res/drawable/arrow_back.xml b/components/src/main/res/drawable/arrow_back.xml similarity index 100% rename from screenshots/src/main/res/drawable/arrow_back.xml rename to components/src/main/res/drawable/arrow_back.xml diff --git a/screenshots/src/main/res/drawable/error_image.png b/components/src/main/res/drawable/error_image.png similarity index 100% rename from screenshots/src/main/res/drawable/error_image.png rename to components/src/main/res/drawable/error_image.png diff --git a/screenshots/src/main/res/drawable/ic_github.xml b/components/src/main/res/drawable/ic_github.xml similarity index 100% rename from screenshots/src/main/res/drawable/ic_github.xml rename to components/src/main/res/drawable/ic_github.xml diff --git a/screenshots/src/main/res/drawable/person_outline_24px.xml b/components/src/main/res/drawable/person_outline_24px.xml similarity index 100% rename from screenshots/src/main/res/drawable/person_outline_24px.xml rename to components/src/main/res/drawable/person_outline_24px.xml diff --git a/screenshots/src/main/res/raw/loading_indicator.json b/components/src/main/res/raw/loading_indicator.json similarity index 100% rename from screenshots/src/main/res/raw/loading_indicator.json rename to components/src/main/res/raw/loading_indicator.json diff --git a/screenshots/src/main/res/values/strings.xml b/components/src/main/res/values/strings.xml similarity index 100% rename from screenshots/src/main/res/values/strings.xml rename to components/src/main/res/values/strings.xml diff --git a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt similarity index 77% rename from screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt rename to components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt index 4e02c6413..6da4285a3 100644 --- a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusButtonTests.kt +++ b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots +package com.appunite.loudius.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -23,11 +23,11 @@ import androidx.compose.ui.graphics.Color import app.cash.paparazzi.DeviceConfig import app.cash.paparazzi.Paparazzi import com.android.ide.common.rendering.api.SessionParams -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonDisabledPreview -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonLargePreview -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonPreview -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonWithIconLargePreview -import com.appunite.loudius.screenshots.components.LoudiusOutlinedButtonWithIconPreview +import com.appunite.loudius.components.components.LoudiusOutlinedButtonDisabledPreview +import com.appunite.loudius.components.components.LoudiusOutlinedButtonLargePreview +import com.appunite.loudius.components.components.LoudiusOutlinedButtonPreview +import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconLargePreview +import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconPreview import org.junit.Rule import org.junit.Test diff --git a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt b/components/src/test/java/com/appunite/loudius/components/LoudiusDialogTest.kt similarity index 83% rename from screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt rename to components/src/test/java/com/appunite/loudius/components/LoudiusDialogTest.kt index ab3200a0e..804573c2d 100644 --- a/screenshots/src/test/java/com/appunite/loudius/screenshots/LoudiusDialogTest.kt +++ b/components/src/test/java/com/appunite/loudius/components/LoudiusDialogTest.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package com.appunite.loudius.screenshots +package com.appunite.loudius.components import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 import app.cash.paparazzi.Paparazzi import com.android.ide.common.rendering.api.SessionParams -import com.appunite.loudius.screenshots.components.LoudiusDialogAdvancedPreview -import com.appunite.loudius.screenshots.components.LoudiusErrorDialogPreview -import com.appunite.loudius.screenshots.components.LoudiusErrorScreenPreview +import com.appunite.loudius.components.components.LoudiusDialogAdvancedPreview +import com.appunite.loudius.components.components.LoudiusErrorDialogPreview +import com.appunite.loudius.components.components.LoudiusErrorScreenPreview import org.junit.Rule import org.junit.Test diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png b/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png similarity index 100% rename from screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png rename to components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png b/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png similarity index 100% rename from screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png rename to components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png b/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png similarity index 100% rename from screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png rename to components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png diff --git a/screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png b/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png similarity index 100% rename from screenshots/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png rename to components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png diff --git a/screenshots/src/main/AndroidManifest.xml b/screenshots/src/main/AndroidManifest.xml deleted file mode 100644 index c6dcb2bbd..000000000 --- a/screenshots/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/settings.gradle b/settings.gradle index ede55b04a..7e7a41fb0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,4 @@ dependencyResolutionManagement { rootProject.name = "Loudius" include ':app' include ':custom-ktlint-rules' -include ':screenshots' +include ':components' From 1a4ccd962e45363af413e2529e1869aa49a1b206 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Mon, 15 May 2023 08:36:58 +0200 Subject: [PATCH 408/526] Add test checking if correct intent is created --- app/build.gradle | 2 ++ .../com/appunite/loudius/LoginScreenTest.kt | 26 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6b0305882..d2795a5b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -131,6 +131,8 @@ dependencies { androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") androidTestImplementation("io.mockk:mockk-android:1.13.3") + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' + // ktlint ktlintRuleset project(":custom-ktlint-rules") } diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 5ae5694b0..9f1114f63 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -16,19 +16,29 @@ package com.appunite.loudius -import androidx.compose.ui.test.assertIsDisplayed +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.intent.matcher.IntentMatchers.hasData import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.Matchers.allOf import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith + @RunWith(AndroidJUnit4::class) @HiltAndroidTest class LoginScreenTest { @@ -46,12 +56,24 @@ class LoginScreenTest { @Test fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { + Intents.init() + intending(hasAction(Intent.ACTION_VIEW)) + .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) + composeTestRule.setContent { LoudiusTheme { LoginScreen() } } - composeTestRule.onNodeWithText("Log in").assertIsDisplayed() + composeTestRule.onNodeWithText("Log in").performClick() + + intended( + allOf( + hasAction(Intent.ACTION_VIEW), + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") + ) + ) + Intents.release() } } From e85c9707a78e4f341ef125dd433c707e27e438fc Mon Sep 17 00:00:00 2001 From: kezc Date: Wed, 17 May 2023 08:23:43 +0000 Subject: [PATCH 409/526] [MegaLinter] Apply linters fixes --- .../androidTest/java/com/appunite/loudius/LoginScreenTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 9f1114f63..421be3e45 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -38,7 +38,6 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith - @RunWith(AndroidJUnit4::class) @HiltAndroidTest class LoginScreenTest { @@ -71,8 +70,8 @@ class LoginScreenTest { intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") - ) + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), + ), ) Intents.release() } From c2689eb22cdb41053be41dd1b0596bd723c689eb Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 17 May 2023 11:09:01 +0200 Subject: [PATCH 410/526] mock getInitialValues() --- .../appunite/loudius/ReviewersScreenTest.kt | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index a3b181adc..300385a1d 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -22,10 +22,9 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.lifecycle.SavedStateHandle import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.domain.repository.PullRequestRepository +import com.appunite.loudius.common.Screen import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.reviewers.ReviewersScreen -import com.appunite.loudius.ui.reviewers.ReviewersViewModel import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import com.appunite.loudius.util.MockWebServerRule @@ -34,13 +33,18 @@ import com.appunite.loudius.util.path import com.appunite.loudius.util.url import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.mockk +import io.mockk.every +import io.mockk.mockkStatic import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import strikt.api.expectThat import strikt.assertions.isEqualTo +import java.time.Clock +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset @RunWith(AndroidJUnit4::class) @HiltAndroidTest @@ -65,25 +69,12 @@ class ReviewersScreenTest { //@Inject //lateinit var repository: PullRequestRepository - // private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") -// private val systemClockFixed = -// Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) + private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") + private val systemClockFixed = + Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) // // private val repository: PullRequestRepository = mockk(relaxed = true) -// private val savedStateHandle: SavedStateHandle = mockk { -// every { get("owner") } returns "exampleOwner" -// every { get("repo") } returns "repo" -// every { get("submission_date") } returns "2022-01-29T08:00:00" -// every { get("pull_request_number") } returns "correctPullRequestNumber" -// } - private var savedStateHandle: SavedStateHandle = SavedStateHandle( - mapOf( - "owner" to OWNER, - "repo" to REPO, - "submission_date" to DATE, - "pull_request_number" to PR_NUMBER - ) - ) + // private lateinit var viewModel: ReviewersViewModel // // private fun createViewModel() = ReviewersViewModel(repository, savedStateHandle) @@ -92,16 +83,30 @@ class ReviewersScreenTest { fun setUp() { composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) hiltRule.inject() - val repositoryMock = mockk() - val viewModel = ReviewersViewModel(repositoryMock, savedStateHandle) - -// MockKAnnotations.init(this) -// mockkStatic(Clock::class) -// every { Clock.systemDefaultZone() } returns systemClockFixed + //MockKAnnotations.init(this) + mockkStatic(Clock::class) + every { Clock.systemDefaultZone() } returns systemClockFixed } @Test fun whenResponseIsCorrectThenReviewersAreVisible() { + val savedStateHandle = SavedStateHandle( + mapOf( + "owner" to OWNER, + "repo" to REPO, + "submission_date" to DATE, + "pull_request_number" to PR_NUMBER + ) + ) + + every { + Screen.Reviewers.getInitialValues(savedStateHandle) + } returns Screen.Reviewers.ReviewersInitialValues( + owner = checkNotNull(savedStateHandle["owner"]), + repo = checkNotNull(savedStateHandle["repo"]), + pullRequestNumber = checkNotNull(savedStateHandle["pull_request_number"]), + submissionTime = checkNotNull(LocalDateTime.parse(savedStateHandle["submission_date"])) + ) mockWebServer.register { expectThat(it).url.path.isEqualTo("/user") From df00f48ae8a2c5c8c93cac8a50a95f1d61bc21de Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 17 May 2023 11:43:31 +0200 Subject: [PATCH 411/526] Update Espresso Intents versions --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d2795a5b0..d1455c06e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -131,7 +131,7 @@ dependencies { androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") androidTestImplementation("io.mockk:mockk-android:1.13.3") - androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' // ktlint ktlintRuleset project(":custom-ktlint-rules") From 8243f048088fd19199f8c6c2621080966f7f2170 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Wed, 17 May 2023 12:40:17 +0200 Subject: [PATCH 412/526] Use IntentsRule --- .../java/com/appunite/loudius/LoginScreenTest.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 421be3e45..aa9d3c828 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -22,11 +22,11 @@ import android.content.Intent import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction import androidx.test.espresso.intent.matcher.IntentMatchers.hasData +import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.theme.LoudiusTheme @@ -48,6 +48,9 @@ class LoginScreenTest { @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() + @get:Rule(order = 2) + val intents = IntentsRule() + @Before fun setUp() { hiltRule.inject() @@ -55,7 +58,6 @@ class LoginScreenTest { @Test fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { - Intents.init() intending(hasAction(Intent.ACTION_VIEW)) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) @@ -73,6 +75,5 @@ class LoginScreenTest { hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), ), ) - Intents.release() } } From 4108b48c76660fe14b3bd50bec34eea413015683 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 17 May 2023 13:21:43 +0200 Subject: [PATCH 413/526] Update golden screens after module name changed. --- ...udius.components_LoudiusButtonTests_loudiusOutlinedButton.png} | 0 ...te.loudius.components_LoudiusDialogTest_loudiusDialogTest.png} | 0 ...udius.components_LoudiusDialogTest_loudiusErrorDialogTest.png} | 0 ...s.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename components/src/test/snapshots/images/{com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png => com.appunite.loudius.components_LoudiusButtonTests_loudiusOutlinedButton.png} (100%) rename components/src/test/snapshots/images/{com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png => com.appunite.loudius.components_LoudiusDialogTest_loudiusDialogTest.png} (100%) rename components/src/test/snapshots/images/{com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png => com.appunite.loudius.components_LoudiusDialogTest_loudiusErrorDialogTest.png} (100%) rename components/src/test/snapshots/images/{com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png => com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png} (100%) diff --git a/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusButtonTests_loudiusOutlinedButton.png similarity index 100% rename from components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusButtonTests_loudiusOutlinedButton.png rename to components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusButtonTests_loudiusOutlinedButton.png diff --git a/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusDialogTest.png similarity index 100% rename from components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusDialogTest.png rename to components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusDialogTest.png diff --git a/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusErrorDialogTest.png similarity index 100% rename from components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusErrorDialogTest.png rename to components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusErrorDialogTest.png diff --git a/components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png similarity index 100% rename from components/src/test/snapshots/images/com.appunite.loudius.screenshots_LoudiusDialogTest_loudiusFullScreenErrorTest.png rename to components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png From 247691d448bf9b9c2adb66523c8963d74db6dc6a Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 18 May 2023 09:20:07 +0200 Subject: [PATCH 414/526] Add components resources prefix. --- .../ui/authenticating/AuthenticatingScreen.kt | 2 +- .../appunite/loudius/ui/login/LoginScreen.kt | 2 +- .../ui/pullrequests/PullRequestsScreen.kt | 2 +- .../loudius/ui/reviewers/ReviewersScreen.kt | 2 +- components/build.gradle | 1 + .../components/components/LoudiusErrorDialog.kt | 6 +++--- .../components/LoudiusFullScreenError.kt | 12 ++++++------ .../components/components/LoudiusListItem.kt | 4 ++-- .../components/LoudiusOutlinedButton.kt | 6 +++--- .../components/LoudiusPlaceholderText.kt | 2 +- .../components/components/LoudiusTopAppBar.kt | 4 ++-- ...arrow_back.xml => components_arrow_back.xml} | 0 ...ror_image.png => components_error_image.png} | Bin .../{ic_github.xml => components_ic_github.xml} | 0 ...x.xml => components_person_outline_24px.xml} | 0 components/src/main/res/values/strings.xml | 16 ++++++++-------- 16 files changed, 30 insertions(+), 29 deletions(-) rename components/src/main/res/drawable/{arrow_back.xml => components_arrow_back.xml} (100%) rename components/src/main/res/drawable/{error_image.png => components_error_image.png} (100%) rename components/src/main/res/drawable/{ic_github.xml => components_ic_github.xml} (100%) rename components/src/main/res/drawable/{person_outline_24px.xml => components_person_outline_24px.xml} (100%) diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 92dd9cae9..6dcf09dcc 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -69,7 +69,7 @@ private fun ShowLoudiusLoginErrorScreen( navigateToLogin: () -> Unit, ) { LoudiusFullScreenError( - errorText = stringResource(id = componentsR.string.error_login_text), + errorText = stringResource(id = componentsR.string.components_error_login_text), buttonText = stringResource(id = R.string.go_to_login), onButtonClick = navigateToLogin, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index a766f1ef0..0a56dc79d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -91,7 +91,7 @@ fun LoginScreenStateless( style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = componentsR.drawable.ic_github), + painter = painterResource(id = componentsR.drawable.components_ic_github), contentDescription = stringResource(R.string.github_icon), ) }, diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index c9a0e8a2b..313130806 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -189,7 +189,7 @@ private fun RepoDetails(modifier: Modifier, pullRequestTitle: String, repository private fun EmptyListPlaceholder(padding: PaddingValues) { Box(modifier = Modifier.padding(padding)) { LoudiusPlaceholderText( - textId = componentsR.string.you_dont_have_any_pull_request, + textId = componentsR.string.components_you_dont_have_any_pull_request, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index d59e1ca04..a61c6d839 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -211,7 +211,7 @@ private fun NotifyButtonOrLoadingIndicator( @Composable private fun ReviewerAvatarView(modifier: Modifier = Modifier) { LoudiusListIcon( - painter = painterResource(id = componentsR.drawable.person_outline_24px), + painter = painterResource(id = componentsR.drawable.components_person_outline_24px), contentDescription = stringResource( R.string.details_screen_user_image_description, ), diff --git a/components/build.gradle b/components/build.gradle index ac71e6c21..8faa086eb 100644 --- a/components/build.gradle +++ b/components/build.gradle @@ -27,6 +27,7 @@ android { composeOptions { kotlinCompilerExtensionVersion '1.4.2' } + resourcePrefix "components_" } dependencies { diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusErrorDialog.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusErrorDialog.kt index 27be9d6b3..b25c7823d 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusErrorDialog.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusErrorDialog.kt @@ -29,9 +29,9 @@ import com.appunite.loudius.components.theme.LoudiusTheme @Composable fun LoudiusErrorDialog( onConfirmButtonClick: () -> Unit, - dialogTitle: String = stringResource(id = R.string.error_dialog_title), - dialogText: String = stringResource(id = R.string.error_dialog_text), - confirmText: String = stringResource(R.string.ok), + dialogTitle: String = stringResource(id = R.string.components_error_dialog_title), + dialogText: String = stringResource(id = R.string.components_error_dialog_text), + confirmText: String = stringResource(R.string.components_ok), ) { var openDialog by remember { mutableStateOf(true) } if (openDialog) { diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt index c2f352de5..866351e81 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt @@ -39,8 +39,8 @@ import com.appunite.loudius.components.theme.LoudiusTheme @Composable fun LoudiusFullScreenError( modifier: Modifier = Modifier, - errorText: String = stringResource(id = R.string.error_dialog_text), - buttonText: String = stringResource(id = R.string.try_again), + errorText: String = stringResource(id = R.string.components_error_dialog_text), + buttonText: String = stringResource(id = R.string.components_try_again), onButtonClick: () -> Unit, ) { ScreenErrorWithSpacers( @@ -87,8 +87,8 @@ private fun ErrorImage( ) { Image( modifier = modifier, - painter = painterResource(id = R.drawable.error_image), - contentDescription = stringResource(R.string.error_image_desc), + painter = painterResource(id = R.drawable.components_error_image), + contentDescription = stringResource(R.string.components_error_image_desc), ) } @@ -106,8 +106,8 @@ private fun ErrorText(text: String) { fun LoudiusErrorScreenPreview() { LoudiusTheme { LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), + errorText = stringResource(id = R.string.components_error_dialog_text), + buttonText = stringResource(R.string.components_try_again), onButtonClick = {}, ) } diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusListItem.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusListItem.kt index 8150a4a1e..a6a6da88a 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusListItem.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusListItem.kt @@ -165,7 +165,7 @@ fun LoudiusListItemContentAndIconPreview() { index = 0, icon = { modifier -> LoudiusListIcon( - painter = painterResource(id = componentsR.drawable.person_outline_24px), + painter = painterResource(id = componentsR.drawable.components_person_outline_24px), contentDescription = "Test", modifier = modifier, ) @@ -187,7 +187,7 @@ private fun LoudiusListItemExample(index: Int) { icon = { modifier -> LoudiusListIcon( modifier = modifier, - painter = painterResource(id = componentsR.drawable.person_outline_24px), + painter = painterResource(id = componentsR.drawable.components_person_outline_24px), contentDescription = "Test", ) }, diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt index 929a57b21..7ef141405 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt @@ -109,7 +109,7 @@ fun LoudiusOutlinedButtonWithIconPreview() { text = "Log In", icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = R.drawable.ic_github), + painter = painterResource(id = R.drawable.components_ic_github), "Github Icon", ) }, @@ -127,7 +127,7 @@ fun LoudiusOutlinedButtonDisabledPreview() { enabled = false, icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = R.drawable.ic_github), + painter = painterResource(id = R.drawable.components_ic_github), "Github Icon", ) }, @@ -145,7 +145,7 @@ fun LoudiusOutlinedButtonWithIconLargePreview() { style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( - painter = painterResource(id = R.drawable.ic_github), + painter = painterResource(id = R.drawable.components_ic_github), "Github Icon", ) }, diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt index d37191dc7..359c5376d 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt @@ -48,6 +48,6 @@ fun LoudiusPlaceholderText(@StringRes textId: Int) { @Composable fun PreviewLoudiusPlaceholderText() { LoudiusTheme { - LoudiusPlaceholderText(R.string.you_dont_have_any_pull_request) + LoudiusPlaceholderText(R.string.components_you_dont_have_any_pull_request) } } diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusTopAppBar.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusTopAppBar.kt index fd5b183ef..40534cf59 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusTopAppBar.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusTopAppBar.kt @@ -46,8 +46,8 @@ fun LoudiusTopAppBar( if (onClickBackArrow != null) { IconButton(onClick = onClickBackArrow) { Icon( - painter = painterResource(id = R.drawable.arrow_back), - contentDescription = stringResource(R.string.back_button), + painter = painterResource(id = R.drawable.components_arrow_back), + contentDescription = stringResource(R.string.components_back_button), ) } } diff --git a/components/src/main/res/drawable/arrow_back.xml b/components/src/main/res/drawable/components_arrow_back.xml similarity index 100% rename from components/src/main/res/drawable/arrow_back.xml rename to components/src/main/res/drawable/components_arrow_back.xml diff --git a/components/src/main/res/drawable/error_image.png b/components/src/main/res/drawable/components_error_image.png similarity index 100% rename from components/src/main/res/drawable/error_image.png rename to components/src/main/res/drawable/components_error_image.png diff --git a/components/src/main/res/drawable/ic_github.xml b/components/src/main/res/drawable/components_ic_github.xml similarity index 100% rename from components/src/main/res/drawable/ic_github.xml rename to components/src/main/res/drawable/components_ic_github.xml diff --git a/components/src/main/res/drawable/person_outline_24px.xml b/components/src/main/res/drawable/components_person_outline_24px.xml similarity index 100% rename from components/src/main/res/drawable/person_outline_24px.xml rename to components/src/main/res/drawable/components_person_outline_24px.xml diff --git a/components/src/main/res/values/strings.xml b/components/src/main/res/values/strings.xml index b32f6f07c..7ccab282b 100644 --- a/components/src/main/res/values/strings.xml +++ b/components/src/main/res/values/strings.xml @@ -1,10 +1,10 @@ - Back button - Error - Something went wrong… - error image - Something went wrong…\nYou need to log in again. - OK - Try again - Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 + Back button + Error + Something went wrong… + error image + Something went wrong…\nYou need to log in again. + OK + Try again + Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 From 85577a21dd2464683def298241434a1787d15638 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 18 May 2023 09:57:30 +0200 Subject: [PATCH 415/526] mock --- .../appunite/loudius/ReviewersScreenTest.kt | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index 300385a1d..25bd6ccb2 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -23,8 +23,16 @@ import androidx.compose.ui.test.onRoot import androidx.lifecycle.SavedStateHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.common.Screen +import com.appunite.loudius.common.Screen.Reviewers.getInitialValues +import com.appunite.loudius.domain.repository.PullRequestRepository +import com.appunite.loudius.network.model.RequestedReviewer +import com.appunite.loudius.network.model.RequestedReviewersResponse +import com.appunite.loudius.network.model.Review +import com.appunite.loudius.network.model.ReviewState +import com.appunite.loudius.network.model.User import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.reviewers.ReviewersScreen +import com.appunite.loudius.ui.reviewers.ReviewersViewModel import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import com.appunite.loudius.util.MockWebServerRule @@ -33,18 +41,17 @@ import com.appunite.loudius.util.path import com.appunite.loudius.util.url import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.coEvery import io.mockk.every -import io.mockk.mockkStatic +import io.mockk.mockk +import io.mockk.mockkObject import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import strikt.api.expectThat import strikt.assertions.isEqualTo -import java.time.Clock import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset @RunWith(AndroidJUnit4::class) @HiltAndroidTest @@ -69,9 +76,9 @@ class ReviewersScreenTest { //@Inject //lateinit var repository: PullRequestRepository - private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") - private val systemClockFixed = - Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) +// private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") +// private val systemClockFixed = +// Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) // // private val repository: PullRequestRepository = mockk(relaxed = true) @@ -84,8 +91,8 @@ class ReviewersScreenTest { composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) hiltRule.inject() //MockKAnnotations.init(this) - mockkStatic(Clock::class) - every { Clock.systemDefaultZone() } returns systemClockFixed +// mockkStatic(Clock::class) +// every { Clock.systemDefaultZone() } returns systemClockFixed } @Test @@ -99,8 +106,9 @@ class ReviewersScreenTest { ) ) + mockkObject(Screen.Reviewers) every { - Screen.Reviewers.getInitialValues(savedStateHandle) + getInitialValues(savedStateHandle) } returns Screen.Reviewers.ReviewersInitialValues( owner = checkNotNull(savedStateHandle["owner"]), repo = checkNotNull(savedStateHandle["repo"]), @@ -108,6 +116,17 @@ class ReviewersScreenTest { submissionTime = checkNotNull(LocalDateTime.parse(savedStateHandle["submission_date"])) ) + val repo: PullRequestRepository = mockk() + val exampleResponse = RequestedReviewersResponse(listOf(RequestedReviewer(1, "john"))) + coEvery { + repo.getRequestedReviewers(any(), any(), any()) + } returns Result.success(exampleResponse) + coEvery { + repo.getReviews(any(), any(), any()) + } returns Result.success(listOf(Review("1", User(1, "example"), ReviewState.APPROVED, LocalDateTime.parse(DATE)))) + + val viewModel = ReviewersViewModel(repo, savedStateHandle) + mockWebServer.register { expectThat(it).url.path.isEqualTo("/user") From 137a872ed64f3ce74872501139292d7687483614 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Thu, 18 May 2023 09:58:27 +0200 Subject: [PATCH 416/526] Check Xiaomi dialog in integration test --- .../com/appunite/loudius/LoginScreenTest.kt | 75 ++++++++++++++++++- .../appunite/loudius/di/GithubHelperModule.kt | 35 +++++++++ .../appunite/loudius/ui/login/GithubHelper.kt | 4 +- 3 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index aa9d3c828..212756f2a 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -24,14 +24,24 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.ComponentNameMatchers.hasClassName +import androidx.test.espresso.intent.matcher.ComponentNameMatchers.hasPackageName import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.intent.matcher.IntentMatchers.hasData +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.ui.login.GithubHelper +import com.appunite.loudius.di.GithubHelperModule import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import io.mockk.every +import io.mockk.mockk import org.hamcrest.Matchers.allOf import org.junit.Before import org.junit.Rule @@ -39,6 +49,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) +@UninstallModules(GithubHelperModule::class) @HiltAndroidTest class LoginScreenTest { @@ -56,8 +67,35 @@ class LoginScreenTest { hiltRule.inject() } + @BindValue + @JvmField + val githubHelper: GithubHelper = mockk().apply { + every { shouldAskForXiaomiIntent() } returns false + } + + @Test + fun whenLoginScreenIsVisible_LoginButtonOpensGithubAuth() { + intending(hasAction(Intent.ACTION_VIEW)) + .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) + + composeTestRule.setContent { + LoudiusTheme { + LoginScreen() + } + } + + composeTestRule.onNodeWithText("Log in").performClick() + intended( + allOf( + hasAction(Intent.ACTION_VIEW), + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") + ) + ) + } + @Test - fun whenTheLoginScreenIsVisibleThenTheLogInButtonIsVisible() { + fun whenClickingPermissionGrantedInXiaomiDialog_OpenGithubAuth() { + every { githubHelper.shouldAskForXiaomiIntent() } returns true intending(hasAction(Intent.ACTION_VIEW)) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) @@ -68,12 +106,43 @@ class LoginScreenTest { } composeTestRule.onNodeWithText("Log in").performClick() + composeTestRule.onNodeWithText("I've already granted").performClick() intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), - ), + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") + ) + ) + } + + + @Test + fun whenClickingGrantPermissionInXiaomiDialog_OpenPermissionEditor() { + every { githubHelper.shouldAskForXiaomiIntent() } returns true + intending(hasAction("miui.intent.action.APP_PERM_EDITOR")) + .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) + + composeTestRule.setContent { + LoudiusTheme { + LoginScreen() + } + } + + composeTestRule.onNodeWithText("Log in").performClick() + composeTestRule.onNodeWithText("Grant permission").performClick() + + intended( + allOf( + hasAction("miui.intent.action.APP_PERM_EDITOR"), + hasExtra("extra_pkgname", "com.github.android"), + hasComponent( + allOf( + hasPackageName("com.miui.securitycenter"), + hasClassName("com.miui.permcenter.permissions.PermissionsEditorActivity") + ) + ) + ) ) } } diff --git a/app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt b/app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt new file mode 100644 index 000000000..1b9df1529 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/di/GithubHelperModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.di + +import android.content.Context +import com.appunite.loudius.ui.login.GithubHelper +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object GithubHelperModule { + @Provides + @Singleton + fun providePullRequestNetworkDataSource(@ApplicationContext context: Context): GithubHelper = + GithubHelper(context) +} diff --git a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt index f4ff14007..a5d34d8f3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/GithubHelper.kt @@ -20,8 +20,6 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject /** * Github app currently have a bug on Xiaomi devices where @@ -40,7 +38,7 @@ import javax.inject.Inject * We've checked 1.107.0 version of Github from 2023-04-06. * If you won't be able to reproduce the issue without the fix, it can be removed. */ -class GithubHelper @Inject constructor(@ApplicationContext private val context: Context) { +class GithubHelper(private val context: Context) { companion object { private const val GITHUB_APP_PACKAGE_NAME = "com.github.android" From b5a56bdf75402e71060ed5e5dcf858e8cc9b826c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 16 May 2023 15:02:57 +0200 Subject: [PATCH 417/526] Add GitHub action for verifying snapshots. --- .github/workflows/run-snapshot-test.yml | 79 +++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/run-snapshot-test.yml diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml new file mode 100644 index 000000000..c857199e4 --- /dev/null +++ b/.github/workflows/run-snapshot-test.yml @@ -0,0 +1,79 @@ +name: Snapshot verification + +on: + issue_comment: + types: [ created, edited ] + pull_request: + branches: [ main, develop ] + +permissions: + checks: write + contents: write + statuses: write + pull-requests: write + +jobs: + test: + if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && contains(github.event.comment.html_url, '/pull/') && github.event.comment.body == '!snapshot') + name: Verify snapshots + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + with: + lfs: true + + - name: Prepare Android Environment + uses: ./.github/actions/prepare-android-env + + - name: Gradle - Verify snapshots with Paparazzi + id: testStep + run: ./gradlew clean components:verifyPaparazziDebug + + - name: Upload snapshot failure deltas + if: failure() + uses: actions/upload-artifact@v3 + with: + name: snapshot-failure-deltas + path: out/failures/ + + - name: Find PR number + uses: jwalton/gh-find-current-pr@v1 + id: findPr + if: always() + with: + state: open + + - name: Find Comment on PR + uses: peter-evans/find-comment@v1 + id: fc + if: always() + with: + issue-number: ${{ steps.findPr.outputs.pr }} + comment-author: 'github-actions[bot]' + body-includes: Snapshot testing result + + - name: Create or update comment on PR (Success) + uses: peter-evans/create-or-update-comment@v1 + if: always() && steps.testStep.outcome == 'success' + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ steps.findPr.outputs.pr }} + body: | + Snapshot testing result: :heavy_check_mark: + Everything looks good! + edit-mode: replace + + - name: Create or update comment on PR (Failure) + uses: peter-evans/create-or-update-comment@v1 + if: always() && steps.testStep.outcome == 'failure' + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ steps.findPr.outputs.pr }} + body: | + Snapshot testing result: :x: + Some of the snapshot tests seem to have failed. Please: + - Head over to the artifacts section of the [CI Run](https://github.com/appunite/Loudius/actions/runs/${{ github.run_id }}). + - Download the zip. + - Unzip and you can find one or more images that show the expected and the actual test results. + - If these changes are fixing an issue or are part of a new feature then please speak to the maintainer. If they are not intended then please fix them and repush again. + edit-mode: replace From c5b9b788e8d5bf8bf19fa6e6a1a5db77f984b4d8 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 18 May 2023 13:13:21 +0200 Subject: [PATCH 418/526] check if users are visible test --- .../appunite/loudius/ReviewersScreenTest.kt | 295 +++--------------- 1 file changed, 42 insertions(+), 253 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index 25bd6ccb2..950674355 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 AppUnite S.A. + * Copyright 2023 owner S.A. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,19 +20,9 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot -import androidx.lifecycle.SavedStateHandle import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.common.Screen -import com.appunite.loudius.common.Screen.Reviewers.getInitialValues -import com.appunite.loudius.domain.repository.PullRequestRepository -import com.appunite.loudius.network.model.RequestedReviewer -import com.appunite.loudius.network.model.RequestedReviewersResponse -import com.appunite.loudius.network.model.Review -import com.appunite.loudius.network.model.ReviewState -import com.appunite.loudius.network.model.User import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.reviewers.ReviewersScreen -import com.appunite.loudius.ui.reviewers.ReviewersViewModel import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import com.appunite.loudius.util.MockWebServerRule @@ -41,29 +31,17 @@ import com.appunite.loudius.util.path import com.appunite.loudius.util.url import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import strikt.api.expectThat import strikt.assertions.isEqualTo -import java.time.LocalDateTime @RunWith(AndroidJUnit4::class) @HiltAndroidTest class ReviewersScreenTest { - companion object { - private const val OWNER = "owner" - private const val REPO = "repo" - private const val DATE = "2022-01-29T08:00:00" - private const val PR_NUMBER = "1" - } - @get:Rule(order = 1) val hiltRule = HiltAndroidRule(this) @@ -73,121 +51,67 @@ class ReviewersScreenTest { @get:Rule(order = 2) val composeTestRule = createAndroidComposeRule() - //@Inject - //lateinit var repository: PullRequestRepository - -// private val systemNow = LocalDateTime.parse("2022-01-29T15:00:00") -// private val systemClockFixed = -// Clock.fixed(systemNow.toInstant(ZoneOffset.UTC), ZoneId.of("UTC")) -// -// private val repository: PullRequestRepository = mockk(relaxed = true) - -// private lateinit var viewModel: ReviewersViewModel -// -// private fun createViewModel() = ReviewersViewModel(repository, savedStateHandle) - @Before fun setUp() { composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) hiltRule.inject() - //MockKAnnotations.init(this) -// mockkStatic(Clock::class) -// every { Clock.systemDefaultZone() } returns systemClockFixed } @Test fun whenResponseIsCorrectThenReviewersAreVisible() { - val savedStateHandle = SavedStateHandle( - mapOf( - "owner" to OWNER, - "repo" to REPO, - "submission_date" to DATE, - "pull_request_number" to PR_NUMBER - ) - ) - - mockkObject(Screen.Reviewers) - every { - getInitialValues(savedStateHandle) - } returns Screen.Reviewers.ReviewersInitialValues( - owner = checkNotNull(savedStateHandle["owner"]), - repo = checkNotNull(savedStateHandle["repo"]), - pullRequestNumber = checkNotNull(savedStateHandle["pull_request_number"]), - submissionTime = checkNotNull(LocalDateTime.parse(savedStateHandle["submission_date"])) - ) - - val repo: PullRequestRepository = mockk() - val exampleResponse = RequestedReviewersResponse(listOf(RequestedReviewer(1, "john"))) - coEvery { - repo.getRequestedReviewers(any(), any(), any()) - } returns Result.success(exampleResponse) - coEvery { - repo.getReviews(any(), any(), any()) - } returns Result.success(listOf(Review("1", User(1, "example"), ReviewState.APPROVED, LocalDateTime.parse(DATE)))) - - val viewModel = ReviewersViewModel(repo, savedStateHandle) + with(composeTestRule.activity.intent) { + putExtra("owner", "owner") + putExtra("repo", "repo") + putExtra("submission_date", "2022-01-29T08:00:00") + putExtra("pull_request_number", "1") + } mockWebServer.register { expectThat(it).url.path.isEqualTo("/user") - jsonResponse("""{"id": 1, "login": "user"}""") } mockWebServer.register { expectThat(it).url.and { get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/appunite/Loudius/pulls/41/requested_reviewers") + path.isEqualTo("/repos/owner/repo/pulls/1/requested_reviewers") } - jsonResponse( """ - { - "users": [ - { - "login": "kezc", - "id": 1, - "node_id": "1", - "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/kezc", - "html_url": "https://github.com/kezc", - "followers_url": "https://api.github.com/users/kezc/followers", - "following_url": "https://api.github.com/users/kezc/following{/other_user}", - "gists_url": "https://api.github.com/users/kezc/gists{/gist_id}", - "starred_url": "https://api.github.com/users/kezc/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/kezc/subscriptions", - "organizations_url": "https://api.github.com/users/kezc/orgs", - "repos_url": "https://api.github.com/users/kezc/repos", - "events_url": "https://api.github.com/users/kezc/events{/privacy}", - "received_events_url": "https://api.github.com/users/kezc/received_events", - "type": "User", - "site_admin": false - }, - { - "login": "Krzysiudan", - "id": 1, - "node_id": "1", - "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Krzysiudan", - "html_url": "https://github.com/Krzysiudan", - "followers_url": "https://api.github.com/users/Krzysiudan/followers", - "following_url": "https://api.github.com/users/Krzysiudan/following{/other_user}", - "gists_url": "https://api.github.com/users/Krzysiudan/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Krzysiudan/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Krzysiudan/subscriptions", - "organizations_url": "https://api.github.com/users/Krzysiudan/orgs", - "repos_url": "https://api.github.com/users/Krzysiudan/repos", - "events_url": "https://api.github.com/users/Krzysiudan/events{/privacy}", - "received_events_url": "https://api.github.com/users/Krzysiudan/received_events", - "type": "User", - "site_admin": false - } - ], - "teams": [] - } - """, + { + "users": [ + { + "login": "userLogin", + "id": 1, + "node_id": "1", + "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/user", + "html_url": "https://github.com/user", + "followers_url": "https://api.github.com/users/user/followers", + "following_url": "https://api.github.com/users/user/following{/other_user}", + "gists_url": "https://api.github.com/users/user/gists{/gist_id}", + "starred_url": "https://api.github.com/users/user/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/user/subscriptions", + "organizations_url": "https://api.github.com/users/user/orgs", + "repos_url": "https://api.github.com/users/user/repos", + "events_url": "https://api.github.com/users/user/events{/privacy}", + "received_events_url": "https://api.github.com/users/user/received_events", + "type": "User", + "site_admin": false + } + ], + "teams": [] + } + """, ) } + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/repos/owner/repo/pulls/1/reviews") + } + jsonResponse("[]") + } composeTestRule.setContent { LoudiusTheme { @@ -196,141 +120,6 @@ class ReviewersScreenTest { } composeTestRule.onRoot() - composeTestRule.onNodeWithText("kezc").assertIsDisplayed() - } - - @Test - fun whenClickOnNotifyThenNotifyReviewer() { - + composeTestRule.onNodeWithText("userLogin").assertIsDisplayed() } } - -private val reviewersJsonResponse = """ - { - "users": [ - { - "login": "kezc", - "id": 1, - "node_id": "1", - "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/kezc", - "html_url": "https://github.com/kezc", - "followers_url": "https://api.github.com/users/kezc/followers", - "following_url": "https://api.github.com/users/kezc/following{/other_user}", - "gists_url": "https://api.github.com/users/kezc/gists{/gist_id}", - "starred_url": "https://api.github.com/users/kezc/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/kezc/subscriptions", - "organizations_url": "https://api.github.com/users/kezc/orgs", - "repos_url": "https://api.github.com/users/kezc/repos", - "events_url": "https://api.github.com/users/kezc/events{/privacy}", - "received_events_url": "https://api.github.com/users/kezc/received_events", - "type": "User", - "site_admin": false - }, - { - "login": "Krzysiudan", - "id": 1, - "node_id": "1", - "avatar_url": "https://avatars.githubusercontent.com/u/33498031?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/Krzysiudan", - "html_url": "https://github.com/Krzysiudan", - "followers_url": "https://api.github.com/users/Krzysiudan/followers", - "following_url": "https://api.github.com/users/Krzysiudan/following{/other_user}", - "gists_url": "https://api.github.com/users/Krzysiudan/gists{/gist_id}", - "starred_url": "https://api.github.com/users/Krzysiudan/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/Krzysiudan/subscriptions", - "organizations_url": "https://api.github.com/users/Krzysiudan/orgs", - "repos_url": "https://api.github.com/users/Krzysiudan/repos", - "events_url": "https://api.github.com/users/Krzysiudan/events{/privacy}", - "received_events_url": "https://api.github.com/users/Krzysiudan/received_events", - "type": "User", - "site_admin": false - } - ], - "teams": [] - } - """.trimIndent() - -private val prsJsonResponse = """ - { - "total_count":1, - "incomplete_results":false, - "items":[ - { - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", - "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", - "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", - "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", - "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", - "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", - "id":1, - "node_id":"example_node_id", - "number":1, - "title":"First Pull-Request title", - "user":{ - "login":"exampleUser", - "id":1, - "node_id":"example_user_node_id", - "avatar_url":"https://avatars.githubusercontent.com/u/1", - "gravatar_id":"", - "url":"https://api.github.com/users/exampleUser", - "html_url":"https://github.com/exampleUser", - "followers_url":"https://api.github.com/users/exampleUser/followers", - "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", - "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", - "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", - "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", - "organizations_url":"https://api.github.com/users/exampleUser/orgs", - "repos_url":"https://api.github.com/users/exampleUser/repos", - "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", - "received_events_url":"https://api.github.com/users/exampleUser/received_events", - "type":"User", - "site_admin":false - }, - "labels":[ - - ], - "state":"open", - "locked":false, - "assignee":null, - "assignees":[ - - ], - "milestone":null, - "comments":1, - "created_at":"2023-03-07T09:21:45Z", - "updated_at":"2023-03-07T09:24:24Z", - "closed_at":null, - "author_association":"COLLABORATOR", - "active_lock_reason":null, - "draft":false, - "pull_request":{ - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", - "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", - "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", - "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", - "merged_at":null - }, - "body":"pr only for demonstration purposes . . . .", - "reactions":{ - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", - "total_count":0, - "+1":0, - "-1":0, - "laugh":0, - "hooray":0, - "confused":0, - "heart":0, - "rocket":0, - "eyes":0 - }, - "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", - "performed_via_github_app":null, - "state_reason":null, - "score":1.0 - } - ] - } - """.trimIndent() From baf72acaf744aa8b5dfc0481adbafc63afdb6088 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 18 May 2023 14:59:37 +0200 Subject: [PATCH 419/526] add rule chain --- .../loudius/PullRequestsScreenTest.kt | 64 +++++++-------- .../appunite/loudius/ReviewersScreenTest.kt | 81 +++++++++---------- .../com/appunite/loudius/util/TestRules.kt | 50 ++++++++++++ 3 files changed, 113 insertions(+), 82 deletions(-) create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/TestRules.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index e9552abfb..e1d13608c 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -17,19 +17,15 @@ package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme -import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource -import com.appunite.loudius.util.MockWebServerRule +import com.appunite.loudius.util.TestRules import com.appunite.loudius.util.jsonResponse import com.appunite.loudius.util.path import com.appunite.loudius.util.queryParameter import com.appunite.loudius.util.url -import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule @@ -42,40 +38,33 @@ import strikt.assertions.isEqualTo @HiltAndroidTest class PullRequestsScreenTest { - @get:Rule(order = 1) - val hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 0) - var mockWebServer: MockWebServerRule = MockWebServerRule() - - @get:Rule(order = 2) - val composeTestRule = createAndroidComposeRule() + @get:Rule + val testRules = TestRules(this) @Before fun setUp() { - composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) - hiltRule.inject() + testRules.setUp() } @Test fun whenResponseIsCorrectThenPullRequestItemIsVisible() { - mockWebServer.register { - expectThat(it).url.path.isEqualTo("/user") - - jsonResponse("""{"id": 1, "login": "jacek"}""") - } - - mockWebServer.register { - expectThat(it).url.and { - get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/search/issues") - queryParameter("q").isEqualTo("author:jacek type:pr state:open") - queryParameter("page").isEqualTo("0") - queryParameter("per_page").isEqualTo("100") + with(testRules) { + mockWebServer.register { + expectThat(it).url.path.isEqualTo("/user") + jsonResponse("""{"id": 1, "login": "jacek"}""") } - jsonResponse( - """ + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/search/issues") + queryParameter("q").isEqualTo("author:jacek type:pr state:open") + queryParameter("page").isEqualTo("0") + queryParameter("per_page").isEqualTo("100") + } + + jsonResponse( + """ { "total_count":1, "incomplete_results":false, @@ -156,15 +145,16 @@ class PullRequestsScreenTest { ] } """, - ) - } + ) + } - composeTestRule.setContent { - LoudiusTheme { - PullRequestsScreen { _, _, _, _ -> } + composeTestRule.setContent { + LoudiusTheme { + PullRequestsScreen { _, _, _, _ -> } + } } - } - composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() + composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() + } } } diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index 950674355..8378d0dd0 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -17,19 +17,15 @@ package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.ui.reviewers.ReviewersScreen import com.appunite.loudius.ui.theme.LoudiusTheme -import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource -import com.appunite.loudius.util.MockWebServerRule +import com.appunite.loudius.util.TestRules import com.appunite.loudius.util.jsonResponse import com.appunite.loudius.util.path import com.appunite.loudius.util.url -import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule @@ -42,41 +38,35 @@ import strikt.assertions.isEqualTo @HiltAndroidTest class ReviewersScreenTest { - @get:Rule(order = 1) - val hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 0) - var mockWebServer: MockWebServerRule = MockWebServerRule() - - @get:Rule(order = 2) - val composeTestRule = createAndroidComposeRule() + @get:Rule + val testRules = TestRules(this) @Before fun setUp() { - composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) - hiltRule.inject() + testRules.setUp() } @Test fun whenResponseIsCorrectThenReviewersAreVisible() { - with(composeTestRule.activity.intent) { - putExtra("owner", "owner") - putExtra("repo", "repo") - putExtra("submission_date", "2022-01-29T08:00:00") - putExtra("pull_request_number", "1") - } + with(testRules) { + with(composeTestRule.activity.intent) { + putExtra("owner", "owner") + putExtra("repo", "repo") + putExtra("submission_date", "2022-01-29T08:00:00") + putExtra("pull_request_number", "1") + } - mockWebServer.register { - expectThat(it).url.path.isEqualTo("/user") - jsonResponse("""{"id": 1, "login": "user"}""") - } - mockWebServer.register { - expectThat(it).url.and { - get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/owner/repo/pulls/1/requested_reviewers") + mockWebServer.register { + expectThat(it).url.path.isEqualTo("/user") + jsonResponse("""{"id": 1, "login": "user"}""") } - jsonResponse( - """ + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/repos/owner/repo/pulls/1/requested_reviewers") + } + jsonResponse( + """ { "users": [ { @@ -103,23 +93,24 @@ class ReviewersScreenTest { "teams": [] } """, - ) - } - mockWebServer.register { - expectThat(it).url.and { - get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/owner/repo/pulls/1/reviews") + ) + } + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/repos/owner/repo/pulls/1/reviews") + } + jsonResponse("[]") } - jsonResponse("[]") - } - composeTestRule.setContent { - LoudiusTheme { - ReviewersScreen { } + composeTestRule.setContent { + LoudiusTheme { + ReviewersScreen { } + } } - } - composeTestRule.onRoot() - composeTestRule.onNodeWithText("userLogin").assertIsDisplayed() + composeTestRule.onRoot() + composeTestRule.onNodeWithText("userLogin").assertIsDisplayed() + } } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/TestRules.kt b/app/src/androidTest/java/com/appunite/loudius/util/TestRules.kt new file mode 100644 index 000000000..5aad813aa --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/TestRules.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.util + +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import com.appunite.loudius.TestActivity +import com.appunite.loudius.ui.components.countingResource +import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +@HiltAndroidTest +class TestRules(testClass: Any) : TestRule { + + val mockWebServer = MockWebServerRule() + val composeTestRule = createAndroidComposeRule() + private val hiltRule = HiltAndroidRule(testClass) + + override fun apply(base: Statement, description: Description): Statement { + return RuleChain.outerRule(mockWebServer) + .around(hiltRule) + .around(composeTestRule) + .apply(base, description) + } + + @Before + fun setUp() { + composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) + hiltRule.inject() + } +} From a8aae2db66dfd3ba2ad58b8073ab4ca6e61683dd Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 18 May 2023 15:24:24 +0200 Subject: [PATCH 420/526] prettier register --- .../loudius/PullRequestsScreenTest.kt | 107 +---------- .../appunite/loudius/ReviewersScreenTest.kt | 55 +----- .../com/appunite/loudius/util/Register.kt | 174 ++++++++++++++++++ 3 files changed, 183 insertions(+), 153 deletions(-) create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/Register.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index e1d13608c..2cefff561 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -21,18 +21,13 @@ import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.Register import com.appunite.loudius.util.TestRules -import com.appunite.loudius.util.jsonResponse -import com.appunite.loudius.util.path -import com.appunite.loudius.util.queryParameter -import com.appunite.loudius.util.url import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import strikt.api.expectThat -import strikt.assertions.isEqualTo @RunWith(AndroidJUnit4::class) @HiltAndroidTest @@ -49,103 +44,9 @@ class PullRequestsScreenTest { @Test fun whenResponseIsCorrectThenPullRequestItemIsVisible() { with(testRules) { - mockWebServer.register { - expectThat(it).url.path.isEqualTo("/user") - jsonResponse("""{"id": 1, "login": "jacek"}""") - } - - mockWebServer.register { - expectThat(it).url.and { - get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/search/issues") - queryParameter("q").isEqualTo("author:jacek type:pr state:open") - queryParameter("page").isEqualTo("0") - queryParameter("per_page").isEqualTo("100") - } - - jsonResponse( - """ - { - "total_count":1, - "incomplete_results":false, - "items":[ - { - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", - "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", - "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", - "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", - "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", - "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", - "id":1, - "node_id":"example_node_id", - "number":1, - "title":"First Pull-Request title", - "user":{ - "login":"exampleUser", - "id":1, - "node_id":"example_user_node_id", - "avatar_url":"https://avatars.githubusercontent.com/u/1", - "gravatar_id":"", - "url":"https://api.github.com/users/exampleUser", - "html_url":"https://github.com/exampleUser", - "followers_url":"https://api.github.com/users/exampleUser/followers", - "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", - "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", - "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", - "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", - "organizations_url":"https://api.github.com/users/exampleUser/orgs", - "repos_url":"https://api.github.com/users/exampleUser/repos", - "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", - "received_events_url":"https://api.github.com/users/exampleUser/received_events", - "type":"User", - "site_admin":false - }, - "labels":[ - - ], - "state":"open", - "locked":false, - "assignee":null, - "assignees":[ - - ], - "milestone":null, - "comments":1, - "created_at":"2023-03-07T09:21:45Z", - "updated_at":"2023-03-07T09:24:24Z", - "closed_at":null, - "author_association":"COLLABORATOR", - "active_lock_reason":null, - "draft":false, - "pull_request":{ - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", - "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", - "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", - "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", - "merged_at":null - }, - "body":"pr only for demonstration purposes . . . .", - "reactions":{ - "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", - "total_count":0, - "+1":0, - "-1":0, - "laugh":0, - "hooray":0, - "confused":0, - "heart":0, - "rocket":0, - "eyes":0 - }, - "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", - "performed_via_github_app":null, - "state_reason":null, - "score":1.0 - } - ] - } - """, - ) + with(Register) { + user(mockWebServer) + issues(mockWebServer) } composeTestRule.setContent { diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index 8378d0dd0..dd808a042 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -22,17 +22,13 @@ import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.reviewers.ReviewersScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.Register import com.appunite.loudius.util.TestRules -import com.appunite.loudius.util.jsonResponse -import com.appunite.loudius.util.path -import com.appunite.loudius.util.url import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import strikt.api.expectThat -import strikt.assertions.isEqualTo @RunWith(AndroidJUnit4::class) @HiltAndroidTest @@ -56,51 +52,10 @@ class ReviewersScreenTest { putExtra("pull_request_number", "1") } - mockWebServer.register { - expectThat(it).url.path.isEqualTo("/user") - jsonResponse("""{"id": 1, "login": "user"}""") - } - mockWebServer.register { - expectThat(it).url.and { - get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/owner/repo/pulls/1/requested_reviewers") - } - jsonResponse( - """ - { - "users": [ - { - "login": "userLogin", - "id": 1, - "node_id": "1", - "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", - "gravatar_id": "", - "url": "https://api.github.com/users/user", - "html_url": "https://github.com/user", - "followers_url": "https://api.github.com/users/user/followers", - "following_url": "https://api.github.com/users/user/following{/other_user}", - "gists_url": "https://api.github.com/users/user/gists{/gist_id}", - "starred_url": "https://api.github.com/users/user/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/user/subscriptions", - "organizations_url": "https://api.github.com/users/user/orgs", - "repos_url": "https://api.github.com/users/user/repos", - "events_url": "https://api.github.com/users/user/events{/privacy}", - "received_events_url": "https://api.github.com/users/user/received_events", - "type": "User", - "site_admin": false - } - ], - "teams": [] - } - """, - ) - } - mockWebServer.register { - expectThat(it).url.and { - get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/owner/repo/pulls/1/reviews") - } - jsonResponse("[]") + with(Register) { + user(mockWebServer) + requestedReviewers(mockWebServer) + reviews(mockWebServer) } composeTestRule.setContent { diff --git a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt new file mode 100644 index 000000000..dd718f93d --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.util + +import strikt.api.expectThat +import strikt.assertions.isEqualTo + +object Register { + + fun user(mockWebServer: MockWebServerRule) { + mockWebServer.register { + expectThat(it).url.path.isEqualTo("/user") + jsonResponse("""{"id": 1, "login": "user"}""") + } + } + + fun reviews(mockWebServer: MockWebServerRule) { + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/repos/owner/repo/pulls/1/reviews") + } + jsonResponse("[]") + } + } + + fun requestedReviewers(mockWebServer: MockWebServerRule) { + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/repos/owner/repo/pulls/1/requested_reviewers") + } + jsonResponse( + """ + { + "users": [ + { + "login": "userLogin", + "id": 1, + "node_id": "1", + "avatar_url": "https://avatars.githubusercontent.com/u/18102775?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/user", + "html_url": "https://github.com/user", + "followers_url": "https://api.github.com/users/user/followers", + "following_url": "https://api.github.com/users/user/following{/other_user}", + "gists_url": "https://api.github.com/users/user/gists{/gist_id}", + "starred_url": "https://api.github.com/users/user/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/user/subscriptions", + "organizations_url": "https://api.github.com/users/user/orgs", + "repos_url": "https://api.github.com/users/user/repos", + "events_url": "https://api.github.com/users/user/events{/privacy}", + "received_events_url": "https://api.github.com/users/user/received_events", + "type": "User", + "site_admin": false + } + ], + "teams": [] + } + """, + ) + } + } + + fun issues(mockWebServer: MockWebServerRule) { + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/search/issues") + queryParameter("q").isEqualTo("author:user type:pr state:open") + queryParameter("page").isEqualTo("0") + queryParameter("per_page").isEqualTo("100") + } + + jsonResponse( + """ + { + "total_count":1, + "incomplete_results":false, + "items":[ + { + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1", + "repository_url":"https://api.github.com/repos/exampleOwner/exampleRepo", + "labels_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/labels{/name}", + "comments_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/comments", + "events_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/events", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "id":1, + "node_id":"example_node_id", + "number":1, + "title":"First Pull-Request title", + "user":{ + "login":"exampleUser", + "id":1, + "node_id":"example_user_node_id", + "avatar_url":"https://avatars.githubusercontent.com/u/1", + "gravatar_id":"", + "url":"https://api.github.com/users/exampleUser", + "html_url":"https://github.com/exampleUser", + "followers_url":"https://api.github.com/users/exampleUser/followers", + "following_url":"https://api.github.com/users/exampleUser/following{/other_user}", + "gists_url":"https://api.github.com/users/exampleUser/gists{/gist_id}", + "starred_url":"https://api.github.com/users/exampleUser/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/exampleUser/subscriptions", + "organizations_url":"https://api.github.com/users/exampleUser/orgs", + "repos_url":"https://api.github.com/users/exampleUser/repos", + "events_url":"https://api.github.com/users/exampleUser/events{/privacy}", + "received_events_url":"https://api.github.com/users/exampleUser/received_events", + "type":"User", + "site_admin":false + }, + "labels":[ + + ], + "state":"open", + "locked":false, + "assignee":null, + "assignees":[ + + ], + "milestone":null, + "comments":1, + "created_at":"2023-03-07T09:21:45Z", + "updated_at":"2023-03-07T09:24:24Z", + "closed_at":null, + "author_association":"COLLABORATOR", + "active_lock_reason":null, + "draft":false, + "pull_request":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/pulls/1", + "html_url":"https://github.com/exampleOwner/exampleRepo/pull/1", + "diff_url":"https://github.com/exampleOwner/exampleRepo/pull/1.diff", + "patch_url":"https://github.com/exampleOwner/exampleRepo/pull/1.patch", + "merged_at":null + }, + "body":"pr only for demonstration purposes . . . .", + "reactions":{ + "url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/reactions", + "total_count":0, + "+1":0, + "-1":0, + "laugh":0, + "hooray":0, + "confused":0, + "heart":0, + "rocket":0, + "eyes":0 + }, + "timeline_url":"https://api.github.com/repos/exampleOwner/exampleRepo/issues/1/timeline", + "performed_via_github_app":null, + "state_reason":null, + "score":1.0 + } + ] + } + """, + ) + } + } +} From 63debe0361fde5cae75ca9a6454aeb9636221afd Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 19 May 2023 11:14:46 +0200 Subject: [PATCH 421/526] code improvements --- .../appunite/loudius/PullRequestsScreenTest.kt | 14 ++++++-------- .../appunite/loudius/ReviewersScreenTest.kt | 18 +++++++----------- .../{TestRules.kt => IntegrationTestRule.kt} | 11 ++++------- 3 files changed, 17 insertions(+), 26 deletions(-) rename app/src/androidTest/java/com/appunite/loudius/util/{TestRules.kt => IntegrationTestRule.kt} (86%) diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 2cefff561..406cdef18 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -21,8 +21,8 @@ import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.pullrequests.PullRequestsScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.IntegrationTestRule import com.appunite.loudius.util.Register -import com.appunite.loudius.util.TestRules import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule @@ -34,20 +34,18 @@ import org.junit.runner.RunWith class PullRequestsScreenTest { @get:Rule - val testRules = TestRules(this) + val integrationTestRule = IntegrationTestRule(this) @Before fun setUp() { - testRules.setUp() + integrationTestRule.setUp() } @Test fun whenResponseIsCorrectThenPullRequestItemIsVisible() { - with(testRules) { - with(Register) { - user(mockWebServer) - issues(mockWebServer) - } + with(integrationTestRule) { + Register.user(mockWebServer) + Register.issues(mockWebServer) composeTestRule.setContent { LoudiusTheme { diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index dd808a042..30b0ea188 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -18,12 +18,11 @@ package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.reviewers.ReviewersScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.IntegrationTestRule import com.appunite.loudius.util.Register -import com.appunite.loudius.util.TestRules import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule @@ -35,16 +34,16 @@ import org.junit.runner.RunWith class ReviewersScreenTest { @get:Rule - val testRules = TestRules(this) + val integrationTestRule = IntegrationTestRule(this) @Before fun setUp() { - testRules.setUp() + integrationTestRule.setUp() } @Test fun whenResponseIsCorrectThenReviewersAreVisible() { - with(testRules) { + with(integrationTestRule) { with(composeTestRule.activity.intent) { putExtra("owner", "owner") putExtra("repo", "repo") @@ -52,11 +51,9 @@ class ReviewersScreenTest { putExtra("pull_request_number", "1") } - with(Register) { - user(mockWebServer) - requestedReviewers(mockWebServer) - reviews(mockWebServer) - } + Register.user(mockWebServer) + Register.requestedReviewers(mockWebServer) + Register.reviews(mockWebServer) composeTestRule.setContent { LoudiusTheme { @@ -64,7 +61,6 @@ class ReviewersScreenTest { } } - composeTestRule.onRoot() composeTestRule.onNodeWithText("userLogin").assertIsDisplayed() } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/TestRules.kt b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt similarity index 86% rename from app/src/androidTest/java/com/appunite/loudius/util/TestRules.kt rename to app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt index 5aad813aa..52b9a8d51 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/TestRules.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt @@ -21,18 +21,17 @@ import com.appunite.loudius.TestActivity import com.appunite.loudius.ui.components.countingResource import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Before import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -@HiltAndroidTest -class TestRules(testClass: Any) : TestRule { +class IntegrationTestRule(testClass: Any) : TestRule { val mockWebServer = MockWebServerRule() - val composeTestRule = createAndroidComposeRule() + val composeTestRule = createAndroidComposeRule().apply{ + registerIdlingResource(countingResource.toIdlingResource()) + } private val hiltRule = HiltAndroidRule(testClass) override fun apply(base: Statement, description: Description): Statement { @@ -42,9 +41,7 @@ class TestRules(testClass: Any) : TestRule { .apply(base, description) } - @Before fun setUp() { - composeTestRule.registerIdlingResource(countingResource.toIdlingResource()) hiltRule.inject() } } From 34b9a8feaf0f3df5bd0a889dbd2f7e1590d1c379 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 19 May 2023 09:20:00 +0000 Subject: [PATCH 422/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/util/IntegrationTestRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt index 52b9a8d51..d8f757fe3 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt @@ -29,7 +29,7 @@ import org.junit.runners.model.Statement class IntegrationTestRule(testClass: Any) : TestRule { val mockWebServer = MockWebServerRule() - val composeTestRule = createAndroidComposeRule().apply{ + val composeTestRule = createAndroidComposeRule().apply { registerIdlingResource(countingResource.toIdlingResource()) } private val hiltRule = HiltAndroidRule(testClass) From 14c5af084a9ce56149039b35dea5cf04a41053cf Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Wed, 17 May 2023 06:59:42 +0000 Subject: [PATCH 423/526] Add GitHub action for verifying snapshots. --- .github/workflows/run-snapshot-test.yml | 15 +++++++++++---- .../components/LoudiusFullScreenError.kt | 10 ++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index c857199e4..8fc2cc908 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -2,9 +2,9 @@ name: Snapshot verification on: issue_comment: - types: [ created, edited ] + types: [created, edited] pull_request: - branches: [ main, develop ] + branches: [main, develop] permissions: checks: write @@ -34,7 +34,14 @@ jobs: uses: actions/upload-artifact@v3 with: name: snapshot-failure-deltas - path: out/failures/ + path: /Users/runner/work/Loudius/Loudius/components/out/failures/ + + - name: Upload snapshot failure report + if: failure() + uses: actions/upload-artifact@v3 + with: + name: snapshot-failure-report + path: /Users/runner/work/Loudius/Loudius/components/build/reports/tests/testDebugUnitTest/ - name: Find PR number uses: jwalton/gh-find-current-pr@v1 @@ -49,7 +56,7 @@ jobs: if: always() with: issue-number: ${{ steps.findPr.outputs.pr }} - comment-author: 'github-actions[bot]' + comment-author: "github-actions[bot]" body-includes: Snapshot testing result - name: Create or update comment on PR (Success) diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt index 866351e81..5b223c6f6 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt @@ -66,10 +66,12 @@ fun ScreenErrorWithSpacers( horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.weight(weight = 0.15f)) - ErrorImage(modifier = Modifier - .weight(weight = .35f) - .sizeIn(maxWidth = 400.dp, maxHeight = 400.dp) - .fillMaxWidth()) + ErrorImage( + modifier = Modifier + .weight(weight = .35f) + .sizeIn(maxWidth = 400.dp, maxHeight = 400.dp) + .fillMaxWidth() + ) Spacer(modifier = Modifier.weight(weight = 0.05f)) ErrorText(text = errorText) LoudiusOutlinedButton( From 554ea50a8ff9d1bbedc0415e87919f8645459ae3 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 17 May 2023 13:48:34 +0200 Subject: [PATCH 424/526] Add golden tests branch to trigger the snapshot tests verify on CI. --- .github/workflows/run-snapshot-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index 8fc2cc908..f57568acc 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -4,7 +4,7 @@ on: issue_comment: types: [created, edited] pull_request: - branches: [main, develop] + branches: [ main, develop, feature/LD-91/golden-tests ] permissions: checks: write From bffef0224a97d9a9459bc327855fddedcf206f91 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 17 May 2023 14:35:29 +0200 Subject: [PATCH 425/526] Make snapshot tests fail. --- .../java/com/appunite/loudius/components/LoudiusButtonTests.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt index 6da4285a3..895ac0508 100644 --- a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt +++ b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt @@ -26,7 +26,6 @@ import com.android.ide.common.rendering.api.SessionParams import com.appunite.loudius.components.components.LoudiusOutlinedButtonDisabledPreview import com.appunite.loudius.components.components.LoudiusOutlinedButtonLargePreview import com.appunite.loudius.components.components.LoudiusOutlinedButtonPreview -import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconLargePreview import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconPreview import org.junit.Rule import org.junit.Test @@ -44,7 +43,6 @@ class LoudiusButtonTests { fun loudiusOutlinedButton() { paparazzi.snapshot { Column(Modifier.background(Color.White)) { - LoudiusOutlinedButtonWithIconLargePreview() LoudiusOutlinedButtonDisabledPreview() LoudiusOutlinedButtonWithIconPreview() LoudiusOutlinedButtonLargePreview() From d15ebcc488d63d2a527bffe6108df31479eb66b4 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 18 May 2023 08:23:46 +0200 Subject: [PATCH 426/526] Make snapshot test success. --- .github/workflows/run-snapshot-test.yml | 3 ++- .../java/com/appunite/loudius/components/LoudiusButtonTests.kt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index f57568acc..5f3aa0559 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -2,7 +2,7 @@ name: Snapshot verification on: issue_comment: - types: [created, edited] + types: [ created, edited ] pull_request: branches: [ main, develop, feature/LD-91/golden-tests ] @@ -11,6 +11,7 @@ permissions: contents: write statuses: write pull-requests: write + actions: write jobs: test: diff --git a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt index 895ac0508..a447d6dfe 100644 --- a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt +++ b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt @@ -43,6 +43,7 @@ class LoudiusButtonTests { fun loudiusOutlinedButton() { paparazzi.snapshot { Column(Modifier.background(Color.White)) { + LoudiusOutlinedButtonWithIconLargePreview() LoudiusOutlinedButtonDisabledPreview() LoudiusOutlinedButtonWithIconPreview() LoudiusOutlinedButtonLargePreview() From ef814c3648440390dbffaa0bd62a916aae151c70 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 18 May 2023 16:42:51 +0200 Subject: [PATCH 427/526] Switch snapshot testing to the ubuntu machine --- .github/workflows/run-snapshot-test.yml | 6 +++--- .../com/appunite/loudius/components/LoudiusButtonTests.kt | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index 5f3aa0559..be3e6ce88 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -17,7 +17,7 @@ jobs: test: if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && contains(github.event.comment.html_url, '/pull/') && github.event.comment.body == '!snapshot') name: Verify snapshots - runs-on: macos-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: @@ -35,14 +35,14 @@ jobs: uses: actions/upload-artifact@v3 with: name: snapshot-failure-deltas - path: /Users/runner/work/Loudius/Loudius/components/out/failures/ + path: /home/runner/work/Loudius/Loudius/components/out/failures/ - name: Upload snapshot failure report if: failure() uses: actions/upload-artifact@v3 with: name: snapshot-failure-report - path: /Users/runner/work/Loudius/Loudius/components/build/reports/tests/testDebugUnitTest/ + path: /home/runner/work/Loudius/Loudius/components/build/reports/tests/testDebugUnitTest/ - name: Find PR number uses: jwalton/gh-find-current-pr@v1 diff --git a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt index a447d6dfe..6da4285a3 100644 --- a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt +++ b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt @@ -26,6 +26,7 @@ import com.android.ide.common.rendering.api.SessionParams import com.appunite.loudius.components.components.LoudiusOutlinedButtonDisabledPreview import com.appunite.loudius.components.components.LoudiusOutlinedButtonLargePreview import com.appunite.loudius.components.components.LoudiusOutlinedButtonPreview +import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconLargePreview import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconPreview import org.junit.Rule import org.junit.Test From 999172d8967b25559b3fe3d46defc695e76c401c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 19 May 2023 11:19:03 +0200 Subject: [PATCH 428/526] Simplify .yml file for snapshot test verifying. --- .github/workflows/run-snapshot-test.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index be3e6ce88..c2ae53ecc 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -1,8 +1,6 @@ name: Snapshot verification on: - issue_comment: - types: [ created, edited ] pull_request: branches: [ main, develop, feature/LD-91/golden-tests ] @@ -15,7 +13,6 @@ permissions: jobs: test: - if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && contains(github.event.comment.html_url, '/pull/') && github.event.comment.body == '!snapshot') name: Verify snapshots runs-on: ubuntu-latest steps: @@ -52,7 +49,7 @@ jobs: state: open - name: Find Comment on PR - uses: peter-evans/find-comment@v1 + uses: peter-evans/find-comment@v2 id: fc if: always() with: @@ -61,7 +58,7 @@ jobs: body-includes: Snapshot testing result - name: Create or update comment on PR (Success) - uses: peter-evans/create-or-update-comment@v1 + uses: peter-evans/create-or-update-comment@v3 if: always() && steps.testStep.outcome == 'success' with: comment-id: ${{ steps.fc.outputs.comment-id }} @@ -72,7 +69,7 @@ jobs: edit-mode: replace - name: Create or update comment on PR (Failure) - uses: peter-evans/create-or-update-comment@v1 + uses: peter-evans/create-or-update-comment@v3 if: always() && steps.testStep.outcome == 'failure' with: comment-id: ${{ steps.fc.outputs.comment-id }} From 996f7b5bc691f8615d6fc4b1c86ef0d1383e59bf Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 19 May 2023 11:37:09 +0200 Subject: [PATCH 429/526] Add reactions to the comments. --- .github/workflows/run-snapshot-test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index c2ae53ecc..ed83fc564 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -67,6 +67,11 @@ jobs: Snapshot testing result: :heavy_check_mark: Everything looks good! edit-mode: replace + reactions: | + heart + hooray + reactions-edit-mode: replace + - name: Create or update comment on PR (Failure) uses: peter-evans/create-or-update-comment@v3 @@ -82,3 +87,6 @@ jobs: - Unzip and you can find one or more images that show the expected and the actual test results. - If these changes are fixing an issue or are part of a new feature then please speak to the maintainer. If they are not intended then please fix them and repush again. edit-mode: replace + reactions: | + confused + reactions-edit-mode: replace From 827b0d2fed59ad7a03cef2ab7b6cd5d22744c244 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 22 May 2023 10:44:20 +0200 Subject: [PATCH 430/526] Correct steps ID's to be more readable. --- .github/workflows/run-snapshot-test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index ed83fc564..b309724f4 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -43,17 +43,17 @@ jobs: - name: Find PR number uses: jwalton/gh-find-current-pr@v1 - id: findPr + id: findPRId if: always() with: state: open - name: Find Comment on PR uses: peter-evans/find-comment@v2 - id: fc + id: findCommentId if: always() with: - issue-number: ${{ steps.findPr.outputs.pr }} + issue-number: ${{ steps.findPRId.outputs.pr }} comment-author: "github-actions[bot]" body-includes: Snapshot testing result @@ -61,8 +61,8 @@ jobs: uses: peter-evans/create-or-update-comment@v3 if: always() && steps.testStep.outcome == 'success' with: - comment-id: ${{ steps.fc.outputs.comment-id }} - issue-number: ${{ steps.findPr.outputs.pr }} + comment-id: ${{ steps.findCommentId.outputs.comment-id }} + issue-number: ${{ steps.findPRId.outputs.pr }} body: | Snapshot testing result: :heavy_check_mark: Everything looks good! @@ -77,8 +77,8 @@ jobs: uses: peter-evans/create-or-update-comment@v3 if: always() && steps.testStep.outcome == 'failure' with: - comment-id: ${{ steps.fc.outputs.comment-id }} - issue-number: ${{ steps.findPr.outputs.pr }} + comment-id: ${{ steps.findCommentId.outputs.comment-id }} + issue-number: ${{ steps.findPRId.outputs.pr }} body: | Snapshot testing result: :x: Some of the snapshot tests seem to have failed. Please: From 93c24163b057c5443f976e867aba2483a1534fff Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 22 May 2023 12:44:52 +0200 Subject: [PATCH 431/526] tests for notify screen --- .../appunite/loudius/ReviewersScreenTest.kt | 55 +++++++++++++++++++ .../com/appunite/loudius/util/Register.kt | 43 +++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index 30b0ea188..d9e200b71 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -18,6 +18,7 @@ package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.ui.reviewers.ReviewersScreen import com.appunite.loudius.ui.theme.LoudiusTheme @@ -64,4 +65,58 @@ class ReviewersScreenTest { composeTestRule.onNodeWithText("userLogin").assertIsDisplayed() } } + + @Test + fun givenInternetConnectionWhenClickOnNotifyThenNotifyReviewer() { + with(integrationTestRule) { + with(composeTestRule.activity.intent) { + putExtra("owner", "owner") + putExtra("repo", "repo") + putExtra("submission_date", "2022-01-29T08:00:00") + putExtra("pull_request_number", "1") + } + + Register.user(mockWebServer) + Register.requestedReviewers(mockWebServer) + Register.reviews(mockWebServer) + Register.comment(mockWebServer) + + composeTestRule.setContent { + LoudiusTheme { + ReviewersScreen { } + } + } + composeTestRule.onNodeWithText("Notify").performClick() + composeTestRule + .onNodeWithText("Awesome! Your collaborator have been pinged for some serious code review action! \uD83C\uDF89") + .assertIsDisplayed() + } + } + + @Test + fun givenNoInternetConnectionWhenClickOnNotifyThenNotifyReviewer() { + with(integrationTestRule) { + with(composeTestRule.activity.intent) { + putExtra("owner", "owner") + putExtra("repo", "repo") + putExtra("submission_date", "2022-01-29T08:00:00") + putExtra("pull_request_number", "1") + } + + Register.user(mockWebServer) + Register.requestedReviewers(mockWebServer) + Register.reviews(mockWebServer) + + composeTestRule.setContent { + LoudiusTheme { + ReviewersScreen { } + } + } + + composeTestRule.onNodeWithText("Notify").performClick() + composeTestRule + .onNodeWithText("Uh-oh, it seems that Loudius has taken a vacation. Don't worry, we're sending a postcard to bring it back ASAP!") + .assertIsDisplayed() + } + } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt index dd718f93d..f2966bebc 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt @@ -38,6 +38,49 @@ object Register { } } + fun comment(mockWebServer: MockWebServerRule) { + mockWebServer.register { + expectThat(it).url.and { + get("host") { host }.isEqualTo("api.github.com") + path.isEqualTo("/repos/owner/repo/issues/1/comments") + + } + jsonResponse(""" + { + "id": 1, + "node_id": "1", + "url": "https://api.github.com/repos/owner/repo/issues/comments/1", + "html_url": "https://github.com/owner/repo/issues/1347#issuecomment-1", + "body": "example body", + "user": { + "login": "userLogin", + "id": 1, + "node_id": "1", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/owner", + "html_url": "https://github.com/owner", + "followers_url": "https://api.github.com/users/owner/followers", + "following_url": "https://api.github.com/users/owner/following{/other_user}", + "gists_url": "https://api.github.com/users/owner/gists{/gist_id}", + "starred_url": "https://api.github.com/users/owner/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/owner/subscriptions", + "organizations_url": "https://api.github.com/users/owner/orgs", + "repos_url": "https://api.github.com/users/owner/repos", + "events_url": "https://api.github.com/users/owner/events{/privacy}", + "received_events_url": "https://api.github.com/users/owner/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "issue_url": "https://api.github.com/repos/owner/repo/issues/1", + "author_association": "COLLABORATOR" + } + """.trimIndent()) + } + } + fun requestedReviewers(mockWebServer: MockWebServerRule) { mockWebServer.register { expectThat(it).url.and { From f1e0864921ecc4081dfbb54d5cf8d7bc28975c0f Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 22 May 2023 12:57:43 +0200 Subject: [PATCH 432/526] extract init method --- .../appunite/loudius/ReviewersScreenTest.kt | 51 ++++++------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index d9e200b71..0cb0c88d6 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -40,45 +40,24 @@ class ReviewersScreenTest { @Before fun setUp() { integrationTestRule.setUp() + integrationTestRule.initTests() } @Test fun whenResponseIsCorrectThenReviewersAreVisible() { with(integrationTestRule) { - with(composeTestRule.activity.intent) { - putExtra("owner", "owner") - putExtra("repo", "repo") - putExtra("submission_date", "2022-01-29T08:00:00") - putExtra("pull_request_number", "1") - } - - Register.user(mockWebServer) - Register.requestedReviewers(mockWebServer) - Register.reviews(mockWebServer) - composeTestRule.setContent { LoudiusTheme { ReviewersScreen { } } } - composeTestRule.onNodeWithText("userLogin").assertIsDisplayed() } } @Test - fun givenInternetConnectionWhenClickOnNotifyThenNotifyReviewer() { + fun whenClickOnNotifyAndCommentThenNotifyReviewer() { with(integrationTestRule) { - with(composeTestRule.activity.intent) { - putExtra("owner", "owner") - putExtra("repo", "repo") - putExtra("submission_date", "2022-01-29T08:00:00") - putExtra("pull_request_number", "1") - } - - Register.user(mockWebServer) - Register.requestedReviewers(mockWebServer) - Register.reviews(mockWebServer) Register.comment(mockWebServer) composeTestRule.setContent { @@ -94,29 +73,29 @@ class ReviewersScreenTest { } @Test - fun givenNoInternetConnectionWhenClickOnNotifyThenNotifyReviewer() { + fun whenClickOnNotifyAndDoNotCommentThenShowError() { with(integrationTestRule) { - with(composeTestRule.activity.intent) { - putExtra("owner", "owner") - putExtra("repo", "repo") - putExtra("submission_date", "2022-01-29T08:00:00") - putExtra("pull_request_number", "1") - } - - Register.user(mockWebServer) - Register.requestedReviewers(mockWebServer) - Register.reviews(mockWebServer) - composeTestRule.setContent { LoudiusTheme { ReviewersScreen { } } } - composeTestRule.onNodeWithText("Notify").performClick() composeTestRule .onNodeWithText("Uh-oh, it seems that Loudius has taken a vacation. Don't worry, we're sending a postcard to bring it back ASAP!") .assertIsDisplayed() } } + + private fun IntegrationTestRule.initTests() { + composeTestRule.activity.intent.apply { + putExtra("owner", "owner") + putExtra("repo", "repo") + putExtra("submission_date", "2022-01-29T08:00:00") + putExtra("pull_request_number", "1") + } + Register.user(mockWebServer) + Register.requestedReviewers(mockWebServer) + Register.reviews(mockWebServer) + } } From 841855152993c6b516ec944b0d527e28ec1329b3 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 22 May 2023 11:01:43 +0000 Subject: [PATCH 433/526] [MegaLinter] Apply linters fixes --- .../androidTest/java/com/appunite/loudius/util/Register.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt index f2966bebc..db19893f1 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt @@ -43,9 +43,9 @@ object Register { expectThat(it).url.and { get("host") { host }.isEqualTo("api.github.com") path.isEqualTo("/repos/owner/repo/issues/1/comments") - } - jsonResponse(""" + jsonResponse( + """ { "id": 1, "node_id": "1", @@ -77,7 +77,8 @@ object Register { "issue_url": "https://api.github.com/repos/owner/repo/issues/1", "author_association": "COLLABORATOR" } - """.trimIndent()) + """.trimIndent(), + ) } } From 9c445725b01ac4209bed274eb54def8d68616521 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Wed, 24 May 2023 14:15:21 +0200 Subject: [PATCH 434/526] Add additional step for checking for git LFS usage. --- .github/workflows/run-snapshot-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index b309724f4..04d7ed5c0 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -23,6 +23,9 @@ jobs: - name: Prepare Android Environment uses: ./.github/actions/prepare-android-env + - name: LFS-warning + uses: ppremk/lfs-warning@v3.2 + - name: Gradle - Verify snapshots with Paparazzi id: testStep run: ./gradlew clean components:verifyPaparazziDebug From c90102e18afab433a284113c84730ac5d81f11e5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 24 May 2023 14:29:33 +0200 Subject: [PATCH 435/526] add pull to refresh on PRs screen --- app/build.gradle | 3 +- .../ui/pullrequests/PullRequestsScreen.kt | 210 ++++++++++-------- .../ui/pullrequests/PullRequestsViewModel.kt | 2 +- 3 files changed, 116 insertions(+), 99 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index b4461e384..57b267c1e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,7 +69,7 @@ dependencies { //Base android deps implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.activity:activity-compose:1.6.1' //Compose @@ -77,6 +77,7 @@ dependencies { implementation composeBom androidTestImplementation composeBom implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.material:material:1.5.0-alpha04' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling-preview' implementation "androidx.navigation:navigation-compose:2.5.3" diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index a173f6924..eedb8bc64 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) package com.appunite.loudius.ui.pullrequests @@ -26,17 +26,21 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon @@ -46,8 +50,6 @@ import com.appunite.loudius.ui.components.LoudiusPlaceholderText import com.appunite.loudius.ui.components.LoudiusText import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar -import com.appunite.loudius.ui.theme.LoudiusTheme -import java.time.LocalDateTime typealias NavigateToReviewers = (String, String, String, String) -> Unit @@ -57,8 +59,13 @@ fun PullRequestsScreen( navigateToReviewers: NavigateToReviewers, ) { val state = viewModel.state + val swipeRefreshState = rememberPullRefreshState( + refreshing = state.data == Data.Loading, + onRefresh = { viewModel.fetchData() } + ) PullRequestsScreenStateless( state = state, + pullRefreshState = swipeRefreshState, onAction = viewModel::onAction, ) LaunchedEffect(state.navigateToReviewers) { @@ -85,6 +92,7 @@ private fun navigateToReviewers( @Composable private fun PullRequestsScreenStateless( state: PullRequestState, + pullRefreshState: PullRefreshState, onAction: (PulLRequestsAction) -> Unit, ) { Scaffold( @@ -98,7 +106,7 @@ private fun PullRequestsScreenStateless( onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Success -> PullRequestContent(state.data, padding, onAction) + is Data.Success -> PullRequestContent(state.data, pullRefreshState, padding, onAction) } }, ) @@ -106,17 +114,20 @@ private fun PullRequestsScreenStateless( @Composable private fun PullRequestContent( - state: Data.Success, + state: Data, + pullRefreshState: PullRefreshState, padding: PaddingValues, onAction: (PulLRequestsAction) -> Unit, ) { - if (state.pullRequests.isEmpty()) { + if ((state as Data.Success).pullRequests.isEmpty()) { EmptyListPlaceholder(padding) } else { PullRequestsList( pullRequests = state.pullRequests, modifier = Modifier.padding(padding), onItemClick = onAction, + pullRefreshState = pullRefreshState, + isLoading = state == Data.Loading ) } } @@ -126,17 +137,22 @@ private fun PullRequestsList( pullRequests: List, modifier: Modifier, onItemClick: (PulLRequestsAction) -> Unit, + pullRefreshState: PullRefreshState, + isLoading: Boolean ) { - LazyColumn( - modifier = modifier.fillMaxSize(), - ) { - itemsIndexed(pullRequests) { index, item -> - PullRequestItem( - index = index, - data = item, - onClick = onItemClick, - ) + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + LazyColumn( + modifier = modifier.fillMaxSize(), + ) { + itemsIndexed(pullRequests) { index, item -> + PullRequestItem( + index = index, + data = item, + onClick = onItemClick, + ) + } } + PullRefreshIndicator(isLoading, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -193,83 +209,83 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { } } -@Preview("Pull requests - filled list") -@Composable -fun PullRequestsScreenPreview() { - LoudiusTheme { - PullRequestsScreenStateless( - state = PullRequestState( - Data.Success( - listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), - ), - PullRequest( - id = 2, - draft = false, - number = 2, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", - title = "[SIL-73] Storing access token", - createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), - ), - PullRequest( - id = 3, - draft = false, - number = 3, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", - title = "[SIL-62/SIL-75] Provide new annotation for API instances", - createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), - ), - ), - ), - ), - onAction = {}, - ) - } -} - -@Preview("Pull requests - empty list") -@Composable -fun PullRequestsScreenEmptyListPreview() { - LoudiusTheme { - PullRequestsScreenStateless( - PullRequestState(Data.Success(emptyList())), - onAction = {}, - ) - } -} - -@Preview("Pull requests - Loading") -@Composable -fun PullRequestsScreenLoadingPreview() { - LoudiusTheme { - PullRequestsScreenStateless( - PullRequestState(Data.Loading), - onAction = {}, - ) - } -} - -@Preview("Pull requests - Error") -@Composable -fun PullRequestsScreenErrorPreview() { - LoudiusTheme { - PullRequestsScreenStateless( - PullRequestState(Data.Error), - onAction = {}, - ) - } -} +//@Preview("Pull requests - filled list") +//@Composable +//fun PullRequestsScreenPreview() { +// LoudiusTheme { +// PullRequestsScreenStateless( +// state = PullRequestState( +// Data.Success( +// listOf( +// PullRequest( +// id = 0, +// draft = false, +// number = 0, +// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", +// title = "[SIL-67] Details screen - network layer", +// createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), +// ), +// PullRequest( +// id = 1, +// draft = true, +// number = 1, +// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", +// title = "[SIL-66] Add client secret to build config", +// createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), +// ), +// PullRequest( +// id = 2, +// draft = false, +// number = 2, +// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", +// title = "[SIL-73] Storing access token", +// createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), +// ), +// PullRequest( +// id = 3, +// draft = false, +// number = 3, +// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", +// title = "[SIL-62/SIL-75] Provide new annotation for API instances", +// createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), +// ), +// ), +// ), +// ), +// onAction = {}, +// ) +// } +//} +// +//@Preview("Pull requests - empty list") +//@Composable +//fun PullRequestsScreenEmptyListPreview() { +// LoudiusTheme { +// PullRequestsScreenStateless( +// PullRequestState(Data.Success(emptyList())), +// onAction = {}, +// ) +// } +//} +// +//@Preview("Pull requests - Loading") +//@Composable +//fun PullRequestsScreenLoadingPreview() { +// LoudiusTheme { +// PullRequestsScreenStateless( +// PullRequestState(Data.Loading), +// onAction = {}, +// ) +// } +//} +// +//@Preview("Pull requests - Error") +//@Composable +//fun PullRequestsScreenErrorPreview() { +// LoudiusTheme { +// PullRequestsScreenStateless( +// PullRequestState(Data.Error), +// onAction = {}, +// ) +// } +//} diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 3aad56aa2..ecdbcd701 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -62,7 +62,7 @@ class PullRequestsViewModel @Inject constructor( fetchData() } - private fun fetchData() { + fun fetchData() { viewModelScope.launch { state = PullRequestState() pullRequestsRepository.getCurrentUserPullRequests() From ce60570e8195a3a3dde160c2764f59172703b1ba Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 24 May 2023 12:34:35 +0000 Subject: [PATCH 436/526] [MegaLinter] Apply linters fixes --- .../ui/pullrequests/PullRequestsScreen.kt | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index eedb8bc64..82cd45153 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -61,7 +61,7 @@ fun PullRequestsScreen( val state = viewModel.state val swipeRefreshState = rememberPullRefreshState( refreshing = state.data == Data.Loading, - onRefresh = { viewModel.fetchData() } + onRefresh = { viewModel.fetchData() }, ) PullRequestsScreenStateless( state = state, @@ -127,7 +127,7 @@ private fun PullRequestContent( modifier = Modifier.padding(padding), onItemClick = onAction, pullRefreshState = pullRefreshState, - isLoading = state == Data.Loading + isLoading = state == Data.Loading, ) } } @@ -138,7 +138,7 @@ private fun PullRequestsList( modifier: Modifier, onItemClick: (PulLRequestsAction) -> Unit, pullRefreshState: PullRefreshState, - isLoading: Boolean + isLoading: Boolean, ) { Box(modifier = Modifier.pullRefresh(pullRefreshState)) { LazyColumn( @@ -209,9 +209,9 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { } } -//@Preview("Pull requests - filled list") -//@Composable -//fun PullRequestsScreenPreview() { +// @Preview("Pull requests - filled list") +// @Composable +// fun PullRequestsScreenPreview() { // LoudiusTheme { // PullRequestsScreenStateless( // state = PullRequestState( @@ -255,37 +255,37 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { // onAction = {}, // ) // } -//} +// } // -//@Preview("Pull requests - empty list") -//@Composable -//fun PullRequestsScreenEmptyListPreview() { +// @Preview("Pull requests - empty list") +// @Composable +// fun PullRequestsScreenEmptyListPreview() { // LoudiusTheme { // PullRequestsScreenStateless( // PullRequestState(Data.Success(emptyList())), // onAction = {}, // ) // } -//} +// } // -//@Preview("Pull requests - Loading") -//@Composable -//fun PullRequestsScreenLoadingPreview() { +// @Preview("Pull requests - Loading") +// @Composable +// fun PullRequestsScreenLoadingPreview() { // LoudiusTheme { // PullRequestsScreenStateless( // PullRequestState(Data.Loading), // onAction = {}, // ) // } -//} +// } // -//@Preview("Pull requests - Error") -//@Composable -//fun PullRequestsScreenErrorPreview() { +// @Preview("Pull requests - Error") +// @Composable +// fun PullRequestsScreenErrorPreview() { // LoudiusTheme { // PullRequestsScreenStateless( // PullRequestState(Data.Error), // onAction = {}, // ) // } -//} +// } From 381934cb3ea70f03b460e0223d07ba327f47bbe1 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 24 May 2023 14:53:52 +0200 Subject: [PATCH 437/526] add pull to refresh on reviewers screen --- .../ui/pullrequests/PullRequestsScreen.kt | 180 ++++++++++-------- .../loudius/ui/reviewers/ReviewersScreen.kt | 58 ++++-- .../ui/reviewers/ReviewersViewModel.kt | 2 +- 3 files changed, 145 insertions(+), 95 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 82cd45153..9284e18f2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -39,8 +39,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R +import com.appunite.loudius.common.Constants import com.appunite.loudius.network.model.PullRequest import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon @@ -50,6 +52,8 @@ import com.appunite.loudius.ui.components.LoudiusPlaceholderText import com.appunite.loudius.ui.components.LoudiusText import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar +import com.appunite.loudius.ui.theme.LoudiusTheme +import java.time.LocalDateTime typealias NavigateToReviewers = (String, String, String, String) -> Unit @@ -209,83 +213,99 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { } } -// @Preview("Pull requests - filled list") -// @Composable -// fun PullRequestsScreenPreview() { -// LoudiusTheme { -// PullRequestsScreenStateless( -// state = PullRequestState( -// Data.Success( -// listOf( -// PullRequest( -// id = 0, -// draft = false, -// number = 0, -// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", -// title = "[SIL-67] Details screen - network layer", -// createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), -// ), -// PullRequest( -// id = 1, -// draft = true, -// number = 1, -// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", -// title = "[SIL-66] Add client secret to build config", -// createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), -// ), -// PullRequest( -// id = 2, -// draft = false, -// number = 2, -// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", -// title = "[SIL-73] Storing access token", -// createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), -// ), -// PullRequest( -// id = 3, -// draft = false, -// number = 3, -// repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", -// title = "[SIL-62/SIL-75] Provide new annotation for API instances", -// createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), -// ), -// ), -// ), -// ), -// onAction = {}, -// ) -// } -// } -// -// @Preview("Pull requests - empty list") -// @Composable -// fun PullRequestsScreenEmptyListPreview() { -// LoudiusTheme { -// PullRequestsScreenStateless( -// PullRequestState(Data.Success(emptyList())), -// onAction = {}, -// ) -// } -// } -// -// @Preview("Pull requests - Loading") -// @Composable -// fun PullRequestsScreenLoadingPreview() { -// LoudiusTheme { -// PullRequestsScreenStateless( -// PullRequestState(Data.Loading), -// onAction = {}, -// ) -// } -// } -// -// @Preview("Pull requests - Error") -// @Composable -// fun PullRequestsScreenErrorPreview() { -// LoudiusTheme { -// PullRequestsScreenStateless( -// PullRequestState(Data.Error), -// onAction = {}, -// ) -// } -// } +@Preview("Pull requests - filled list") +@Composable +fun PullRequestsScreenPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + state = PullRequestState( + Data.Success( + listOf( + PullRequest( + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", + title = "[SIL-67] Details screen - network layer", + createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), + ), + PullRequest( + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", + title = "[SIL-66] Add client secret to build config", + createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + ), + PullRequest( + id = 2, + draft = false, + number = 2, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", + title = "[SIL-73] Storing access token", + createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), + ), + PullRequest( + id = 3, + draft = false, + number = 3, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", + title = "[SIL-62/SIL-75] Provide new annotation for API instances", + createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), + ), + ), + ), + ), + onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = false, + onRefresh = {} + ) + ) + } +} + +@Preview("Pull requests - empty list") +@Composable +fun PullRequestsScreenEmptyListPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + PullRequestState(Data.Success(emptyList())), + onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = false, + onRefresh = {} + ) + ) + } +} + +@Preview("Pull requests - Loading") +@Composable +fun PullRequestsScreenLoadingPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + PullRequestState(Data.Loading), + onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = false, + onRefresh = {} + ) + ) + } +} + +@Preview("Pull requests - Error") +@Composable +fun PullRequestsScreenErrorPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + PullRequestState(Data.Error), + onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = false, + onRefresh = {} + ) + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 7ab8baf4e..a740a4244 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterialApi::class) + package com.appunite.loudius.ui.reviewers import androidx.compose.foundation.layout.Box @@ -24,6 +26,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold @@ -33,6 +40,7 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -62,10 +70,15 @@ fun ReviewersScreen( ) { val state = viewModel.state val snackbarHostState = remember { SnackbarHostState() } + val swipeRefreshState = rememberPullRefreshState( + refreshing = state.data == Data.Loading, + onRefresh = { viewModel.fetchData() } + ) ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, data = state.data, + pullRefreshState = swipeRefreshState, onClickBackArrow = navigateBack, snackbarHostState = snackbarHostState, onAction = viewModel::onAction, @@ -107,6 +120,7 @@ private fun resolveSnackbarMessage(snackbarTypeShown: ReviewersSnackbarType) = private fun ReviewersScreenStateless( pullRequestNumber: String, data: Data, + pullRefreshState: PullRefreshState, onClickBackArrow: () -> Unit, snackbarHostState: SnackbarHostState, onAction: (ReviewersAction) -> Unit, @@ -127,7 +141,7 @@ private fun ReviewersScreenStateless( ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Success -> ReviewersScreenContent(data, padding, onAction) + is Data.Success -> ReviewersScreenContent(data, pullRefreshState, padding, onAction) } }, ) @@ -135,15 +149,18 @@ private fun ReviewersScreenStateless( @Composable private fun ReviewersScreenContent( - data: Data.Success, + data: Data, + pullRefreshState: PullRefreshState, padding: PaddingValues, onAction: (ReviewersAction) -> Unit, ) { - if (data.reviewers.isNotEmpty()) { + if ((data as Data.Success).reviewers.isNotEmpty()) { ReviewersList( - reviewers = data.reviewers, + data = data, + pullRefreshState = pullRefreshState, modifier = Modifier.padding(padding), onNotifyClick = onAction, + isLoading = data == Data.Loading ) } else { EmptyListPlaceholder(padding) @@ -152,20 +169,25 @@ private fun ReviewersScreenContent( @Composable private fun ReviewersList( - reviewers: List, + data: Data, + pullRefreshState: PullRefreshState, modifier: Modifier, onNotifyClick: (ReviewersAction) -> Unit, + isLoading: Boolean ) { - LazyColumn( - modifier = modifier.fillMaxWidth(), - ) { - itemsIndexed(reviewers) { index, reviewer -> - ReviewerItem( - reviewer = reviewer, - index = index, - onNotifyClick = onNotifyClick, - ) + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + LazyColumn( + modifier = modifier.fillMaxWidth(), + ) { + itemsIndexed((data as Data.Success).reviewers) { index, reviewer -> + ReviewerItem( + reviewer = reviewer, + index = index, + onNotifyClick = onNotifyClick, + ) + } } + PullRefreshIndicator(isLoading, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -276,6 +298,10 @@ fun DetailsScreenPreview() { onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = false, + onRefresh = {} + ) ) } } @@ -290,6 +316,10 @@ fun DetailsScreenNoReviewsPreview() { onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = false, + onRefresh = {} + ) ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 05af0bcbf..2283ee8cf 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -74,7 +74,7 @@ class ReviewersViewModel @Inject constructor( fetchData() } - private fun fetchData() { + fun fetchData() { viewModelScope.launch { state = state.copy(data = Data.Loading) From 368c2b572a780d4188927f930d8df2c3fd66b7c5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 24 May 2023 12:57:45 +0000 Subject: [PATCH 438/526] [MegaLinter] Apply linters fixes --- .../ui/pullrequests/PullRequestsScreen.kt | 16 ++++++++-------- .../loudius/ui/reviewers/ReviewersScreen.kt | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 9284e18f2..cb5ed69d6 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -259,8 +259,8 @@ fun PullRequestsScreenPreview() { onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, - onRefresh = {} - ) + onRefresh = {}, + ), ) } } @@ -274,8 +274,8 @@ fun PullRequestsScreenEmptyListPreview() { onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, - onRefresh = {} - ) + onRefresh = {}, + ), ) } } @@ -289,8 +289,8 @@ fun PullRequestsScreenLoadingPreview() { onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, - onRefresh = {} - ) + onRefresh = {}, + ), ) } } @@ -304,8 +304,8 @@ fun PullRequestsScreenErrorPreview() { onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, - onRefresh = {} - ) + onRefresh = {}, + ), ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index a740a4244..b8a3e63ab 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -72,7 +72,7 @@ fun ReviewersScreen( val snackbarHostState = remember { SnackbarHostState() } val swipeRefreshState = rememberPullRefreshState( refreshing = state.data == Data.Loading, - onRefresh = { viewModel.fetchData() } + onRefresh = { viewModel.fetchData() }, ) ReviewersScreenStateless( @@ -160,7 +160,7 @@ private fun ReviewersScreenContent( pullRefreshState = pullRefreshState, modifier = Modifier.padding(padding), onNotifyClick = onAction, - isLoading = data == Data.Loading + isLoading = data == Data.Loading, ) } else { EmptyListPlaceholder(padding) @@ -173,7 +173,7 @@ private fun ReviewersList( pullRefreshState: PullRefreshState, modifier: Modifier, onNotifyClick: (ReviewersAction) -> Unit, - isLoading: Boolean + isLoading: Boolean, ) { Box(modifier = Modifier.pullRefresh(pullRefreshState)) { LazyColumn( @@ -300,8 +300,8 @@ fun DetailsScreenPreview() { onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, - onRefresh = {} - ) + onRefresh = {}, + ), ) } } @@ -318,8 +318,8 @@ fun DetailsScreenNoReviewsPreview() { onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, - onRefresh = {} - ) + onRefresh = {}, + ), ) } } From 8cc1a79dbfa4001435f84b2323b68c0291173326 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 10:05:01 +0200 Subject: [PATCH 439/526] fix dependency --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 57b267c1e..69961add9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,7 +69,7 @@ dependencies { //Base android deps implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' implementation 'androidx.activity:activity-compose:1.6.1' //Compose @@ -77,7 +77,7 @@ dependencies { implementation composeBom androidTestImplementation composeBom implementation 'androidx.compose.material3:material3' - implementation 'androidx.compose.material:material:1.5.0-alpha04' + implementation 'androidx.compose.material:material:1.4.0' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling-preview' implementation "androidx.navigation:navigation-compose:2.5.3" From 19740a4dee984fecc383d4cfa342b2992907eb29 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 25 May 2023 13:16:35 +0200 Subject: [PATCH 440/526] Set run-snapshot-test.yml workflow to start on pull requests to every branch. --- .github/workflows/run-snapshot-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index 04d7ed5c0..e3cfe0229 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -2,7 +2,6 @@ name: Snapshot verification on: pull_request: - branches: [ main, develop, feature/LD-91/golden-tests ] permissions: checks: write From 6efe89a632bd52d0cc10da667da357e46c1c95b4 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 25 May 2023 13:59:19 +0200 Subject: [PATCH 441/526] chore: cleanup string resources --- app/src/main/AndroidManifest.xml | 2 +- .../java/com/appunite/loudius/MainActivity.kt | 2 +- .../ui/authenticating/AuthenticatingScreen.kt | 4 +- .../ui/components/LoudiusErrorDialog.kt | 4 +- .../ui/components/LoudiusFullScreenError.kt | 33 ++++++++---- .../ui/components/LoudiusPlaceholderText.kt | 9 ++-- .../loudius/ui/components/LoudiusTopAppBar.kt | 2 +- .../appunite/loudius/ui/login/LoginScreen.kt | 4 +- .../ui/pullrequests/PullRequestsScreen.kt | 4 +- .../loudius/ui/reviewers/ReviewersScreen.kt | 16 +++--- app/src/main/res/values/strings.xml | 54 +++++++++++-------- 11 files changed, 79 insertions(+), 55 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 33f1f9148..e1f17d154 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_app" - android:label="@string/app_name" + android:label="@string/common_app_name" android:supportsRtl="true" android:theme="@style/Theme.Loudius" tools:targetApi="31"> diff --git a/app/src/main/java/com/appunite/loudius/MainActivity.kt b/app/src/main/java/com/appunite/loudius/MainActivity.kt index 83a0c46c4..daac9cc4b 100644 --- a/app/src/main/java/com/appunite/loudius/MainActivity.kt +++ b/app/src/main/java/com/appunite/loudius/MainActivity.kt @@ -118,7 +118,7 @@ class MainActivity : ComponentActivity() { private fun showAuthFailureToast() { Toast.makeText( this@MainActivity, - getString(R.string.user_unauthorized_message), + getString(R.string.common_user_unauthorized_error_message), Toast.LENGTH_LONG, ).show() } diff --git a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt index 22d50bf27..207e26f57 100644 --- a/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/authenticating/AuthenticatingScreen.kt @@ -68,8 +68,8 @@ private fun ShowLoudiusLoginErrorScreen( navigateToLogin: () -> Unit, ) { LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_login_text), - buttonText = stringResource(id = R.string.go_to_login), + errorText = stringResource(id = R.string.authenticating_screen_error_screen_error_message), + buttonText = stringResource(id = R.string.authenticating_screen_error_screen_login_button), onButtonClick = navigateToLogin, ) } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt index a4914802f..05e1ed566 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusErrorDialog.kt @@ -30,8 +30,8 @@ import com.appunite.loudius.ui.theme.LoudiusTheme fun LoudiusErrorDialog( onConfirmButtonClick: () -> Unit, dialogTitle: String = stringResource(id = R.string.error_dialog_title), - dialogText: String = stringResource(id = R.string.error_dialog_text), - confirmText: String = stringResource(R.string.ok), + dialogText: String = stringResource(id = R.string.error_dialog_description), + confirmText: String = stringResource(R.string.error_dialog_confirm_button), ) { var openDialog by remember { mutableStateOf(true) } if (openDialog) { diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index cf16d05a6..7156f468f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.appunite.loudius.R import com.appunite.loudius.ui.components.utils.MultiScreenPreviews @@ -36,8 +37,8 @@ import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusFullScreenError( modifier: Modifier = Modifier, - errorText: String = stringResource(id = R.string.error_dialog_text), - buttonText: String = stringResource(id = R.string.try_again), + errorText: String = stringResource(id = R.string.error_dialog_description), + buttonText: String = stringResource(id = R.string.error_dialog_try_again_button), onButtonClick: () -> Unit, ) { ScreenErrorWithSpacers( @@ -56,11 +57,18 @@ fun ScreenErrorWithSpacers( onButtonClick: () -> Unit, ) { Column( - modifier = modifier.padding(32.dp).fillMaxSize(), + modifier = modifier + .padding(32.dp) + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { Spacer(modifier = Modifier.weight(weight = 0.15f)) - ErrorImage(modifier = Modifier.weight(weight = .35f).sizeIn(maxWidth = 400.dp, maxHeight = 400.dp).fillMaxWidth()) + ErrorImage( + modifier = Modifier + .weight(weight = .35f) + .sizeIn(maxWidth = 400.dp, maxHeight = 400.dp) + .fillMaxWidth() + ) Spacer(modifier = Modifier.weight(weight = 0.05f)) ErrorText(text = errorText) LoudiusOutlinedButton( @@ -79,7 +87,7 @@ private fun ErrorImage( Image( modifier = modifier, painter = painterResource(id = R.drawable.error_image), - contentDescription = stringResource(R.string.error_image_desc), + contentDescription = stringResource(R.string.error_dialog_image_content_description), ) } @@ -95,11 +103,18 @@ private fun ErrorText(text: String) { @MultiScreenPreviews @Composable fun LoudiusErrorScreenPreview() { + LoudiusTheme { + LoudiusFullScreenError {} + } +} + +@Preview(showSystemUi = true) +@Composable +fun LoudiusErrorScreenCustomTextsPreview() { LoudiusTheme { LoudiusFullScreenError( - errorText = stringResource(id = R.string.error_dialog_text), - buttonText = stringResource(R.string.try_again), - onButtonClick = {}, - ) + errorText = "Custom title", + buttonText = "My Button Text" + ) {} } } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt index 06bb70c3b..7ce279df0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPlaceholderText.kt @@ -16,21 +16,18 @@ package com.appunite.loudius.ui.components -import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.R import com.appunite.loudius.ui.theme.LoudiusTheme @Composable -fun LoudiusPlaceholderText(@StringRes textId: Int) { +fun LoudiusPlaceholderText(text: String) { Box( modifier = Modifier .fillMaxSize() @@ -38,7 +35,7 @@ fun LoudiusPlaceholderText(@StringRes textId: Int) { contentAlignment = Alignment.Center, ) { LoudiusText( - text = stringResource(id = textId), + text = text, style = LoudiusTextStyle.ScreenContent, ) } @@ -48,6 +45,6 @@ fun LoudiusPlaceholderText(@StringRes textId: Int) { @Composable fun PreviewLoudiusPlaceholderText() { LoudiusTheme { - LoudiusPlaceholderText(R.string.you_dont_have_any_pull_request) + LoudiusPlaceholderText("Sorry! Your list of pull requests is empty.\\nGet back to work! \uD83E\uDDD1\u200D") } } diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt index 279224df7..4b008a57d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusTopAppBar.kt @@ -47,7 +47,7 @@ fun LoudiusTopAppBar( IconButton(onClick = onClickBackArrow) { Icon( painter = painterResource(id = R.drawable.arrow_back), - contentDescription = stringResource(R.string.back_button), + contentDescription = stringResource(R.string.common_back_button_icon_content_description), ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index f5d1d679e..109d1ff68 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -84,12 +84,12 @@ fun LoginScreenStateless( onClick = { onAction(LoginAction.ClickLogIn) }, - text = stringResource(id = R.string.login_screen_login), + text = stringResource(id = R.string.login_screen_login_button), style = LoudiusOutlinedButtonStyle.Large, icon = { LoudiusOutlinedButtonIcon( painter = painterResource(id = R.drawable.ic_github), - contentDescription = stringResource(R.string.github_icon), + contentDescription = stringResource(R.string.login_screen_github_icon_content_description), ) }, ) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index a173f6924..6c394a2d9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -89,7 +89,7 @@ private fun PullRequestsScreenStateless( ) { Scaffold( topBar = { - LoudiusTopAppBar(title = stringResource(R.string.app_name)) + LoudiusTopAppBar(title = stringResource(R.string.common_app_name)) }, content = { padding -> when (state.data) { @@ -188,7 +188,7 @@ private fun RepoDetails(modifier: Modifier, pullRequestTitle: String, repository private fun EmptyListPlaceholder(padding: PaddingValues) { Box(modifier = Modifier.padding(padding)) { LoudiusPlaceholderText( - textId = R.string.you_dont_have_any_pull_request, + text = stringResource(id = R.string.ull_requests_screen_you_dont_have_any_pull_request_message), ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 7ab8baf4e..0c5b2d382 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -98,8 +98,8 @@ private fun SnackbarLaunchedEffect( @Composable private fun resolveSnackbarMessage(snackbarTypeShown: ReviewersSnackbarType) = when (snackbarTypeShown) { - SUCCESS -> stringResource(id = R.string.reviewers_snackbar_success) - FAILURE -> stringResource(id = R.string.reviewers_snackbar_failure) + SUCCESS -> stringResource(id = R.string.reviewers_screen_snackbar_success_message) + FAILURE -> stringResource(id = R.string.reviewers_screen_snackbar_failure_message) } @OptIn(ExperimentalMaterial3Api::class) @@ -115,7 +115,7 @@ private fun ReviewersScreenStateless( topBar = { LoudiusTopAppBar( onClickBackArrow = onClickBackArrow, - title = stringResource(id = R.string.details_title, pullRequestNumber), + title = stringResource(id = R.string.reviewers_screen_title, pullRequestNumber), ) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, @@ -197,7 +197,7 @@ private fun NotifyButtonOrLoadingIndicator( ) { Box(contentAlignment = Center) { LoudiusOutlinedButton( - text = stringResource(R.string.details_notify), + text = stringResource(R.string.reviewers_screen_notify_button), onClick = { onNotifyClick(ReviewersAction.Notify(reviewer.login)) }, modifier = Modifier.alpha(if (reviewer.isLoading) 0f else 1f), ) @@ -212,7 +212,7 @@ private fun ReviewerAvatarView(modifier: Modifier = Modifier) { LoudiusListIcon( painter = painterResource(id = R.drawable.person_outline_24px), contentDescription = stringResource( - R.string.details_screen_user_image_description, + R.string.reviewers_screen_user_image_content_description, ), modifier = modifier, ) @@ -228,9 +228,9 @@ private fun IsReviewedHeadlineText(reviewer: Reviewer) { @Composable private fun resolveIsReviewedText(reviewer: Reviewer) = if (reviewer.isReviewDone) { - stringResource(id = R.string.details_reviewed, reviewer.hoursFromReviewDone ?: 0) + stringResource(id = R.string.reviewers_screen_reviewed_message, reviewer.hoursFromReviewDone ?: 0) } else { - stringResource(id = R.string.details_not_reviewed, reviewer.hoursFromPRStart) + stringResource(id = R.string.reviewers_screen_not_reviewed_message, reviewer.hoursFromPRStart) } @Composable @@ -245,7 +245,7 @@ private fun ReviewerName(reviewer: Reviewer) { private fun EmptyListPlaceholder(padding: PaddingValues) { Box(modifier = Modifier.padding(padding)) { LoudiusPlaceholderText( - textId = R.string.you_dont_have_any_reviewers, + text = stringResource(R.string.reviewers_screen_you_dont_have_any_reviewers_message), ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cfe38d44..cae5023b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,32 +1,44 @@ - Loudius - Back button - Pull request - User image - Notify - Reviewed %d h ago. - Not reviewed for %d h. - Pull request # %s - Github icon + + Loudius + Back button + Unauthorized collaborator detected! Please login again. + + Error - Something went wrong… - OK - error image - Try again - Something went wrong…\nYou need to log in again. - Take me to login - Awesome! Your collaborator have been pinged for some serious code review action! 🎉 - Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! - Unauthorized collaborator detected! Please login again. - Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 - Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 + Something went wrong… + OK + error image + Try again + + + + Something went wrong…\nYou need to log in again. + Take me to login - Log in + Log in + Github icon Loudius logo + + Grant permission You\'re using a Xiaomi device and you have Github App installed, please note that there\'s a known bug that requires you to grant the \"Display pup-up windows while running in the background\" permission. This will allow you to continue using the app without any interruptions. Necessary permissions I\'ve already granted + + Sorry! Your list of pull requests is empty.\nGet back to work! 🧑‍💻 + Pull request + + + Awesome! Your collaborator have been pinged for some serious code review action! 🎉 + Uh-oh, it seems that Loudius has taken a vacation. Don\'t worry, we\'re sending a postcard to bring it back ASAP! + User image + Notify + Reviewed %d h ago. + Not reviewed for %d h. + Pull request # %s + Sorry! Your list of reviewers is empty.\n Go to pull request and mark your colleagues as the reviewers! 🤞 + From 86923e380e1eb0ea22477d6542ce463c29ee569b Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Thu, 25 May 2023 12:25:41 +0000 Subject: [PATCH 442/526] [MegaLinter] Apply linters fixes --- .../appunite/loudius/ui/components/LoudiusFullScreenError.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt index 7156f468f..76f6e9170 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusFullScreenError.kt @@ -67,7 +67,7 @@ fun ScreenErrorWithSpacers( modifier = Modifier .weight(weight = .35f) .sizeIn(maxWidth = 400.dp, maxHeight = 400.dp) - .fillMaxWidth() + .fillMaxWidth(), ) Spacer(modifier = Modifier.weight(weight = 0.05f)) ErrorText(text = errorText) @@ -114,7 +114,7 @@ fun LoudiusErrorScreenCustomTextsPreview() { LoudiusTheme { LoudiusFullScreenError( errorText = "Custom title", - buttonText = "My Button Text" + buttonText = "My Button Text", ) {} } } From 4dd5761e67a0d96fb987836c6551bc97d38668c9 Mon Sep 17 00:00:00 2001 From: nowakweronika <72873966+nowakweronika@users.noreply.github.com> Date: Thu, 25 May 2023 14:53:04 +0200 Subject: [PATCH 443/526] [LD-73] Update README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3f3d6de5..0cfebb10d 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,16 @@ Here's an example of an experiment that meets our rules: In order to properly start the application and use it, the CLIENT_SECRET environment variable must be set on your computer. CLIENT_SECRET is a GitHub client secret key provided from ``Settings -> Developer Settings -> OAuth Apps -> my application``. +If you're AppUniter, you can find this secrets [here](https://www.notion.so/appunite/Github-Secrets-0c2c6c1b56e2472c8a4752241f1e20d3?pvs=4). + +If you're not, don't worry, here's a [video](https://github.com/appunite/Loudius/assets/72873966/c366e56d-03cd-412e-914c-0a9777477d65) to help you create a new one. + ### How to set environmental variable on mac? 1. Launch zsh (command `zsh`) 2. `$ echo 'export CLIENT_SECRET=you know what' >> ~/.zshenv` -3. `$ echo $CLIENT_SECRET` -4. Restart your computer. +3. Restart Android studio and Terminal. +4. `$ echo $CLIENT_SECRET` ## 🧑🏻‍🎓 Contributing @@ -96,4 +100,3 @@ from each other. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - From f8e09054be01d69dbbd9080514eb677eb76c7ad5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 16:01:46 +0200 Subject: [PATCH 444/526] add markdown link check rule --- .markdown-link-check.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .markdown-link-check.json diff --git a/.markdown-link-check.json b/.markdown-link-check.json new file mode 100644 index 000000000..2b05bee0e --- /dev/null +++ b/.markdown-link-check.json @@ -0,0 +1,10 @@ +{ + "retryOn429": true, + "retryCount": 5, + "aliveStatusCodes": [ 200, 203 ], + "ignorePatterns": [ + { + "pattern": "^http://www.notion.so/appunite" + } + ] +} From c69e8b52328d1258e955c6736c34a4a4c5573db1 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 16:04:36 +0200 Subject: [PATCH 445/526] Revert "add markdown link check rule" This reverts commit f8e09054be01d69dbbd9080514eb677eb76c7ad5. --- .markdown-link-check.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 .markdown-link-check.json diff --git a/.markdown-link-check.json b/.markdown-link-check.json deleted file mode 100644 index 2b05bee0e..000000000 --- a/.markdown-link-check.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "retryOn429": true, - "retryCount": 5, - "aliveStatusCodes": [ 200, 203 ], - "ignorePatterns": [ - { - "pattern": "^http://www.notion.so/appunite" - } - ] -} From c9c8b98456f63dff4e17ad20d53668e6ea87ae43 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 16:07:19 +0200 Subject: [PATCH 446/526] add markdown link check rule --- .markdown-link-check.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .markdown-link-check.json diff --git a/.markdown-link-check.json b/.markdown-link-check.json new file mode 100644 index 000000000..2b05bee0e --- /dev/null +++ b/.markdown-link-check.json @@ -0,0 +1,10 @@ +{ + "retryOn429": true, + "retryCount": 5, + "aliveStatusCodes": [ 200, 203 ], + "ignorePatterns": [ + { + "pattern": "^http://www.notion.so/appunite" + } + ] +} From eb1db41264429bc379adf757fca2865c39c27f65 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 16:22:19 +0200 Subject: [PATCH 447/526] add idlingResourceWrapper to progress indicator on ReviewersScreen --- .../com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index b8a3e63ab..ffc1b9cee 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R +import com.appunite.loudius.ui.components.IdlingResourceWrapper import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon import com.appunite.loudius.ui.components.LoudiusListItem @@ -224,7 +225,9 @@ private fun NotifyButtonOrLoadingIndicator( modifier = Modifier.alpha(if (reviewer.isLoading) 0f else 1f), ) if (reviewer.isLoading) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) + IdlingResourceWrapper { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } } } } From 8944f2791891b7ba8f8236fb2da28f5deb97bace Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 25 May 2023 16:23:28 +0200 Subject: [PATCH 448/526] chore: Fix incorrect loading indicator used on Reviewers screen --- .../loudius/ui/reviewers/ReviewersScreen.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 7ab8baf4e..f7e91eef0 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -202,7 +201,7 @@ private fun NotifyButtonOrLoadingIndicator( modifier = Modifier.alpha(if (reviewer.isLoading) 0f else 1f), ) if (reviewer.isLoading) { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) + LoudiusLoadingIndicator(modifier = Modifier.size(24.dp)) } } } @@ -261,6 +260,17 @@ private fun ReviewerViewPreview() { } } +@Preview(showBackground = true) +@Composable +private fun ReviewerViewLoadingPreview() { + LoudiusTheme { + ReviewerItem( + index = 0, + reviewer = Reviewer(1, "Kezc", true, 12, 12, isLoading = true), + ) {} + } +} + @Preview @Composable fun DetailsScreenPreview() { From d3bf90205723d518cea4c7db526144587f48cbd5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 16:40:09 +0200 Subject: [PATCH 449/526] Revert "add idlingResourceWrapper to progress indicator on ReviewersScreen" This reverts commit eb1db41264429bc379adf757fca2865c39c27f65. --- .../com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index ffc1b9cee..b8a3e63ab 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R -import com.appunite.loudius.ui.components.IdlingResourceWrapper import com.appunite.loudius.ui.components.LoudiusFullScreenError import com.appunite.loudius.ui.components.LoudiusListIcon import com.appunite.loudius.ui.components.LoudiusListItem @@ -225,9 +224,7 @@ private fun NotifyButtonOrLoadingIndicator( modifier = Modifier.alpha(if (reviewer.isLoading) 0f else 1f), ) if (reviewer.isLoading) { - IdlingResourceWrapper { - CircularProgressIndicator(modifier = Modifier.size(24.dp)) - } + CircularProgressIndicator(modifier = Modifier.size(24.dp)) } } } From 9c00280c5e69041930a39a6b2200ee22aebe2573 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 14:51:22 +0000 Subject: [PATCH 450/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 60e161e44..53fed7d4d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost From 4d5ea0bc8ce215ee4b7dc0efa743b56c843862ac Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 17:08:56 +0200 Subject: [PATCH 451/526] http -> https --- .markdown-link-check.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.markdown-link-check.json b/.markdown-link-check.json index 2b05bee0e..132bbbed1 100644 --- a/.markdown-link-check.json +++ b/.markdown-link-check.json @@ -4,7 +4,7 @@ "aliveStatusCodes": [ 200, 203 ], "ignorePatterns": [ { - "pattern": "^http://www.notion.so/appunite" + "pattern": "^https://www.notion.so/appunite" } ] } From cc0c44df0ed470eabe54067913e46c931952aca2 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Thu, 25 May 2023 15:12:56 +0000 Subject: [PATCH 452/526] [MegaLinter] Apply linters fixes --- .markdown-link-check.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.markdown-link-check.json b/.markdown-link-check.json index 132bbbed1..ca6150231 100644 --- a/.markdown-link-check.json +++ b/.markdown-link-check.json @@ -1,7 +1,7 @@ { "retryOn429": true, "retryCount": 5, - "aliveStatusCodes": [ 200, 203 ], + "aliveStatusCodes": [200, 203], "ignorePatterns": [ { "pattern": "^https://www.notion.so/appunite" From 68486feb727da10b5017af340247271d04523d1a Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 25 May 2023 18:40:08 +0200 Subject: [PATCH 453/526] chore: add test flakyness detection --- .github/workflows/run-ui-test.yml | 54 +++++++++++++++++++-- .mega-linter.yml | 8 ++++ build-tools/requirements.txt | 1 + build-tools/upload-junit-to-cloud.py | 72 ++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 build-tools/requirements.txt create mode 100644 build-tools/upload-junit-to-cloud.py diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index 1043cad24..ae8d7ba37 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -6,6 +6,8 @@ on: branches: - "develop" - "main" + schedule: + - cron: "0 0 * * *" permissions: read-all @@ -18,15 +20,57 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install Python dependencies + uses: py-actions/py-dependency-install@v4 + with: + path: "build-tools/requirements.txt" + + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v1 + with: + credentials_json: ${{ secrets.SERVICE_ACCOUNT }} + - name: Prepare Android Environment uses: ./.github/actions/prepare-android-env - name: Assemble App Debug APK and Android Instrumentation Tests run: ./gradlew assembleDebug assembleDebugAndroidTest - - name: Run tests on Firebase Test Lab - uses: asadmansr/Firebase-Test-Lab-Action@v1.0 + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v1 with: - arg-spec: ".github/tests.yml:android-pixel-2" - env: - SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} + install_components: 'gsutil' + + - id: generate-dir + name: Generate random directory + run: |- + echo "results_dir=$(date +%F_%T)" >> "$GITHUB_OUTPUT" + echo "bucket=test-lab-07qs3ns6c51bi-iazpthysivhkq" >> "$GITHUB_OUTPUT" + + - name: Run tests on Firebase Test Lab + run: |- + gcloud firebase test android run ".github/tests.yml:android-pixel-2" --results-dir="${{ steps.generate-dir.outputs.results_dir }}" --results-bucket="${{ steps.generate-dir.outputs.bucket }}" + + - name: Upload to Big Query + if: always() + run: |- + mkdir "build/test-results" + gsutil cp -r "gs://${{ steps.generate-dir.outputs.bucket }}/${{ steps.generate-dir.outputs.results_dir }}/Pixel2-30-en-portrait/test_result_1.xml" "build/test-results/results.xml" + if [[ "${{ github.event_name }}" != "pull_request" ]]; then + python "build-tools/upload-junit-to-cloud.py" --final + else + python "build-tools/upload-junit-to-cloud.py" + fi + + + + + + + diff --git a/.mega-linter.yml b/.mega-linter.yml index 3e1d26c81..be56a0aae 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -5,3 +5,11 @@ DISABLE_LINTERS: - SPELL_CSPELL - GROOVY_NPM_GROOVY_LINT - REPOSITORY_TRIVY + - PYTHON_BANDIT + - PYTHON_BLACK + - PYTHON_FLAKE8 + - PYTHON_ISORT + - PYTHON_MYPY + - PYTHON_PYLINT + - PYTHON_PYRIGHT + - PYTHON_RUFF diff --git a/build-tools/requirements.txt b/build-tools/requirements.txt new file mode 100644 index 000000000..7590ccd6d --- /dev/null +++ b/build-tools/requirements.txt @@ -0,0 +1 @@ +google-cloud-bigquery==3.10.0 \ No newline at end of file diff --git a/build-tools/upload-junit-to-cloud.py b/build-tools/upload-junit-to-cloud.py new file mode 100644 index 000000000..8427a6a92 --- /dev/null +++ b/build-tools/upload-junit-to-cloud.py @@ -0,0 +1,72 @@ +from google.cloud import bigquery +import xml.etree.ElementTree as ET +import argparse + +def upload(final: bool): + + # set up BigQuery client and dataset/table info + client = bigquery.Client() + + # You need to create this data set in the UI https://console.cloud.google.com/bigquery + dataset_id = 'test_results' + table_id = 'my_table' + + # parse the JUnit test results XML file + tree = ET.parse('build/test-results/results.xml') + root = tree.getroot() + timestamp = root.attrib['timestamp'] + + # create list of dictionaries for BigQuery upload + rows = [] + for testcase in root.iter('testcase'): + success = len(testcase.findall('failure')) == 0 + row = { + 'timestamp': timestamp, + 'testcase_final': final, + 'testcase_name': testcase.attrib['name'], + 'testcase_classname': testcase.attrib['classname'], + 'testcase_time': float(testcase.attrib['time']), + 'testcase_status_success': success + } + rows.append(row) + + # create BigQuery table if it doesn't already exist + dataset_ref = client.dataset(dataset_id) + dataset = bigquery.Dataset(dataset_ref) + table_ref = dataset.table(table_id) + schema = [ + bigquery.SchemaField("timestamp", "TIMESTAMP"), + bigquery.SchemaField('testcase_final', 'BOOLEAN', mode='REQUIRED'), + bigquery.SchemaField('testcase_name', 'STRING', mode='REQUIRED'), + bigquery.SchemaField('testcase_classname', 'STRING', mode='REQUIRED'), + bigquery.SchemaField('testcase_time', 'FLOAT', mode='REQUIRED'), + bigquery.SchemaField('testcase_status_success', 'BOOLEAN', mode='REQUIRED') + ] + table = bigquery.Table(table_ref, schema=schema) + + def table_exists(table_ref): + try: + client.get_table(table_ref) + return True + except Exception as e: + if "Not found" in str(e): + return False + else: + raise e + if not table_exists(table_ref): + client.create_table(table) + + # upload data to BigQuery + errors = client.insert_rows(table, rows) + if errors: + raise Exception(f'Encountered errors while uploading to BigQuery: {errors}') + else: + print('Successfully uploaded to BigQuery!') + + +parser = argparse.ArgumentParser() +parser.add_argument('--final', action='store_true', help='Enable final mode') +args = parser.parse_args() + +# Access the value of --final +upload(args.final) \ No newline at end of file From 561e50bdbf5044838fe717f0045c05d61dca3b08 Mon Sep 17 00:00:00 2001 From: jacek-marchwicki Date: Thu, 25 May 2023 20:29:17 +0000 Subject: [PATCH 454/526] [MegaLinter] Apply linters fixes --- .github/workflows/run-ui-test.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index ae8d7ba37..46147590f 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -45,7 +45,7 @@ jobs: - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v1 with: - install_components: 'gsutil' + install_components: "gsutil" - id: generate-dir name: Generate random directory @@ -67,10 +67,3 @@ jobs: else python "build-tools/upload-junit-to-cloud.py" fi - - - - - - - From cfffd341bbb2651d3a0c1ae24b719192f91c643d Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Thu, 25 May 2023 22:34:50 +0200 Subject: [PATCH 455/526] chore: cleanup in the bigquery upload script --- build-tools/upload-junit-to-cloud.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/build-tools/upload-junit-to-cloud.py b/build-tools/upload-junit-to-cloud.py index 8427a6a92..d1fdba10f 100644 --- a/build-tools/upload-junit-to-cloud.py +++ b/build-tools/upload-junit-to-cloud.py @@ -2,21 +2,17 @@ import xml.etree.ElementTree as ET import argparse +# Uploading JUnit test results to BigQuery def upload(final: bool): - - # set up BigQuery client and dataset/table info client = bigquery.Client() - # You need to create this data set in the UI https://console.cloud.google.com/bigquery dataset_id = 'test_results' table_id = 'my_table' - # parse the JUnit test results XML file tree = ET.parse('build/test-results/results.xml') root = tree.getroot() timestamp = root.attrib['timestamp'] - # create list of dictionaries for BigQuery upload rows = [] for testcase in root.iter('testcase'): success = len(testcase.findall('failure')) == 0 @@ -30,7 +26,6 @@ def upload(final: bool): } rows.append(row) - # create BigQuery table if it doesn't already exist dataset_ref = client.dataset(dataset_id) dataset = bigquery.Dataset(dataset_ref) table_ref = dataset.table(table_id) @@ -56,7 +51,6 @@ def table_exists(table_ref): if not table_exists(table_ref): client.create_table(table) - # upload data to BigQuery errors = client.insert_rows(table, rows) if errors: raise Exception(f'Encountered errors while uploading to BigQuery: {errors}') @@ -68,5 +62,4 @@ def table_exists(table_ref): parser.add_argument('--final', action='store_true', help='Enable final mode') args = parser.parse_args() -# Access the value of --final upload(args.final) \ No newline at end of file From 016c14e601aed01911d8208b1f2221088f133c1d Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Fri, 26 May 2023 11:04:09 +0200 Subject: [PATCH 456/526] chore: schedule runs at different time --- .github/workflows/run-ui-test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index 46147590f..3c5ae6ad4 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -7,7 +7,10 @@ on: - "develop" - "main" schedule: - - cron: "0 0 * * *" + # Run twice a day the sanity check, at 9:13 and 21:13. + # You ask why 13? because probably less people schedule their tasks at exactly this time, so I + # guess CI is less occupied. And 13 is a lucky number ;) + - cron: "13 9,21 * * *" permissions: read-all From dc010ccf67d7900d7f9ca621c0fd19c8bb56daf0 Mon Sep 17 00:00:00 2001 From: Jacek Marchwicki Date: Fri, 26 May 2023 12:42:10 +0200 Subject: [PATCH 457/526] chore: better results-dir with random number --- .github/workflows/run-ui-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index 3c5ae6ad4..df2cc8b59 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -53,7 +53,7 @@ jobs: - id: generate-dir name: Generate random directory run: |- - echo "results_dir=$(date +%F_%T)" >> "$GITHUB_OUTPUT" + echo "results_dir=$(date +%F_%T)-${RANDOM}" >> "$GITHUB_OUTPUT" echo "bucket=test-lab-07qs3ns6c51bi-iazpthysivhkq" >> "$GITHUB_OUTPUT" - name: Run tests on Firebase Test Lab From 146a23ec42e4f06956f2ab94610864fe0518406d Mon Sep 17 00:00:00 2001 From: nowakweronika <72873966+nowakweronika@users.noreply.github.com> Date: Fri, 26 May 2023 13:19:12 +0200 Subject: [PATCH 458/526] addition of lower resolution video --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0cfebb10d..49e8c49aa 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,9 @@ In order to properly start the application and use it, the CLIENT_SECRET environ If you're AppUniter, you can find this secrets [here](https://www.notion.so/appunite/Github-Secrets-0c2c6c1b56e2472c8a4752241f1e20d3?pvs=4). -If you're not, don't worry, here's a [video](https://github.com/appunite/Loudius/assets/72873966/c366e56d-03cd-412e-914c-0a9777477d65) to help you create a new one. +If you're not, don't worry, here's a video to help you create a new one: + +https://github.com/appunite/Loudius/assets/72873966/4820b6df-81ca-48ed-9f3c-425011b758dd ### How to set environmental variable on mac? From ff4c358d28ffba6dcee289f3e849eaa91cc7fa67 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 26 May 2023 11:23:36 +0000 Subject: [PATCH 459/526] [MegaLinter] Apply linters fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49e8c49aa..055cea0d5 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ If you're AppUniter, you can find this secrets [here](https://www.notion.so/appu If you're not, don't worry, here's a video to help you create a new one: -https://github.com/appunite/Loudius/assets/72873966/4820b6df-81ca-48ed-9f3c-425011b758dd + ### How to set environmental variable on mac? From 5484d1444953b631778cb150512c34915d0bb5ee Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Sun, 28 May 2023 17:49:45 +0200 Subject: [PATCH 460/526] code cleanup --- .../ui/pullrequests/PullRequestsScreen.kt | 34 ++++++++++++------- .../ui/pullrequests/PullRequestsViewModel.kt | 21 +++++++++++- .../loudius/ui/reviewers/ReviewersScreen.kt | 34 ++++++++++++------- .../ui/reviewers/ReviewersViewModel.kt | 18 +++++++++- 4 files changed, 80 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index cb5ed69d6..1c34f80f9 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -35,6 +35,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -63,13 +65,15 @@ fun PullRequestsScreen( navigateToReviewers: NavigateToReviewers, ) { val state = viewModel.state - val swipeRefreshState = rememberPullRefreshState( - refreshing = state.data == Data.Loading, - onRefresh = { viewModel.fetchData() }, + val refreshing by viewModel.isRefreshing.collectAsState() + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = viewModel::refreshData, ) PullRequestsScreenStateless( state = state, - pullRefreshState = swipeRefreshState, + pullRefreshState = pullRefreshState, + refreshing = refreshing, onAction = viewModel::onAction, ) LaunchedEffect(state.navigateToReviewers) { @@ -97,6 +101,7 @@ private fun navigateToReviewers( private fun PullRequestsScreenStateless( state: PullRequestState, pullRefreshState: PullRefreshState, + refreshing: Boolean, onAction: (PulLRequestsAction) -> Unit, ) { Scaffold( @@ -110,7 +115,7 @@ private fun PullRequestsScreenStateless( onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Success -> PullRequestContent(state.data, pullRefreshState, padding, onAction) + is Data.Success -> PullRequestContent(state.data, pullRefreshState, refreshing, padding, onAction) } }, ) @@ -118,12 +123,13 @@ private fun PullRequestsScreenStateless( @Composable private fun PullRequestContent( - state: Data, + state: Data.Success, pullRefreshState: PullRefreshState, + refreshing: Boolean, padding: PaddingValues, onAction: (PulLRequestsAction) -> Unit, ) { - if ((state as Data.Success).pullRequests.isEmpty()) { + if (state.pullRequests.isEmpty()) { EmptyListPlaceholder(padding) } else { PullRequestsList( @@ -131,7 +137,7 @@ private fun PullRequestContent( modifier = Modifier.padding(padding), onItemClick = onAction, pullRefreshState = pullRefreshState, - isLoading = state == Data.Loading, + refreshing = refreshing, ) } } @@ -142,11 +148,11 @@ private fun PullRequestsList( modifier: Modifier, onItemClick: (PulLRequestsAction) -> Unit, pullRefreshState: PullRefreshState, - isLoading: Boolean, + refreshing: Boolean, ) { - Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + Box(modifier = modifier.pullRefresh(pullRefreshState)) { LazyColumn( - modifier = modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { itemsIndexed(pullRequests) { index, item -> PullRequestItem( @@ -156,7 +162,7 @@ private fun PullRequestsList( ) } } - PullRefreshIndicator(isLoading, pullRefreshState, Modifier.align(Alignment.TopCenter)) + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -261,6 +267,7 @@ fun PullRequestsScreenPreview() { refreshing = false, onRefresh = {}, ), + refreshing = false ) } } @@ -276,6 +283,7 @@ fun PullRequestsScreenEmptyListPreview() { refreshing = false, onRefresh = {}, ), + refreshing = false ) } } @@ -291,6 +299,7 @@ fun PullRequestsScreenLoadingPreview() { refreshing = false, onRefresh = {}, ), + refreshing = false ) } } @@ -306,6 +315,7 @@ fun PullRequestsScreenErrorPreview() { refreshing = false, onRefresh = {}, ), + refreshing = false ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index ecdbcd701..30cbfb9ec 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -24,6 +24,9 @@ import androidx.lifecycle.viewModelScope import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -58,11 +61,27 @@ class PullRequestsViewModel @Inject constructor( var state: PullRequestState by mutableStateOf(PullRequestState()) private set + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow + get() = _isRefreshing.asStateFlow() + init { fetchData() } - fun fetchData() { + fun refreshData() { + viewModelScope.launch { + pullRequestsRepository.getCurrentUserPullRequests() + .onSuccess { + state = state.copy(data = Data.Success(it.items)) + }.onFailure { + state = state.copy(data = Data.Error) + } + _isRefreshing.emit(false) + } + } + + private fun fetchData() { viewModelScope.launch { state = PullRequestState() pullRequestsRepository.getCurrentUserPullRequests() diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 53fed7d4d..389f4a573 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -21,7 +21,7 @@ package com.appunite.loudius.ui.reviewers import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -38,6 +38,8 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.Center @@ -69,15 +71,17 @@ fun ReviewersScreen( ) { val state = viewModel.state val snackbarHostState = remember { SnackbarHostState() } - val swipeRefreshState = rememberPullRefreshState( - refreshing = state.data == Data.Loading, - onRefresh = { viewModel.fetchData() }, + val refreshing by viewModel.isRefreshing.collectAsState() + val pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = viewModel::refreshData, ) ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, data = state.data, - pullRefreshState = swipeRefreshState, + pullRefreshState = pullRefreshState, + refreshing = refreshing, onClickBackArrow = navigateBack, snackbarHostState = snackbarHostState, onAction = viewModel::onAction, @@ -120,6 +124,7 @@ private fun ReviewersScreenStateless( pullRequestNumber: String, data: Data, pullRefreshState: PullRefreshState, + refreshing: Boolean, onClickBackArrow: () -> Unit, snackbarHostState: SnackbarHostState, onAction: (ReviewersAction) -> Unit, @@ -140,7 +145,7 @@ private fun ReviewersScreenStateless( ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Success -> ReviewersScreenContent(data, pullRefreshState, padding, onAction) + is Data.Success-> ReviewersScreenContent(data, pullRefreshState, refreshing, padding, onAction) } }, ) @@ -148,18 +153,19 @@ private fun ReviewersScreenStateless( @Composable private fun ReviewersScreenContent( - data: Data, + data: Data.Success, pullRefreshState: PullRefreshState, + refreshing: Boolean, padding: PaddingValues, onAction: (ReviewersAction) -> Unit, ) { - if ((data as Data.Success).reviewers.isNotEmpty()) { + if (data.reviewers.isNotEmpty()) { ReviewersList( data = data, pullRefreshState = pullRefreshState, modifier = Modifier.padding(padding), onNotifyClick = onAction, - isLoading = data == Data.Loading, + refreshing = refreshing, ) } else { EmptyListPlaceholder(padding) @@ -172,11 +178,11 @@ private fun ReviewersList( pullRefreshState: PullRefreshState, modifier: Modifier, onNotifyClick: (ReviewersAction) -> Unit, - isLoading: Boolean, + refreshing: Boolean, ) { - Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + Box(modifier = modifier.pullRefresh(pullRefreshState)) { LazyColumn( - modifier = modifier.fillMaxWidth(), + modifier = Modifier.fillMaxSize(), ) { itemsIndexed((data as Data.Success).reviewers) { index, reviewer -> ReviewerItem( @@ -186,7 +192,7 @@ private fun ReviewersList( ) } } - PullRefreshIndicator(isLoading, pullRefreshState, Modifier.align(Alignment.TopCenter)) + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } @@ -312,6 +318,7 @@ fun DetailsScreenPreview() { refreshing = false, onRefresh = {}, ), + refreshing = false ) } } @@ -330,6 +337,7 @@ fun DetailsScreenNoReviewsPreview() { refreshing = false, onRefresh = {}, ), + refreshing = false ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 2283ee8cf..560a67d9d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -32,6 +32,9 @@ import com.appunite.loudius.ui.reviewers.ReviewersSnackbarType.SUCCESS import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDateTime import java.time.temporal.ChronoUnit @@ -69,12 +72,25 @@ class ReviewersViewModel @Inject constructor( var state: ReviewersState by mutableStateOf(ReviewersState()) private set + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow + get() = _isRefreshing.asStateFlow() + init { state = state.copy(pullRequestNumber = initialValues.pullRequestNumber) fetchData() } - fun fetchData() { + fun refreshData() { + viewModelScope.launch { + getMergedData() + .onSuccess { state = state.copy(data = Data.Success(reviewers = it)) } + .onFailure { state = state.copy(data = Data.Error) } + _isRefreshing.emit(false) + } + } + + private fun fetchData() { viewModelScope.launch { state = state.copy(data = Data.Loading) From ae86327dd608d04a804debe175ba957ce0f054cb Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Sun, 28 May 2023 18:27:41 +0200 Subject: [PATCH 461/526] add custom pull to refresh box --- .../ui/components/LoudiusPullToRefreshBox.kt | 41 +++++++++++++++++++ .../ui/pullrequests/PullRequestsScreen.kt | 34 +++++++-------- .../loudius/ui/reviewers/ReviewersScreen.kt | 34 +++++++-------- 3 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt new file mode 100644 index 000000000..e5f61fc12 --- /dev/null +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMaterialApi::class) + +package com.appunite.loudius.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun LoudiusPullToRefreshBox( + content: @Composable () -> Unit, + pullRefreshState: PullRefreshState, + refreshing: Boolean, + modifier: Modifier = Modifier +) { + Box(modifier = modifier.pullRefresh(pullRefreshState)) { + content() + PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 1c34f80f9..8bfffe784 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -27,9 +27,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshState -import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold @@ -37,7 +35,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -51,6 +48,7 @@ import com.appunite.loudius.ui.components.LoudiusListIcon import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusPlaceholderText +import com.appunite.loudius.ui.components.LoudiusPullToRefreshBox import com.appunite.loudius.ui.components.LoudiusText import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar @@ -150,20 +148,24 @@ private fun PullRequestsList( pullRefreshState: PullRefreshState, refreshing: Boolean, ) { - Box(modifier = modifier.pullRefresh(pullRefreshState)) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - itemsIndexed(pullRequests) { index, item -> - PullRequestItem( - index = index, - data = item, - onClick = onItemClick, - ) + LoudiusPullToRefreshBox( + content = { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + itemsIndexed(pullRequests) { index, item -> + PullRequestItem( + index = index, + data = item, + onClick = onItemClick, + ) + } } - } - PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) - } + }, + pullRefreshState = pullRefreshState, + refreshing = refreshing, + modifier = modifier + ) } @Composable diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 389f4a573..285da8a0b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -27,9 +27,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshState -import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold @@ -41,7 +39,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -57,6 +54,7 @@ import com.appunite.loudius.ui.components.LoudiusListItem import com.appunite.loudius.ui.components.LoudiusLoadingIndicator import com.appunite.loudius.ui.components.LoudiusOutlinedButton import com.appunite.loudius.ui.components.LoudiusPlaceholderText +import com.appunite.loudius.ui.components.LoudiusPullToRefreshBox import com.appunite.loudius.ui.components.LoudiusText import com.appunite.loudius.ui.components.LoudiusTextStyle import com.appunite.loudius.ui.components.LoudiusTopAppBar @@ -180,20 +178,24 @@ private fun ReviewersList( onNotifyClick: (ReviewersAction) -> Unit, refreshing: Boolean, ) { - Box(modifier = modifier.pullRefresh(pullRefreshState)) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - itemsIndexed((data as Data.Success).reviewers) { index, reviewer -> - ReviewerItem( - reviewer = reviewer, - index = index, - onNotifyClick = onNotifyClick, - ) + LoudiusPullToRefreshBox( + content = { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + itemsIndexed((data as Data.Success).reviewers) { index, reviewer -> + ReviewerItem( + reviewer = reviewer, + index = index, + onNotifyClick = onNotifyClick, + ) + } } - } - PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) - } + }, + pullRefreshState = pullRefreshState, + refreshing = refreshing, + modifier = modifier + ) } @Composable From 16e5ec97d6f024292c71a198a16f43bf0e8f98f5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Sun, 28 May 2023 19:27:15 +0200 Subject: [PATCH 462/526] fix is refreshing state --- .../loudius/ui/pullrequests/PullRequestsViewModel.kt | 9 +++++---- .../appunite/loudius/ui/reviewers/ReviewersViewModel.kt | 7 +++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index 30cbfb9ec..a0c473760 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -16,6 +16,7 @@ package com.appunite.loudius.ui.pullrequests +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -25,7 +26,6 @@ import com.appunite.loudius.domain.repository.PullRequestRepository import com.appunite.loudius.network.model.PullRequest import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -62,8 +62,7 @@ class PullRequestsViewModel @Inject constructor( private set private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow - get() = _isRefreshing.asStateFlow() + val isRefreshing = _isRefreshing.asStateFlow() init { fetchData() @@ -71,19 +70,21 @@ class PullRequestsViewModel @Inject constructor( fun refreshData() { viewModelScope.launch { + _isRefreshing.value = true pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { state = state.copy(data = Data.Success(it.items)) }.onFailure { state = state.copy(data = Data.Error) } - _isRefreshing.emit(false) + _isRefreshing.value = false } } private fun fetchData() { viewModelScope.launch { state = PullRequestState() + Log.i("#fetch is refrieshing value", isRefreshing.value.toString()) pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { state = state.copy(data = Data.Success(it.items)) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt index 560a67d9d..602d4078b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersViewModel.kt @@ -33,7 +33,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import java.time.LocalDateTime @@ -73,8 +72,7 @@ class ReviewersViewModel @Inject constructor( private set private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow - get() = _isRefreshing.asStateFlow() + val isRefreshing = _isRefreshing.asStateFlow() init { state = state.copy(pullRequestNumber = initialValues.pullRequestNumber) @@ -83,10 +81,11 @@ class ReviewersViewModel @Inject constructor( fun refreshData() { viewModelScope.launch { + _isRefreshing.value = true getMergedData() .onSuccess { state = state.copy(data = Data.Success(reviewers = it)) } .onFailure { state = state.copy(data = Data.Error) } - _isRefreshing.emit(false) + _isRefreshing.value = false } } From c1bd97f1fcf84670c8e498caa2664879f2074dc4 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Sun, 28 May 2023 19:48:07 +0200 Subject: [PATCH 463/526] add tests for refresh data --- .../ui/pullrequests/PullRequestsViewModel.kt | 2 -- .../ui/pullrequests/PullRequestsViewModelTest.kt | 11 +++++++++++ .../ui/reviewers/ReviewersViewModelTest.kt | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt index a0c473760..3dce46ac3 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModel.kt @@ -16,7 +16,6 @@ package com.appunite.loudius.ui.pullrequests -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -84,7 +83,6 @@ class PullRequestsViewModel @Inject constructor( private fun fetchData() { viewModelScope.launch { state = PullRequestState() - Log.i("#fetch is refrieshing value", isRefreshing.value.toString()) pullRequestsRepository.getCurrentUserPullRequests() .onSuccess { state = state.copy(data = Data.Success(it.items)) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 6f024c500..5875d1999 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -40,6 +40,17 @@ class PullRequestsViewModelTest { private val pullRequestRepository = spyk(FakePullRequestRepository()) private fun createViewModel() = PullRequestsViewModel(pullRequestRepository) + @Test + fun `WHEN refresh data THEN refresh data and display pull requests`() = runTest { + val viewModel = createViewModel() + + viewModel.refreshData() + + expectThat(viewModel.state.data).isA().and { + get(Data.Success::pullRequests).single().isEqualTo(Defaults.pullRequest()) + } + } + @Test fun `WHEN init THEN display loading`() = runTest { coEvery { pullRequestRepository.getCurrentUserPullRequests() } coAnswers { neverCompletingSuspension() } diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index b39c2a01e..d5a9073c7 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -78,6 +78,21 @@ class ReviewersViewModelTest { @Nested inner class InitTest { + @Test + fun `WHEN refresh data THEN refresh data and display reviewers`() = runTest { + viewModel = createViewModel() + + viewModel.refreshData() + + expectThat(viewModel.state.data).isA().and { + get(Data.Success::reviewers).containsExactly( + Reviewer(1, "user1", true, 7, 5), + Reviewer(2, "user2", false, 7, null), + Reviewer(3, "user3", false, 7, null), + ) + } + } + @Test fun `GIVEN no values in saved state WHEN init THEN throw IllegalStateException`() { every { savedStateHandle.get(any()) } returns null From 6617e490efe86f9d622e3010abae2573e9bf741d Mon Sep 17 00:00:00 2001 From: Wojtek Date: Fri, 19 May 2023 12:17:25 +0200 Subject: [PATCH 464/526] Add walk through app test --- .../appunite/loudius/ReviewersScreenTest.kt | 4 +- .../appunite/loudius/WalkThroughAppTest.kt | 83 +++++++++++++++++++ .../loudius/util/IntegrationTestRule.kt | 8 +- .../com/appunite/loudius/util/Register.kt | 19 ++++- 4 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index 0cb0c88d6..77409a631 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -89,8 +89,8 @@ class ReviewersScreenTest { private fun IntegrationTestRule.initTests() { composeTestRule.activity.intent.apply { - putExtra("owner", "owner") - putExtra("repo", "repo") + putExtra("owner", "exampleOwner") + putExtra("repo", "exampleRepo") putExtra("submission_date", "2022-01-29T08:00:00") putExtra("pull_request_number", "1") } diff --git a/app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt b/app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt new file mode 100644 index 000000000..f6a1dbcd6 --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.net.Uri +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers +import androidx.test.espresso.intent.rule.IntentsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.util.IntegrationTestRule +import com.appunite.loudius.util.Register +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +@HiltAndroidTest +class WalkThroughAppTest { + + @get:Rule(order = 0) + val integrationTestRule = IntegrationTestRule(this, MainActivity::class.java) + + @get:Rule(order = 1) + val intents = IntentsRule() + + @Test + fun whenLoginScreenIsVisible_LoginButtonOpensGithubAuth(): Unit = with(integrationTestRule) { + Register.run { + user(mockWebServer) + accessToken(mockWebServer) + issues(mockWebServer) + requestedReviewers(mockWebServer) + reviews(mockWebServer) + comment(mockWebServer) + } + + Intents.intending(IntentMatchers.hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo")) + .respondWithFunction { + Instrumentation.ActivityResult(Activity.RESULT_OK, null) + } + + composeTestRule.onNodeWithText("Log in").performClick() + + // simulate opening a deeplink + ActivityScenario.launch(Intent( + Intent.ACTION_VIEW, + Uri.parse("loudius://callback?code=example_code") + ).apply { + setPackage(composeTestRule.activity.packageName) + }) + + composeTestRule.onNodeWithText("First Pull-Request title").performClick() + + composeTestRule.onNodeWithText("Notify").performClick() + composeTestRule + .onNodeWithText("Awesome! Your collaborator have been pinged for some serious code review action! \uD83C\uDF89") + .assertIsDisplayed() + + } +} diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt index d8f757fe3..3cd1a320c 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt @@ -16,6 +16,7 @@ package com.appunite.loudius.util +import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import com.appunite.loudius.TestActivity import com.appunite.loudius.ui.components.countingResource @@ -26,10 +27,13 @@ import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -class IntegrationTestRule(testClass: Any) : TestRule { +class IntegrationTestRule( + testClass: Any, + testActivity: Class = TestActivity::class.java, +) : TestRule { val mockWebServer = MockWebServerRule() - val composeTestRule = createAndroidComposeRule().apply { + val composeTestRule = createAndroidComposeRule(testActivity).apply { registerIdlingResource(countingResource.toIdlingResource()) } private val hiltRule = HiltAndroidRule(testClass) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt index db19893f1..c2c1b9147 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt @@ -28,11 +28,24 @@ object Register { } } + fun accessToken(mockWebServer: MockWebServerRule) { + mockWebServer.register { + expectThat(it).url.path.isEqualTo("/login/oauth/access_token") + jsonResponse(""" + { + "access_token": "example_access_token", + "token_type": "bearer", + "scope": "repo" + } + """.trimIndent()) + } + } + fun reviews(mockWebServer: MockWebServerRule) { mockWebServer.register { expectThat(it).url.and { get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/owner/repo/pulls/1/reviews") + path.isEqualTo("/repos/exampleOwner/exampleRepo/pulls/1/reviews") } jsonResponse("[]") } @@ -42,7 +55,7 @@ object Register { mockWebServer.register { expectThat(it).url.and { get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/owner/repo/issues/1/comments") + path.isEqualTo("/repos/exampleOwner/exampleRepo/issues/1/comments") } jsonResponse( """ @@ -86,7 +99,7 @@ object Register { mockWebServer.register { expectThat(it).url.and { get("host") { host }.isEqualTo("api.github.com") - path.isEqualTo("/repos/owner/repo/pulls/1/requested_reviewers") + path.isEqualTo("/repos/exampleOwner/exampleRepo/pulls/1/requested_reviewers") } jsonResponse( """ From 113c6f8b0e12deeb01f13314fecd7b07dd52eb0d Mon Sep 17 00:00:00 2001 From: kezc Date: Mon, 29 May 2023 07:15:22 +0000 Subject: [PATCH 465/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/WalkThroughAppTest.kt | 16 ++++++++-------- .../java/com/appunite/loudius/util/Register.kt | 6 ++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt b/app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt index f6a1dbcd6..a8293ba40 100644 --- a/app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/WalkThroughAppTest.kt @@ -35,7 +35,6 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith - @RunWith(AndroidJUnit4::class) @HiltAndroidTest class WalkThroughAppTest { @@ -65,12 +64,14 @@ class WalkThroughAppTest { composeTestRule.onNodeWithText("Log in").performClick() // simulate opening a deeplink - ActivityScenario.launch(Intent( - Intent.ACTION_VIEW, - Uri.parse("loudius://callback?code=example_code") - ).apply { - setPackage(composeTestRule.activity.packageName) - }) + ActivityScenario.launch( + Intent( + Intent.ACTION_VIEW, + Uri.parse("loudius://callback?code=example_code"), + ).apply { + setPackage(composeTestRule.activity.packageName) + }, + ) composeTestRule.onNodeWithText("First Pull-Request title").performClick() @@ -78,6 +79,5 @@ class WalkThroughAppTest { composeTestRule .onNodeWithText("Awesome! Your collaborator have been pinged for some serious code review action! \uD83C\uDF89") .assertIsDisplayed() - } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt index c2c1b9147..8979585c0 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/Register.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/Register.kt @@ -31,13 +31,15 @@ object Register { fun accessToken(mockWebServer: MockWebServerRule) { mockWebServer.register { expectThat(it).url.path.isEqualTo("/login/oauth/access_token") - jsonResponse(""" + jsonResponse( + """ { "access_token": "example_access_token", "token_type": "bearer", "scope": "repo" } - """.trimIndent()) + """.trimIndent(), + ) } } From b1f3cd00c3cf80b2619e27bfa9292d48166b2583 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 29 May 2023 09:19:09 +0200 Subject: [PATCH 466/526] add preview for refreshing --- .../ui/pullrequests/PullRequestsScreen.kt | 37 +++++++++++++++++++ .../loudius/ui/reviewers/ReviewersScreen.kt | 24 ++++++++++++ 2 files changed, 61 insertions(+) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 8bfffe784..a1093bafa 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -321,3 +321,40 @@ fun PullRequestsScreenErrorPreview() { ) } } + +@Preview("Pull requests - refreshing") +@Composable +fun PullRequestsScreenRefreshingPreview() { + LoudiusTheme { + PullRequestsScreenStateless( + state = PullRequestState( + Data.Success( + listOf( + PullRequest( + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", + title = "[SIL-67] Details screen - network layer", + createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), + ), + PullRequest( + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", + title = "[SIL-66] Add client secret to build config", + createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + ), + ), + ), + ), + onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = true, + onRefresh = {}, + ), + refreshing = true + ) + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 285da8a0b..28bb60c1d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -343,3 +343,27 @@ fun DetailsScreenNoReviewsPreview() { ) } } + +@Preview +@Composable +fun DetailsScreenRefreshingPreview() { + val reviewer1 = Reviewer(1, "Kezc", true, 24, 12) + val reviewer2 = Reviewer(2, "Krzysiudan", false, 24, 0) + val reviewer3 = Reviewer(3, "Weronika", false, 24, 0) + val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) + val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) + LoudiusTheme { + ReviewersScreenStateless( + pullRequestNumber = "1", + data = Data.Success(reviewers), + onClickBackArrow = {}, + snackbarHostState = SnackbarHostState(), + onAction = {}, + pullRefreshState = rememberPullRefreshState( + refreshing = true, + onRefresh = {}, + ), + refreshing = true + ) + } +} From 83f91723de63e4545c622ee24ba4d73b4fb6bd2c Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 29 May 2023 10:15:18 +0200 Subject: [PATCH 467/526] extract success data --- .../ui/pullrequests/PullRequestsScreen.kt | 99 ++++++++----------- .../loudius/ui/reviewers/ReviewersScreen.kt | 21 ++-- 2 files changed, 48 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index a1093bafa..b9b7b208e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -221,49 +221,49 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { } } +private val successData = Data.Success( + listOf( + PullRequest( + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", + title = "[SIL-67] Details screen - network layer", + createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), + ), + PullRequest( + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", + title = "[SIL-66] Add client secret to build config", + createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + ), + PullRequest( + id = 2, + draft = false, + number = 2, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", + title = "[SIL-73] Storing access token", + createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), + ), + PullRequest( + id = 3, + draft = false, + number = 3, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", + title = "[SIL-62/SIL-75] Provide new annotation for API instances", + createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), + ), + ), +) + @Preview("Pull requests - filled list") @Composable fun PullRequestsScreenPreview() { LoudiusTheme { PullRequestsScreenStateless( - state = PullRequestState( - Data.Success( - listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), - ), - PullRequest( - id = 2, - draft = false, - number = 2, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", - title = "[SIL-73] Storing access token", - createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), - ), - PullRequest( - id = 3, - draft = false, - number = 3, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", - title = "[SIL-62/SIL-75] Provide new annotation for API instances", - createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), - ), - ), - ), - ), + state = PullRequestState(successData), onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, @@ -327,28 +327,7 @@ fun PullRequestsScreenErrorPreview() { fun PullRequestsScreenRefreshingPreview() { LoudiusTheme { PullRequestsScreenStateless( - state = PullRequestState( - Data.Success( - listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), - ), - ), - ), - ), + state = PullRequestState(successData), onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = true, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 28bb60c1d..33c8f00e2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -301,18 +301,20 @@ private fun ReviewerViewLoadingPreview() { } } +private val successData = listOf( + Reviewer(1, "Kezc", true, 24, 12), + Reviewer(2, "Krzysiudan", false, 24, 0), + Reviewer(3, "Weronika", false, 24, 0, true), + Reviewer(4, "Jacek", false, 24, 0) +) + @Preview @Composable fun DetailsScreenPreview() { - val reviewer1 = Reviewer(1, "Kezc", true, 24, 12) - val reviewer2 = Reviewer(2, "Krzysiudan", false, 24, 0) - val reviewer3 = Reviewer(3, "Weronika", false, 24, 0, true) - val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) - val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - data = Data.Success(reviewers), + data = Data.Success(successData), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, @@ -347,15 +349,10 @@ fun DetailsScreenNoReviewsPreview() { @Preview @Composable fun DetailsScreenRefreshingPreview() { - val reviewer1 = Reviewer(1, "Kezc", true, 24, 12) - val reviewer2 = Reviewer(2, "Krzysiudan", false, 24, 0) - val reviewer3 = Reviewer(3, "Weronika", false, 24, 0) - val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) - val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - data = Data.Success(reviewers), + data = Data.Success(successData), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, From 259444b786b61c74edb4b86ec471d05de04acd6b Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 29 May 2023 10:15:18 +0200 Subject: [PATCH 468/526] extract success data - skip fix --- .../ui/pullrequests/PullRequestsScreen.kt | 99 ++++++++----------- .../loudius/ui/reviewers/ReviewersScreen.kt | 21 ++-- 2 files changed, 48 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index a1093bafa..b9b7b208e 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -221,49 +221,49 @@ private fun EmptyListPlaceholder(padding: PaddingValues) { } } +private val successData = Data.Success( + listOf( + PullRequest( + id = 0, + draft = false, + number = 0, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", + title = "[SIL-67] Details screen - network layer", + createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), + ), + PullRequest( + id = 1, + draft = true, + number = 1, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", + title = "[SIL-66] Add client secret to build config", + createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), + ), + PullRequest( + id = 2, + draft = false, + number = 2, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", + title = "[SIL-73] Storing access token", + createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), + ), + PullRequest( + id = 3, + draft = false, + number = 3, + repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", + title = "[SIL-62/SIL-75] Provide new annotation for API instances", + createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), + ), + ), +) + @Preview("Pull requests - filled list") @Composable fun PullRequestsScreenPreview() { LoudiusTheme { PullRequestsScreenStateless( - state = PullRequestState( - Data.Success( - listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), - ), - PullRequest( - id = 2, - draft = false, - number = 2, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Loudius", - title = "[SIL-73] Storing access token", - createdAt = LocalDateTime.parse("2023-01-29T16:31:41"), - ), - PullRequest( - id = 3, - draft = false, - number = 3, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Blocktrade", - title = "[SIL-62/SIL-75] Provide new annotation for API instances", - createdAt = LocalDateTime.parse("2022-01-29T16:31:41"), - ), - ), - ), - ), + state = PullRequestState(successData), onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = false, @@ -327,28 +327,7 @@ fun PullRequestsScreenErrorPreview() { fun PullRequestsScreenRefreshingPreview() { LoudiusTheme { PullRequestsScreenStateless( - state = PullRequestState( - Data.Success( - listOf( - PullRequest( - id = 0, - draft = false, - number = 0, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Stefan", - title = "[SIL-67] Details screen - network layer", - createdAt = LocalDateTime.parse("2021-11-29T16:31:41"), - ), - PullRequest( - id = 1, - draft = true, - number = 1, - repositoryUrl = "${Constants.BASE_API_URL}/repos/appunite/Silentus", - title = "[SIL-66] Add client secret to build config", - createdAt = LocalDateTime.parse("2022-11-29T16:31:41"), - ), - ), - ), - ), + state = PullRequestState(successData), onAction = {}, pullRefreshState = rememberPullRefreshState( refreshing = true, diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 28bb60c1d..33c8f00e2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -301,18 +301,20 @@ private fun ReviewerViewLoadingPreview() { } } +private val successData = listOf( + Reviewer(1, "Kezc", true, 24, 12), + Reviewer(2, "Krzysiudan", false, 24, 0), + Reviewer(3, "Weronika", false, 24, 0, true), + Reviewer(4, "Jacek", false, 24, 0) +) + @Preview @Composable fun DetailsScreenPreview() { - val reviewer1 = Reviewer(1, "Kezc", true, 24, 12) - val reviewer2 = Reviewer(2, "Krzysiudan", false, 24, 0) - val reviewer3 = Reviewer(3, "Weronika", false, 24, 0, true) - val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) - val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - data = Data.Success(reviewers), + data = Data.Success(successData), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, @@ -347,15 +349,10 @@ fun DetailsScreenNoReviewsPreview() { @Preview @Composable fun DetailsScreenRefreshingPreview() { - val reviewer1 = Reviewer(1, "Kezc", true, 24, 12) - val reviewer2 = Reviewer(2, "Krzysiudan", false, 24, 0) - val reviewer3 = Reviewer(3, "Weronika", false, 24, 0) - val reviewer4 = Reviewer(4, "Jacek", false, 24, 0) - val reviewers = listOf(reviewer1, reviewer2, reviewer3, reviewer4) LoudiusTheme { ReviewersScreenStateless( pullRequestNumber = "1", - data = Data.Success(reviewers), + data = Data.Success(successData), onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, From e558002e2ace5a30089ccaa6252b4c0283322632 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 29 May 2023 09:06:42 +0000 Subject: [PATCH 469/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/components/LoudiusPullToRefreshBox.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 12 ++++++------ .../appunite/loudius/ui/reviewers/ReviewersScreen.kt | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt index e5f61fc12..91f1586af 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt @@ -32,7 +32,7 @@ fun LoudiusPullToRefreshBox( content: @Composable () -> Unit, pullRefreshState: PullRefreshState, refreshing: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Box(modifier = modifier.pullRefresh(pullRefreshState)) { content() diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 36b80f107..ceeebc1e2 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -164,7 +164,7 @@ private fun PullRequestsList( }, pullRefreshState = pullRefreshState, refreshing = refreshing, - modifier = modifier + modifier = modifier, ) } @@ -269,7 +269,7 @@ fun PullRequestsScreenPreview() { refreshing = false, onRefresh = {}, ), - refreshing = false + refreshing = false, ) } } @@ -285,7 +285,7 @@ fun PullRequestsScreenEmptyListPreview() { refreshing = false, onRefresh = {}, ), - refreshing = false + refreshing = false, ) } } @@ -301,7 +301,7 @@ fun PullRequestsScreenLoadingPreview() { refreshing = false, onRefresh = {}, ), - refreshing = false + refreshing = false, ) } } @@ -317,7 +317,7 @@ fun PullRequestsScreenErrorPreview() { refreshing = false, onRefresh = {}, ), - refreshing = false + refreshing = false, ) } } @@ -333,7 +333,7 @@ fun PullRequestsScreenRefreshingPreview() { refreshing = true, onRefresh = {}, ), - refreshing = true + refreshing = true, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 61e1e2140..af132d52f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -143,7 +143,7 @@ private fun ReviewersScreenStateless( ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Success-> ReviewersScreenContent(data, pullRefreshState, refreshing, padding, onAction) + is Data.Success -> ReviewersScreenContent(data, pullRefreshState, refreshing, padding, onAction) } }, ) @@ -194,7 +194,7 @@ private fun ReviewersList( }, pullRefreshState = pullRefreshState, refreshing = refreshing, - modifier = modifier + modifier = modifier, ) } @@ -305,7 +305,7 @@ private val successData = listOf( Reviewer(1, "Kezc", true, 24, 12), Reviewer(2, "Krzysiudan", false, 24, 0), Reviewer(3, "Weronika", false, 24, 0, true), - Reviewer(4, "Jacek", false, 24, 0) + Reviewer(4, "Jacek", false, 24, 0), ) @Preview @@ -322,7 +322,7 @@ fun DetailsScreenPreview() { refreshing = false, onRefresh = {}, ), - refreshing = false + refreshing = false, ) } } @@ -341,7 +341,7 @@ fun DetailsScreenNoReviewsPreview() { refreshing = false, onRefresh = {}, ), - refreshing = false + refreshing = false, ) } } @@ -360,7 +360,7 @@ fun DetailsScreenRefreshingPreview() { refreshing = true, onRefresh = {}, ), - refreshing = true + refreshing = true, ) } } From 6b972a489a2f45ee0e0a3174642e8acced254c92 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 29 May 2023 09:32:52 +0000 Subject: [PATCH 470/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 9bca1a886..af132d52f 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -305,7 +305,7 @@ private val successData = listOf( Reviewer(1, "Kezc", true, 24, 12), Reviewer(2, "Krzysiudan", false, 24, 0), Reviewer(3, "Weronika", false, 24, 0, true), - Reviewer(4, "Jacek", false, 24, 0) + Reviewer(4, "Jacek", false, 24, 0), ) @Preview From 98c20ccd339e83a79279290f5d97f45c5576a611 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Thu, 25 May 2023 14:53:35 +0200 Subject: [PATCH 471/526] Add github action for recording snapshots. --- .github/workflows/run-snapshot-generation.yml | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/run-snapshot-generation.yml diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml new file mode 100644 index 000000000..31b439b0f --- /dev/null +++ b/.github/workflows/run-snapshot-generation.yml @@ -0,0 +1,46 @@ +name: Snapshot recording + +on: + pull_request: + types: [ opened, edited , synchronize ] + +permissions: + checks: write + contents: write + statuses: write + pull-requests: write + actions: write + +jobs: + generate_snapshots: + if: ${{contains(github.event.pull_request.title, '[New snapshots]')}} + name: Generate snapshots + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + lfs: true + + - name: Set up Git LFS + run: | + git lfs install + + - name: Gradle - Record snapshots with Paparazzi + id: testStep + run: ./gradlew clean components:recordPaparazziDebug + + - name: Commit and push recorded screenshots + uses: stefanzweifel/git-auto-commit-action@v4 + with: + branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} + commit_message: "[Paparazzi] Record new snapshots" + + - name: Upload snapshot record report + if: failure() + uses: actions/upload-artifact@v3 + with: + name: snapshot-failure-deltas + path: /home/runner/work/Loudius/Loudius/components/build/reports/paparazzi/ + + - name: LFS-warning + uses: ppremk/lfs-warning@v3.2 From 53399f9af8dc064f96afb4c345684ba83e1ceea9 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Thu, 25 May 2023 15:29:49 +0000 Subject: [PATCH 472/526] [Paparazzi] Record new snapshots --- ...omponents_LoudiusDialogTest_loudiusFullScreenErrorTest.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png index 3e33a5388..20276bd3a 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png +++ b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fc6324ad4c8908a9c7697621ef4a95783558b37a2b5932b5a4cadc8c377cd6b -size 55745 +oid sha256:2cf086878480f21ad538d389920718c38fc320479c4a0a7cc8cc4e92c492e7dc +size 55744 From 8dc0af013869b00d45e8326d766e9172ff28814d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 26 May 2023 12:48:09 +0200 Subject: [PATCH 473/526] Turn off launching snapshot verification when pull request has a name '[New snapshots]'. --- .github/workflows/run-snapshot-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index e3cfe0229..3c88c67df 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -12,6 +12,7 @@ permissions: jobs: test: + if: ${{!contains(github.event.pull_request.title, '[New snapshots]')}} name: Verify snapshots runs-on: ubuntu-latest steps: From 73f3819500adf8375e3494b7a0c5a1ed250b7953 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 26 May 2023 14:54:33 +0200 Subject: [PATCH 474/526] Add comments after generating a snapshots. --- .github/workflows/run-snapshot-generation.yml | 61 ++++++++++++++++++- .github/workflows/run-snapshot-test.yml | 3 +- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index 31b439b0f..faff11c58 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -1,3 +1,4 @@ +--- name: Snapshot recording on: @@ -39,8 +40,64 @@ jobs: if: failure() uses: actions/upload-artifact@v3 with: - name: snapshot-failure-deltas - path: /home/runner/work/Loudius/Loudius/components/build/reports/paparazzi/ + name: snapshot-recording-failure-report + path: /home/runner/work/Loudius/Loudius/components/build/reports/tests/testDebugUnitTest/ - name: LFS-warning uses: ppremk/lfs-warning@v3.2 + + - name: Find PR number + uses: jwalton/gh-find-current-pr@v1 + id: findPRId + if: always() + with: + state: open + + - name: Find Comment on PR + uses: peter-evans/find-comment@v2 + id: findCommentId + if: always() + with: + issue-number: ${{ steps.findPRId.outputs.pr }} + comment-author: "github-actions[bot]" + body-regex: 'Snapshot (testing|recording) result:' + + - name: Create or update comment on PR (Success) + uses: peter-evans/create-or-update-comment@v3 + if: always() && steps.testStep.outcome == 'success' + with: + comment-id: ${{ steps.findCommentId.outputs.comment-id }} + issue-number: ${{ steps.findPRId.outputs.pr }} + body: | + Snapshot recording result: :heavy_check_mark: + New snapshots recorded! Everything looks good! + + If there were changes in the user interface, a new commit has been created. You can review the new snapshots in the diff. If you find them acceptable, please proceed to merge this pull request. + + However, if you did not intend to record new snapshots, please remove the '[New snapshots]' part from your title (and discard the commit that includes the new snapshots). It will cause to run snapshot verification instead of recording. + edit-mode: replace + reactions: | + heart + hooray + reactions-edit-mode: replace + + + - name: Create or update comment on PR (Failure) + uses: peter-evans/create-or-update-comment@v3 + if: always() && steps.testStep.outcome == 'failure' + with: + comment-id: ${{ steps.findCommentId.outputs.comment-id }} + issue-number: ${{ steps.findPRId.outputs.pr }} + body: | + Snapshot recording result: :x: + Something went wrong during snapshot recording. + If you need further investigation: + - Head over to the artifacts section of the [CI Run](https://github.com/appunite/Loudius/actions/runs/${{ github.run_id }}). + - Download the zip. + - Unzip and you can find report showing the problem + + If you not intended to record new snapshots please remove '[New snapshots]' part from your pull request title. It will cause to run snapshot verification instead of recording. + edit-mode: replace + reactions: | + confused + reactions-edit-mode: replace diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index 3c88c67df..d7c866417 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -1,3 +1,4 @@ +--- name: Snapshot verification on: @@ -58,7 +59,7 @@ jobs: with: issue-number: ${{ steps.findPRId.outputs.pr }} comment-author: "github-actions[bot]" - body-includes: Snapshot testing result + body-regex: 'Snapshot (testing|recording) result:' - name: Create or update comment on PR (Success) uses: peter-evans/create-or-update-comment@v3 From ca3f845b134337f06d4a18a3abe041779192034f Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 30 May 2023 08:53:38 +0200 Subject: [PATCH 475/526] Remove workflows directory from the jscpd linter checks. --- .jscpd.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.jscpd.json b/.jscpd.json index c4ce4ed0e..efb470d4f 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,7 +1,17 @@ { "threshold": 0, - "reporters": ["html", "console", "xml"], - "ignore": ["**/__snapshots__/**", "**/src/test/java/**"], - "ignorePattern": ["import .*"], + "reporters": [ + "html", + "console", + "xml" + ], + "ignore": [ + "**/__snapshots__/**", + "**/src/test/java/**", + "**/workflows/**" + ], + "ignorePattern": [ + "import .*" + ], "absolute": true } From c6a35151dfbbfce82a6ca3156816fbe3fbedba68 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 30 May 2023 08:56:51 +0200 Subject: [PATCH 476/526] Remove trailing spaces from the run-snapshot-generation.yml. --- .github/workflows/run-snapshot-generation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index faff11c58..8e8d5240b 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -90,8 +90,8 @@ jobs: issue-number: ${{ steps.findPRId.outputs.pr }} body: | Snapshot recording result: :x: - Something went wrong during snapshot recording. - If you need further investigation: + Something went wrong during snapshot recording. + If you need further investigation: - Head over to the artifacts section of the [CI Run](https://github.com/appunite/Loudius/actions/runs/${{ github.run_id }}). - Download the zip. - Unzip and you can find report showing the problem From a07906cfbd74014c44cdc76aa84a3471c1013828 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 30 May 2023 11:57:09 +0200 Subject: [PATCH 477/526] add screenshot test rule --- .../loudius/PullRequestsScreenTest.kt | 2 +- .../loudius/util/EspressoScreenshot.kt | 101 ++++++++++++++++++ .../loudius/util/IntegrationTestRule.kt | 2 + .../loudius/util/ScreenshotTestRule.kt | 75 +++++++++++++ app/src/main/AndroidManifest.xml | 1 + 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt create mode 100644 app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index 406cdef18..bbc7f2e23 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -53,7 +53,7 @@ class PullRequestsScreenTest { } } - composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() + composeTestRule.onNodeWithText("Ffirst Pull-Request title").assertIsDisplayed() } } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt new file mode 100644 index 000000000..21c35583a --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.util + +import android.util.Log +import androidx.test.runner.screenshot.Screenshot +import org.junit.runner.Description +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.concurrent.atomic.AtomicInteger + +/** + * Used to automatically capture screenshots of failed tests. + */ +object EspressoScreenshot { + private val imageCounter = AtomicInteger(0) + private val dotPNG = ".png" + private val underscore = "_" + + // Firebase Test Lab requires screenshots to be saved to /sdcard/screenshots + // https://github.com/firebase/firebase-testlab-instr-lib/blob/f0a21a526499f051ac5074dc382cf79e237d2f4e/firebase-testlab-instr-lib/testlab-instr-lib/src/main/java/com/google/firebase/testlab/screenshot/FirebaseScreenCaptureProcessor.java#L36 + private val screenshotFolder = File("/sdcard/screenshots") + private val TAG = EspressoScreenshot::class.java.simpleName + + private fun getScreenshotName(description: Description): String { + val className = description.className + val methodName = description.methodName + + val imageNumberInt = imageCounter.incrementAndGet() + var number = imageNumberInt.toString() + if (imageNumberInt < 10) number = "0$number" + + val components = arrayOf(className, underscore, methodName, underscore, number, dotPNG) + + var length = 0 + + for (component in components) { + length += component.length + } + + val result = StringBuilder(length) + + for (component in components) { + result.append(component) + } + + return result.toString() + } + + private fun prepareScreenshotPath() { + try { + screenshotFolder.mkdirs() + } catch (ignored: Exception) { + Log.e(TAG, "Failed to make screenshot folder $screenshotFolder") + } + } + + fun takeScreenshot(description: Description) { + prepareScreenshotPath() + + val screenshotName = getScreenshotName(description) + val capture = Screenshot.capture() // default format is PNG + + // based on BasicScreenCaptureProcessor#process + val imageFile = File(screenshotFolder, screenshotName) + var out: BufferedOutputStream? = null + try { + Log.i(TAG, "Saving screenshot to " + imageFile.absolutePath) + out = BufferedOutputStream(FileOutputStream(imageFile)) + capture.bitmap.compress(capture.format, 100, out) + out.flush() + Log.i(TAG, "Screenshot exists? " + imageFile.exists()) + } catch (ignored: Exception) { + Log.e(TAG, ignored.toString()) + ignored.printStackTrace() + } finally { + try { + out?.close() + } catch (ignored: IOException) { + Log.e(TAG, ignored.toString()) + ignored.printStackTrace() + } + } + } +} diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt index d8f757fe3..30be611a0 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt @@ -33,9 +33,11 @@ class IntegrationTestRule(testClass: Any) : TestRule { registerIdlingResource(countingResource.toIdlingResource()) } private val hiltRule = HiltAndroidRule(testClass) + private val screenshotTestRule = ScreenshotTestRule() override fun apply(base: Statement, description: Description): Statement { return RuleChain.outerRule(mockWebServer) + .around(screenshotTestRule) .around(hiltRule) .around(composeTestRule) .apply(base, description) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt new file mode 100644 index 000000000..e339cb98d --- /dev/null +++ b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius.util + +import androidx.test.espresso.Espresso +import androidx.test.espresso.base.DefaultFailureHandler +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import java.util.concurrent.atomic.AtomicBoolean + +/** + * TestRule used to run all test methods try count 1 time. Take screenshots on failure. + */ +open class ScreenshotTestRule : TestRule { + // Note: Data seeding must happen before we run a test. As a result, retrying failed tests + // at the JUnit level doesn"t make sense because we can"t run data seeeding. + // + // Run all test methods tryCount times. Take screenshots on failure. + // A method rule would allow targeting specific (method.getAnnotation(Retry.class)) + private val tryCount = 1 + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + var error: Throwable? = null + + val errorHandled = AtomicBoolean(false) + + // Espresso failure handler will capture accurate UI screenshots. + // if we wait for `try { base.evaluate() } catch ()` then the UI will be in a different state + // + // Only espresso failures trigger the espresso failure handlers. For JUnit assert errors, + // those must be captured in `try { base.evaluate() } catch ()` + Espresso.setFailureHandler { throwable, matcher -> + EspressoScreenshot.takeScreenshot(description) + errorHandled.set(true) + val targetContext = getInstrumentation().targetContext + DefaultFailureHandler(targetContext).handle(throwable, matcher) + } + + for (i in 0 until tryCount) { + errorHandled.set(false) + try { + base.evaluate() + return + } catch (t: Throwable) { + if (!errorHandled.get()) { + EspressoScreenshot.takeScreenshot(description) + } + error = t + } + } + + if (error != null) throw error + } + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e1f17d154..deef63706 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + From c921ce7f3cca913f8a3216b1b2b57b6d2a3de5fe Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 30 May 2023 12:16:41 +0200 Subject: [PATCH 478/526] move lambda to the last parameter in LoudiusPullToRefreshBox --- .../ui/components/LoudiusPullToRefreshBox.kt | 2 +- .../ui/pullrequests/PullRequestsScreen.kt | 27 +++++++++---------- .../loudius/ui/reviewers/ReviewersScreen.kt | 27 +++++++++---------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt index 91f1586af..e779afbe8 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt @@ -29,10 +29,10 @@ import androidx.compose.ui.Modifier @Composable fun LoudiusPullToRefreshBox( - content: @Composable () -> Unit, pullRefreshState: PullRefreshState, refreshing: Boolean, modifier: Modifier = Modifier, + content: @Composable () -> Unit, ) { Box(modifier = modifier.pullRefresh(pullRefreshState)) { content() diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index ceeebc1e2..7070be42b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -149,23 +149,22 @@ private fun PullRequestsList( refreshing: Boolean, ) { LoudiusPullToRefreshBox( - content = { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - itemsIndexed(pullRequests) { index, item -> - PullRequestItem( - index = index, - data = item, - onClick = onItemClick, - ) - } - } - }, pullRefreshState = pullRefreshState, refreshing = refreshing, modifier = modifier, - ) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + itemsIndexed(pullRequests) { index, item -> + PullRequestItem( + index = index, + data = item, + onClick = onItemClick, + ) + } + } + } } @Composable diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index af132d52f..2ea6088ef 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -179,23 +179,22 @@ private fun ReviewersList( refreshing: Boolean, ) { LoudiusPullToRefreshBox( - content = { - LazyColumn( - modifier = Modifier.fillMaxSize(), - ) { - itemsIndexed((data as Data.Success).reviewers) { index, reviewer -> - ReviewerItem( - reviewer = reviewer, - index = index, - onNotifyClick = onNotifyClick, - ) - } - } - }, pullRefreshState = pullRefreshState, refreshing = refreshing, modifier = modifier, - ) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + itemsIndexed((data as Data.Success).reviewers) { index, reviewer -> + ReviewerItem( + reviewer = reviewer, + index = index, + onNotifyClick = onNotifyClick, + ) + } + } + } } @Composable From 642d53276b3b9e57af46f7121de313184c2453f5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 31 May 2023 11:25:40 +0200 Subject: [PATCH 479/526] screenshots on API 30,31,32,33 --- .../com/appunite/loudius/LoginScreenTest.kt | 7 ++++++- .../loudius/PullRequestsScreenTest.kt | 2 +- .../loudius/util/EspressoScreenshot.kt | 20 ++++++++++++++++++- .../loudius/util/IntegrationTestRule.kt | 2 +- app/src/main/AndroidManifest.xml | 3 ++- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 212756f2a..170c0c3ab 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -32,10 +32,11 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasData import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.ui.login.GithubHelper import com.appunite.loudius.di.GithubHelperModule +import com.appunite.loudius.ui.login.GithubHelper import com.appunite.loudius.ui.login.LoginScreen import com.appunite.loudius.ui.theme.LoudiusTheme +import com.appunite.loudius.util.ScreenshotTestRule import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest @@ -62,6 +63,10 @@ class LoginScreenTest { @get:Rule(order = 2) val intents = IntentsRule() + @Rule + @JvmField + val screenshotTestRule = ScreenshotTestRule() + @Before fun setUp() { hiltRule.inject() diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index bbc7f2e23..406cdef18 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -53,7 +53,7 @@ class PullRequestsScreenTest { } } - composeTestRule.onNodeWithText("Ffirst Pull-Request title").assertIsDisplayed() + composeTestRule.onNodeWithText("First Pull-Request title").assertIsDisplayed() } } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt index 21c35583a..76ddb9983 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt @@ -16,6 +16,8 @@ package com.appunite.loudius.util +import android.os.Environment +import android.os.Environment.DIRECTORY_DOWNLOADS import android.util.Log import androidx.test.runner.screenshot.Screenshot import org.junit.runner.Description @@ -35,7 +37,7 @@ object EspressoScreenshot { // Firebase Test Lab requires screenshots to be saved to /sdcard/screenshots // https://github.com/firebase/firebase-testlab-instr-lib/blob/f0a21a526499f051ac5074dc382cf79e237d2f4e/firebase-testlab-instr-lib/testlab-instr-lib/src/main/java/com/google/firebase/testlab/screenshot/FirebaseScreenCaptureProcessor.java#L36 - private val screenshotFolder = File("/sdcard/screenshots") + private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) private val TAG = EspressoScreenshot::class.java.simpleName private fun getScreenshotName(description: Description): String { @@ -71,6 +73,18 @@ object EspressoScreenshot { } } + // Checks if a volume containing external storage is available +// for read and write. + fun isExternalStorageWritable(): Boolean { + return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED + } + + // Checks if a volume containing external storage is available to at least read. + fun isExternalStorageReadable(): Boolean { + return Environment.getExternalStorageState() in + setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY) + } + fun takeScreenshot(description: Description) { prepareScreenshotPath() @@ -80,6 +94,10 @@ object EspressoScreenshot { // based on BasicScreenCaptureProcessor#process val imageFile = File(screenshotFolder, screenshotName) var out: BufferedOutputStream? = null + + Log.i(TAG, "isExternalStorageWritable " + isExternalStorageWritable()) + Log.i(TAG, "isExternalStorageReadable " + isExternalStorageReadable()) + try { Log.i(TAG, "Saving screenshot to " + imageFile.absolutePath) out = BufferedOutputStream(FileOutputStream(imageFile)) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt index 30be611a0..0b1ae08ad 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt @@ -37,9 +37,9 @@ class IntegrationTestRule(testClass: Any) : TestRule { override fun apply(base: Statement, description: Description): Statement { return RuleChain.outerRule(mockWebServer) - .around(screenshotTestRule) .around(hiltRule) .around(composeTestRule) + .around(screenshotTestRule) .apply(base, description) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index deef63706..49629d2ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,8 @@ - + From 3c6ff1c19742bd836579eb4eb36e260c135b1f8b Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 31 May 2023 09:29:54 +0000 Subject: [PATCH 480/526] [MegaLinter] Apply linters fixes --- .../com/appunite/loudius/LoginScreenTest.kt | 17 ++++++++--------- .../appunite/loudius/util/EspressoScreenshot.kt | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 170c0c3ab..35a61ab55 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -93,8 +93,8 @@ class LoginScreenTest { intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") - ) + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), + ), ) } @@ -116,12 +116,11 @@ class LoginScreenTest { intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") - ) + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), + ), ) } - @Test fun whenClickingGrantPermissionInXiaomiDialog_OpenPermissionEditor() { every { githubHelper.shouldAskForXiaomiIntent() } returns true @@ -144,10 +143,10 @@ class LoginScreenTest { hasComponent( allOf( hasPackageName("com.miui.securitycenter"), - hasClassName("com.miui.permcenter.permissions.PermissionsEditorActivity") - ) - ) - ) + hasClassName("com.miui.permcenter.permissions.PermissionsEditorActivity"), + ), + ), + ), ) } } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt index 76ddb9983..7f5c4a080 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt @@ -82,7 +82,7 @@ object EspressoScreenshot { // Checks if a volume containing external storage is available to at least read. fun isExternalStorageReadable(): Boolean { return Environment.getExternalStorageState() in - setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY) + setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY) } fun takeScreenshot(description: Description) { From 4fefc0d275b4b2907d982732295d70f2aa39127f Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 31 May 2023 11:46:19 +0200 Subject: [PATCH 481/526] test if firebase is working properly --- .../androidTest/java/com/appunite/loudius/LoginScreenTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 35a61ab55..719f54fa1 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -93,7 +93,7 @@ class LoginScreenTest { intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), + hasData("wrong data"), ), ) } From 26f1595f65003430962709415543361628d31c3b Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 31 May 2023 12:08:32 +0200 Subject: [PATCH 482/526] change path tp test firebase --- .../java/com/appunite/loudius/util/EspressoScreenshot.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt index 7f5c4a080..7b67ee937 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt @@ -17,7 +17,6 @@ package com.appunite.loudius.util import android.os.Environment -import android.os.Environment.DIRECTORY_DOWNLOADS import android.util.Log import androidx.test.runner.screenshot.Screenshot import org.junit.runner.Description @@ -37,7 +36,8 @@ object EspressoScreenshot { // Firebase Test Lab requires screenshots to be saved to /sdcard/screenshots // https://github.com/firebase/firebase-testlab-instr-lib/blob/f0a21a526499f051ac5074dc382cf79e237d2f4e/firebase-testlab-instr-lib/testlab-instr-lib/src/main/java/com/google/firebase/testlab/screenshot/FirebaseScreenCaptureProcessor.java#L36 - private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) + //private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) + private val screenshotFolder = File("/sdcard/screenshots") private val TAG = EspressoScreenshot::class.java.simpleName private fun getScreenshotName(description: Description): String { From a64e46cab01bba8c769d146663fb0420ce10d5c6 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Wed, 31 May 2023 10:12:37 +0000 Subject: [PATCH 483/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/util/EspressoScreenshot.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt index 7b67ee937..2d44e665a 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt @@ -36,7 +36,7 @@ object EspressoScreenshot { // Firebase Test Lab requires screenshots to be saved to /sdcard/screenshots // https://github.com/firebase/firebase-testlab-instr-lib/blob/f0a21a526499f051ac5074dc382cf79e237d2f4e/firebase-testlab-instr-lib/testlab-instr-lib/src/main/java/com/google/firebase/testlab/screenshot/FirebaseScreenCaptureProcessor.java#L36 - //private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) + // private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) private val screenshotFolder = File("/sdcard/screenshots") private val TAG = EspressoScreenshot::class.java.simpleName From d48f429d7df8be36c9c106cf9515b37409c1a267 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 2 Jun 2023 10:15:13 +0200 Subject: [PATCH 484/526] Remove not needed additional tests. --- .github/workflows/run-snapshot-generation.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index 8e8d5240b..359703d36 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -22,10 +22,6 @@ jobs: with: lfs: true - - name: Set up Git LFS - run: | - git lfs install - - name: Gradle - Record snapshots with Paparazzi id: testStep run: ./gradlew clean components:recordPaparazziDebug @@ -43,9 +39,6 @@ jobs: name: snapshot-recording-failure-report path: /home/runner/work/Loudius/Loudius/components/build/reports/tests/testDebugUnitTest/ - - name: LFS-warning - uses: ppremk/lfs-warning@v3.2 - - name: Find PR number uses: jwalton/gh-find-current-pr@v1 id: findPRId From df9c754282b4d87e9fd44c65a2576fe9b97208b9 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 2 Jun 2023 10:27:49 +0200 Subject: [PATCH 485/526] Add description for run-snapshot-generation.yml. --- .github/workflows/run-snapshot-generation.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index 359703d36..71d0c6175 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -1,4 +1,7 @@ --- +# Golden tests recording with Paparazzi configuration file +# Add [New snapshots] to the title of your PR to record new snapshots. +# Remove it to verify snapshots instead. name: Snapshot recording on: From cc46dd045d85ec3ceb0446af36342ae31448d168 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 2 Jun 2023 11:06:45 +0200 Subject: [PATCH 486/526] code cleanup --- .../ui/components/LoudiusPullToRefreshBox.kt | 14 ++++++ .../ui/pullrequests/PullRequestsScreen.kt | 43 ++++++------------- .../loudius/ui/reviewers/ReviewersScreen.kt | 32 +++++--------- 3 files changed, 39 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt index e779afbe8..d4aa3ebbe 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt @@ -23,9 +23,12 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.appunite.loudius.ui.theme.LoudiusTheme @Composable fun LoudiusPullToRefreshBox( @@ -39,3 +42,14 @@ fun LoudiusPullToRefreshBox( PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } } + +@Preview +@Composable +fun LoudiusPullToRefreshBoxPreview() { + LoudiusTheme { + LoudiusPullToRefreshBox( + pullRefreshState = rememberPullRefreshState(refreshing = true, onRefresh = {}), + refreshing = true + ) {} + } +} diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 7070be42b..4dd9caf19 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -64,14 +64,11 @@ fun PullRequestsScreen( ) { val state = viewModel.state val refreshing by viewModel.isRefreshing.collectAsState() - val pullRefreshState = rememberPullRefreshState( - refreshing = refreshing, - onRefresh = viewModel::refreshData, - ) + PullRequestsScreenStateless( state = state, - pullRefreshState = pullRefreshState, refreshing = refreshing, + onRefresh = viewModel::refreshData, onAction = viewModel::onAction, ) LaunchedEffect(state.navigateToReviewers) { @@ -98,8 +95,8 @@ private fun navigateToReviewers( @Composable private fun PullRequestsScreenStateless( state: PullRequestState, - pullRefreshState: PullRefreshState, refreshing: Boolean, + onRefresh: () -> Unit, onAction: (PulLRequestsAction) -> Unit, ) { Scaffold( @@ -113,7 +110,7 @@ private fun PullRequestsScreenStateless( onButtonClick = { onAction(PulLRequestsAction.RetryClick) }, ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Success -> PullRequestContent(state.data, pullRefreshState, refreshing, padding, onAction) + is Data.Success -> PullRequestContent(state.data, refreshing, onRefresh, padding, onAction) } }, ) @@ -122,8 +119,8 @@ private fun PullRequestsScreenStateless( @Composable private fun PullRequestContent( state: Data.Success, - pullRefreshState: PullRefreshState, refreshing: Boolean, + onRefresh: () -> Unit, padding: PaddingValues, onAction: (PulLRequestsAction) -> Unit, ) { @@ -134,7 +131,10 @@ private fun PullRequestContent( pullRequests = state.pullRequests, modifier = Modifier.padding(padding), onItemClick = onAction, - pullRefreshState = pullRefreshState, + pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = onRefresh + ), refreshing = refreshing, ) } @@ -264,11 +264,8 @@ fun PullRequestsScreenPreview() { PullRequestsScreenStateless( state = PullRequestState(successData), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = {}, - ), refreshing = false, + onRefresh = {} ) } } @@ -280,11 +277,8 @@ fun PullRequestsScreenEmptyListPreview() { PullRequestsScreenStateless( PullRequestState(Data.Success(emptyList())), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = {}, - ), refreshing = false, + onRefresh = {} ) } } @@ -296,11 +290,8 @@ fun PullRequestsScreenLoadingPreview() { PullRequestsScreenStateless( PullRequestState(Data.Loading), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = {}, - ), refreshing = false, + onRefresh = {} ) } } @@ -312,11 +303,8 @@ fun PullRequestsScreenErrorPreview() { PullRequestsScreenStateless( PullRequestState(Data.Error), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = {}, - ), refreshing = false, + onRefresh = {} ) } } @@ -328,11 +316,8 @@ fun PullRequestsScreenRefreshingPreview() { PullRequestsScreenStateless( state = PullRequestState(successData), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = true, - onRefresh = {}, - ), refreshing = true, + onRefresh = {} ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index 2ea6088ef..fcaab5169 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -70,16 +70,12 @@ fun ReviewersScreen( val state = viewModel.state val snackbarHostState = remember { SnackbarHostState() } val refreshing by viewModel.isRefreshing.collectAsState() - val pullRefreshState = rememberPullRefreshState( - refreshing = refreshing, - onRefresh = viewModel::refreshData, - ) ReviewersScreenStateless( pullRequestNumber = state.pullRequestNumber, data = state.data, - pullRefreshState = pullRefreshState, refreshing = refreshing, + onRefresh = viewModel::refreshData, onClickBackArrow = navigateBack, snackbarHostState = snackbarHostState, onAction = viewModel::onAction, @@ -121,8 +117,8 @@ private fun resolveSnackbarMessage(snackbarTypeShown: ReviewersSnackbarType) = private fun ReviewersScreenStateless( pullRequestNumber: String, data: Data, - pullRefreshState: PullRefreshState, refreshing: Boolean, + onRefresh: () -> Unit, onClickBackArrow: () -> Unit, snackbarHostState: SnackbarHostState, onAction: (ReviewersAction) -> Unit, @@ -143,7 +139,7 @@ private fun ReviewersScreenStateless( ) is Data.Loading -> LoudiusLoadingIndicator(Modifier.padding(padding)) - is Data.Success -> ReviewersScreenContent(data, pullRefreshState, refreshing, padding, onAction) + is Data.Success -> ReviewersScreenContent(data, refreshing, onRefresh, padding, onAction) } }, ) @@ -152,15 +148,18 @@ private fun ReviewersScreenStateless( @Composable private fun ReviewersScreenContent( data: Data.Success, - pullRefreshState: PullRefreshState, refreshing: Boolean, + onRefreshing: () -> Unit, padding: PaddingValues, onAction: (ReviewersAction) -> Unit, ) { if (data.reviewers.isNotEmpty()) { ReviewersList( data = data, - pullRefreshState = pullRefreshState, + pullRefreshState = rememberPullRefreshState( + refreshing = refreshing, + onRefresh = onRefreshing + ), modifier = Modifier.padding(padding), onNotifyClick = onAction, refreshing = refreshing, @@ -317,11 +316,8 @@ fun DetailsScreenPreview() { onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = {}, - ), refreshing = false, + onRefresh = {} ) } } @@ -336,11 +332,8 @@ fun DetailsScreenNoReviewsPreview() { onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = {}, - ), refreshing = false, + onRefresh = {} ) } } @@ -355,11 +348,8 @@ fun DetailsScreenRefreshingPreview() { onClickBackArrow = {}, snackbarHostState = SnackbarHostState(), onAction = {}, - pullRefreshState = rememberPullRefreshState( - refreshing = true, - onRefresh = {}, - ), refreshing = true, + onRefresh = {} ) } } From fa3c97610c8775b3094d2f28e19f5053b8005a36 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 2 Jun 2023 09:10:48 +0000 Subject: [PATCH 487/526] [MegaLinter] Apply linters fixes --- .../loudius/ui/components/LoudiusPullToRefreshBox.kt | 2 +- .../loudius/ui/pullrequests/PullRequestsScreen.kt | 12 ++++++------ .../appunite/loudius/ui/reviewers/ReviewersScreen.kt | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt index d4aa3ebbe..b226dc97b 100644 --- a/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt +++ b/app/src/main/java/com/appunite/loudius/ui/components/LoudiusPullToRefreshBox.kt @@ -49,7 +49,7 @@ fun LoudiusPullToRefreshBoxPreview() { LoudiusTheme { LoudiusPullToRefreshBox( pullRefreshState = rememberPullRefreshState(refreshing = true, onRefresh = {}), - refreshing = true + refreshing = true, ) {} } } diff --git a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt index 4dd9caf19..870134471 100644 --- a/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/pullrequests/PullRequestsScreen.kt @@ -133,7 +133,7 @@ private fun PullRequestContent( onItemClick = onAction, pullRefreshState = rememberPullRefreshState( refreshing = refreshing, - onRefresh = onRefresh + onRefresh = onRefresh, ), refreshing = refreshing, ) @@ -265,7 +265,7 @@ fun PullRequestsScreenPreview() { state = PullRequestState(successData), onAction = {}, refreshing = false, - onRefresh = {} + onRefresh = {}, ) } } @@ -278,7 +278,7 @@ fun PullRequestsScreenEmptyListPreview() { PullRequestState(Data.Success(emptyList())), onAction = {}, refreshing = false, - onRefresh = {} + onRefresh = {}, ) } } @@ -291,7 +291,7 @@ fun PullRequestsScreenLoadingPreview() { PullRequestState(Data.Loading), onAction = {}, refreshing = false, - onRefresh = {} + onRefresh = {}, ) } } @@ -304,7 +304,7 @@ fun PullRequestsScreenErrorPreview() { PullRequestState(Data.Error), onAction = {}, refreshing = false, - onRefresh = {} + onRefresh = {}, ) } } @@ -317,7 +317,7 @@ fun PullRequestsScreenRefreshingPreview() { state = PullRequestState(successData), onAction = {}, refreshing = true, - onRefresh = {} + onRefresh = {}, ) } } diff --git a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt index fcaab5169..ea4546214 100644 --- a/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/reviewers/ReviewersScreen.kt @@ -158,7 +158,7 @@ private fun ReviewersScreenContent( data = data, pullRefreshState = rememberPullRefreshState( refreshing = refreshing, - onRefresh = onRefreshing + onRefresh = onRefreshing, ), modifier = Modifier.padding(padding), onNotifyClick = onAction, @@ -317,7 +317,7 @@ fun DetailsScreenPreview() { snackbarHostState = SnackbarHostState(), onAction = {}, refreshing = false, - onRefresh = {} + onRefresh = {}, ) } } @@ -333,7 +333,7 @@ fun DetailsScreenNoReviewsPreview() { snackbarHostState = SnackbarHostState(), onAction = {}, refreshing = false, - onRefresh = {} + onRefresh = {}, ) } } @@ -349,7 +349,7 @@ fun DetailsScreenRefreshingPreview() { snackbarHostState = SnackbarHostState(), onAction = {}, refreshing = true, - onRefresh = {} + onRefresh = {}, ) } } From 90bb4652c4d4bfe1600aa516a99993a1f0b8f6b8 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Fri, 2 Jun 2023 11:41:35 +0200 Subject: [PATCH 488/526] adjust tests --- .../pullrequests/PullRequestsViewModelTest.kt | 24 +++++++++++++ .../ui/reviewers/ReviewersViewModelTest.kt | 34 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt index 5875d1999..b0b20502e 100644 --- a/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/pullrequests/PullRequestsViewModelTest.kt @@ -31,7 +31,9 @@ import org.junit.jupiter.api.extension.ExtendWith import strikt.api.expectThat import strikt.assertions.isA import strikt.assertions.isEqualTo +import strikt.assertions.isFalse import strikt.assertions.isNull +import strikt.assertions.isTrue import strikt.assertions.single @OptIn(ExperimentalCoroutinesApi::class) @@ -40,6 +42,28 @@ class PullRequestsViewModelTest { private val pullRequestRepository = spyk(FakePullRequestRepository()) private fun createViewModel() = PullRequestsViewModel(pullRequestRepository) + @Test + fun `WHEN refresh data THEN start refreshing data and set isRefreshing to true`() = runTest { + val viewModel = createViewModel() + + coEvery { + pullRequestRepository.getCurrentUserPullRequests() + } coAnswers { neverCompletingSuspension() } + + viewModel.refreshData() + + expectThat(viewModel.isRefreshing.value).isTrue() + } + + @Test + fun `WHEN refresh data THEN refresh data and set isRefreshing to false`() = runTest { + val viewModel = createViewModel() + + viewModel.refreshData() + + expectThat(viewModel.isRefreshing.value).isFalse() + } + @Test fun `WHEN refresh data THEN refresh data and display pull requests`() = runTest { val viewModel = createViewModel() diff --git a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt index d5a9073c7..1b64dca1d 100644 --- a/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt +++ b/app/src/test/java/com/appunite/loudius/ui/reviewers/ReviewersViewModelTest.kt @@ -93,6 +93,40 @@ class ReviewersViewModelTest { } } + @Test + fun `WHEN refresh data THEN start refreshing data and set isRefreshing to true`() = runTest { + viewModel = createViewModel() + + coEvery { + repository.getRequestedReviewers( + any(), + any(), + any(), + ) + } coAnswers { neverCompletingSuspension() } + + coEvery { + repository.getReviews( + any(), + any(), + any(), + ) + } coAnswers { neverCompletingSuspension() } + + viewModel.refreshData() + + expectThat(viewModel.isRefreshing.value).isTrue() + } + + @Test + fun `WHEN refresh data THEN refresh data and set isRefreshing to false`() = runTest { + viewModel = createViewModel() + + viewModel.refreshData() + + expectThat(viewModel.isRefreshing.value).isFalse() + } + @Test fun `GIVEN no values in saved state WHEN init THEN throw IllegalStateException`() { every { savedStateHandle.get(any()) } returns null From 777b7b8c9632c7027ee4970b579f098fce3136f2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 2 Jun 2023 15:26:52 +0200 Subject: [PATCH 489/526] Add a section about screenshot tests to README.md. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3f3d6de5..8a644bf0a 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,9 @@ Here's an example of an experiment that meets our rules: ## 🚀 Project setup -In order to properly start the application and use it, the CLIENT_SECRET environment variable must be set on your computer. CLIENT_SECRET is a GitHub client secret key provided from ``Settings -> Developer Settings -> OAuth Apps -> my application``. +In order to properly start the application and use it, the CLIENT_SECRET environment variable must +be set on your computer. CLIENT_SECRET is a GitHub client secret key provided +from ``Settings -> Developer Settings -> OAuth Apps -> my application``. ### How to set environmental variable on mac? @@ -75,6 +77,11 @@ In order to properly start the application and use it, the CLIENT_SECRET environ 3. `$ echo $CLIENT_SECRET` 4. Restart your computer. +### Screenshots tests + +We are using screenshot tests to check if the UI is not broken. We are recording screenshots on CI. +To do that - add `[New snapshots]` to the pull request title. Otherwise the snapshots are tested. + ## 🧑🏻‍🎓 Contributing We believe that there is no ideal code and that every code can be improved. Therefore, we welcome From 3ef956695094af6a6bbbf49f8686bbec34d0fa1e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 5 Jun 2023 08:39:56 +0200 Subject: [PATCH 490/526] Repair instrumented tests. --- .../java/com/appunite/loudius/LoginScreenTest.kt | 3 +-- .../java/com/appunite/loudius/PullRequestsScreenTest.kt | 8 -------- .../java/com/appunite/loudius/ReviewersScreenTest.kt | 2 +- .../java/com/appunite/loudius/util/IntegrationTestRule.kt | 2 +- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 4ca5b4c18..b3a3fcf8f 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -33,10 +33,9 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.appunite.loudius.components.theme.LoudiusTheme -import com.appunite.loudius.ui.login.GithubHelper import com.appunite.loudius.di.GithubHelperModule +import com.appunite.loudius.ui.login.GithubHelper import com.appunite.loudius.ui.login.LoginScreen -import com.appunite.loudius.ui.theme.LoudiusTheme import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest diff --git a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt index d3a0f3b68..9ebdbd54f 100644 --- a/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/PullRequestsScreenTest.kt @@ -19,18 +19,10 @@ package com.appunite.loudius import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.appunite.loudius.components.components.countingResource import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.ui.pullrequests.PullRequestsScreen -import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.IntegrationTestRule import com.appunite.loudius.util.Register -import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource -import com.appunite.loudius.util.MockWebServerRule -import com.appunite.loudius.util.jsonResponse -import com.appunite.loudius.util.matchArg -import com.appunite.loudius.util.path -import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Before import org.junit.Rule diff --git a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt index 77409a631..69fbc1386 100644 --- a/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/ReviewersScreenTest.kt @@ -20,8 +20,8 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.ui.reviewers.ReviewersScreen -import com.appunite.loudius.ui.theme.LoudiusTheme import com.appunite.loudius.util.IntegrationTestRule import com.appunite.loudius.util.Register import dagger.hilt.android.testing.HiltAndroidTest diff --git a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt index 3cd1a320c..6f4757a1c 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/IntegrationTestRule.kt @@ -19,7 +19,7 @@ package com.appunite.loudius.util import androidx.activity.ComponentActivity import androidx.compose.ui.test.junit4.createAndroidComposeRule import com.appunite.loudius.TestActivity -import com.appunite.loudius.ui.components.countingResource +import com.appunite.loudius.components.components.countingResource import com.appunite.loudius.util.IdlingResourceExtensions.toIdlingResource import dagger.hilt.android.testing.HiltAndroidRule import org.junit.rules.RuleChain From 58e6a609bd4c1535f05b6ef5cb5c2f1ae88e1575 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 5 Jun 2023 06:47:27 +0000 Subject: [PATCH 491/526] [MegaLinter] Apply linters fixes --- .github/workflows/run-snapshot-generation.yml | 7 +++---- .github/workflows/run-snapshot-test.yml | 3 +-- .jscpd.json | 16 +++------------- .../com/appunite/loudius/LoginScreenTest.kt | 17 ++++++++--------- .../components/LoudiusFullScreenError.kt | 1 - .../components/LoudiusPlaceholderText.kt | 1 - 6 files changed, 15 insertions(+), 30 deletions(-) diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index 71d0c6175..f9ece5462 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -6,7 +6,7 @@ name: Snapshot recording on: pull_request: - types: [ opened, edited , synchronize ] + types: [opened, edited, synchronize] permissions: checks: write @@ -56,7 +56,7 @@ jobs: with: issue-number: ${{ steps.findPRId.outputs.pr }} comment-author: "github-actions[bot]" - body-regex: 'Snapshot (testing|recording) result:' + body-regex: "Snapshot (testing|recording) result:" - name: Create or update comment on PR (Success) uses: peter-evans/create-or-update-comment@v3 @@ -77,7 +77,6 @@ jobs: hooray reactions-edit-mode: replace - - name: Create or update comment on PR (Failure) uses: peter-evans/create-or-update-comment@v3 if: always() && steps.testStep.outcome == 'failure' @@ -91,7 +90,7 @@ jobs: - Head over to the artifacts section of the [CI Run](https://github.com/appunite/Loudius/actions/runs/${{ github.run_id }}). - Download the zip. - Unzip and you can find report showing the problem - + If you not intended to record new snapshots please remove '[New snapshots]' part from your pull request title. It will cause to run snapshot verification instead of recording. edit-mode: replace reactions: | diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index d7c866417..aef0c5c00 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -59,7 +59,7 @@ jobs: with: issue-number: ${{ steps.findPRId.outputs.pr }} comment-author: "github-actions[bot]" - body-regex: 'Snapshot (testing|recording) result:' + body-regex: "Snapshot (testing|recording) result:" - name: Create or update comment on PR (Success) uses: peter-evans/create-or-update-comment@v3 @@ -76,7 +76,6 @@ jobs: hooray reactions-edit-mode: replace - - name: Create or update comment on PR (Failure) uses: peter-evans/create-or-update-comment@v3 if: always() && steps.testStep.outcome == 'failure' diff --git a/.jscpd.json b/.jscpd.json index efb470d4f..6549de1f0 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,17 +1,7 @@ { "threshold": 0, - "reporters": [ - "html", - "console", - "xml" - ], - "ignore": [ - "**/__snapshots__/**", - "**/src/test/java/**", - "**/workflows/**" - ], - "ignorePattern": [ - "import .*" - ], + "reporters": ["html", "console", "xml"], + "ignore": ["**/__snapshots__/**", "**/src/test/java/**", "**/workflows/**"], + "ignorePattern": ["import .*"], "absolute": true } diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index b3a3fcf8f..4a58d39f5 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -88,8 +88,8 @@ class LoginScreenTest { intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") - ) + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), + ), ) } @@ -111,12 +111,11 @@ class LoginScreenTest { intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo") - ) + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), + ), ) } - @Test fun whenClickingGrantPermissionInXiaomiDialog_OpenPermissionEditor() { every { githubHelper.shouldAskForXiaomiIntent() } returns true @@ -139,10 +138,10 @@ class LoginScreenTest { hasComponent( allOf( hasPackageName("com.miui.securitycenter"), - hasClassName("com.miui.permcenter.permissions.PermissionsEditorActivity") - ) - ) - ) + hasClassName("com.miui.permcenter.permissions.PermissionsEditorActivity"), + ), + ), + ), ) } } diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt index 07854ae3d..d96969dcc 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusFullScreenError.kt @@ -36,7 +36,6 @@ import com.appunite.loudius.components.R import com.appunite.loudius.components.components.utils.MultiScreenPreviews import com.appunite.loudius.components.theme.LoudiusTheme - @Composable fun LoudiusFullScreenError( modifier: Modifier = Modifier, diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt index fbea12a79..2223cfd25 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.appunite.loudius.components.R import com.appunite.loudius.components.theme.LoudiusTheme @Composable From e84fe5e47b2f1190f1b9f2410a38e0b42ba0d0dc Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 2 Jun 2023 13:27:51 +0200 Subject: [PATCH 492/526] Add paparazzi screenshots by compose previews. --- components/build.gradle | 12 +++ .../loudius/PaparazziShowkaseTests.kt | 82 +++++++++++++++++++ .../java/com/appunite/loudius/ShowkaseRoot.kt | 23 ++++++ 3 files changed, 117 insertions(+) create mode 100644 components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt create mode 100644 components/src/test/java/com/appunite/loudius/ShowkaseRoot.kt diff --git a/components/build.gradle b/components/build.gradle index 5d195363d..988cbbb1f 100644 --- a/components/build.gradle +++ b/components/build.gradle @@ -28,6 +28,12 @@ android { kotlinCompilerExtensionVersion '1.4.2' } resourcePrefix "components_" + + kapt { + arguments { + arg("skipPrivatePreviews", "true") + } + } } dependencies { @@ -38,6 +44,12 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' + //Showkase + implementation "com.airbnb.android:showkase:1.0.0-beta18" + kapt "com.airbnb.android:showkase-processor:1.0.0-beta18" + testImplementation "com.google.testparameterinjector:test-parameter-injector:1.8" + kaptTest "com.airbnb.android:showkase-processor:1.0.0-beta18" + //Lottie - Compose implementation("com.airbnb.android:lottie-compose:5.2.0") diff --git a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt new file mode 100644 index 000000000..22f96859b --- /dev/null +++ b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.unit.Density +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import com.airbnb.android.showkase.models.Showkase +import com.airbnb.android.showkase.models.ShowkaseBrowserComponent +import com.appunite.loudius.components.theme.LoudiusTheme +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +class ComponentPreview( + private val showkaseBrowserComponent: ShowkaseBrowserComponent +) { + val content: @Composable () -> Unit = showkaseBrowserComponent.component + override fun toString(): String = + showkaseBrowserComponent.group + ":" + showkaseBrowserComponent.componentName +} + +@RunWith(TestParameterInjector::class) +class PaparazziShowkaseTests { + + object PreviewProvider : TestParameter.TestParameterValuesProvider { + override fun provideValues(): List = + Showkase.getMetadata().componentList.map(::ComponentPreview) + } + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = DeviceConfig.PIXEL_5.copy(softButtons = false), + ) + + @Test + fun preview_tests( + @TestParameter(valuesProvider = PreviewProvider::class) componentPreview: ComponentPreview, + @TestParameter(value = ["1.0", "2"]) fontScale: Float, + @TestParameter(value = ["light"]) theme: String, + ) { + paparazzi.snapshot { + CompositionLocalProvider( + LocalInspectionMode provides true, + LocalDensity provides Density( + density = LocalDensity.current.density, + fontScale = fontScale + ) + ) { + LoudiusTheme(darkTheme = (theme == "dark")) { + Box(modifier = Modifier.background(Color.White)) { + componentPreview.content() + } + } + } + } + } +} diff --git a/components/src/test/java/com/appunite/loudius/ShowkaseRoot.kt b/components/src/test/java/com/appunite/loudius/ShowkaseRoot.kt new file mode 100644 index 000000000..1f63bbea7 --- /dev/null +++ b/components/src/test/java/com/appunite/loudius/ShowkaseRoot.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 AppUnite S.A. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.appunite.loudius + +import com.airbnb.android.showkase.annotation.ShowkaseRoot +import com.airbnb.android.showkase.annotation.ShowkaseRootModule + +@ShowkaseRoot +class ShowkaseRoot : ShowkaseRootModule From db7b6b26ea765f11ad39b36ea9deee0803102c3c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 2 Jun 2023 13:28:55 +0200 Subject: [PATCH 493/526] Remove not needed manual tests for paparazzi and make private indicator previews. --- .../components/LoudiusLoadingIndicator.kt | 4 +- .../loudius/components/LoudiusButtonTests.kt | 55 ------------------ .../loudius/components/LoudiusDialogTest.kt | 56 ------------------- 3 files changed, 2 insertions(+), 113 deletions(-) delete mode 100644 components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt delete mode 100644 components/src/test/java/com/appunite/loudius/components/LoudiusDialogTest.kt diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusLoadingIndicator.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusLoadingIndicator.kt index 46358857e..412cc8b24 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusLoadingIndicator.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusLoadingIndicator.kt @@ -55,9 +55,9 @@ fun LoudiusLoadingIndicator(modifier: Modifier = Modifier) { } } -@Preview +@Preview() @Composable -fun LoudiusLoadingIndicatorPreview() { +private fun LoudiusLoadingIndicatorPreview() { LoudiusTheme { LoudiusLoadingIndicator() } diff --git a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt b/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt deleted file mode 100644 index 6da4285a3..000000000 --- a/components/src/test/java/com/appunite/loudius/components/LoudiusButtonTests.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import app.cash.paparazzi.DeviceConfig -import app.cash.paparazzi.Paparazzi -import com.android.ide.common.rendering.api.SessionParams -import com.appunite.loudius.components.components.LoudiusOutlinedButtonDisabledPreview -import com.appunite.loudius.components.components.LoudiusOutlinedButtonLargePreview -import com.appunite.loudius.components.components.LoudiusOutlinedButtonPreview -import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconLargePreview -import com.appunite.loudius.components.components.LoudiusOutlinedButtonWithIconPreview -import org.junit.Rule -import org.junit.Test - -class LoudiusButtonTests { - - @get:Rule - val paparazzi = Paparazzi( - deviceConfig = DeviceConfig.PIXEL_5, - renderingMode = SessionParams.RenderingMode.V_SCROLL, - showSystemUi = false, - ) - - @Test - fun loudiusOutlinedButton() { - paparazzi.snapshot { - Column(Modifier.background(Color.White)) { - LoudiusOutlinedButtonWithIconLargePreview() - LoudiusOutlinedButtonDisabledPreview() - LoudiusOutlinedButtonWithIconPreview() - LoudiusOutlinedButtonLargePreview() - LoudiusOutlinedButtonPreview() - } - } - } -} diff --git a/components/src/test/java/com/appunite/loudius/components/LoudiusDialogTest.kt b/components/src/test/java/com/appunite/loudius/components/LoudiusDialogTest.kt deleted file mode 100644 index 804573c2d..000000000 --- a/components/src/test/java/com/appunite/loudius/components/LoudiusDialogTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.components - -import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 -import app.cash.paparazzi.Paparazzi -import com.android.ide.common.rendering.api.SessionParams -import com.appunite.loudius.components.components.LoudiusDialogAdvancedPreview -import com.appunite.loudius.components.components.LoudiusErrorDialogPreview -import com.appunite.loudius.components.components.LoudiusErrorScreenPreview -import org.junit.Rule -import org.junit.Test - -class LoudiusDialogTest { - @get:Rule - val paparazzi = Paparazzi( - deviceConfig = PIXEL_5, - renderingMode = SessionParams.RenderingMode.V_SCROLL, - showSystemUi = false, - ) - - @Test - fun loudiusDialogTest() { - paparazzi.snapshot { - LoudiusDialogAdvancedPreview() - } - } - - @Test - fun loudiusErrorDialogTest() { - paparazzi.snapshot { - LoudiusErrorDialogPreview() - } - } - - @Test - fun loudiusFullScreenErrorTest() { - paparazzi.snapshot { - LoudiusErrorScreenPreview() - } - } -} From 68b195d34cae001d0136d5e2cdaf0d1204d6e5a1 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Fri, 2 Jun 2023 11:44:27 +0000 Subject: [PATCH 494/526] [Paparazzi] Record new snapshots --- ...sts[Default Group:LoudiusDialogAdvancedPreview,1,light].png | 3 +++ ...sts[Default Group:LoudiusDialogAdvancedPreview,2,light].png | 3 +++ ...tests[Default Group:LoudiusDialogSimplePreview,1,light].png | 3 +++ ...tests[Default Group:LoudiusDialogSimplePreview,2,light].png | 3 +++ ..._tests[Default Group:LoudiusErrorDialogPreview,1,light].png | 3 +++ ..._tests[Default Group:LoudiusErrorDialogPreview,2,light].png | 3 +++ ...t Group:LoudiusListItemContentAndActionPreview,1,light].png | 3 +++ ...t Group:LoudiusListItemContentAndActionPreview,2,light].png | 3 +++ ...ult Group:LoudiusListItemContentAndIconPreview,1,light].png | 3 +++ ...ult Group:LoudiusListItemContentAndIconPreview,2,light].png | 3 +++ ...efault Group:LoudiusListItemJustContentPreview,1,light].png | 3 +++ ...efault Group:LoudiusListItemJustContentPreview,2,light].png | 3 +++ ...t Group:LoudiusListItemManyWithAllItemsPreview,1,light].png | 3 +++ ...t Group:LoudiusListItemManyWithAllItemsPreview,2,light].png | 3 +++ ...s[Default Group:LoudiusListItemMultiplePreview,1,light].png | 3 +++ ...s[Default Group:LoudiusListItemMultiplePreview,2,light].png | 3 +++ ...Default Group:LoudiusListItemWithHeaderPreview,1,light].png | 3 +++ ...Default Group:LoudiusListItemWithHeaderPreview,2,light].png | 3 +++ ...ult Group:LoudiusOutlinedButtonDisabledPreview,1,light].png | 3 +++ ...ult Group:LoudiusOutlinedButtonDisabledPreview,2,light].png | 3 +++ ...efault Group:LoudiusOutlinedButtonLargePreview,1,light].png | 3 +++ ...efault Group:LoudiusOutlinedButtonLargePreview,2,light].png | 3 +++ ...sts[Default Group:LoudiusOutlinedButtonPreview,1,light].png | 3 +++ ...sts[Default Group:LoudiusOutlinedButtonPreview,2,light].png | 3 +++ ...roup:LoudiusOutlinedButtonWithIconLargePreview,1,light].png | 3 +++ ...roup:LoudiusOutlinedButtonWithIconLargePreview,2,light].png | 3 +++ ...ult Group:LoudiusOutlinedButtonWithIconPreview,1,light].png | 3 +++ ...ult Group:LoudiusOutlinedButtonWithIconPreview,2,light].png | 3 +++ ..._preview_tests[Default Group:LoudiusTextStyles,1,light].png | 3 +++ ..._preview_tests[Default Group:LoudiusTextStyles,2,light].png | 3 +++ ...s_preview_tests[Default Group:LoudiusTopAppBar,1,light].png | 3 +++ ...s_preview_tests[Default Group:LoudiusTopAppBar,2,light].png | 3 +++ ...efault Group:LoudiusTopAppBarWithoutBackButton,1,light].png | 3 +++ ...efault Group:LoudiusTopAppBarWithoutBackButton,2,light].png | 3 +++ ...ts[Default Group:PreviewLoudiusPlaceholderText,1,light].png | 3 +++ ...ts[Default Group:PreviewLoudiusPlaceholderText,2,light].png | 3 +++ 36 files changed, 108 insertions(+) create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png new file mode 100644 index 000000000..0821c6e95 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab +size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png new file mode 100644 index 000000000..0821c6e95 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab +size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png new file mode 100644 index 000000000..ebf67112b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 +size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png new file mode 100644 index 000000000..ebf67112b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 +size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png new file mode 100644 index 000000000..adb4923f0 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b +size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png new file mode 100644 index 000000000..adb4923f0 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b +size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png new file mode 100644 index 000000000..1d7fd727d --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6725a5bcaefb69980781727b2a936ca99f2ca47134ee2618b2c72178307409f +size 7925 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png new file mode 100644 index 000000000..5a27940fc --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb7af9e228761637e5190f3690baa42cdd1ffd014b9ded38c414eccda90c4232 +size 10295 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png new file mode 100644 index 000000000..86f7f6425 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1da0576d813e25aeac678373fa5e3fb1fb05cc157dd41c43f9096f254c99da4 +size 5611 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png new file mode 100644 index 000000000..2fe0f23a6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e097e1a40221cc89a919f334926e96ef8b355013b0c63d80fc609c0e1a08434a +size 6421 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png new file mode 100644 index 000000000..6f27b9758 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5e8df4de7a2da35fce98e3c04645f8900cb36112d3005f695da0393331a9b7c +size 4947 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png new file mode 100644 index 000000000..bf696f543 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff605ac691be201cd27364d7aaed789635f02c0db4d892926831b439e93225c2 +size 5830 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png new file mode 100644 index 000000000..3f3a5bbf9 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e049cb3b8b4a2c947e277388781ecaea6ef284652197558bd9d1d0b72b26c7f5 +size 17165 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png new file mode 100644 index 000000000..09cbc0cd6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13dfe08fcd0d9136dd04364cad36165254498bdb3d28996700be39552c606654 +size 23148 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png new file mode 100644 index 000000000..1f5f6132a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:390c6656e0698ef92c4a998d1aaa5c92892bcb18ba7ca8bb39da89604d526dd8 +size 6382 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png new file mode 100644 index 000000000..55225c48a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1b60a7450f390c1be149b1c7836cfb7ebb8069809166e77f3a1b74e8b37b7f +size 8595 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png new file mode 100644 index 000000000..ac9bfef88 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83f2bfb3b6ed7ce3c0caccfcf72d2eb54121006258a1d902c61bff8b24b079d6 +size 6536 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png new file mode 100644 index 000000000..d1110b48d --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f39e9acc9b77154c75bd8f00cd7f3124891daeafff136d2f72f4dcec18bcfea2 +size 9085 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png new file mode 100644 index 000000000..7cb9bec3c --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db03a758d601ef7bfe30e78a0a6cc68ead55dd0e9222238b4ac73a415ac5ac14 +size 9179 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png new file mode 100644 index 000000000..94b65e6f1 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4afbc366736fb39d7a5d89fa60f40426746c5f6c9be7fcb4de21d0c0e53fd752 +size 12306 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png new file mode 100644 index 000000000..64575e3a4 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:894711d35bb46a1b4ee54efdf226a0db7304324927d90217095b551a658963eb +size 8256 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png new file mode 100644 index 000000000..54dc14eaa --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a2719d46b2fbeb9f8f49608fc11dd94f90c6705a1b0cee84c8ae1e119b38cd9 +size 11131 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png new file mode 100644 index 000000000..cf2146f5a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c41969240d89c104b829e9212fa731ed7ab355df9bc744e0ae0305d8ef3022de +size 7808 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png new file mode 100644 index 000000000..d3cad85dd --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e08b79c1a40997c849e7b7a7045234066148c346905f0745a09350606ac220a0 +size 10324 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png new file mode 100644 index 000000000..d4445452f --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c98321726f8315622a8df1d6382c77a2f46a3ab60c7db99a0d133a969672c3ec +size 8106 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png new file mode 100644 index 000000000..9a329b0c0 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1789f67b5370986a0d2353bf8ae91f83d74eefa86cc5b092c0a6476d92c5420a +size 9716 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png new file mode 100644 index 000000000..90b008d2b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d126060fda523c5967a3b032b770ecc140dd1e401dfe23984e8be9842d4fa8b +size 7705 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png new file mode 100644 index 000000000..981b75fcf --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dde844a44b4e9d7af34dd7d2fd3d13e2df5dfa858bf83bdd34a460cb207b312 +size 9052 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png new file mode 100644 index 000000000..c7ede9de6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:274b96572e05c39d8450807f7ed82f62b5247265b8cb4c96b268ec8b622edda1 +size 21460 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png new file mode 100644 index 000000000..8c46f59ef --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66192be096f30b06730295c0a2535c11dbb146045d6e1285fb39277d11b8e2b5 +size 37493 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png new file mode 100644 index 000000000..471736297 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d061170eec87312e8ad42306fd8ab4f1419c4d695997e66eee2a816b14c2a650 +size 6380 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png new file mode 100644 index 000000000..8eb541da2 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e6855268936ee876434f3259fb7805ee4e3ad549a96d8d5d306d49e87b9a8e1 +size 8273 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png new file mode 100644 index 000000000..f4dd2a916 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5f3a12a19a5b0f06bac010beff42186c00d70d2fff96d845b6314c9ecec63f1 +size 5989 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png new file mode 100644 index 000000000..64486edbd --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea9407269eeaafb256fc3c05ebfeda3dbc6eaee9b628ca6ef9fa0301065d111d +size 7775 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png new file mode 100644 index 000000000..1601411e6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e24eab6fa05e87a8f66790c77ff7c122d3b84983598ed40661d8297dbc6b2e0 +size 14627 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png new file mode 100644 index 000000000..64efbe59c --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c09ff526255eb7a9bc534d9ed3dcabba5540ed9efe8dfee6c1d1dcf5216c5257 +size 26388 From d31f365a95f67613fa183b5fdf8700133c682860 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 2 Jun 2023 16:45:45 +0200 Subject: [PATCH 495/526] Make LoudiusPullToRefreshBoxPreview private to exclude from the snapshots test. --- .../loudius/components/components/LoudiusPullToRefreshBox.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPullToRefreshBox.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPullToRefreshBox.kt index 3437fed93..5bb9ac05f 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPullToRefreshBox.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPullToRefreshBox.kt @@ -45,7 +45,7 @@ fun LoudiusPullToRefreshBox( @Preview @Composable -fun LoudiusPullToRefreshBoxPreview() { +private fun LoudiusPullToRefreshBoxPreview() { LoudiusTheme { LoudiusPullToRefreshBox( pullRefreshState = rememberPullRefreshState(refreshing = true, onRefresh = {}), From a7d35d47ba735cac22b423579638373dc4a7cea4 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Fri, 2 Jun 2023 14:51:32 +0000 Subject: [PATCH 496/526] [Paparazzi] Record new snapshots --- ...lt Group:LoudiusErrorScreenCustomTextsPreview,1,light].png | 3 +++ ...lt Group:LoudiusErrorScreenCustomTextsPreview,2,light].png | 3 +++ ...s[Default Group:PreviewLoudiusPlaceholderText,1,light].png | 4 ++-- ...s[Default Group:PreviewLoudiusPlaceholderText,2,light].png | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png new file mode 100644 index 000000000..b855a64cb --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa17e0bcd4ff0a12ab3416486f3cbebbabeec8764736ec90faf25ea70731ffc6 +size 54890 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png new file mode 100644 index 000000000..465c364d1 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7553e9dfe58e79cee2090775a675c6c195a8bfeff90a2310cd524204088fd0b9 +size 57091 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png index 1601411e6..7f7df0724 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e24eab6fa05e87a8f66790c77ff7c122d3b84983598ed40661d8297dbc6b2e0 -size 14627 +oid sha256:dd4c898e680ad7668081b1ec4006b6d70c987804152bfeb082fb169fc4aa1bcd +size 14788 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png index 64efbe59c..cacfae227 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c09ff526255eb7a9bc534d9ed3dcabba5540ed9efe8dfee6c1d1dcf5216c5257 -size 26388 +oid sha256:222a26a18c355332f40744caf688db5fc5c645b27da7458380fc0059fb8cb06d +size 26272 From a0a9a2929e89327c09492d2a93ddc1e44bd55b3d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 5 Jun 2023 09:16:49 +0200 Subject: [PATCH 497/526] Remove extra slash from the Placeholder preview text. --- .../loudius/components/components/LoudiusPlaceholderText.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt index 2223cfd25..68f44346e 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusPlaceholderText.kt @@ -45,6 +45,6 @@ fun LoudiusPlaceholderText(text: String) { @Composable fun PreviewLoudiusPlaceholderText() { LoudiusTheme { - LoudiusPlaceholderText("Sorry! Your list of pull requests is empty.\\nGet back to work! \uD83E\uDDD1\u200D") + LoudiusPlaceholderText("Sorry! Your list of pull requests is empty.\nGet back to work! \uD83E\uDDD1\u200D") } } From a13ee9bedb5ac711b3bc344f94bb24ea72f60023 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 5 Jun 2023 09:18:18 +0200 Subject: [PATCH 498/526] Remove external padding from the LoudiusOutlinedButton style for a Large Button. --- .../main/java/com/appunite/loudius/ui/login/LoginScreen.kt | 6 +++++- .../loudius/components/components/LoudiusOutlinedButton.kt | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt index 59957bdf9..00336212d 100644 --- a/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt +++ b/app/src/main/java/com/appunite/loudius/ui/login/LoginScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -32,6 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.appunite.loudius.R import com.appunite.loudius.common.Constants.AUTHORIZATION_URL @@ -81,7 +83,9 @@ fun LoginScreenStateless( ) { LoginImage() LoudiusOutlinedButton( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 46.dp), onClick = { onAction(LoginAction.ClickLogIn) }, diff --git a/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt b/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt index 7ef141405..a140e1e01 100644 --- a/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt +++ b/components/src/main/java/com/appunite/loudius/components/components/LoudiusOutlinedButton.kt @@ -46,8 +46,7 @@ fun LoudiusOutlinedButton( OutlinedButton( enabled = enabled, onClick = onClick, - modifier = modifier - .applyIf(style == LoudiusOutlinedButtonStyle.Large) { padding(horizontal = 46.dp) }, + modifier = modifier, ) { icon?.invoke() From 5fe67b59f20fce25f77dc59181022893f136443d Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 5 Jun 2023 08:22:19 +0000 Subject: [PATCH 499/526] [Paparazzi] Record new snapshots --- ...fault Group:LoudiusOutlinedButtonLargePreview,1,light].png | 4 ++-- ...fault Group:LoudiusOutlinedButtonLargePreview,2,light].png | 4 ++-- ...oup:LoudiusOutlinedButtonWithIconLargePreview,1,light].png | 4 ++-- ...oup:LoudiusOutlinedButtonWithIconLargePreview,2,light].png | 4 ++-- ...s[Default Group:PreviewLoudiusPlaceholderText,1,light].png | 4 ++-- ...s[Default Group:PreviewLoudiusPlaceholderText,2,light].png | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png index 64575e3a4..4f5294697 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:894711d35bb46a1b4ee54efdf226a0db7304324927d90217095b551a658963eb -size 8256 +oid sha256:c1350b3c689190bcfc58e721068290b46368b4c71ebfe4dc331c0ad047aa9f95 +size 8212 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png index 54dc14eaa..16797a450 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a2719d46b2fbeb9f8f49608fc11dd94f90c6705a1b0cee84c8ae1e119b38cd9 -size 11131 +oid sha256:133d80fd13aa8bb2035fbe93828c421778bbdf5167c5cfe91e4420d7fd19bfa9 +size 11036 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png index d4445452f..b41b79676 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c98321726f8315622a8df1d6382c77a2f46a3ab60c7db99a0d133a969672c3ec -size 8106 +oid sha256:9a9cb890d5cd6bb67a587d45ee77ef2814198bade1be89ea5f9f60e1bc1a58f8 +size 8064 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png index 9a329b0c0..70212b474 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1789f67b5370986a0d2353bf8ae91f83d74eefa86cc5b092c0a6476d92c5420a -size 9716 +oid sha256:b99dd141685ab4aef55a9b9b8cc37b86af54e866d89264992eab631a554114fd +size 9676 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png index 7f7df0724..ea3f98807 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd4c898e680ad7668081b1ec4006b6d70c987804152bfeb082fb169fc4aa1bcd -size 14788 +oid sha256:33678f192a03020affb7893ff08ff36f585a8bcdc549df78ae8f85121405dc00 +size 14437 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png index cacfae227..d6e0da992 100644 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:222a26a18c355332f40744caf688db5fc5c645b27da7458380fc0059fb8cb06d -size 26272 +oid sha256:f0062b9966dd887d174fa10bd7c6311043ef3e084ad0de5e3a8a436ae9306262 +size 25698 From 270687d2e765c6e399ed742ae154fa4b7b956328 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 5 Jun 2023 11:01:33 +0200 Subject: [PATCH 500/526] another test if firebase screenshots are working --- .../java/com/appunite/loudius/util/EspressoScreenshot.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt index 2d44e665a..fbe148767 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt @@ -37,7 +37,7 @@ object EspressoScreenshot { // Firebase Test Lab requires screenshots to be saved to /sdcard/screenshots // https://github.com/firebase/firebase-testlab-instr-lib/blob/f0a21a526499f051ac5074dc382cf79e237d2f4e/firebase-testlab-instr-lib/testlab-instr-lib/src/main/java/com/google/firebase/testlab/screenshot/FirebaseScreenCaptureProcessor.java#L36 // private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) - private val screenshotFolder = File("/sdcard/screenshots") + private val screenshotFolder = File("/sdcard/Download") private val TAG = EspressoScreenshot::class.java.simpleName private fun getScreenshotName(description: Description): String { @@ -73,14 +73,13 @@ object EspressoScreenshot { } } - // Checks if a volume containing external storage is available -// for read and write. - fun isExternalStorageWritable(): Boolean { + // Checks if a volume containing external storage is available for read and write. + private fun isExternalStorageWritable(): Boolean { return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED } // Checks if a volume containing external storage is available to at least read. - fun isExternalStorageReadable(): Boolean { + private fun isExternalStorageReadable(): Boolean { return Environment.getExternalStorageState() in setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY) } From e1162e325ffa198f992fffcb483013c20e3509ae Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 5 Jun 2023 11:29:26 +0200 Subject: [PATCH 501/526] Remove orphaned screenshots - which are without test. --- ...ius.components_LoudiusButtonTests_loudiusOutlinedButton.png | 3 --- ....loudius.components_LoudiusDialogTest_loudiusDialogTest.png | 3 --- ...ius.components_LoudiusDialogTest_loudiusErrorDialogTest.png | 3 --- ...components_LoudiusDialogTest_loudiusFullScreenErrorTest.png | 3 --- 4 files changed, 12 deletions(-) delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusButtonTests_loudiusOutlinedButton.png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusDialogTest.png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusErrorDialogTest.png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png diff --git a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusButtonTests_loudiusOutlinedButton.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusButtonTests_loudiusOutlinedButton.png deleted file mode 100644 index 0e5c42055..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusButtonTests_loudiusOutlinedButton.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b5e9a594d9e3abdd88b4384b98e0157e4528c676a6be8a1db90f65e404edaf6 -size 23354 diff --git a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusDialogTest.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusDialogTest.png deleted file mode 100644 index 23b70e73a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusDialogTest.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4bd222e8ebc495897cd5814b1e5e27bbb9c72745be658d4e5d337704ced9e767 -size 43084 diff --git a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusErrorDialogTest.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusErrorDialogTest.png deleted file mode 100644 index 12f7986b6..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusErrorDialogTest.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:84dff7ee7c10a4cde15c3e8021f754cc634c1884cd51df8386c25db9d48de5e6 -size 13930 diff --git a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png b/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png deleted file mode 100644 index 20276bd3a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius.components_LoudiusDialogTest_loudiusFullScreenErrorTest.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2cf086878480f21ad538d389920718c38fc320479c4a0a7cc8cc4e92c492e7dc -size 55744 From 4ea00837ce37a229d62e109b9e3a61f172509ed2 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 5 Jun 2023 09:34:07 +0000 Subject: [PATCH 502/526] [MegaLinter] Apply linters fixes --- .../java/com/appunite/loudius/PaparazziShowkaseTests.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt index 22f96859b..b6e63ad8e 100644 --- a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt +++ b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt @@ -37,7 +37,7 @@ import org.junit.Test import org.junit.runner.RunWith class ComponentPreview( - private val showkaseBrowserComponent: ShowkaseBrowserComponent + private val showkaseBrowserComponent: ShowkaseBrowserComponent, ) { val content: @Composable () -> Unit = showkaseBrowserComponent.component override fun toString(): String = @@ -68,8 +68,8 @@ class PaparazziShowkaseTests { LocalInspectionMode provides true, LocalDensity provides Density( density = LocalDensity.current.density, - fontScale = fontScale - ) + fontScale = fontScale, + ), ) { LoudiusTheme(darkTheme = (theme == "dark")) { Box(modifier = Modifier.background(Color.White)) { From 0e1155dbcc591d7c757a7fcfe5671e18cfc87549 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 5 Jun 2023 12:13:23 +0200 Subject: [PATCH 503/526] add write external storage permission --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 49629d2ea..cbb97f94b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ + From 745db2c25264f73c446c183d984cf579ef2140e9 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 5 Jun 2023 12:39:36 +0200 Subject: [PATCH 504/526] try to save screenshots on firebase --- .../java/com/appunite/loudius/util/EspressoScreenshot.kt | 2 +- app/src/debug/AndroidManifest.xml | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt index fbe148767..dd5c2648b 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt @@ -37,7 +37,7 @@ object EspressoScreenshot { // Firebase Test Lab requires screenshots to be saved to /sdcard/screenshots // https://github.com/firebase/firebase-testlab-instr-lib/blob/f0a21a526499f051ac5074dc382cf79e237d2f4e/firebase-testlab-instr-lib/testlab-instr-lib/src/main/java/com/google/firebase/testlab/screenshot/FirebaseScreenCaptureProcessor.java#L36 // private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) - private val screenshotFolder = File("/sdcard/Download") + private val screenshotFolder = File("/sdcard") private val TAG = EspressoScreenshot::class.java.simpleName private fun getScreenshotName(description: Description): String { diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index a6615e09c..f525128cb 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,6 +1,11 @@ - + + + + + From af45f353941ac9da6e654f49717230ebccd640a3 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 5 Jun 2023 12:54:20 +0200 Subject: [PATCH 505/526] try to save screenshots on firebase --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cbb97f94b..cc92c61b9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ android:maxSdkVersion="32" /> + From 33aeb804e80dd33bd222d609a858333dd6191d8d Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 6 Jun 2023 11:36:28 +0200 Subject: [PATCH 506/526] add testlab-instr-lib and take screenshot --- app/build.gradle | 3 +++ .../java/com/appunite/loudius/LoginScreenTest.kt | 3 +++ app/src/debug/AndroidManifest.xml | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c093b9d6e..2c6b401cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -132,6 +132,9 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' + // Firebase instrumentation lib + androidTestImplementation 'com.google.firebase:testlab-instr-lib:0.2' + // ktlint ktlintRuleset project(":custom-ktlint-rules") } diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 40f264576..309ee5826 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -32,6 +32,7 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasData import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.runner.screenshot.Screenshot import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.di.GithubHelperModule import com.appunite.loudius.ui.login.GithubHelper @@ -88,8 +89,10 @@ class LoginScreenTest { LoginScreen() } } + Screenshot.capture().process() composeTestRule.onNodeWithText("Log in").performClick() + Screenshot.capture().process() intended( allOf( hasAction(Intent.ACTION_VIEW), diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index f525128cb..a0eda3021 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,14 +1,25 @@ - - + + + + + From 50c9c206cc174d51a5195674e1ccadce1c91b1d1 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 6 Jun 2023 12:15:30 +0200 Subject: [PATCH 507/526] code cleanup --- .../com/appunite/loudius/LoginScreenTest.kt | 5 +- .../loudius/util/EspressoScreenshot.kt | 118 ------------------ .../loudius/util/ScreenshotTestRule.kt | 5 +- app/src/debug/AndroidManifest.xml | 16 --- app/src/main/AndroidManifest.xml | 6 - 5 files changed, 4 insertions(+), 146 deletions(-) delete mode 100644 app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt diff --git a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt index 309ee5826..9cdd4869d 100644 --- a/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt +++ b/app/src/androidTest/java/com/appunite/loudius/LoginScreenTest.kt @@ -32,7 +32,6 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasData import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.intent.rule.IntentsRule import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.runner.screenshot.Screenshot import com.appunite.loudius.components.theme.LoudiusTheme import com.appunite.loudius.di.GithubHelperModule import com.appunite.loudius.ui.login.GithubHelper @@ -89,14 +88,12 @@ class LoginScreenTest { LoginScreen() } } - Screenshot.capture().process() composeTestRule.onNodeWithText("Log in").performClick() - Screenshot.capture().process() intended( allOf( hasAction(Intent.ACTION_VIEW), - hasData("wrong data"), + hasData("https://github.com/login/oauth/authorize?client_id=91131449e417c7e29912&scope=repo"), ), ) } diff --git a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt b/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt deleted file mode 100644 index dd5c2648b..000000000 --- a/app/src/androidTest/java/com/appunite/loudius/util/EspressoScreenshot.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2023 AppUnite S.A. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.appunite.loudius.util - -import android.os.Environment -import android.util.Log -import androidx.test.runner.screenshot.Screenshot -import org.junit.runner.Description -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.util.concurrent.atomic.AtomicInteger - -/** - * Used to automatically capture screenshots of failed tests. - */ -object EspressoScreenshot { - private val imageCounter = AtomicInteger(0) - private val dotPNG = ".png" - private val underscore = "_" - - // Firebase Test Lab requires screenshots to be saved to /sdcard/screenshots - // https://github.com/firebase/firebase-testlab-instr-lib/blob/f0a21a526499f051ac5074dc382cf79e237d2f4e/firebase-testlab-instr-lib/testlab-instr-lib/src/main/java/com/google/firebase/testlab/screenshot/FirebaseScreenCaptureProcessor.java#L36 - // private val screenshotFolder = Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS) - private val screenshotFolder = File("/sdcard") - private val TAG = EspressoScreenshot::class.java.simpleName - - private fun getScreenshotName(description: Description): String { - val className = description.className - val methodName = description.methodName - - val imageNumberInt = imageCounter.incrementAndGet() - var number = imageNumberInt.toString() - if (imageNumberInt < 10) number = "0$number" - - val components = arrayOf(className, underscore, methodName, underscore, number, dotPNG) - - var length = 0 - - for (component in components) { - length += component.length - } - - val result = StringBuilder(length) - - for (component in components) { - result.append(component) - } - - return result.toString() - } - - private fun prepareScreenshotPath() { - try { - screenshotFolder.mkdirs() - } catch (ignored: Exception) { - Log.e(TAG, "Failed to make screenshot folder $screenshotFolder") - } - } - - // Checks if a volume containing external storage is available for read and write. - private fun isExternalStorageWritable(): Boolean { - return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED - } - - // Checks if a volume containing external storage is available to at least read. - private fun isExternalStorageReadable(): Boolean { - return Environment.getExternalStorageState() in - setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY) - } - - fun takeScreenshot(description: Description) { - prepareScreenshotPath() - - val screenshotName = getScreenshotName(description) - val capture = Screenshot.capture() // default format is PNG - - // based on BasicScreenCaptureProcessor#process - val imageFile = File(screenshotFolder, screenshotName) - var out: BufferedOutputStream? = null - - Log.i(TAG, "isExternalStorageWritable " + isExternalStorageWritable()) - Log.i(TAG, "isExternalStorageReadable " + isExternalStorageReadable()) - - try { - Log.i(TAG, "Saving screenshot to " + imageFile.absolutePath) - out = BufferedOutputStream(FileOutputStream(imageFile)) - capture.bitmap.compress(capture.format, 100, out) - out.flush() - Log.i(TAG, "Screenshot exists? " + imageFile.exists()) - } catch (ignored: Exception) { - Log.e(TAG, ignored.toString()) - ignored.printStackTrace() - } finally { - try { - out?.close() - } catch (ignored: IOException) { - Log.e(TAG, ignored.toString()) - ignored.printStackTrace() - } - } - } -} diff --git a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt index e339cb98d..5532325e8 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt @@ -19,6 +19,7 @@ package com.appunite.loudius.util import androidx.test.espresso.Espresso import androidx.test.espresso.base.DefaultFailureHandler import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.runner.screenshot.Screenshot import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement @@ -49,7 +50,7 @@ open class ScreenshotTestRule : TestRule { // Only espresso failures trigger the espresso failure handlers. For JUnit assert errors, // those must be captured in `try { base.evaluate() } catch ()` Espresso.setFailureHandler { throwable, matcher -> - EspressoScreenshot.takeScreenshot(description) + Screenshot.capture().process() errorHandled.set(true) val targetContext = getInstrumentation().targetContext DefaultFailureHandler(targetContext).handle(throwable, matcher) @@ -62,7 +63,7 @@ open class ScreenshotTestRule : TestRule { return } catch (t: Throwable) { if (!errorHandled.get()) { - EspressoScreenshot.takeScreenshot(description) + Screenshot.capture().process() } error = t } diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index a0eda3021..18be05ec0 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,25 +1,9 @@ - - - - - - - - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc92c61b9..e1f17d154 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,12 +5,6 @@ - - - From ecb77642fb8a7d67536717b6200a03cdbac851e9 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Mon, 12 Jun 2023 09:01:39 +0200 Subject: [PATCH 508/526] removal of tryCount value --- .../loudius/util/ScreenshotTestRule.kt | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt index 5532325e8..0f035f29c 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt @@ -26,29 +26,17 @@ import org.junit.runners.model.Statement import java.util.concurrent.atomic.AtomicBoolean /** - * TestRule used to run all test methods try count 1 time. Take screenshots on failure. + * TestRule used to take screenshot on failure. */ open class ScreenshotTestRule : TestRule { - // Note: Data seeding must happen before we run a test. As a result, retrying failed tests - // at the JUnit level doesn"t make sense because we can"t run data seeeding. - // - // Run all test methods tryCount times. Take screenshots on failure. - // A method rule would allow targeting specific (method.getAnnotation(Retry.class)) - private val tryCount = 1 override fun apply(base: Statement, description: Description): Statement { return object : Statement() { @Throws(Throwable::class) override fun evaluate() { var error: Throwable? = null - val errorHandled = AtomicBoolean(false) - // Espresso failure handler will capture accurate UI screenshots. - // if we wait for `try { base.evaluate() } catch ()` then the UI will be in a different state - // - // Only espresso failures trigger the espresso failure handlers. For JUnit assert errors, - // those must be captured in `try { base.evaluate() } catch ()` Espresso.setFailureHandler { throwable, matcher -> Screenshot.capture().process() errorHandled.set(true) @@ -56,17 +44,15 @@ open class ScreenshotTestRule : TestRule { DefaultFailureHandler(targetContext).handle(throwable, matcher) } - for (i in 0 until tryCount) { - errorHandled.set(false) - try { - base.evaluate() - return - } catch (t: Throwable) { - if (!errorHandled.get()) { - Screenshot.capture().process() - } - error = t + errorHandled.set(false) + try { + base.evaluate() + return + } catch (t: Throwable) { + if (!errorHandled.get()) { + Screenshot.capture().process() } + error = t } if (error != null) throw error From e8ffea4a60a1b304189b08e7013b7cbde4a711af Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 13 Jun 2023 10:24:26 +0200 Subject: [PATCH 509/526] add name for screenshot --- .../java/com/appunite/loudius/util/ScreenshotTestRule.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt index 0f035f29c..c72450993 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt @@ -50,7 +50,7 @@ open class ScreenshotTestRule : TestRule { return } catch (t: Throwable) { if (!errorHandled.get()) { - Screenshot.capture().process() + captureScreenshot(description) } error = t } @@ -59,4 +59,10 @@ open class ScreenshotTestRule : TestRule { } } } + + private fun captureScreenshot(description: Description) { + val screenshot = Screenshot.capture() + screenshot.name = "${description.testClass.simpleName}_${description.methodName}-failure" + screenshot.process() + } } From c3a908e0bda1ca885324b7536b1650a03dd0dfe5 Mon Sep 17 00:00:00 2001 From: nowakweronika Date: Tue, 13 Jun 2023 11:46:22 +0200 Subject: [PATCH 510/526] add name for screenshot --- .../java/com/appunite/loudius/util/ScreenshotTestRule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt index c72450993..5725e2074 100644 --- a/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt +++ b/app/src/androidTest/java/com/appunite/loudius/util/ScreenshotTestRule.kt @@ -38,7 +38,7 @@ open class ScreenshotTestRule : TestRule { val errorHandled = AtomicBoolean(false) Espresso.setFailureHandler { throwable, matcher -> - Screenshot.capture().process() + captureScreenshot(description) errorHandled.set(true) val targetContext = getInstrumentation().targetContext DefaultFailureHandler(targetContext).handle(throwable, matcher) From 041b8176cc78d5db5dd24bdefe4cca65a06b0d18 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 19 Jun 2023 14:19:12 +0200 Subject: [PATCH 511/526] Add concurrency rules for run-ui-test.yml - cancel previous workflow on the same branch. --- .github/workflows/run-ui-test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index df2cc8b59..de35ab3ef 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -14,6 +14,10 @@ on: permissions: read-all +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + jobs: apk: name: Run UI tests on Firebase Test Lab From 1f33aeaa46b1061a51ebee5fdd1857bb3ad3d4dc Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 19 Jun 2023 14:32:30 +0200 Subject: [PATCH 512/526] Use concurrency rule instead of @cancel-workflow action. --- .github/actions/prepare-android-env/action.yml | 5 ----- .github/workflows/run-snapshot-test.yml | 4 ++++ .github/workflows/run-unit-test.yml | 4 ++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/actions/prepare-android-env/action.yml b/.github/actions/prepare-android-env/action.yml index 07db52bed..76c262a17 100644 --- a/.github/actions/prepare-android-env/action.yml +++ b/.github/actions/prepare-android-env/action.yml @@ -3,11 +3,6 @@ description: "Cancels previous runs, set ups jdk, validates gradle wrapper and u runs: using: "composite" steps: - - name: Cancel previous runs for the same branch - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - - name: Set up JDK uses: actions/setup-java@v3 with: diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index aef0c5c00..fddecb8b0 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -4,6 +4,10 @@ name: Snapshot verification on: pull_request: +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + permissions: checks: write contents: write diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 35ec114c5..9af7ecd26 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -7,6 +7,10 @@ on: - "develop" - "main" +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + permissions: read-all jobs: From 97daebef4b544587741247144544c37c214d6f76 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 20 Jun 2023 11:06:01 +0200 Subject: [PATCH 513/526] Add additional workflow for notifying on slack about workflow failure. --- .../workflows/check-for-workflow-failure.yml | 22 +++++++++++++++++++ .github/workflows/run-unit-test.yml | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/check-for-workflow-failure.yml diff --git a/.github/workflows/check-for-workflow-failure.yml b/.github/workflows/check-for-workflow-failure.yml new file mode 100644 index 000000000..0f3a5fd7e --- /dev/null +++ b/.github/workflows/check-for-workflow-failure.yml @@ -0,0 +1,22 @@ +name: Check for master or prod failure +on: + workflow_run: + workflows: [ "MegaLinter", "Snapshot recording", "Snapshot verification", "Android Release", "Run unit tests" ] + types: [ completed ] + +jobs: + on-failure: + name: Notify Slack on failure + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'timed_out' + steps: + - uses: actions/checkout@v2 + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_CHANNEL: loudius-internal + SLACK_COLOR: #C73E1D + SLACK_MESSAGE: 'Test message - Failure on develop or master branch :cry:' + SLACK_TITLE: Workflow failure + SLACK_USERNAME: Failure bot diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 9af7ecd26..42acc861c 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -1,4 +1,4 @@ -name: "Run unit tests" +name: Run unit tests on: pull_request: From 31be3cad7154535c717674dcdd235c7f843be33c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 20 Jun 2023 15:41:05 +0200 Subject: [PATCH 514/526] Update slack message with branch name and workflow type. --- .github/workflows/check-for-workflow-failure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-for-workflow-failure.yml b/.github/workflows/check-for-workflow-failure.yml index 0f3a5fd7e..483a6883d 100644 --- a/.github/workflows/check-for-workflow-failure.yml +++ b/.github/workflows/check-for-workflow-failure.yml @@ -17,6 +17,6 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_CHANNEL: loudius-internal SLACK_COLOR: #C73E1D - SLACK_MESSAGE: 'Test message - Failure on develop or master branch :cry:' + SLACK_MESSAGE: "Failure on ${{ github.event.workflow_run.branch.name }} branch for ${{ github.event.workflow_run.workflow }}:cry:" SLACK_TITLE: Workflow failure SLACK_USERNAME: Failure bot From 3d18f2b57c2ed1b4a5bd152355eb6c548f87a1b2 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Tue, 20 Jun 2023 15:56:50 +0200 Subject: [PATCH 515/526] Set permission and correct check-for-workflow-failure.yml. --- .github/workflows/check-for-workflow-failure.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-for-workflow-failure.yml b/.github/workflows/check-for-workflow-failure.yml index 483a6883d..a4aacf46d 100644 --- a/.github/workflows/check-for-workflow-failure.yml +++ b/.github/workflows/check-for-workflow-failure.yml @@ -4,19 +4,23 @@ on: workflows: [ "MegaLinter", "Snapshot recording", "Snapshot verification", "Android Release", "Run unit tests" ] types: [ completed ] +permissions: read-all + jobs: on-failure: name: Notify Slack on failure runs-on: ubuntu-latest if: github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'timed_out' steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v3 + - name: Slack Notification uses: rtCamp/action-slack-notify@v2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_CHANNEL: loudius-internal - SLACK_COLOR: #C73E1D + SLACK_COLOR: "#C73E1D" SLACK_MESSAGE: "Failure on ${{ github.event.workflow_run.branch.name }} branch for ${{ github.event.workflow_run.workflow }}:cry:" SLACK_TITLE: Workflow failure SLACK_USERNAME: Failure bot From 6320d17d9f937196cade05ac8922e2f7d7b04807 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Tue, 20 Jun 2023 14:00:52 +0000 Subject: [PATCH 516/526] [MegaLinter] Apply linters fixes --- .github/workflows/check-for-workflow-failure.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-for-workflow-failure.yml b/.github/workflows/check-for-workflow-failure.yml index a4aacf46d..18bc1cd49 100644 --- a/.github/workflows/check-for-workflow-failure.yml +++ b/.github/workflows/check-for-workflow-failure.yml @@ -1,8 +1,15 @@ name: Check for master or prod failure on: workflow_run: - workflows: [ "MegaLinter", "Snapshot recording", "Snapshot verification", "Android Release", "Run unit tests" ] - types: [ completed ] + workflows: + [ + "MegaLinter", + "Snapshot recording", + "Snapshot verification", + "Android Release", + "Run unit tests", + ] + types: [completed] permissions: read-all From 1a6e82ac786f1833a6b1100e48f67c1ef6642a5c Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 23 Jun 2023 08:41:31 +0200 Subject: [PATCH 517/526] Correct slack message on workflow failure. --- .github/workflows/check-for-workflow-failure.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-for-workflow-failure.yml b/.github/workflows/check-for-workflow-failure.yml index 18bc1cd49..73f48f6cc 100644 --- a/.github/workflows/check-for-workflow-failure.yml +++ b/.github/workflows/check-for-workflow-failure.yml @@ -28,6 +28,6 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_CHANNEL: loudius-internal SLACK_COLOR: "#C73E1D" - SLACK_MESSAGE: "Failure on ${{ github.event.workflow_run.branch.name }} branch for ${{ github.event.workflow_run.workflow }}:cry:" + SLACK_MESSAGE: "Failure on ${{ github.event.workflow_run.head_branch }} branch for ${{ github.event.workflow_run.workflow.name }}:cry:" SLACK_TITLE: Workflow failure SLACK_USERNAME: Failure bot From 33f2872de77a3573b430d21b1ed030f2fab3df7e Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 23 Jun 2023 12:31:33 +0200 Subject: [PATCH 518/526] Check way of notifying Slack directly after the workflow. --- .github/workflows/run-snapshot-test.yml | 11 +++++++++++ .../com/appunite/loudius/PaparazziShowkaseTests.kt | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index fddecb8b0..f3963e3f7 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -97,3 +97,14 @@ jobs: reactions: | confused reactions-edit-mode: replace + + - name: Notify Slack on failure + if: failure() + uses: rtCamp/action-slack-notify@v2.1.0 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_CHANNEL: loudius-internal + SLACK_COLOR: "#C73E1D" + SLACK_MESSAGE: "Failure on ${{ github.ref.name }} branch for ${{ github.workflow.name }} workflow :cry:" + SLACK_TITLE: Workflow failure + SLACK_USERNAME: Failure bot diff --git a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt index b6e63ad8e..3360c937d 100644 --- a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt +++ b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt @@ -72,7 +72,7 @@ class PaparazziShowkaseTests { ), ) { LoudiusTheme(darkTheme = (theme == "dark")) { - Box(modifier = Modifier.background(Color.White)) { + Box(modifier = Modifier.background(Color.Black)) { componentPreview.content() } } From 38f4fa769266221a6c84c9224107fc83b37e0a3d Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk <33498031+Krzysiudan@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:50:45 +0200 Subject: [PATCH 519/526] Delete check-for-workflow-failure.yml --- .../workflows/check-for-workflow-failure.yml | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 .github/workflows/check-for-workflow-failure.yml diff --git a/.github/workflows/check-for-workflow-failure.yml b/.github/workflows/check-for-workflow-failure.yml deleted file mode 100644 index 18bc1cd49..000000000 --- a/.github/workflows/check-for-workflow-failure.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Check for master or prod failure -on: - workflow_run: - workflows: - [ - "MegaLinter", - "Snapshot recording", - "Snapshot verification", - "Android Release", - "Run unit tests", - ] - types: [completed] - -permissions: read-all - -jobs: - on-failure: - name: Notify Slack on failure - runs-on: ubuntu-latest - if: github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'timed_out' - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Slack Notification - uses: rtCamp/action-slack-notify@v2 - env: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_CHANNEL: loudius-internal - SLACK_COLOR: "#C73E1D" - SLACK_MESSAGE: "Failure on ${{ github.event.workflow_run.branch.name }} branch for ${{ github.event.workflow_run.workflow }}:cry:" - SLACK_TITLE: Workflow failure - SLACK_USERNAME: Failure bot From d02288f75a910d953fa74e8b9077aa44080908e8 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 23 Jun 2023 13:18:42 +0200 Subject: [PATCH 520/526] Add Slack notification on failure step into every existing workflow. --- .github/actions/slack-notification/action.yml | 14 ++++++++++++++ .github/workflows/run-code-quality-check.yml | 8 ++++++++ .github/workflows/run-snapshot-generation.yml | 8 ++++++++ .github/workflows/run-snapshot-test.yml | 13 +++++-------- .github/workflows/run-ui-test.yml | 10 ++++++++++ .github/workflows/run-unit-test.yml | 8 ++++++++ 6 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 .github/actions/slack-notification/action.yml diff --git a/.github/actions/slack-notification/action.yml b/.github/actions/slack-notification/action.yml new file mode 100644 index 000000000..38690e95e --- /dev/null +++ b/.github/actions/slack-notification/action.yml @@ -0,0 +1,14 @@ +name: "Send notification to Slack" +description: "Sending notification to Slack on loudius-internal channel with workflow failure message. " +runs: + using: "composite" + steps: + - name: Slack Notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ env.SLACK_WEBHOOK }} + SLACK_CHANNEL: loudius-internal + SLACK_COLOR: "#C73E1D" + SLACK_MESSAGE: "Uh-oh! The code monkeys are having a party on *${{ env.SLACK_GIT_REF }}* 🐒🎉 But it seems like they broke something in the *'${{ env.SLACK_WORKFLOW}}' workflow!* Can someone please bring some bananas to fix it? 🍌🔧" + SLACK_TITLE: ⚠️ Workflow Failure Alert ⚠️ + SLACK_USERNAME: Code Monkey Bot 🐵 diff --git a/.github/workflows/run-code-quality-check.yml b/.github/workflows/run-code-quality-check.yml index fbccd0a85..f0d881c81 100644 --- a/.github/workflows/run-code-quality-check.yml +++ b/.github/workflows/run-code-quality-check.yml @@ -97,3 +97,11 @@ jobs: with: branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} commit_message: "[MegaLinter] Apply linters fixes" + + - name: Include Slack Notification + if: failure() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + uses: ./.github/actions/slack-notification + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_GIT_REF: ${{ github.ref }} + SLACK_WORKFLOW: ${{ github.workflow }} diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index f9ece5462..ec2486591 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -96,3 +96,11 @@ jobs: reactions: | confused reactions-edit-mode: replace + + - name: Include Slack Notification + if: failure() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + uses: ./.github/actions/slack-notification + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_GIT_REF: ${{ github.ref }} + SLACK_WORKFLOW: ${{ github.workflow }} diff --git a/.github/workflows/run-snapshot-test.yml b/.github/workflows/run-snapshot-test.yml index f3963e3f7..da5c4a66d 100644 --- a/.github/workflows/run-snapshot-test.yml +++ b/.github/workflows/run-snapshot-test.yml @@ -98,13 +98,10 @@ jobs: confused reactions-edit-mode: replace - - name: Notify Slack on failure - if: failure() - uses: rtCamp/action-slack-notify@v2.1.0 + - name: Include Slack Notification + if: failure() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + uses: ./.github/actions/slack-notification env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - SLACK_CHANNEL: loudius-internal - SLACK_COLOR: "#C73E1D" - SLACK_MESSAGE: "Failure on ${{ github.ref.name }} branch for ${{ github.workflow.name }} workflow :cry:" - SLACK_TITLE: Workflow failure - SLACK_USERNAME: Failure bot + SLACK_GIT_REF: ${{ github.ref }} + SLACK_WORKFLOW: ${{ github.workflow }} diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index de35ab3ef..7b2ee5b85 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -74,3 +74,13 @@ jobs: else python "build-tools/upload-junit-to-cloud.py" fi + + + - name: Include Slack Notification + if: failure() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + uses: ./.github/actions/slack-notification + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_GIT_REF: ${{ github.ref }} + SLACK_WORKFLOW: ${{ github.workflow }} + diff --git a/.github/workflows/run-unit-test.yml b/.github/workflows/run-unit-test.yml index 42acc861c..3c45f0e96 100644 --- a/.github/workflows/run-unit-test.yml +++ b/.github/workflows/run-unit-test.yml @@ -29,3 +29,11 @@ jobs: env: GITHUB_USER: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Include Slack Notification + if: failure() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') + uses: ./.github/actions/slack-notification + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_GIT_REF: ${{ github.ref }} + SLACK_WORKFLOW: ${{ github.workflow }} From 36b2b72e92cf13014f08d09480d0967aba5c0818 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 23 Jun 2023 14:02:13 +0200 Subject: [PATCH 521/526] Add concurrency settings into the run-snapshot-generation.yml workflow. --- .github/workflows/run-snapshot-generation.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index ec2486591..a9e86385d 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -6,7 +6,11 @@ name: Snapshot recording on: pull_request: - types: [opened, edited, synchronize] + types: [ opened, edited, synchronize ] + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true permissions: checks: write From ef9a93e975ad7ac3d82efe7aba3f0f41200ea5e6 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Fri, 23 Jun 2023 15:35:29 +0200 Subject: [PATCH 522/526] Correct snapshot tests. --- .../test/java/com/appunite/loudius/PaparazziShowkaseTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt index 3360c937d..b6e63ad8e 100644 --- a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt +++ b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt @@ -72,7 +72,7 @@ class PaparazziShowkaseTests { ), ) { LoudiusTheme(darkTheme = (theme == "dark")) { - Box(modifier = Modifier.background(Color.Black)) { + Box(modifier = Modifier.background(Color.White)) { componentPreview.content() } } From 0b1048fbd2bbffc9306c4983885fa76c28301062 Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Fri, 23 Jun 2023 13:39:55 +0000 Subject: [PATCH 523/526] [MegaLinter] Apply linters fixes --- .github/workflows/run-snapshot-generation.yml | 2 +- .github/workflows/run-ui-test.yml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/run-snapshot-generation.yml b/.github/workflows/run-snapshot-generation.yml index a9e86385d..ca7bc41d6 100644 --- a/.github/workflows/run-snapshot-generation.yml +++ b/.github/workflows/run-snapshot-generation.yml @@ -6,7 +6,7 @@ name: Snapshot recording on: pull_request: - types: [ opened, edited, synchronize ] + types: [opened, edited, synchronize] concurrency: group: ${{ github.ref }}-${{ github.workflow }} diff --git a/.github/workflows/run-ui-test.yml b/.github/workflows/run-ui-test.yml index 7b2ee5b85..cd2d23fcc 100644 --- a/.github/workflows/run-ui-test.yml +++ b/.github/workflows/run-ui-test.yml @@ -75,7 +75,6 @@ jobs: python "build-tools/upload-junit-to-cloud.py" fi - - name: Include Slack Notification if: failure() && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master') uses: ./.github/actions/slack-notification @@ -83,4 +82,3 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_GIT_REF: ${{ github.ref }} SLACK_WORKFLOW: ${{ github.workflow }} - From e5caf1144693a3255e35b2cbff1624625cc389b3 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 26 Jun 2023 10:29:12 +0200 Subject: [PATCH 524/526] Change a colon into dash. --- .../test/java/com/appunite/loudius/PaparazziShowkaseTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt index b6e63ad8e..58adab37a 100644 --- a/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt +++ b/components/src/test/java/com/appunite/loudius/PaparazziShowkaseTests.kt @@ -41,7 +41,7 @@ class ComponentPreview( ) { val content: @Composable () -> Unit = showkaseBrowserComponent.component override fun toString(): String = - showkaseBrowserComponent.group + ":" + showkaseBrowserComponent.componentName + showkaseBrowserComponent.group + "-" + showkaseBrowserComponent.componentName } @RunWith(TestParameterInjector::class) From f079e13fd887623a92786c228a15f642bf5bb41c Mon Sep 17 00:00:00 2001 From: Krzysiudan Date: Mon, 26 Jun 2023 08:39:47 +0000 Subject: [PATCH 525/526] [Paparazzi] Record new snapshots --- ...sts[Default Group-LoudiusDialogAdvancedPreview,1,light].png | 3 +++ ...sts[Default Group-LoudiusDialogAdvancedPreview,2,light].png | 3 +++ ...tests[Default Group-LoudiusDialogSimplePreview,1,light].png | 3 +++ ...tests[Default Group-LoudiusDialogSimplePreview,2,light].png | 3 +++ ..._tests[Default Group-LoudiusErrorDialogPreview,1,light].png | 3 +++ ..._tests[Default Group-LoudiusErrorDialogPreview,2,light].png | 3 +++ ...ult Group-LoudiusErrorScreenCustomTextsPreview,1,light].png | 3 +++ ...ult Group-LoudiusErrorScreenCustomTextsPreview,2,light].png | 3 +++ ...t Group-LoudiusListItemContentAndActionPreview,1,light].png | 3 +++ ...t Group-LoudiusListItemContentAndActionPreview,2,light].png | 3 +++ ...ult Group-LoudiusListItemContentAndIconPreview,1,light].png | 3 +++ ...ult Group-LoudiusListItemContentAndIconPreview,2,light].png | 3 +++ ...efault Group-LoudiusListItemJustContentPreview,1,light].png | 3 +++ ...efault Group-LoudiusListItemJustContentPreview,2,light].png | 3 +++ ...t Group-LoudiusListItemManyWithAllItemsPreview,1,light].png | 3 +++ ...t Group-LoudiusListItemManyWithAllItemsPreview,2,light].png | 3 +++ ...s[Default Group-LoudiusListItemMultiplePreview,1,light].png | 3 +++ ...s[Default Group-LoudiusListItemMultiplePreview,2,light].png | 3 +++ ...Default Group-LoudiusListItemWithHeaderPreview,1,light].png | 3 +++ ...Default Group-LoudiusListItemWithHeaderPreview,2,light].png | 3 +++ ...ult Group-LoudiusOutlinedButtonDisabledPreview,1,light].png | 3 +++ ...ult Group-LoudiusOutlinedButtonDisabledPreview,2,light].png | 3 +++ ...efault Group-LoudiusOutlinedButtonLargePreview,1,light].png | 3 +++ ...efault Group-LoudiusOutlinedButtonLargePreview,2,light].png | 3 +++ ...sts[Default Group-LoudiusOutlinedButtonPreview,1,light].png | 3 +++ ...sts[Default Group-LoudiusOutlinedButtonPreview,2,light].png | 3 +++ ...roup-LoudiusOutlinedButtonWithIconLargePreview,1,light].png | 3 +++ ...roup-LoudiusOutlinedButtonWithIconLargePreview,2,light].png | 3 +++ ...ult Group-LoudiusOutlinedButtonWithIconPreview,1,light].png | 3 +++ ...ult Group-LoudiusOutlinedButtonWithIconPreview,2,light].png | 3 +++ ..._preview_tests[Default Group-LoudiusTextStyles,1,light].png | 3 +++ ..._preview_tests[Default Group-LoudiusTextStyles,2,light].png | 3 +++ ...s_preview_tests[Default Group-LoudiusTopAppBar,1,light].png | 3 +++ ...s_preview_tests[Default Group-LoudiusTopAppBar,2,light].png | 3 +++ ...efault Group-LoudiusTopAppBarWithoutBackButton,1,light].png | 3 +++ ...efault Group-LoudiusTopAppBarWithoutBackButton,2,light].png | 3 +++ ...ts[Default Group-PreviewLoudiusPlaceholderText,1,light].png | 3 +++ ...ts[Default Group-PreviewLoudiusPlaceholderText,2,light].png | 3 +++ ...sts[Default Group_LoudiusDialogAdvancedPreview,1,light].png | 3 +++ ...sts[Default Group_LoudiusDialogAdvancedPreview,2,light].png | 3 +++ ...tests[Default Group_LoudiusDialogSimplePreview,1,light].png | 3 +++ ...tests[Default Group_LoudiusDialogSimplePreview,2,light].png | 3 +++ ..._tests[Default Group_LoudiusErrorDialogPreview,1,light].png | 3 +++ ..._tests[Default Group_LoudiusErrorDialogPreview,2,light].png | 3 +++ ...ult Group_LoudiusErrorScreenCustomTextsPreview,1,light].png | 3 +++ ...ult Group_LoudiusErrorScreenCustomTextsPreview,2,light].png | 3 +++ ...t Group_LoudiusListItemContentAndActionPreview,1,light].png | 3 +++ ...t Group_LoudiusListItemContentAndActionPreview,2,light].png | 3 +++ ...ult Group_LoudiusListItemContentAndIconPreview,1,light].png | 3 +++ ...ult Group_LoudiusListItemContentAndIconPreview,2,light].png | 3 +++ ...efault Group_LoudiusListItemJustContentPreview,1,light].png | 3 +++ ...efault Group_LoudiusListItemJustContentPreview,2,light].png | 3 +++ ...t Group_LoudiusListItemManyWithAllItemsPreview,1,light].png | 3 +++ ...t Group_LoudiusListItemManyWithAllItemsPreview,2,light].png | 3 +++ ...s[Default Group_LoudiusListItemMultiplePreview,1,light].png | 3 +++ ...s[Default Group_LoudiusListItemMultiplePreview,2,light].png | 3 +++ ...Default Group_LoudiusListItemWithHeaderPreview,1,light].png | 3 +++ ...Default Group_LoudiusListItemWithHeaderPreview,2,light].png | 3 +++ ...ult Group_LoudiusOutlinedButtonDisabledPreview,1,light].png | 3 +++ ...ult Group_LoudiusOutlinedButtonDisabledPreview,2,light].png | 3 +++ ...efault Group_LoudiusOutlinedButtonLargePreview,1,light].png | 3 +++ ...efault Group_LoudiusOutlinedButtonLargePreview,2,light].png | 3 +++ ...sts[Default Group_LoudiusOutlinedButtonPreview,1,light].png | 3 +++ ...sts[Default Group_LoudiusOutlinedButtonPreview,2,light].png | 3 +++ ...roup_LoudiusOutlinedButtonWithIconLargePreview,1,light].png | 3 +++ ...roup_LoudiusOutlinedButtonWithIconLargePreview,2,light].png | 3 +++ ...ult Group_LoudiusOutlinedButtonWithIconPreview,1,light].png | 3 +++ ...ult Group_LoudiusOutlinedButtonWithIconPreview,2,light].png | 3 +++ ..._preview_tests[Default Group_LoudiusTextStyles,1,light].png | 3 +++ ..._preview_tests[Default Group_LoudiusTextStyles,2,light].png | 3 +++ ...s_preview_tests[Default Group_LoudiusTopAppBar,1,light].png | 3 +++ ...s_preview_tests[Default Group_LoudiusTopAppBar,2,light].png | 3 +++ ...efault Group_LoudiusTopAppBarWithoutBackButton,1,light].png | 3 +++ ...efault Group_LoudiusTopAppBarWithoutBackButton,2,light].png | 3 +++ ...ts[Default Group_PreviewLoudiusPlaceholderText,1,light].png | 3 +++ ...ts[Default Group_PreviewLoudiusPlaceholderText,2,light].png | 3 +++ 76 files changed, 228 insertions(+) create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png create mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,1,light].png new file mode 100644 index 000000000..0821c6e95 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab +size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,2,light].png new file mode 100644 index 000000000..0821c6e95 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogAdvancedPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab +size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,1,light].png new file mode 100644 index 000000000..ebf67112b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 +size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,2,light].png new file mode 100644 index 000000000..ebf67112b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusDialogSimplePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 +size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,1,light].png new file mode 100644 index 000000000..adb4923f0 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b +size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,2,light].png new file mode 100644 index 000000000..adb4923f0 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorDialogPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b +size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,1,light].png new file mode 100644 index 000000000..b855a64cb --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa17e0bcd4ff0a12ab3416486f3cbebbabeec8764736ec90faf25ea70731ffc6 +size 54890 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,2,light].png new file mode 100644 index 000000000..465c364d1 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusErrorScreenCustomTextsPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7553e9dfe58e79cee2090775a675c6c195a8bfeff90a2310cd524204088fd0b9 +size 57091 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,1,light].png new file mode 100644 index 000000000..1d7fd727d --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6725a5bcaefb69980781727b2a936ca99f2ca47134ee2618b2c72178307409f +size 7925 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,2,light].png new file mode 100644 index 000000000..5a27940fc --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndActionPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb7af9e228761637e5190f3690baa42cdd1ffd014b9ded38c414eccda90c4232 +size 10295 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,1,light].png new file mode 100644 index 000000000..86f7f6425 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1da0576d813e25aeac678373fa5e3fb1fb05cc157dd41c43f9096f254c99da4 +size 5611 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,2,light].png new file mode 100644 index 000000000..2fe0f23a6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemContentAndIconPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e097e1a40221cc89a919f334926e96ef8b355013b0c63d80fc609c0e1a08434a +size 6421 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,1,light].png new file mode 100644 index 000000000..6f27b9758 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5e8df4de7a2da35fce98e3c04645f8900cb36112d3005f695da0393331a9b7c +size 4947 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,2,light].png new file mode 100644 index 000000000..bf696f543 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemJustContentPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff605ac691be201cd27364d7aaed789635f02c0db4d892926831b439e93225c2 +size 5830 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,1,light].png new file mode 100644 index 000000000..3f3a5bbf9 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e049cb3b8b4a2c947e277388781ecaea6ef284652197558bd9d1d0b72b26c7f5 +size 17165 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,2,light].png new file mode 100644 index 000000000..09cbc0cd6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemManyWithAllItemsPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13dfe08fcd0d9136dd04364cad36165254498bdb3d28996700be39552c606654 +size 23148 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,1,light].png new file mode 100644 index 000000000..1f5f6132a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:390c6656e0698ef92c4a998d1aaa5c92892bcb18ba7ca8bb39da89604d526dd8 +size 6382 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,2,light].png new file mode 100644 index 000000000..55225c48a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemMultiplePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1b60a7450f390c1be149b1c7836cfb7ebb8069809166e77f3a1b74e8b37b7f +size 8595 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,1,light].png new file mode 100644 index 000000000..ac9bfef88 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83f2bfb3b6ed7ce3c0caccfcf72d2eb54121006258a1d902c61bff8b24b079d6 +size 6536 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,2,light].png new file mode 100644 index 000000000..d1110b48d --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusListItemWithHeaderPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f39e9acc9b77154c75bd8f00cd7f3124891daeafff136d2f72f4dcec18bcfea2 +size 9085 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,1,light].png new file mode 100644 index 000000000..7cb9bec3c --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db03a758d601ef7bfe30e78a0a6cc68ead55dd0e9222238b4ac73a415ac5ac14 +size 9179 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,2,light].png new file mode 100644 index 000000000..94b65e6f1 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonDisabledPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4afbc366736fb39d7a5d89fa60f40426746c5f6c9be7fcb4de21d0c0e53fd752 +size 12306 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,1,light].png new file mode 100644 index 000000000..4f5294697 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1350b3c689190bcfc58e721068290b46368b4c71ebfe4dc331c0ad047aa9f95 +size 8212 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,2,light].png new file mode 100644 index 000000000..16797a450 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonLargePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:133d80fd13aa8bb2035fbe93828c421778bbdf5167c5cfe91e4420d7fd19bfa9 +size 11036 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,1,light].png new file mode 100644 index 000000000..cf2146f5a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c41969240d89c104b829e9212fa731ed7ab355df9bc744e0ae0305d8ef3022de +size 7808 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,2,light].png new file mode 100644 index 000000000..d3cad85dd --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e08b79c1a40997c849e7b7a7045234066148c346905f0745a09350606ac220a0 +size 10324 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,1,light].png new file mode 100644 index 000000000..b41b79676 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a9cb890d5cd6bb67a587d45ee77ef2814198bade1be89ea5f9f60e1bc1a58f8 +size 8064 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,2,light].png new file mode 100644 index 000000000..70212b474 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconLargePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b99dd141685ab4aef55a9b9b8cc37b86af54e866d89264992eab631a554114fd +size 9676 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,1,light].png new file mode 100644 index 000000000..90b008d2b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d126060fda523c5967a3b032b770ecc140dd1e401dfe23984e8be9842d4fa8b +size 7705 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,2,light].png new file mode 100644 index 000000000..981b75fcf --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusOutlinedButtonWithIconPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dde844a44b4e9d7af34dd7d2fd3d13e2df5dfa858bf83bdd34a460cb207b312 +size 9052 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,1,light].png new file mode 100644 index 000000000..c7ede9de6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:274b96572e05c39d8450807f7ed82f62b5247265b8cb4c96b268ec8b622edda1 +size 21460 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,2,light].png new file mode 100644 index 000000000..8c46f59ef --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTextStyles,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66192be096f30b06730295c0a2535c11dbb146045d6e1285fb39277d11b8e2b5 +size 37493 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,1,light].png new file mode 100644 index 000000000..471736297 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d061170eec87312e8ad42306fd8ab4f1419c4d695997e66eee2a816b14c2a650 +size 6380 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,2,light].png new file mode 100644 index 000000000..8eb541da2 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBar,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e6855268936ee876434f3259fb7805ee4e3ad549a96d8d5d306d49e87b9a8e1 +size 8273 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,1,light].png new file mode 100644 index 000000000..f4dd2a916 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5f3a12a19a5b0f06bac010beff42186c00d70d2fff96d845b6314c9ecec63f1 +size 5989 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,2,light].png new file mode 100644 index 000000000..64486edbd --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-LoudiusTopAppBarWithoutBackButton,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea9407269eeaafb256fc3c05ebfeda3dbc6eaee9b628ca6ef9fa0301065d111d +size 7775 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,1,light].png new file mode 100644 index 000000000..ea3f98807 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33678f192a03020affb7893ff08ff36f585a8bcdc549df78ae8f85121405dc00 +size 14437 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,2,light].png new file mode 100644 index 000000000..d6e0da992 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group-PreviewLoudiusPlaceholderText,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0062b9966dd887d174fa10bd7c6311043ef3e084ad0de5e3a8a436ae9306262 +size 25698 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png new file mode 100644 index 000000000..0821c6e95 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab +size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png new file mode 100644 index 000000000..0821c6e95 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab +size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png new file mode 100644 index 000000000..ebf67112b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 +size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png new file mode 100644 index 000000000..ebf67112b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 +size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png new file mode 100644 index 000000000..adb4923f0 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b +size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png new file mode 100644 index 000000000..adb4923f0 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b +size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png new file mode 100644 index 000000000..b855a64cb --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa17e0bcd4ff0a12ab3416486f3cbebbabeec8764736ec90faf25ea70731ffc6 +size 54890 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png new file mode 100644 index 000000000..465c364d1 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7553e9dfe58e79cee2090775a675c6c195a8bfeff90a2310cd524204088fd0b9 +size 57091 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png new file mode 100644 index 000000000..1d7fd727d --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6725a5bcaefb69980781727b2a936ca99f2ca47134ee2618b2c72178307409f +size 7925 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png new file mode 100644 index 000000000..5a27940fc --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb7af9e228761637e5190f3690baa42cdd1ffd014b9ded38c414eccda90c4232 +size 10295 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png new file mode 100644 index 000000000..86f7f6425 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1da0576d813e25aeac678373fa5e3fb1fb05cc157dd41c43f9096f254c99da4 +size 5611 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png new file mode 100644 index 000000000..2fe0f23a6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e097e1a40221cc89a919f334926e96ef8b355013b0c63d80fc609c0e1a08434a +size 6421 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png new file mode 100644 index 000000000..6f27b9758 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5e8df4de7a2da35fce98e3c04645f8900cb36112d3005f695da0393331a9b7c +size 4947 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png new file mode 100644 index 000000000..bf696f543 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff605ac691be201cd27364d7aaed789635f02c0db4d892926831b439e93225c2 +size 5830 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png new file mode 100644 index 000000000..3f3a5bbf9 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e049cb3b8b4a2c947e277388781ecaea6ef284652197558bd9d1d0b72b26c7f5 +size 17165 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png new file mode 100644 index 000000000..09cbc0cd6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13dfe08fcd0d9136dd04364cad36165254498bdb3d28996700be39552c606654 +size 23148 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png new file mode 100644 index 000000000..1f5f6132a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:390c6656e0698ef92c4a998d1aaa5c92892bcb18ba7ca8bb39da89604d526dd8 +size 6382 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png new file mode 100644 index 000000000..55225c48a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1b60a7450f390c1be149b1c7836cfb7ebb8069809166e77f3a1b74e8b37b7f +size 8595 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png new file mode 100644 index 000000000..ac9bfef88 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83f2bfb3b6ed7ce3c0caccfcf72d2eb54121006258a1d902c61bff8b24b079d6 +size 6536 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png new file mode 100644 index 000000000..d1110b48d --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f39e9acc9b77154c75bd8f00cd7f3124891daeafff136d2f72f4dcec18bcfea2 +size 9085 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png new file mode 100644 index 000000000..7cb9bec3c --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db03a758d601ef7bfe30e78a0a6cc68ead55dd0e9222238b4ac73a415ac5ac14 +size 9179 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png new file mode 100644 index 000000000..94b65e6f1 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4afbc366736fb39d7a5d89fa60f40426746c5f6c9be7fcb4de21d0c0e53fd752 +size 12306 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png new file mode 100644 index 000000000..4f5294697 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1350b3c689190bcfc58e721068290b46368b4c71ebfe4dc331c0ad047aa9f95 +size 8212 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png new file mode 100644 index 000000000..16797a450 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:133d80fd13aa8bb2035fbe93828c421778bbdf5167c5cfe91e4420d7fd19bfa9 +size 11036 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png new file mode 100644 index 000000000..cf2146f5a --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c41969240d89c104b829e9212fa731ed7ab355df9bc744e0ae0305d8ef3022de +size 7808 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png new file mode 100644 index 000000000..d3cad85dd --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e08b79c1a40997c849e7b7a7045234066148c346905f0745a09350606ac220a0 +size 10324 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png new file mode 100644 index 000000000..b41b79676 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a9cb890d5cd6bb67a587d45ee77ef2814198bade1be89ea5f9f60e1bc1a58f8 +size 8064 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png new file mode 100644 index 000000000..70212b474 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b99dd141685ab4aef55a9b9b8cc37b86af54e866d89264992eab631a554114fd +size 9676 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png new file mode 100644 index 000000000..90b008d2b --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d126060fda523c5967a3b032b770ecc140dd1e401dfe23984e8be9842d4fa8b +size 7705 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png new file mode 100644 index 000000000..981b75fcf --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dde844a44b4e9d7af34dd7d2fd3d13e2df5dfa858bf83bdd34a460cb207b312 +size 9052 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png new file mode 100644 index 000000000..c7ede9de6 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:274b96572e05c39d8450807f7ed82f62b5247265b8cb4c96b268ec8b622edda1 +size 21460 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png new file mode 100644 index 000000000..8c46f59ef --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66192be096f30b06730295c0a2535c11dbb146045d6e1285fb39277d11b8e2b5 +size 37493 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png new file mode 100644 index 000000000..471736297 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d061170eec87312e8ad42306fd8ab4f1419c4d695997e66eee2a816b14c2a650 +size 6380 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png new file mode 100644 index 000000000..8eb541da2 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e6855268936ee876434f3259fb7805ee4e3ad549a96d8d5d306d49e87b9a8e1 +size 8273 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png new file mode 100644 index 000000000..f4dd2a916 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5f3a12a19a5b0f06bac010beff42186c00d70d2fff96d845b6314c9ecec63f1 +size 5989 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png new file mode 100644 index 000000000..64486edbd --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea9407269eeaafb256fc3c05ebfeda3dbc6eaee9b628ca6ef9fa0301065d111d +size 7775 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png new file mode 100644 index 000000000..ea3f98807 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33678f192a03020affb7893ff08ff36f585a8bcdc549df78ae8f85121405dc00 +size 14437 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png new file mode 100644 index 000000000..d6e0da992 --- /dev/null +++ b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0062b9966dd887d174fa10bd7c6311043ef3e084ad0de5e3a8a436ae9306262 +size 25698 From 4d08b02208540e2f2180319c5920db71eebf9a29 Mon Sep 17 00:00:00 2001 From: Krzysztof Daniluk Date: Mon, 26 Jun 2023 12:35:07 +0200 Subject: [PATCH 526/526] Remove snapshots with old naming. --- ...sts[Default Group:LoudiusDialogAdvancedPreview,1,light].png | 3 --- ...sts[Default Group:LoudiusDialogAdvancedPreview,2,light].png | 3 --- ...tests[Default Group:LoudiusDialogSimplePreview,1,light].png | 3 --- ...tests[Default Group:LoudiusDialogSimplePreview,2,light].png | 3 --- ..._tests[Default Group:LoudiusErrorDialogPreview,1,light].png | 3 --- ..._tests[Default Group:LoudiusErrorDialogPreview,2,light].png | 3 --- ...ult Group:LoudiusErrorScreenCustomTextsPreview,1,light].png | 3 --- ...ult Group:LoudiusErrorScreenCustomTextsPreview,2,light].png | 3 --- ...t Group:LoudiusListItemContentAndActionPreview,1,light].png | 3 --- ...t Group:LoudiusListItemContentAndActionPreview,2,light].png | 3 --- ...ult Group:LoudiusListItemContentAndIconPreview,1,light].png | 3 --- ...ult Group:LoudiusListItemContentAndIconPreview,2,light].png | 3 --- ...efault Group:LoudiusListItemJustContentPreview,1,light].png | 3 --- ...efault Group:LoudiusListItemJustContentPreview,2,light].png | 3 --- ...t Group:LoudiusListItemManyWithAllItemsPreview,1,light].png | 3 --- ...t Group:LoudiusListItemManyWithAllItemsPreview,2,light].png | 3 --- ...s[Default Group:LoudiusListItemMultiplePreview,1,light].png | 3 --- ...s[Default Group:LoudiusListItemMultiplePreview,2,light].png | 3 --- ...Default Group:LoudiusListItemWithHeaderPreview,1,light].png | 3 --- ...Default Group:LoudiusListItemWithHeaderPreview,2,light].png | 3 --- ...ult Group:LoudiusOutlinedButtonDisabledPreview,1,light].png | 3 --- ...ult Group:LoudiusOutlinedButtonDisabledPreview,2,light].png | 3 --- ...efault Group:LoudiusOutlinedButtonLargePreview,1,light].png | 3 --- ...efault Group:LoudiusOutlinedButtonLargePreview,2,light].png | 3 --- ...sts[Default Group:LoudiusOutlinedButtonPreview,1,light].png | 3 --- ...sts[Default Group:LoudiusOutlinedButtonPreview,2,light].png | 3 --- ...roup:LoudiusOutlinedButtonWithIconLargePreview,1,light].png | 3 --- ...roup:LoudiusOutlinedButtonWithIconLargePreview,2,light].png | 3 --- ...ult Group:LoudiusOutlinedButtonWithIconPreview,1,light].png | 3 --- ...ult Group:LoudiusOutlinedButtonWithIconPreview,2,light].png | 3 --- ..._preview_tests[Default Group:LoudiusTextStyles,1,light].png | 3 --- ..._preview_tests[Default Group:LoudiusTextStyles,2,light].png | 3 --- ...s_preview_tests[Default Group:LoudiusTopAppBar,1,light].png | 3 --- ...s_preview_tests[Default Group:LoudiusTopAppBar,2,light].png | 3 --- ...efault Group:LoudiusTopAppBarWithoutBackButton,1,light].png | 3 --- ...efault Group:LoudiusTopAppBarWithoutBackButton,2,light].png | 3 --- ...ts[Default Group:PreviewLoudiusPlaceholderText,1,light].png | 3 --- ...ts[Default Group:PreviewLoudiusPlaceholderText,2,light].png | 3 --- ...sts[Default Group_LoudiusDialogAdvancedPreview,1,light].png | 3 --- ...sts[Default Group_LoudiusDialogAdvancedPreview,2,light].png | 3 --- ...tests[Default Group_LoudiusDialogSimplePreview,1,light].png | 3 --- ...tests[Default Group_LoudiusDialogSimplePreview,2,light].png | 3 --- ..._tests[Default Group_LoudiusErrorDialogPreview,1,light].png | 3 --- ..._tests[Default Group_LoudiusErrorDialogPreview,2,light].png | 3 --- ...ult Group_LoudiusErrorScreenCustomTextsPreview,1,light].png | 3 --- ...ult Group_LoudiusErrorScreenCustomTextsPreview,2,light].png | 3 --- ...t Group_LoudiusListItemContentAndActionPreview,1,light].png | 3 --- ...t Group_LoudiusListItemContentAndActionPreview,2,light].png | 3 --- ...ult Group_LoudiusListItemContentAndIconPreview,1,light].png | 3 --- ...ult Group_LoudiusListItemContentAndIconPreview,2,light].png | 3 --- ...efault Group_LoudiusListItemJustContentPreview,1,light].png | 3 --- ...efault Group_LoudiusListItemJustContentPreview,2,light].png | 3 --- ...t Group_LoudiusListItemManyWithAllItemsPreview,1,light].png | 3 --- ...t Group_LoudiusListItemManyWithAllItemsPreview,2,light].png | 3 --- ...s[Default Group_LoudiusListItemMultiplePreview,1,light].png | 3 --- ...s[Default Group_LoudiusListItemMultiplePreview,2,light].png | 3 --- ...Default Group_LoudiusListItemWithHeaderPreview,1,light].png | 3 --- ...Default Group_LoudiusListItemWithHeaderPreview,2,light].png | 3 --- ...ult Group_LoudiusOutlinedButtonDisabledPreview,1,light].png | 3 --- ...ult Group_LoudiusOutlinedButtonDisabledPreview,2,light].png | 3 --- ...efault Group_LoudiusOutlinedButtonLargePreview,1,light].png | 3 --- ...efault Group_LoudiusOutlinedButtonLargePreview,2,light].png | 3 --- ...sts[Default Group_LoudiusOutlinedButtonPreview,1,light].png | 3 --- ...sts[Default Group_LoudiusOutlinedButtonPreview,2,light].png | 3 --- ...roup_LoudiusOutlinedButtonWithIconLargePreview,1,light].png | 3 --- ...roup_LoudiusOutlinedButtonWithIconLargePreview,2,light].png | 3 --- ...ult Group_LoudiusOutlinedButtonWithIconPreview,1,light].png | 3 --- ...ult Group_LoudiusOutlinedButtonWithIconPreview,2,light].png | 3 --- ..._preview_tests[Default Group_LoudiusTextStyles,1,light].png | 3 --- ..._preview_tests[Default Group_LoudiusTextStyles,2,light].png | 3 --- ...s_preview_tests[Default Group_LoudiusTopAppBar,1,light].png | 3 --- ...s_preview_tests[Default Group_LoudiusTopAppBar,2,light].png | 3 --- ...efault Group_LoudiusTopAppBarWithoutBackButton,1,light].png | 3 --- ...efault Group_LoudiusTopAppBarWithoutBackButton,2,light].png | 3 --- ...ts[Default Group_PreviewLoudiusPlaceholderText,1,light].png | 3 --- ...ts[Default Group_PreviewLoudiusPlaceholderText,2,light].png | 3 --- 76 files changed, 228 deletions(-) delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png delete mode 100644 components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png deleted file mode 100644 index 0821c6e95..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab -size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png deleted file mode 100644 index 0821c6e95..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogAdvancedPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab -size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png deleted file mode 100644 index ebf67112b..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 -size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png deleted file mode 100644 index ebf67112b..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusDialogSimplePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 -size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png deleted file mode 100644 index adb4923f0..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b -size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png deleted file mode 100644 index adb4923f0..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorDialogPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b -size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png deleted file mode 100644 index b855a64cb..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa17e0bcd4ff0a12ab3416486f3cbebbabeec8764736ec90faf25ea70731ffc6 -size 54890 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png deleted file mode 100644 index 465c364d1..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusErrorScreenCustomTextsPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7553e9dfe58e79cee2090775a675c6c195a8bfeff90a2310cd524204088fd0b9 -size 57091 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png deleted file mode 100644 index 1d7fd727d..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d6725a5bcaefb69980781727b2a936ca99f2ca47134ee2618b2c72178307409f -size 7925 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png deleted file mode 100644 index 5a27940fc..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndActionPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb7af9e228761637e5190f3690baa42cdd1ffd014b9ded38c414eccda90c4232 -size 10295 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png deleted file mode 100644 index 86f7f6425..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1da0576d813e25aeac678373fa5e3fb1fb05cc157dd41c43f9096f254c99da4 -size 5611 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png deleted file mode 100644 index 2fe0f23a6..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemContentAndIconPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e097e1a40221cc89a919f334926e96ef8b355013b0c63d80fc609c0e1a08434a -size 6421 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png deleted file mode 100644 index 6f27b9758..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d5e8df4de7a2da35fce98e3c04645f8900cb36112d3005f695da0393331a9b7c -size 4947 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png deleted file mode 100644 index bf696f543..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemJustContentPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff605ac691be201cd27364d7aaed789635f02c0db4d892926831b439e93225c2 -size 5830 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png deleted file mode 100644 index 3f3a5bbf9..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e049cb3b8b4a2c947e277388781ecaea6ef284652197558bd9d1d0b72b26c7f5 -size 17165 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png deleted file mode 100644 index 09cbc0cd6..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemManyWithAllItemsPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13dfe08fcd0d9136dd04364cad36165254498bdb3d28996700be39552c606654 -size 23148 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png deleted file mode 100644 index 1f5f6132a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:390c6656e0698ef92c4a998d1aaa5c92892bcb18ba7ca8bb39da89604d526dd8 -size 6382 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png deleted file mode 100644 index 55225c48a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemMultiplePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b1b60a7450f390c1be149b1c7836cfb7ebb8069809166e77f3a1b74e8b37b7f -size 8595 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png deleted file mode 100644 index ac9bfef88..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:83f2bfb3b6ed7ce3c0caccfcf72d2eb54121006258a1d902c61bff8b24b079d6 -size 6536 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png deleted file mode 100644 index d1110b48d..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusListItemWithHeaderPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f39e9acc9b77154c75bd8f00cd7f3124891daeafff136d2f72f4dcec18bcfea2 -size 9085 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png deleted file mode 100644 index 7cb9bec3c..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:db03a758d601ef7bfe30e78a0a6cc68ead55dd0e9222238b4ac73a415ac5ac14 -size 9179 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png deleted file mode 100644 index 94b65e6f1..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonDisabledPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4afbc366736fb39d7a5d89fa60f40426746c5f6c9be7fcb4de21d0c0e53fd752 -size 12306 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png deleted file mode 100644 index 4f5294697..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1350b3c689190bcfc58e721068290b46368b4c71ebfe4dc331c0ad047aa9f95 -size 8212 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png deleted file mode 100644 index 16797a450..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonLargePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:133d80fd13aa8bb2035fbe93828c421778bbdf5167c5cfe91e4420d7fd19bfa9 -size 11036 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png deleted file mode 100644 index cf2146f5a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c41969240d89c104b829e9212fa731ed7ab355df9bc744e0ae0305d8ef3022de -size 7808 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png deleted file mode 100644 index d3cad85dd..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e08b79c1a40997c849e7b7a7045234066148c346905f0745a09350606ac220a0 -size 10324 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png deleted file mode 100644 index b41b79676..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9a9cb890d5cd6bb67a587d45ee77ef2814198bade1be89ea5f9f60e1bc1a58f8 -size 8064 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png deleted file mode 100644 index 70212b474..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconLargePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b99dd141685ab4aef55a9b9b8cc37b86af54e866d89264992eab631a554114fd -size 9676 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png deleted file mode 100644 index 90b008d2b..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d126060fda523c5967a3b032b770ecc140dd1e401dfe23984e8be9842d4fa8b -size 7705 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png deleted file mode 100644 index 981b75fcf..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusOutlinedButtonWithIconPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9dde844a44b4e9d7af34dd7d2fd3d13e2df5dfa858bf83bdd34a460cb207b312 -size 9052 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png deleted file mode 100644 index c7ede9de6..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:274b96572e05c39d8450807f7ed82f62b5247265b8cb4c96b268ec8b622edda1 -size 21460 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png deleted file mode 100644 index 8c46f59ef..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTextStyles,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:66192be096f30b06730295c0a2535c11dbb146045d6e1285fb39277d11b8e2b5 -size 37493 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png deleted file mode 100644 index 471736297..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d061170eec87312e8ad42306fd8ab4f1419c4d695997e66eee2a816b14c2a650 -size 6380 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png deleted file mode 100644 index 8eb541da2..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBar,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e6855268936ee876434f3259fb7805ee4e3ad549a96d8d5d306d49e87b9a8e1 -size 8273 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png deleted file mode 100644 index f4dd2a916..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a5f3a12a19a5b0f06bac010beff42186c00d70d2fff96d845b6314c9ecec63f1 -size 5989 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png deleted file mode 100644 index 64486edbd..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:LoudiusTopAppBarWithoutBackButton,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ea9407269eeaafb256fc3c05ebfeda3dbc6eaee9b628ca6ef9fa0301065d111d -size 7775 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png deleted file mode 100644 index ea3f98807..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33678f192a03020affb7893ff08ff36f585a8bcdc549df78ae8f85121405dc00 -size 14437 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png deleted file mode 100644 index d6e0da992..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group:PreviewLoudiusPlaceholderText,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0062b9966dd887d174fa10bd7c6311043ef3e084ad0de5e3a8a436ae9306262 -size 25698 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png deleted file mode 100644 index 0821c6e95..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab -size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png deleted file mode 100644 index 0821c6e95..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogAdvancedPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0f4baffc333067835c7351e562f993a50ec8dd11b2d53d6a69ddee173def9ab -size 42942 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png deleted file mode 100644 index ebf67112b..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 -size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png deleted file mode 100644 index ebf67112b..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusDialogSimplePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e0ba612fa5970c61074647bb0067172bf22fd9c8441681afbb045cf3b1bd3b9 -size 10006 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png deleted file mode 100644 index adb4923f0..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b -size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png deleted file mode 100644 index adb4923f0..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorDialogPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:319c074ae5d66628418fb3f73441dec42fc9d05b9133b6cd2c4d69f61e484b7b -size 13840 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png deleted file mode 100644 index b855a64cb..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa17e0bcd4ff0a12ab3416486f3cbebbabeec8764736ec90faf25ea70731ffc6 -size 54890 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png deleted file mode 100644 index 465c364d1..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusErrorScreenCustomTextsPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7553e9dfe58e79cee2090775a675c6c195a8bfeff90a2310cd524204088fd0b9 -size 57091 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png deleted file mode 100644 index 1d7fd727d..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d6725a5bcaefb69980781727b2a936ca99f2ca47134ee2618b2c72178307409f -size 7925 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png deleted file mode 100644 index 5a27940fc..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndActionPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fb7af9e228761637e5190f3690baa42cdd1ffd014b9ded38c414eccda90c4232 -size 10295 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png deleted file mode 100644 index 86f7f6425..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1da0576d813e25aeac678373fa5e3fb1fb05cc157dd41c43f9096f254c99da4 -size 5611 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png deleted file mode 100644 index 2fe0f23a6..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemContentAndIconPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e097e1a40221cc89a919f334926e96ef8b355013b0c63d80fc609c0e1a08434a -size 6421 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png deleted file mode 100644 index 6f27b9758..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d5e8df4de7a2da35fce98e3c04645f8900cb36112d3005f695da0393331a9b7c -size 4947 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png deleted file mode 100644 index bf696f543..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemJustContentPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff605ac691be201cd27364d7aaed789635f02c0db4d892926831b439e93225c2 -size 5830 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png deleted file mode 100644 index 3f3a5bbf9..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e049cb3b8b4a2c947e277388781ecaea6ef284652197558bd9d1d0b72b26c7f5 -size 17165 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png deleted file mode 100644 index 09cbc0cd6..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemManyWithAllItemsPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13dfe08fcd0d9136dd04364cad36165254498bdb3d28996700be39552c606654 -size 23148 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png deleted file mode 100644 index 1f5f6132a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:390c6656e0698ef92c4a998d1aaa5c92892bcb18ba7ca8bb39da89604d526dd8 -size 6382 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png deleted file mode 100644 index 55225c48a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemMultiplePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b1b60a7450f390c1be149b1c7836cfb7ebb8069809166e77f3a1b74e8b37b7f -size 8595 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png deleted file mode 100644 index ac9bfef88..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:83f2bfb3b6ed7ce3c0caccfcf72d2eb54121006258a1d902c61bff8b24b079d6 -size 6536 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png deleted file mode 100644 index d1110b48d..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusListItemWithHeaderPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f39e9acc9b77154c75bd8f00cd7f3124891daeafff136d2f72f4dcec18bcfea2 -size 9085 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png deleted file mode 100644 index 7cb9bec3c..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:db03a758d601ef7bfe30e78a0a6cc68ead55dd0e9222238b4ac73a415ac5ac14 -size 9179 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png deleted file mode 100644 index 94b65e6f1..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonDisabledPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4afbc366736fb39d7a5d89fa60f40426746c5f6c9be7fcb4de21d0c0e53fd752 -size 12306 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png deleted file mode 100644 index 4f5294697..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1350b3c689190bcfc58e721068290b46368b4c71ebfe4dc331c0ad047aa9f95 -size 8212 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png deleted file mode 100644 index 16797a450..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonLargePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:133d80fd13aa8bb2035fbe93828c421778bbdf5167c5cfe91e4420d7fd19bfa9 -size 11036 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png deleted file mode 100644 index cf2146f5a..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c41969240d89c104b829e9212fa731ed7ab355df9bc744e0ae0305d8ef3022de -size 7808 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png deleted file mode 100644 index d3cad85dd..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e08b79c1a40997c849e7b7a7045234066148c346905f0745a09350606ac220a0 -size 10324 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png deleted file mode 100644 index b41b79676..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9a9cb890d5cd6bb67a587d45ee77ef2814198bade1be89ea5f9f60e1bc1a58f8 -size 8064 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png deleted file mode 100644 index 70212b474..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconLargePreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b99dd141685ab4aef55a9b9b8cc37b86af54e866d89264992eab631a554114fd -size 9676 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png deleted file mode 100644 index 90b008d2b..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d126060fda523c5967a3b032b770ecc140dd1e401dfe23984e8be9842d4fa8b -size 7705 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png deleted file mode 100644 index 981b75fcf..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusOutlinedButtonWithIconPreview,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9dde844a44b4e9d7af34dd7d2fd3d13e2df5dfa858bf83bdd34a460cb207b312 -size 9052 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png deleted file mode 100644 index c7ede9de6..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:274b96572e05c39d8450807f7ed82f62b5247265b8cb4c96b268ec8b622edda1 -size 21460 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png deleted file mode 100644 index 8c46f59ef..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTextStyles,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:66192be096f30b06730295c0a2535c11dbb146045d6e1285fb39277d11b8e2b5 -size 37493 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png deleted file mode 100644 index 471736297..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d061170eec87312e8ad42306fd8ab4f1419c4d695997e66eee2a816b14c2a650 -size 6380 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png deleted file mode 100644 index 8eb541da2..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBar,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e6855268936ee876434f3259fb7805ee4e3ad549a96d8d5d306d49e87b9a8e1 -size 8273 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png deleted file mode 100644 index f4dd2a916..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a5f3a12a19a5b0f06bac010beff42186c00d70d2fff96d845b6314c9ecec63f1 -size 5989 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png deleted file mode 100644 index 64486edbd..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_LoudiusTopAppBarWithoutBackButton,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ea9407269eeaafb256fc3c05ebfeda3dbc6eaee9b628ca6ef9fa0301065d111d -size 7775 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png deleted file mode 100644 index ea3f98807..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,1,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:33678f192a03020affb7893ff08ff36f585a8bcdc549df78ae8f85121405dc00 -size 14437 diff --git a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png b/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png deleted file mode 100644 index d6e0da992..000000000 --- a/components/src/test/snapshots/images/com.appunite.loudius_PaparazziShowkaseTests_preview_tests[Default Group_PreviewLoudiusPlaceholderText,2,light].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0062b9966dd887d174fa10bd7c6311043ef3e084ad0de5e3a8a436ae9306262 -size 25698