diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a60e97bcf..0088792bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,7 @@ android { compileSdk = libs.versions.android.sdk.target.get().toInt() composeOptions.kotlinCompilerExtensionVersion = libs.versions.android.compose.compiler.get() flavorDimensions += Dimensions.VERSION - lintOptions.disable += "Instantiatable" + lint.disable += "Instantiatable" namespace = namespaceFor("app") buildFeatures { @@ -71,6 +71,7 @@ android { dependencies { "androidTestDemoImplementation"(project(":core:sample-test")) + "androidTestDemoImplementation"(project(":feature:gallery-test")) "androidTestDemoImplementation"(project(":platform:ui")) "androidTestDemoImplementation"(libs.android.activity.ktx) "androidTestDemoImplementation"(libs.android.compose.ui.test.junit) @@ -103,6 +104,6 @@ dependencies { implementation(libs.android.constraintlayout) implementation(libs.android.fragment.ktx) implementation(libs.android.material) - implementation(libs.kotlin.reflect) - implementation(libs.time4j) + + releaseImplementation(libs.kotlin.reflect) } diff --git a/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/FeedTests.kt b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/FeedTests.kt index 302e0bc72..d8726e93e 100644 --- a/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/FeedTests.kt +++ b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/FeedTests.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeDown +import com.jeanbarrossilva.orca.app.demo.test.onSearchAction import com.jeanbarrossilva.orca.app.demo.test.performScrollToPostPreviewWithGalleryPreview import com.jeanbarrossilva.orca.app.demo.test.performScrollToPostPreviewWithLinkCard import com.jeanbarrossilva.orca.core.feed.profile.post.content.highlight.Highlight @@ -30,7 +31,11 @@ import com.jeanbarrossilva.orca.ext.intents.intendBrowsingTo import com.jeanbarrossilva.orca.ext.intents.intendStartingOf import com.jeanbarrossilva.orca.feature.composer.ComposerActivity import com.jeanbarrossilva.orca.feature.feed.FEED_FLOATING_ACTION_BUTTON_TAG +import com.jeanbarrossilva.orca.feature.feed.FeedFragment import com.jeanbarrossilva.orca.feature.gallery.GalleryActivity +import com.jeanbarrossilva.orca.feature.gallery.test.ui.onCloseActionButton +import com.jeanbarrossilva.orca.feature.search.SearchActivity +import com.jeanbarrossilva.orca.platform.ui.test.assertIsAtFragment import com.jeanbarrossilva.orca.platform.ui.test.component.timeline.onRefreshIndicator import com.jeanbarrossilva.orca.platform.ui.test.component.timeline.onTimeline import com.jeanbarrossilva.orca.platform.ui.test.component.timeline.post.figure.gallery.thumbnail.onThumbnails @@ -42,6 +47,11 @@ import org.junit.Test internal class FeedTests { @get:Rule val composeRule = createAndroidComposeRule() + @Test + fun navigatesToSearch() { + intendStartingOf { composeRule.onSearchAction().performClick() } + } + @Test fun refreshes() { composeRule.onTimeline().performTouchInput(TouchInjectionScope::swipeDown) @@ -67,6 +77,17 @@ internal class FeedTests { } } + @Test + fun navigatesToGalleryAndGoesBackToFeedWhenClosingIt() { + with(composeRule) { + onTimeline().performScrollToPostPreviewWithGalleryPreview { + onThumbnails().onFirst().performClick() + onCloseActionButton().performClick() + assertIsAtFragment(composeRule.activity, FeedFragment.ROUTE) + } + } + } + @Test fun navigatesToComposerOnFabClick() { intendStartingOf { diff --git a/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/ProfileDetailsTests.kt b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/ProfileDetailsTests.kt index 5779f1df7..bedc6292e 100644 --- a/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/ProfileDetailsTests.kt +++ b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/ProfileDetailsTests.kt @@ -24,10 +24,10 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import com.jeanbarrossilva.orca.app.R import com.jeanbarrossilva.orca.app.demo.test.performScrollToPostPreviewWithLinkCard import com.jeanbarrossilva.orca.app.demo.test.performStartClick -import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.highlight.Highlight +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.content.highlight.sample -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.samples +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples import com.jeanbarrossilva.orca.ext.intents.intendBrowsingTo import com.jeanbarrossilva.orca.feature.postdetails.PostDetailsFragment import com.jeanbarrossilva.orca.platform.ui.test.assertIsAtFragment @@ -51,6 +51,9 @@ internal class ProfileDetailsTests { fun navigatesToPostDetailsOnPostPreviewClick() { onView(withId(R.id.profile_details)).perform(click()) composeRule.onPostPreviews().onFirst().performStartClick() - assertIsAtFragment(composeRule.activity, PostDetailsFragment.getRoute(Post.samples.first().id)) + assertIsAtFragment( + composeRule.activity, + PostDetailsFragment.getRoute(Posts.withSamples.first().id) + ) } } diff --git a/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/SemanticsNodeInteractionsProviderExtensionsTests.kt b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/SemanticsNodeInteractionsProviderExtensionsTests.kt new file mode 100644 index 000000000..dcd4da4bf --- /dev/null +++ b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/SemanticsNodeInteractionsProviderExtensionsTests.kt @@ -0,0 +1,31 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.app.demo + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import com.jeanbarrossilva.orca.app.demo.test.onSearchAction +import org.junit.Rule +import org.junit.Test + +internal class SemanticsNodeInteractionsProviderExtensionsTests { + @get:Rule val composeRule = createAndroidComposeRule() + + @Test + fun findsSearchAction() { + composeRule.onSearchAction().assertIsDisplayed() + } +} diff --git a/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/test/SemanticsNodeInteractionsProvider.extensions.kt b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/test/SemanticsNodeInteractionsProvider.extensions.kt new file mode 100644 index 000000000..3468a0e05 --- /dev/null +++ b/app/src/androidTestDemo/java/com/jeanbarrossilva/orca/app/demo/test/SemanticsNodeInteractionsProvider.extensions.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.app.demo.test + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.onNodeWithTag +import com.jeanbarrossilva.orca.feature.feed.FEED_SEARCH_ACTION_TAG + +/** [SemanticsNodeInteraction] of the feed's search action. */ +internal fun SemanticsNodeInteractionsProvider.onSearchAction(): SemanticsNodeInteraction { + return onNodeWithTag(FEED_SEARCH_ACTION_TAG) +} diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/OrcaActivity.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/OrcaActivity.kt index 47ee659a0..80654e613 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/OrcaActivity.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/OrcaActivity.kt @@ -92,8 +92,8 @@ open class OrcaActivity : NavigationActivity(), OnBottomAreaAvailabilityChangeLi register(MainPostDetailsModule(this@OrcaActivity)) register(MainProfileDetailsModule(this@OrcaActivity)) register(MainSearchModule(navigator)) - register(MainSettingsModule(navigator)) - register(MainTermMutingModule(navigator)) + register(MainSettingsModule(this@OrcaActivity)) + register(MainTermMutingModule) } } diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/feed/NavigatorFeedBoundary.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/feed/NavigatorFeedBoundary.kt index 5f56fe379..5f45e0333 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/feed/NavigatorFeedBoundary.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/feed/NavigatorFeedBoundary.kt @@ -23,7 +23,7 @@ import com.jeanbarrossilva.orca.feature.composer.ComposerActivity import com.jeanbarrossilva.orca.feature.feed.FeedBoundary import com.jeanbarrossilva.orca.feature.gallery.GalleryActivity import com.jeanbarrossilva.orca.feature.postdetails.PostDetailsFragment -import com.jeanbarrossilva.orca.feature.search.SearchFragment +import com.jeanbarrossilva.orca.feature.search.SearchActivity import com.jeanbarrossilva.orca.platform.ui.core.browseTo import com.jeanbarrossilva.orca.platform.ui.core.navigation.Navigator import java.net.URL @@ -33,7 +33,7 @@ internal class NavigatorFeedBoundary( private val navigator: Navigator ) : FeedBoundary { override fun navigateToSearch() { - SearchFragment.navigate(navigator) + SearchActivity.start(context) } override fun navigateTo(url: URL) { diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/gallery/NavigatorGalleryBoundary.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/gallery/NavigatorGalleryBoundary.kt index b3895c6dd..7c1bd815b 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/gallery/NavigatorGalleryBoundary.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/gallery/NavigatorGalleryBoundary.kt @@ -23,8 +23,4 @@ internal class NavigatorGalleryBoundary(private val navigator: Navigator) : Gall override fun navigateToPostDetails(id: String) { PostDetailsFragment.navigate(navigator, id) } - - override fun pop() { - navigator.pop() - } } diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/search/NavigatorSearchBoundary.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/search/NavigatorSearchBoundary.kt index 3fe1b86e0..eb6a01a79 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/search/NavigatorSearchBoundary.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/search/NavigatorSearchBoundary.kt @@ -23,8 +23,4 @@ internal class NavigatorSearchBoundary(private val navigator: Navigator) : Searc override fun navigateToProfileDetails(id: String) { ProfileDetailsFragment.navigate(navigator, id) } - - override fun pop() { - navigator.pop() - } } diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/NavigatorSettingsBoundary.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/ContextualSettingsBoundary.kt similarity index 80% rename from app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/NavigatorSettingsBoundary.kt rename to app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/ContextualSettingsBoundary.kt index 7cd038b15..4b96e08c7 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/NavigatorSettingsBoundary.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/ContextualSettingsBoundary.kt @@ -15,12 +15,12 @@ package com.jeanbarrossilva.orca.app.module.feature.settings +import android.content.Context import com.jeanbarrossilva.orca.feature.settings.SettingsBoundary -import com.jeanbarrossilva.orca.feature.settings.termmuting.TermMutingFragment -import com.jeanbarrossilva.orca.platform.ui.core.navigation.Navigator +import com.jeanbarrossilva.orca.feature.settings.termmuting.TermMutingActivity -internal class NavigatorSettingsBoundary(private val navigator: Navigator) : SettingsBoundary { +internal class ContextualSettingsBoundary(private val context: Context) : SettingsBoundary { override fun navigateToTermMuting() { - TermMutingFragment.navigate(navigator) + TermMutingActivity.start(context) } } diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/MainSettingsModule.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/MainSettingsModule.kt index 2afa57de4..d0d6b8e89 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/MainSettingsModule.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/MainSettingsModule.kt @@ -15,14 +15,14 @@ package com.jeanbarrossilva.orca.app.module.feature.settings +import android.content.Context import com.jeanbarrossilva.orca.core.module.CoreModule import com.jeanbarrossilva.orca.core.module.termMuter import com.jeanbarrossilva.orca.feature.settings.SettingsModule -import com.jeanbarrossilva.orca.platform.ui.core.navigation.Navigator import com.jeanbarrossilva.orca.std.injector.Injector -internal class MainSettingsModule(private val navigator: Navigator) : +internal class MainSettingsModule(private val context: Context) : SettingsModule( { Injector.from().termMuter() }, - { NavigatorSettingsBoundary(navigator) } + { ContextualSettingsBoundary(context) } ) diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/termmuting/MainTermMutingModule.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/termmuting/MainTermMutingModule.kt index 9d468b7db..c1a1a6902 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/termmuting/MainTermMutingModule.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/termmuting/MainTermMutingModule.kt @@ -18,11 +18,7 @@ package com.jeanbarrossilva.orca.app.module.feature.settings.termmuting import com.jeanbarrossilva.orca.core.module.CoreModule import com.jeanbarrossilva.orca.core.module.termMuter import com.jeanbarrossilva.orca.feature.settings.termmuting.TermMutingModule -import com.jeanbarrossilva.orca.platform.ui.core.navigation.Navigator import com.jeanbarrossilva.orca.std.injector.Injector -internal class MainTermMutingModule(private val navigator: Navigator) : - TermMutingModule( - { Injector.from().termMuter() }, - { NavigatorTermMutingBoundary(navigator) } - ) +internal object MainTermMutingModule : + TermMutingModule({ Injector.from().termMuter() }) diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/navigation/BottomNavigation.kt b/app/src/main/java/com/jeanbarrossilva/orca/app/navigation/BottomNavigation.kt index b605facc0..6b48d17fb 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/navigation/BottomNavigation.kt +++ b/app/src/main/java/com/jeanbarrossilva/orca/app/navigation/BottomNavigation.kt @@ -33,8 +33,8 @@ internal enum class BottomNavigation { override val id = R.id.feed override suspend fun getDestination(): Navigator.Navigation.Destination<*> { - return authenticationLock.requestUnlock { - Navigator.Navigation.Destination("feed") { FeedFragment(it.id) } + return authenticationLock.scheduleUnlock { + Navigator.Navigation.Destination(FeedFragment.ROUTE) { FeedFragment(it.id) } } } }, @@ -42,7 +42,7 @@ internal enum class BottomNavigation { override val id = R.id.profile_details override suspend fun getDestination(): Navigator.Navigation.Destination<*> { - return authenticationLock.requestUnlock { + return authenticationLock.scheduleUnlock { Navigator.Navigation.Destination(ProfileDetailsFragment.createRoute(it.id)) { ProfileDetailsFragment(BackwardsNavigationState.Unavailable, it.id) } diff --git a/core-test/src/main/java/com/jeanbarrossilva/orca/core/test/TestAuthenticationLock.kt b/core-test/src/main/java/com/jeanbarrossilva/orca/core/test/TestAuthenticationLock.kt index 61e3f186d..bd2932825 100644 --- a/core-test/src/main/java/com/jeanbarrossilva/orca/core/test/TestAuthenticationLock.kt +++ b/core-test/src/main/java/com/jeanbarrossilva/orca/core/test/TestAuthenticationLock.kt @@ -22,7 +22,7 @@ import com.jeanbarrossilva.orca.core.auth.actor.Actor * [AuthenticationLock] with test-specific default structures. * * @param authenticator [TestAuthenticator] through which the [Actor] will be authenticated if it - * isn't and [requestUnlock][AuthenticationLock.requestUnlock] is called. + * isn't and [requestUnlock][AuthenticationLock.scheduleUnlock] is called. * @param actorProvider [TestActorProvider] whose provided [Actor] will be ensured to be either * [unauthenticated][Actor.Unauthenticated] or [authenticated][Actor.Authenticated]. */ diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f5d09eb49..e0b2d6458 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { testImplementation(project(":core:sample")) testImplementation(project(":core:sample-test")) testImplementation(project(":core-test")) - testImplementation(libs.assertk) + testImplementation(project(":ext:testing")) testImplementation(libs.kotlin.coroutines.test) testImplementation(libs.kotlin.test) testImplementation(libs.turbine) diff --git a/core/mastodon/build.gradle.kts b/core/mastodon/build.gradle.kts index 1faa1b5ac..fbf1ea375 100644 --- a/core/mastodon/build.gradle.kts +++ b/core/mastodon/build.gradle.kts @@ -43,6 +43,7 @@ android { dependencies { androidTestImplementation(project(":platform:autos-test")) androidTestImplementation(project(":platform:intents")) + androidTestImplementation(project(":platform:testing")) androidTestImplementation(project(":std:injector-test")) androidTestImplementation(libs.android.compose.ui.test.junit) androidTestImplementation(libs.android.test.core) @@ -54,6 +55,7 @@ dependencies { ksp(project(":std:injector-processor")) implementation(project(":core:sample")) + implementation(project(":ext:coroutines")) implementation(project(":platform:autos")) implementation(project(":platform:cache")) implementation(project(":platform:ui")) diff --git a/core/mastodon/src/androidTest/java/com/jeanbarrossilva/orca/core/mastodon/auth/authorization/MastodonAuthorizationActivityTests.kt b/core/mastodon/src/androidTest/java/com/jeanbarrossilva/orca/core/mastodon/auth/authorization/MastodonAuthorizationActivityTests.kt index 8df9d5744..b764ed594 100644 --- a/core/mastodon/src/androidTest/java/com/jeanbarrossilva/orca/core/mastodon/auth/authorization/MastodonAuthorizationActivityTests.kt +++ b/core/mastodon/src/androidTest/java/com/jeanbarrossilva/orca/core/mastodon/auth/authorization/MastodonAuthorizationActivityTests.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performTextInput -import androidx.test.platform.app.InstrumentationRegistry import com.jeanbarrossilva.orca.core.instance.Instance import com.jeanbarrossilva.orca.core.instance.InstanceProvider import com.jeanbarrossilva.orca.core.instance.domain.Domain @@ -33,15 +32,14 @@ import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.SampleTerm import com.jeanbarrossilva.orca.core.sample.instance.domain.sample import com.jeanbarrossilva.orca.ext.intents.intendBrowsingTo import com.jeanbarrossilva.orca.platform.autos.test.kit.input.text.onTextFieldErrors +import com.jeanbarrossilva.orca.platform.testing.asString +import com.jeanbarrossilva.orca.platform.testing.context import com.jeanbarrossilva.orca.platform.ui.core.sample import com.jeanbarrossilva.orca.std.injector.test.InjectorTestRule import org.junit.Rule import org.junit.Test internal class MastodonAuthorizationActivityTests { - private val context - get() = InstrumentationRegistry.getInstrumentation().context - @get:Rule val injectorRule = InjectorTestRule { register( @@ -58,43 +56,37 @@ internal class MastodonAuthorizationActivityTests { @Test fun showsErrorWhenSigningInWithBlankDomain() { composeRule - .onNodeWithText(context.getString(R.string.core_http_authorization_domain)) + .onNodeWithText(R.string.core_http_authorization_domain.asString()) .apply { performTextInput(" ") } .performImeAction() composeRule .onTextFieldErrors() .assertTextEquals( - context.getString( - com.jeanbarrossilva.orca.platform.autos.R.string - .platform_ui_text_field_consecutive_error_message, - context.getString(R.string.core_http_authorization_empty_domain) - ) + com.jeanbarrossilva.orca.platform.autos.R.string + .platform_ui_text_field_consecutive_error_message + .asString(R.string.core_http_authorization_empty_domain.asString()) ) } @Test fun showsErrorWhenSigningInWithInvalidDomain() { composeRule - .onNodeWithText(context.getString(R.string.core_http_authorization_domain)) + .onNodeWithText(R.string.core_http_authorization_domain.asString()) .apply { performTextInput("1️⃣🏛️🖼️") } .performImeAction() composeRule .onTextFieldErrors() .assertTextEquals( - context.getString( - com.jeanbarrossilva.orca.platform.autos.R.string - .platform_ui_text_field_consecutive_error_message, - context.getString(R.string.core_http_authorization_invalid_domain) - ) + com.jeanbarrossilva.orca.platform.autos.R.string + .platform_ui_text_field_consecutive_error_message + .asString(R.string.core_http_authorization_invalid_domain.asString()) ) } @Test fun browsesToHelpArticle() { intendBrowsingTo("${MastodonAuthorizationActivity.helpUri}") { - composeRule - .onNodeWithText(context.getString(R.string.core_http_authorization_help)) - .performClick() + composeRule.onNodeWithText(R.string.core_http_authorization_help.asString()).performClick() } } @@ -102,7 +94,7 @@ internal class MastodonAuthorizationActivityTests { fun browsesToInstanceWhenSigningInWithValidDomain() { intendBrowsingTo("${MastodonAuthorizationViewModel.createURL(context, Domain.sample)}") { composeRule - .onNodeWithText(context.getString(R.string.core_http_authorization_domain)) + .onNodeWithText(R.string.core_http_authorization_domain.asString()) .apply { performTextInput("${Domain.sample}") } .performImeAction() } diff --git a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClient.kt b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClient.kt index 6378155dc..b8f27e366 100644 --- a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClient.kt +++ b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClient.kt @@ -33,6 +33,7 @@ import io.ktor.client.plugins.observer.ResponseObserver import io.ktor.client.request.HttpRequest import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.bearerAuth +import io.ktor.client.request.delete import io.ktor.client.request.forms.FormDataContent import io.ktor.client.request.forms.submitForm import io.ktor.client.request.forms.submitFormWithBinaryData @@ -97,12 +98,29 @@ fun > CoreHttpClient( } } +/** + * Performs a DELETE [HttpRequest] to the [route] that requires an + * [authenticated][Actor.Authenticated] [Actor]. + * + * @param route URL [String] to which the [HttpRequest] will be sent + * @param build Additional configuration for the [HttpRequest] to be performed. + */ +suspend inline fun HttpClient.authenticateAndDelete( + route: String, + crossinline build: HttpRequestBuilder.() -> Unit = {} +): HttpResponse { + return delete(route) { + authenticate() + build.invoke(this) + } +} + /** * Performs a GET [HttpRequest] to the [route] that requires an [authenticated][Actor.Authenticated] * [Actor]. * * @param route URL [String] to which the [HttpRequest] will be sent. - * @param build Additional configuration for the [HttpResponse] to be performed. + * @param build Additional configuration for the [HttpRequest] to be performed. */ suspend inline fun HttpClient.authenticateAndGet( route: String, @@ -119,7 +137,7 @@ suspend inline fun HttpClient.authenticateAndGet( * [authenticated][Actor.Authenticated] [Actor]. * * @param route URL [String] to which the [HttpRequest] will be sent. - * @param build Additional configuration for the [HttpResponse] to be performed. + * @param build Additional configuration for the [HttpRequest] to be performed. */ suspend inline fun HttpClient.authenticateAndPost( route: String, @@ -137,14 +155,14 @@ suspend inline fun HttpClient.authenticateAndPost( * * @param route URL [String] to which the [HttpRequest] will be sent. * @param parameters [Parameters] to be added to the form. - * @param build Additional configuration for the [HttpResponse] to be performed. + * @param build Additional configuration for the [HttpRequest] to be performed. */ suspend inline fun HttpClient.authenticateAndSubmitForm( route: String, parameters: Parameters, crossinline build: HttpRequestBuilder.() -> Unit = {} ): HttpResponse { - return authenticationLock.requestUnlock { + return authenticationLock.scheduleUnlock { submitForm(route, parameters) { bearerAuth(it.accessToken) build.invoke(this) @@ -159,7 +177,7 @@ suspend inline fun HttpClient.authenticateAndSubmitForm( * * @param route URL [String] to which the [HttpRequest] will be sent. * @param formData [List] with [PartData] to be included in the form. - * @param build Additional configuration for the [HttpResponse] to be performed. + * @param build Additional configuration for the [HttpRequest] to be performed. */ suspend inline fun HttpClient.authenticateAndSubmitFormWithBinaryData( route: String, @@ -178,7 +196,7 @@ suspend inline fun HttpClient.authenticateAndSubmitFormWithBinaryData( */ @PublishedApi internal suspend fun HttpMessageBuilder.authenticate() { - authenticationLock.requestUnlock { bearerAuth(it.accessToken) } + authenticationLock.scheduleUnlock { bearerAuth(it.accessToken) } } /** diff --git a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/account/MastodonAccount.kt b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/account/MastodonAccount.kt index 2076be618..1ca31698c 100644 --- a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/account/MastodonAccount.kt +++ b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/account/MastodonAccount.kt @@ -115,7 +115,7 @@ internal data class MastodonAccount( .instanceProvider() .provide() .authenticationLock - .requestUnlock { it.id == id } + .scheduleUnlock { it.id == id } } /** diff --git a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonDeletablePost.kt b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonDeletablePost.kt new file mode 100644 index 000000000..4488af504 --- /dev/null +++ b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonDeletablePost.kt @@ -0,0 +1,37 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.mastodon.feed.profile.post + +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost +import com.jeanbarrossilva.orca.core.mastodon.MastodonCoreModule +import com.jeanbarrossilva.orca.core.mastodon.client.authenticateAndDelete +import com.jeanbarrossilva.orca.core.mastodon.instance.SomeHttpInstance +import com.jeanbarrossilva.orca.core.mastodon.instanceProvider +import com.jeanbarrossilva.orca.std.injector.Injector + +/** + * [DeletablePost] that is deleted by sending a request to the Mastodon API. + * + * @param delegate [MastodonPost] to delegate its functionality to. + */ +internal data class MastodonDeletablePost(private val delegate: MastodonPost) : + DeletablePost(delegate) { + override suspend fun delete() { + (Injector.from().instanceProvider().provide() as SomeHttpInstance) + .client + .authenticateAndDelete("api/v1/statuses/$id") + } +} diff --git a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPost.kt b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPost.kt index 19008bd39..ebe26d3de 100644 --- a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPost.kt +++ b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPost.kt @@ -16,6 +16,7 @@ package com.jeanbarrossilva.orca.core.mastodon.feed.profile.post import com.jeanbarrossilva.orca.core.feed.profile.post.Author +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.mastodon.feed.profile.post.stat.CommentStat @@ -50,4 +51,8 @@ internal constructor( override val comment = CommentStat(id, commentCount, imageLoaderProvider) override val favorite = FavoriteStat(id, favoriteCount) override val repost = ReblogStat(id, reblogCount) + + override fun asDeletable(): DeletablePost { + return MastodonDeletablePost(this) + } } diff --git a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPostProvider.kt b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPostProvider.kt index 47501a64f..bff06dc8f 100644 --- a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPostProvider.kt +++ b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/MastodonPostProvider.kt @@ -15,6 +15,7 @@ package com.jeanbarrossilva.orca.core.mastodon.feed.profile.post +import com.jeanbarrossilva.orca.core.auth.SomeAuthenticationLock import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.PostProvider import com.jeanbarrossilva.orca.platform.cache.Cache @@ -27,8 +28,12 @@ import kotlinx.coroutines.flow.flowOf * * @param cache [Cache] of [Post]s by which [Post]s will be obtained. */ -class MastodonPostProvider internal constructor(private val cache: Cache) : PostProvider { - override suspend fun provide(id: String): Flow { +class MastodonPostProvider +internal constructor( + override val authenticationLock: SomeAuthenticationLock, + private val cache: Cache +) : PostProvider() { + override suspend fun onProvide(id: String): Flow { val post = cache.get(id) return flowOf(post) } diff --git a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/pagination/MastodonPostPaginator.kt b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/pagination/MastodonPostPaginator.kt index 18bba6570..9d184907e 100644 --- a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/pagination/MastodonPostPaginator.kt +++ b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/feed/profile/post/pagination/MastodonPostPaginator.kt @@ -22,7 +22,7 @@ import com.jeanbarrossilva.orca.core.mastodon.feed.profile.post.status.MastodonS import com.jeanbarrossilva.orca.core.mastodon.instance.SomeHttpInstance import com.jeanbarrossilva.orca.core.module.CoreModule import com.jeanbarrossilva.orca.core.module.instanceProvider -import com.jeanbarrossilva.orca.platform.ui.core.mapEach +import com.jeanbarrossilva.orca.ext.coroutines.mapEach import com.jeanbarrossilva.orca.std.image.ImageLoader import com.jeanbarrossilva.orca.std.image.SomeImageLoaderProvider import com.jeanbarrossilva.orca.std.injector.Injector diff --git a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/instance/ContextualMastodonInstance.kt b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/instance/ContextualMastodonInstance.kt index 2af3e127a..013b2d3bf 100644 --- a/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/instance/ContextualMastodonInstance.kt +++ b/core/mastodon/src/main/java/com/jeanbarrossilva/orca/core/mastodon/instance/ContextualMastodonInstance.kt @@ -138,5 +138,5 @@ class ContextualMastodonInstance( override val feedProvider = MastodonFeedProvider(actorProvider, termMuter, feedPostPaginator) override val profileProvider = MastodonProfileProvider(profileCache) override val profileSearcher = MastodonProfileSearcher(profileSearchResultsCache) - override val postProvider = MastodonPostProvider(postCache) + override val postProvider = MastodonPostProvider(authenticationLock, postCache) } diff --git a/core/mastodon/src/test/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClientTests.kt b/core/mastodon/src/test/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClientTests.kt index 9c8ffa8da..9d0cfd531 100644 --- a/core/mastodon/src/test/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClientTests.kt +++ b/core/mastodon/src/test/java/com/jeanbarrossilva/orca/core/mastodon/client/CoreHttpClientTests.kt @@ -102,4 +102,21 @@ internal class CoreHttpClientTests { .isEqualTo("Bearer ${actor.accessToken}") } } + + @Test + fun requestsAuthenticationOnAuthenticateAndDeleteWithAnUnauthenticatedActor() { + var isAuthenticated = false + runUnauthenticatedTest(onAuthentication = { isAuthenticated = true }) { + client.authenticateAndDelete(route = "") + assertThat(isAuthenticated).isTrue() + } + } + + @Test + fun setsBearerAuthHeaderOnAuthenticateAndDeleteWithAnAuthenticatedActor() { + runAuthenticatedTest { + assertThatRequestAuthorizationHeaderOf(client.authenticateAndDelete(route = "")) + .isEqualTo("Bearer ${actor.accessToken}") + } + } } diff --git a/core/sample-test/build.gradle.kts b/core/sample-test/build.gradle.kts index 353af86d5..09795c61e 100644 --- a/core/sample-test/build.gradle.kts +++ b/core/sample-test/build.gradle.kts @@ -23,3 +23,5 @@ dependencies { implementation(project(":core:sample")) implementation(kotlin("test-junit")) } + +kotlin.compilerOptions.freeCompilerArgs.add("-Xcontext-receivers") diff --git a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Author.extensions.kt b/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Author.extensions.kt index b43854e3b..1a93291a5 100644 --- a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Author.extensions.kt +++ b/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Author.extensions.kt @@ -19,7 +19,7 @@ import com.jeanbarrossilva.orca.core.feed.profile.post.Author import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSample import com.jeanbarrossilva.orca.core.sample.test.image.TestSampleImageLoader -/** [Author] returned by [sample]. */ +/** [Author] returned by [withSample]. */ private val testSampleAuthor = Author.createSample(TestSampleImageLoader.Provider) /** Test sample [Author]. */ diff --git a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Post.extensions.kt b/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Posts.extensions.kt similarity index 64% rename from core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Post.extensions.kt rename to core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Posts.extensions.kt index eeef9d817..e19335365 100644 --- a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Post.extensions.kt +++ b/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/Posts.extensions.kt @@ -16,20 +16,25 @@ package com.jeanbarrossilva.orca.core.sample.test.feed.profile.post import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSample import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSamples import com.jeanbarrossilva.orca.core.sample.test.image.TestSampleImageLoader -/** [Post] returned by [sample]. */ -private val testSamplePost = Post.createSample(TestSampleImageLoader.Provider) +/** [Posts] returned by [withSample]. */ +private val testPostsWithSample = Posts { + add { Post.createSample(TestSampleImageLoader.Provider) } +} -/** [Post]s returned by [samples]. */ -private val testSamplePosts = Post.createSamples(TestSampleImageLoader.Provider) +/** [Posts] returned by [withSamples]. */ +private val testPostsWithSamples = Posts { + addAll { Post.createSamples(TestSampleImageLoader.Provider) } +} -/** Test sample [Post]. */ -val Post.Companion.sample - get() = testSamplePost +/** Test [Posts] with a sample. */ +val Posts.Companion.withSample + get() = testPostsWithSample -/** Test sample [Post]s. */ -val Post.Companion.samples - get() = testSamplePosts +/** Test [Posts] with samples. */ +val Posts.Companion.withSamples + get() = testPostsWithSamples diff --git a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/instance/Instance.extensions.kt b/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/instance/Instance.extensions.kt index b9d50e950..47e3a662e 100644 --- a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/instance/Instance.extensions.kt +++ b/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/instance/Instance.extensions.kt @@ -15,17 +15,17 @@ package com.jeanbarrossilva.orca.core.sample.test.instance -import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.instance.Instance +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.instance.SampleInstance import com.jeanbarrossilva.orca.core.sample.instance.createSample import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.content.highlight.sample -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.samples +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples import com.jeanbarrossilva.orca.core.sample.test.image.TestSampleImageLoader /** [SampleInstance] returned by [sample]. */ private val testSampleInstance = - Instance.createSample(TestSampleImageLoader.Provider, defaultPosts = Post.samples) + Instance.createSample(TestSampleImageLoader.Provider, Posts.withSamples) /** Test [SampleInstance]. */ val Instance.Companion.sample diff --git a/core/sample/build.gradle.kts b/core/sample/build.gradle.kts index 62501177c..e61087da4 100644 --- a/core/sample/build.gradle.kts +++ b/core/sample/build.gradle.kts @@ -23,9 +23,15 @@ plugins { dependencies { api(project(":core-module")) + implementation(project(":ext:coroutines")) + ksp(project(":std:injector-processor")) testImplementation(project(":core:sample-test")) + testImplementation(project(":ext:testing")) testImplementation(libs.kotlin.coroutines.test) testImplementation(libs.kotlin.test) + testImplementation(libs.turbine) } + +kotlin.compilerOptions.freeCompilerArgs.add("-Xcontext-receivers") diff --git a/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/test/Post.extensions.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/auth/AuthenticationLock.extensions.kt similarity index 53% rename from feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/test/Post.extensions.kt rename to core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/auth/AuthenticationLock.extensions.kt index 1d68e49f7..ba4e4ea02 100644 --- a/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/test/Post.extensions.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/auth/AuthenticationLock.extensions.kt @@ -13,16 +13,16 @@ * not, see https://www.gnu.org/licenses. */ -package com.jeanbarrossilva.orca.feature.feed.test +package com.jeanbarrossilva.orca.core.sample.auth -import com.jeanbarrossilva.orca.core.feed.profile.post.Post -import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSamples -import com.jeanbarrossilva.orca.platform.ui.core.image.sample -import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader +import com.jeanbarrossilva.orca.core.auth.AuthenticationLock +import com.jeanbarrossilva.orca.core.auth.Authenticator +import com.jeanbarrossilva.orca.core.sample.auth.actor.SampleActorProvider -/** [Post]s returned by [samples]. */ -private val samplePosts = Post.createSamples(ComposableImageLoader.Provider.sample) +/** [AuthenticationLock] returned by [sample]. */ +private val sampleAuthenticationLock: AuthenticationLock = + AuthenticationLock(SampleAuthenticator, SampleActorProvider) -/** Sample [Post]s whose images are loaded by a [ComposableImageLoader]. */ -internal val Post.Companion.samples - get() = samplePosts +/** Sample [AuthenticationLock]. */ +internal val AuthenticationLock.Companion.sample + get() = sampleAuthenticationLock diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/auth/SampleAuthenticator.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/auth/SampleAuthenticator.kt index 8710b0d64..ed8e23d6a 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/auth/SampleAuthenticator.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/auth/SampleAuthenticator.kt @@ -21,12 +21,7 @@ import com.jeanbarrossilva.orca.core.auth.actor.Actor import com.jeanbarrossilva.orca.core.auth.actor.ActorProvider import com.jeanbarrossilva.orca.core.sample.auth.actor.sample -/** - * [Authenticator] that provides a sample [Actor]. - * - * @param actorProvider [ActorProvider] to which the [authenticated][Actor.Authenticated] [Actor] - * will be sent to be remembered when authentication occurs. - */ +/** [Authenticator] that provides a sample [Actor]. */ internal object SampleAuthenticator : Authenticator() { override val authorizer: Authorizer = Authorizer.sample override val actorProvider = ActorProvider.sample diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/SampleFeedProvider.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/SampleFeedProvider.kt index 0bfe9a7aa..73165c803 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/SampleFeedProvider.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/SampleFeedProvider.kt @@ -26,7 +26,7 @@ import com.jeanbarrossilva.orca.core.sample.image.SampleImageSource import com.jeanbarrossilva.orca.std.image.ImageLoader import com.jeanbarrossilva.orca.std.image.SomeImageLoaderProvider import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.map /** @@ -42,8 +42,8 @@ internal class SampleFeedProvider( ) : FeedProvider() { override val termMuter = SampleTermMuter() - /** [Flow] with the posts to be provided in the feed. */ - private val postsFlow = postProvider.postsFlow.asStateFlow() + /** [Flow] with the [Post]s to be provided in the feed. */ + private val postsFlow = postProvider.postsFlow.asSharedFlow() override suspend fun onProvide(userID: String, page: Int): Flow> { return postsFlow.map { posts -> diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/DeletablePost.extensions.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/DeletablePost.extensions.kt new file mode 100644 index 000000000..ce7b83e46 --- /dev/null +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/DeletablePost.extensions.kt @@ -0,0 +1,56 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import com.jeanbarrossilva.orca.core.feed.profile.post.Author +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost +import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.highlight.createSample +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.sample +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.repost.createSample +import com.jeanbarrossilva.orca.core.sample.image.SampleImageSource +import com.jeanbarrossilva.orca.std.image.ImageLoader +import com.jeanbarrossilva.orca.std.image.SomeImageLoaderProvider +import java.net.URL +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.UUID + +/** ID of a [DeletablePost] created by [createSample]. */ +internal val sampleDeletablePostID = UUID.randomUUID().toString() + +/** + * Creates a sample [DeletablePost]. + * + * @param imageLoaderProvider [ImageLoader.Provider] that provides the [ImageLoader] by which images + * will be loaded from a [SampleImageSource]. + */ +context(Posts.Builder.AdditionScope) + +fun DeletablePost.Companion.createSample( + imageLoaderProvider: SomeImageLoaderProvider +): DeletablePost { + return SampleDeletablePost( + SamplePost( + sampleDeletablePostID, + Author.createSample(imageLoaderProvider), + Content.sample, + publicationDateTime = ZonedDateTime.of(2_003, 10, 8, 8, 0, 0, 0, ZoneId.of("GMT-3")), + URL("https://mastodon.social/@christianselig/110492858891694580"), + writerProvider + ) + ) +} diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Post.extensions.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Post.extensions.kt index bd0736871..b919ed7d0 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Post.extensions.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Post.extensions.kt @@ -16,6 +16,7 @@ package com.jeanbarrossilva.orca.core.sample.feed.profile.post import com.jeanbarrossilva.orca.core.feed.profile.post.Author +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.feed.profile.post.content.highlight.Headline @@ -23,7 +24,6 @@ import com.jeanbarrossilva.orca.core.feed.profile.post.content.highlight.Highlig import com.jeanbarrossilva.orca.core.feed.profile.post.repost.Repost import com.jeanbarrossilva.orca.core.instance.domain.Domain import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.highlight.createSample -import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.sample import com.jeanbarrossilva.orca.core.sample.feed.profile.post.repost.createSample import com.jeanbarrossilva.orca.core.sample.image.SampleImageSource import com.jeanbarrossilva.orca.core.sample.instance.domain.sample @@ -35,9 +35,6 @@ import java.time.ZoneId import java.time.ZonedDateTime import java.util.UUID -/** ID of a [Post] created by [createSample]. */ -private val samplePostID = UUID.randomUUID().toString() - /** ID of the third [Post] in the [List] returned by [createSamples]. */ private val thirdPostID = UUID.randomUUID().toString() @@ -47,6 +44,8 @@ private val thirdPostID = UUID.randomUUID().toString() * @param imageLoaderProvider [ImageLoader.Provider] that provides the [ImageLoader] by which images * will be loaded from a [SampleImageSource]. */ +context(Posts.Builder.AdditionScope) + fun Post.Companion.createSamples( imageLoaderProvider: SomeImageLoaderProvider ): List { @@ -70,7 +69,8 @@ fun Post.Companion.createSamples( }, publicationDateTime = ZonedDateTime.of(2_023, 11, 27, 18, 26, 0, 0, ZoneId.of("America/Halifax")), - URL("https://mastodon.social/@christianselig/111484624066823391") + URL("https://mastodon.social/@christianselig/111484624066823391"), + writerProvider ) ) } @@ -81,14 +81,10 @@ fun Post.Companion.createSamples( * @param imageLoaderProvider [ImageLoader.Provider] that provides the [ImageLoader] by which images * will be loaded from a [SampleImageSource]. */ +context(Posts.Builder.AdditionScope) + fun Post.Companion.createSample( imageLoaderProvider: SomeImageLoaderProvider ): Post { - return SamplePost( - samplePostID, - Author.createSample(imageLoaderProvider), - Content.sample, - publicationDateTime = ZonedDateTime.of(2_003, 10, 8, 8, 0, 0, 0, ZoneId.of("GMT-3")), - URL("https://mastodon.social/@christianselig/110492858891694580") - ) + return DeletablePost.createSample(imageLoaderProvider) } diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Posts.extensions.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Posts.extensions.kt new file mode 100644 index 000000000..ecd4d71b0 --- /dev/null +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Posts.extensions.kt @@ -0,0 +1,25 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +/** + * Creates [Posts]. + * + * @param build Additional configuration to be performed. + */ +fun Posts(build: Posts.Builder.() -> Unit = {}): Posts { + return Posts.Builder().apply(build).build() +} diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Posts.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Posts.kt new file mode 100644 index 000000000..a8d3a72f5 --- /dev/null +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/Posts.kt @@ -0,0 +1,112 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import com.jeanbarrossilva.orca.core.feed.profile.post.Post + +/** + * [List] of [Post]s that have been added from a [Builder.AdditionScope]. + * + * @param additionScope [Builder.AdditionScope] in which the [Post]s were added. + * @param delegate [List] to which the functionality will be delegated. + */ +class Posts +private constructor( + internal val additionScope: Builder.AdditionScope, + private val delegate: List +) : List by delegate { + /** + * Configures and builds [Posts]. + * + * @see build + */ + class Builder internal constructor() { + /** [AdditionScope] in which this [Builder] will add [Post]s. */ + private val additionScope = AdditionScope() + + /** [List] on top of which [Posts] will be created. */ + private val delegate = mutableListOf() + + /** Scope in which a [Post] is added. */ + class AdditionScope internal constructor() { + /** + * [SamplePostWriter.Provider] that provides the [SamplePostWriter] for the [Post] to perform + * its write operations. + */ + internal val writerProvider = SamplePostWriter.Provider() + + /** + * Marks the [Post] addition process as finished, specifying a [SamplePostWriter] with the + * given [posts] to be provided by the [writerProvider]. + * + * @param posts [Posts] with which the [SamplePostWriter] will be provided. + */ + internal fun finish(posts: Posts) { + val postProvider = SamplePostProvider(posts) + val writer = SamplePostWriter(postProvider) + writerProvider.provide(writer) + } + } + + /** + * Adds multiple [Post]s. + * + * @param addition Returns the [Post]s to be added within the [additionScope]. + */ + fun addAll(addition: AdditionScope.() -> Collection): Builder { + val posts = additionScope.addition() + delegate.addAll(posts) + return this + } + + /** + * Adds a [Post]. + * + * @param addition Returns the [Post] to be added within the [additionScope]. + */ + fun add(addition: AdditionScope.() -> Post): Builder { + val post = additionScope.addition() + delegate.add(post) + return this + } + + /** Builds [Posts]. */ + internal fun build(): Posts { + val delegateAsList = delegate.toList() + return Posts(additionScope, delegateAsList).also(additionScope::finish) + } + } + + /** + * Creates [Posts] with the given [Post] appended to it. + * + * @param other [Post] to be added. + */ + internal operator fun plus(other: Post): Posts { + return Posts { addAll { delegate + other } } + } + + /** + * Creates [Posts] without the given [Post]. + * + * @param other [Post] to be removed. + */ + internal operator fun minus(other: Post): Posts { + return Posts { addAll { delegate - other } } + } + + companion object +} diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SampleDeletablePost.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SampleDeletablePost.kt new file mode 100644 index 000000000..3f3a3aa8a --- /dev/null +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SampleDeletablePost.kt @@ -0,0 +1,31 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost + +/** + * [DeletablePost] whose deletion is performed by the [writer]. + * + * @param delegate [SamplePost] to which this [SampleDeletablePost]'s functionality will be + * delegated. + */ +internal data class SampleDeletablePost(private val delegate: SamplePost) : + DeletablePost(delegate) { + override suspend fun delete() { + delegate.writerProvider.provide().delete(id) + } +} diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePost.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePost.kt index 6e2a139ac..421a1e4e2 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePost.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePost.kt @@ -17,6 +17,7 @@ package com.jeanbarrossilva.orca.core.sample.feed.profile.post import com.jeanbarrossilva.orca.core.feed.profile.Profile import com.jeanbarrossilva.orca.core.feed.profile.post.Author +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.feed.profile.post.stat.Stat @@ -24,15 +25,25 @@ import com.jeanbarrossilva.orca.core.feed.profile.post.stat.toggleable.Toggleabl import java.net.URL import java.time.ZonedDateTime -/** [Post] whose operations are performed in memory and serves as a sample. */ +/** + * [Post] whose operations are performed in memory and serves as a sample. + * + * @param writerProvider [SamplePostWriter.Provider] by which a [SamplePostWriter] for creating a + * [SampleDeletablePost] from this [SamplePost] can be provided. + */ internal data class SamplePost( override val id: String, override val author: Author, override val content: Content, override val publicationDateTime: ZonedDateTime, - override val url: URL + override val url: URL, + val writerProvider: SamplePostWriter.Provider ) : Post() { override val comment = Stat() override val favorite = ToggleableStat() override val repost = ToggleableStat() + + override fun asDeletable(): DeletablePost { + return SampleDeletablePost(this) + } } diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProvider.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProvider.kt index 60368eef3..7dc018b85 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProvider.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProvider.kt @@ -15,24 +15,28 @@ package com.jeanbarrossilva.orca.core.sample.feed.profile.post +import com.jeanbarrossilva.orca.core.auth.AuthenticationLock import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.PostProvider +import com.jeanbarrossilva.orca.core.sample.auth.sample import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull /** - * [PostProvider] that provides sample [Post]s. + * [PostProvider] that provides sample [Posts]. * * @param defaultPosts [Post]s that are present by default. */ -class SamplePostProvider internal constructor(internal val defaultPosts: List) : - PostProvider { +class SamplePostProvider internal constructor(internal val defaultPosts: Posts = Posts()) : + PostProvider() { /** [MutableStateFlow] that provides the [Post]s. */ internal val postsFlow = MutableStateFlow(defaultPosts) - override suspend fun provide(id: String): Flow { + override val authenticationLock = AuthenticationLock.sample + + override suspend fun onProvide(id: String): Flow { return postsFlow.mapNotNull { posts -> posts.find { post -> post.id == id } } } diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriter.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriter.kt index 5a8dab385..8ce0f61f7 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriter.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriter.kt @@ -19,18 +19,69 @@ import com.jeanbarrossilva.orca.core.feed.profile.post.Post import kotlinx.coroutines.flow.update /** - * Performs [Post]-related writing operations. + * Performs [SamplePost]-related writing operations. * - * @param postProvider [SamplePostProvider] by which [Post]s will be provided. + * @param postProvider [SamplePostProvider] by which [SamplePost]s will be provided. */ -class SamplePostWriter internal constructor(private val postProvider: SamplePostProvider) { - /** Clears all added [Post]s, including the default ones. */ - fun clear() { - postProvider.postsFlow.update { emptyList() } +class SamplePostWriter +internal constructor(internal val postProvider: SamplePostProvider = SamplePostProvider()) { + /** Provides a [SamplePostWriter] through [provide]. */ + class Provider internal constructor() { + /** [SamplePostWriter] to be provided. */ + private var writer: SamplePostWriter? = null + + /** + * [IllegalStateException] to be thrown if a [SamplePostWriter] is requested to be provided but + * none has been specified. + */ + internal class UnspecifiedWriterException : + IllegalStateException("A post writer to be provided hasn't been specified.") + + /** + * Provides the specified [SamplePostWriter]. + * + * @throws UnspecifiedWriterException If a [SamplePostWriter] to be provided hasn't been + * specified. + */ + @Throws(UnspecifiedWriterException::class) + fun provide(): SamplePostWriter { + return writer ?: throw UnspecifiedWriterException() + } + + /** Defines the given [SamplePostWriter] as the one to be provided. */ + internal fun provide(writer: SamplePostWriter) { + this.writer = writer + } + } + + /** + * Adds the [post]. + * + * @param post [Post] to be added. + * @throws IllegalArgumentException If a [Post] with the same ID as the given one's is already + * present. + */ + fun add(post: Post) { + val isUnique = post.id !in postProvider.postsFlow.value.map(Post::id) + if (isUnique) { + postProvider.postsFlow.update { it + post } + } else { + throw IllegalArgumentException("A post with the same ID (${post.id}) already exists.") + } + } + + /** + * Deletes the [SamplePost] identified by the [id]. + * + * @param id ID of the [SamplePost] to be deleted. + * @see SamplePost.id + */ + fun delete(id: String) { + postProvider.postsFlow.update { posts -> posts - posts.single { post -> post.id == id } } } /** Resets this [SamplePostWriter] to its default state. */ fun reset() { - postProvider.postsFlow.value = postProvider.defaultPosts + postProvider.postsFlow.update { postProvider.defaultPosts } } } diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/repost/Repost.extensions.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/repost/Repost.extensions.kt index 32ddbf8c7..c2fad0154 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/repost/Repost.extensions.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/repost/Repost.extensions.kt @@ -19,6 +19,7 @@ import com.jeanbarrossilva.orca.core.feed.profile.post.Author import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.feed.profile.post.repost.Repost import com.jeanbarrossilva.orca.core.instance.domain.Domain +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.SamplePost import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createRamboSample import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSample @@ -41,6 +42,8 @@ private val sampleRepostID = UUID.randomUUID().toString() * @param imageLoaderProvider [ImageLoader.Provider] that provides the [ImageLoader] by which images * will be loaded from a [SampleImageSource]. */ +context(Posts.Builder.AdditionScope) + fun Repost.Companion.createSample( imageLoaderProvider: SomeImageLoaderProvider ): Repost { @@ -61,7 +64,8 @@ fun Repost.Companion.createSample( null }, publicationDateTime = ZonedDateTime.of(2023, 8, 16, 16, 48, 43, 384, ZoneId.of("GMT-3")), - url = URL("https://mastodon.social/@_inside/110900315644335855") + url = URL("https://mastodon.social/@_inside/110900315644335855"), + writerProvider ), reblogger = Author.createSample(imageLoaderProvider) ) diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/Instance.extensions.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/Instance.extensions.kt index 9ed3e334c..f4d1639a5 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/Instance.extensions.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/Instance.extensions.kt @@ -17,6 +17,7 @@ package com.jeanbarrossilva.orca.core.sample.instance import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.instance.Instance +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSamples import com.jeanbarrossilva.orca.core.sample.image.SampleImageSource import com.jeanbarrossilva.orca.std.image.ImageLoader @@ -27,11 +28,11 @@ import com.jeanbarrossilva.orca.std.image.SomeImageLoaderProvider * * @param imageLoaderProvider [ImageLoader.Provider] that provides the [ImageLoader] by which images * can be loaded from a [SampleImageSource]. - * @param defaultPosts [Post]s that are provided by default within the [SampleInstance]. + * @param defaultPosts [Posts] that are provided by default within the [SampleInstance]. */ fun Instance.Companion.createSample( imageLoaderProvider: SomeImageLoaderProvider, - defaultPosts: List = Post.createSamples(imageLoaderProvider) + defaultPosts: Posts = Posts { addAll { Post.createSamples(imageLoaderProvider) } } ): SampleInstance { - return SampleInstance(defaultPosts, imageLoaderProvider) + return SampleInstance(imageLoaderProvider, defaultPosts) } diff --git a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/SampleInstance.kt b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/SampleInstance.kt index b60da6657..cc5771233 100644 --- a/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/SampleInstance.kt +++ b/core/sample/src/main/java/com/jeanbarrossilva/orca/core/sample/instance/SampleInstance.kt @@ -17,7 +17,6 @@ package com.jeanbarrossilva.orca.core.sample.instance import com.jeanbarrossilva.orca.core.auth.AuthenticationLock import com.jeanbarrossilva.orca.core.auth.Authenticator -import com.jeanbarrossilva.orca.core.auth.actor.ActorProvider import com.jeanbarrossilva.orca.core.feed.FeedProvider import com.jeanbarrossilva.orca.core.feed.profile.ProfileProvider import com.jeanbarrossilva.orca.core.feed.profile.post.Post @@ -25,11 +24,11 @@ import com.jeanbarrossilva.orca.core.feed.profile.search.ProfileSearcher import com.jeanbarrossilva.orca.core.instance.Instance import com.jeanbarrossilva.orca.core.instance.domain.Domain import com.jeanbarrossilva.orca.core.sample.auth.SampleAuthenticator -import com.jeanbarrossilva.orca.core.sample.auth.actor.sample +import com.jeanbarrossilva.orca.core.sample.auth.sample import com.jeanbarrossilva.orca.core.sample.feed.SampleFeedProvider import com.jeanbarrossilva.orca.core.sample.feed.profile.SampleProfileProvider import com.jeanbarrossilva.orca.core.sample.feed.profile.SampleProfileWriter -import com.jeanbarrossilva.orca.core.sample.feed.profile.post.SamplePostProvider +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.SamplePostWriter import com.jeanbarrossilva.orca.core.sample.feed.profile.search.SampleProfileSearcher import com.jeanbarrossilva.orca.core.sample.image.SampleImageSource @@ -40,19 +39,19 @@ import com.jeanbarrossilva.orca.std.image.SomeImageLoaderProvider /** * [Instance] made out of sample underlying core structures. * - * @param defaultPosts [Post]s that are provided by default by the [postProvider]. * @param imageLoaderProvider [ImageLoader.Provider] that provides the [ImageLoader] by which images * will be loaded from a [SampleImageSource]. + * @param posts [Post]s that are provided by default by the [postProvider]. */ class SampleInstance internal constructor( - defaultPosts: List, - internal val imageLoaderProvider: SomeImageLoaderProvider + internal val imageLoaderProvider: SomeImageLoaderProvider, + private val defaultPosts: Posts ) : Instance() { override val domain = Domain.sample override val authenticator: Authenticator = SampleAuthenticator - override val authenticationLock = AuthenticationLock(authenticator, ActorProvider.sample) - override val postProvider = SamplePostProvider(defaultPosts) + override val authenticationLock = AuthenticationLock.sample + override val postProvider = postWriter.postProvider override val feedProvider: FeedProvider = SampleFeedProvider(imageLoaderProvider, postProvider) override val profileProvider: ProfileProvider = SampleProfileProvider(postProvider, imageLoaderProvider) @@ -63,5 +62,6 @@ internal constructor( val profileWriter = SampleProfileWriter(profileProvider as SampleProfileProvider) /** [SamplePostWriter] for performing write operations on the [postProvider]. */ - val postWriter = SamplePostWriter(postProvider) + val postWriter + get() = defaultPosts.additionScope.writerProvider.provide() } diff --git a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/ProfileExtensionsTests.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/ProfileExtensionsTests.kt index 2a46487fd..9f8b6929d 100644 --- a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/ProfileExtensionsTests.kt +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/ProfileExtensionsTests.kt @@ -17,9 +17,9 @@ package com.jeanbarrossilva.orca.core.sample.feed.profile import com.jeanbarrossilva.orca.core.feed.profile.Profile import com.jeanbarrossilva.orca.core.feed.profile.post.Author -import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.samples +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples import com.jeanbarrossilva.orca.core.sample.test.feed.profile.sample import kotlin.test.Test import kotlin.test.assertContentEquals @@ -31,7 +31,7 @@ internal class ProfileExtensionsTests { fun `GIVEN a sample profile WHEN getting its posts THEN they are the sample ones`() { runTest { assertContentEquals( - Post.samples.filter { it.author == Author.sample }.take(SampleProfile.POSTS_PER_PAGE), + Posts.withSamples.filter { it.author == Author.sample }.take(SampleProfile.POSTS_PER_PAGE), Profile.sample.getPosts(0).first() ) } diff --git a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsBuilderAdditionScopeTests.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsBuilderAdditionScopeTests.kt new file mode 100644 index 000000000..fdb6abbe4 --- /dev/null +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsBuilderAdditionScopeTests.kt @@ -0,0 +1,30 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import kotlin.test.Test + +internal class PostsBuilderAdditionScopeTests { + @Test(expected = SamplePostWriter.Provider.UnspecifiedWriterException::class) + fun doesNotProviderPostWriterWhenUnfinished() { + Posts.Builder.AdditionScope().writerProvider.provide() + } + + @Test + fun providesPostWriterWhenFinished() { + Posts.Builder.AdditionScope().apply { finish(Posts()) }.writerProvider.provide() + } +} diff --git a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsBuilderTests.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsBuilderTests.kt new file mode 100644 index 000000000..af3bd84f4 --- /dev/null +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsBuilderTests.kt @@ -0,0 +1,37 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import assertk.assertThat +import assertk.assertions.containsOnly +import assertk.assertions.containsSubList +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSample +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples +import kotlin.test.Test + +internal class PostsBuilderTests { + @Test + fun addsOnePost() { + val post = Posts.withSample.single() + assertThat(Posts { add { post } }).containsOnly(post) + } + + @Test + fun addsMultiplePosts() { + val posts = Posts.withSamples + assertThat(Posts { addAll { posts } }).containsSubList(posts) + } +} diff --git a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsExtensionsTests.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsExtensionsTests.kt new file mode 100644 index 000000000..37cfe6230 --- /dev/null +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsExtensionsTests.kt @@ -0,0 +1,27 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import com.jeanbarrossilva.orca.core.sample.test.image.TestSampleImageLoader +import kotlin.test.Test + +internal class PostsExtensionsTests { + @Test + fun createsPosts() { + Posts { addAll { Post.createSamples(TestSampleImageLoader.Provider) } } + } +} diff --git a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsTests.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsTests.kt new file mode 100644 index 000000000..30adda7a9 --- /dev/null +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/PostsTests.kt @@ -0,0 +1,34 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import assertk.assertThat +import assertk.assertions.containsSubList +import assertk.assertions.isEmpty +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSample +import kotlin.test.Test + +internal class PostsTests { + @Test + fun adds() { + assertThat(Posts() + Posts.withSample.single()).containsSubList(Posts.withSample) + } + + @Test + fun subtracts() { + assertThat(Posts.withSample - Posts.withSample.single()).isEmpty() + } +} diff --git a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProviderTests.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProviderTests.kt index 486a86a12..c14798786 100644 --- a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProviderTests.kt +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostProviderTests.kt @@ -15,13 +15,17 @@ package com.jeanbarrossilva.orca.core.sample.feed.profile.post -import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import assertk.assertThat +import assertk.assertions.isEmpty +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost import com.jeanbarrossilva.orca.core.instance.Instance -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.samples +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples +import com.jeanbarrossilva.orca.core.sample.test.image.TestSampleImageLoader import com.jeanbarrossilva.orca.core.sample.test.instance.SampleInstanceTestRule import com.jeanbarrossilva.orca.core.sample.test.instance.sample +import com.jeanbarrossilva.testing.hasPropertiesEqualToThoseOf import kotlin.test.Test -import kotlin.test.assertEquals +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -30,9 +34,31 @@ internal class SamplePostProviderTests { @get:Rule val instanceRule = SampleInstanceTestRule(Instance.sample) @Test - fun `GIVEN all post samples WHEN getting them by their IDs THEN they're returned`() { + fun getsPostsByTheirIDs() { runTest { - Post.samples.forEach { assertEquals(it, Instance.sample.postProvider.provide(it.id).first()) } + Posts.withSamples.forEach { + assertThat(Instance.sample.postProvider.provide(it.id).first()) + .hasPropertiesEqualToThoseOf(it) + } } } + + @Test + fun doesNotProvidePostWhenItIsDeleted() { + assertThat( + Posts { add { DeletablePost.createSample(TestSampleImageLoader.Provider) } } + .additionScope + .writerProvider + .provide() + .postProvider + .apply { + runTest { + provide(sampleDeletablePostID).filterIsInstance().first().delete() + } + } + .postsFlow + .value + ) + .isEmpty() + } } diff --git a/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriterProviderTests.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriterProviderTests.kt new file mode 100644 index 000000000..cc77a25cf --- /dev/null +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriterProviderTests.kt @@ -0,0 +1,39 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.sample.feed.profile.post + +import assertk.assertThat +import assertk.assertions.isSameAs +import com.jeanbarrossilva.orca.core.instance.Instance +import com.jeanbarrossilva.orca.core.sample.test.instance.SampleInstanceTestRule +import com.jeanbarrossilva.orca.core.sample.test.instance.sample +import kotlin.test.Test +import org.junit.Rule + +internal class SamplePostWriterProviderTests { + @get:Rule val sampleInstanceRule = SampleInstanceTestRule(Instance.sample) + + @Test(expected = SamplePostWriter.Provider.UnspecifiedWriterException::class) + fun throwsWhenProvidingWithoutSpecifiedWriter() { + SamplePostWriter.Provider().provide() + } + + @Test + fun providesWriterWhenItHasBeenProvided() { + val writer = Instance.sample.postWriter + assertThat(SamplePostWriter.Provider().apply { provide(writer) }.provide()).isSameAs(writer) + } +} diff --git a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/repost/Repost.extensions.kt b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriterTests.kt similarity index 57% rename from core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/repost/Repost.extensions.kt rename to core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriterTests.kt index e58218eca..7c1c6e524 100644 --- a/core/sample-test/src/main/java/com/jeanbarrossilva/orca/core/sample/test/feed/profile/post/repost/Repost.extensions.kt +++ b/core/sample/src/test/java/com/jeanbarrossilva/orca/core/sample/feed/profile/post/SamplePostWriterTests.kt @@ -13,16 +13,20 @@ * not, see https://www.gnu.org/licenses. */ -package com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.repost +package com.jeanbarrossilva.orca.core.sample.feed.profile.post -import com.jeanbarrossilva.orca.core.feed.profile.post.repost.Repost -import com.jeanbarrossilva.orca.core.sample.feed.profile.post.repost.createSample -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.content.highlight.sample -import com.jeanbarrossilva.orca.core.sample.test.image.TestSampleImageLoader +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSample +import kotlin.test.Test -/** [Repost] returned by [sample]. */ -private val testSampleRepost = Repost.createSample(TestSampleImageLoader.Provider) +internal class SamplePostWriterTests { + @Test(expected = IllegalArgumentException::class) + fun throwsWhenAddingDuplicatePost() { + SamplePostWriter(SamplePostProvider(defaultPosts = Posts.withSample)) + .add(Posts.withSample.single()) + } -/** Test sample [Repost]. */ -val Repost.Companion.sample - get() = testSampleRepost + @Test + fun addsUniquePost() { + SamplePostWriter().add(Posts.withSample.single()) + } +} diff --git a/core/shared-preferences/build.gradle.kts b/core/shared-preferences/build.gradle.kts index 3ccac4d96..991e7bcf9 100644 --- a/core/shared-preferences/build.gradle.kts +++ b/core/shared-preferences/build.gradle.kts @@ -29,6 +29,7 @@ android { dependencies { androidTestImplementation(project(":core-test")) + androidTestImplementation(project(":platform:testing")) androidTestImplementation(libs.android.test.runner) androidTestImplementation(libs.junit) androidTestImplementation(libs.kotlin.coroutines.test) @@ -36,6 +37,7 @@ dependencies { implementation(project(":core")) implementation(libs.android.core) + implementation(libs.android.lifecycle.runtime) implementation(libs.kotlin.coroutines.android) implementation(libs.kotlin.serialization.json) } diff --git a/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/actor/test/SharedPreferencesCoreTestRule.kt b/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/actor/test/SharedPreferencesCoreTestRule.kt index 830e01a3e..0a0e94d9a 100644 --- a/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/actor/test/SharedPreferencesCoreTestRule.kt +++ b/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/actor/test/SharedPreferencesCoreTestRule.kt @@ -15,16 +15,13 @@ package com.jeanbarrossilva.orca.core.sharedpreferences.actor.test -import androidx.test.platform.app.InstrumentationRegistry import com.jeanbarrossilva.orca.core.feed.profile.post.content.TermMuter import com.jeanbarrossilva.orca.core.sharedpreferences.actor.SharedPreferencesActorProvider import com.jeanbarrossilva.orca.core.sharedpreferences.feed.profile.post.content.SharedPreferencesTermMuter +import com.jeanbarrossilva.orca.platform.testing.context import org.junit.rules.ExternalResource internal class SharedPreferencesCoreTestRule : ExternalResource() { - private val context - get() = InstrumentationRegistry.getInstrumentation().context - lateinit var actorProvider: SharedPreferencesActorProvider private set diff --git a/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/feed/profile/post/content/SharedPreferencesTermMuterTests.kt b/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/feed/profile/post/content/SharedPreferencesTermMuterTests.kt index 0cd6ffeb8..86f18758f 100644 --- a/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/feed/profile/post/content/SharedPreferencesTermMuterTests.kt +++ b/core/shared-preferences/src/androidTest/java/com/jeanbarrossilva/orca/core/sharedpreferences/feed/profile/post/content/SharedPreferencesTermMuterTests.kt @@ -15,9 +15,9 @@ package com.jeanbarrossilva.orca.core.sharedpreferences.feed.profile.post.content -import androidx.test.platform.app.InstrumentationRegistry import app.cash.turbine.test import com.jeanbarrossilva.orca.core.sharedpreferences.actor.test.SharedPreferencesCoreTestRule +import com.jeanbarrossilva.orca.platform.testing.context import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -25,9 +25,6 @@ import org.junit.Rule import org.junit.Test internal class SharedPreferencesTermMuterTests { - private val context - get() = InstrumentationRegistry.getInstrumentation().context - @get:Rule val coreRule = SharedPreferencesCoreTestRule() @Test diff --git a/core/src/main/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLock.kt b/core/src/main/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLock.kt index 94e96d5c7..942b32485 100644 --- a/core/src/main/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLock.kt +++ b/core/src/main/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLock.kt @@ -1,40 +1,54 @@ -/* - * Copyright © 2023 Orca - * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program. If - * not, see https://www.gnu.org/licenses. - */ - package com.jeanbarrossilva.orca.core.auth import com.jeanbarrossilva.orca.core.auth.actor.Actor import com.jeanbarrossilva.orca.core.auth.actor.ActorProvider +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.flow.MutableStateFlow + +/** An [AuthenticationLock.OnUnlockListener] with a generic return type. */ +private typealias SomeOnUnlockListener = AuthenticationLock.OnUnlockListener<*> /** An [AuthenticationLock] with a generic [Authenticator]. */ typealias SomeAuthenticationLock = AuthenticationLock<*> /** - * Ensures that an operation is only performed by an [authenticated][Actor.Authenticated] [Actor], - * through [requestUnlock]. + * Ensures that operations are only performed by an [authenticated][Actor.Authenticated] [Actor]. * - * @param T [Authenticator] to authenticate the [Actor] with. + * @param A [Authenticator] to authenticate the [Actor] with. * @param authenticator [Authenticator] through which the [Actor] will be requested to be * [authenticated][Actor.Authenticated]. * @param actorProvider [ActorProvider] whose provided [Actor] will be ensured to be * [authenticated][Actor.Authenticated]. + * @see scheduleUnlock + * @see scheduleUnlock */ -class AuthenticationLock( - private val authenticator: T, +class AuthenticationLock( + private val authenticator: A, private val actorProvider: ActorProvider ) { + /** + * [MutableStateFlow] to which [Boolean]s that indicate whether this [AuthenticationLock] has + * ongoing unlocks are emitted. + */ + private val activenessFlow = MutableStateFlow(false) + + /** + * [Continuation]s associated to their respective [OnUnlockListener]s of unlocks that are awaiting + * the one being currently performed. + */ + private val schedule = hashMapOf>() + + /** + * Result of a successful unlock. + * + * @param R Type of the [value]. + * @param actor [Actor] that has been [authenticated][Actor.Authenticated]. + * @param value [R] that's been returned by the [OnUnlockListener]. + */ + private class Unlock(val actor: Actor.Authenticated, val value: R) + /** [IllegalStateException] thrown if authentication fails. */ class FailedAuthenticationException internal constructor() : IllegalStateException("Could not authenticate properly.") @@ -42,33 +56,83 @@ class AuthenticationLock( /** * Listens to an unlock. * - * @param T Value returned by [onUnlock]. + * @param R Value returned by [onUnlock]. */ - fun interface OnUnlockListener { + fun interface OnUnlockListener { /** - * Callback called when the [Actor] provided by the [actorProvider] is + * Callback run when the [Actor] provided by the [actorProvider] is * [authenticated][Actor.Authenticated]. * * @param actor Provided [authenticated][Actor.Authenticated] [Actor]. */ - suspend fun onUnlock(actor: Actor.Authenticated): T + suspend fun onUnlock(actor: Actor.Authenticated): R } /** * Ensures that the operation in the [listener]'s [onUnlock][OnUnlockListener.onUnlock] callback - * is only performed if the [Actor] is [authenticated][Actor.Authenticated]; if it isn't, then + * is only performed when the [Actor] is [authenticated][Actor.Authenticated]; if it isn't, then * authentication is requested and, if it succeeds, the operation is performed. * + * If an unlock has already been requested and is still ongoing, this one is queued for it to be + * run as soon as the current finishes (suspending this method's execution flow until then), + * reusing the [Actor] that's been first obtained from the [actorProvider] by the preceding + * unlock. + * * @param T Value returned by the [listener]'s [onUnlock][OnUnlockListener.onUnlock]. - * @param listener [OnUnlockListener] to be notified if the [Actor] is + * @param listener [OnUnlockListener] to be notified when the [Actor] is + * [authenticated][Actor.Authenticated]. + * @throws FailedAuthenticationException If authentication fails. + */ + @Throws(FailedAuthenticationException::class) + suspend fun scheduleUnlock(listener: OnUnlockListener): T { + val isActive = activenessFlow.value + return if (isActive) awaitUnlock(listener) else requestUnlock(listener) + } + + /** + * Suspends until the [Continuation] associated to the given [listener] is resumed with the value + * returned by its [onUnlock][OnUnlockListener.onUnlock] callback. + * + * @param T Value returned by the [listener]'s [onUnlock][OnUnlockListener.onUnlock]. + * @param listener [OnUnlockListener] whose returned value will be awaited. + */ + private suspend fun awaitUnlock(listener: OnUnlockListener): T { + return suspendCoroutine { schedule[listener] = it } + } + + /** + * Ensures that the operation in the [listener]'s [onUnlock][OnUnlockListener.onUnlock] callback + * is only performed when the [Actor] is [authenticated][Actor.Authenticated]; if it isn't, then + * authentication is requested and, if it succeeds, the operation is performed. + * + * @param T Value returned by the [listener]'s [onUnlock][OnUnlockListener.onUnlock]. + * @param listener [OnUnlockListener] to be notified when the [Actor] is * [authenticated][Actor.Authenticated]. * @return Result of the [listener]'s [onUnlock][OnUnlockListener.onUnlock]. * @throws FailedAuthenticationException If authentication fails. */ - suspend fun requestUnlock(listener: OnUnlockListener): T { + @Throws(FailedAuthenticationException::class) + private suspend fun requestUnlock(listener: OnUnlockListener): T { + val unlock = activate { requestUnlockWithProvidedActor(listener) } + requestScheduledUnlocks(unlock.actor) + return unlock.value + } + + /** + * Suspends until the [Actor] provided by the [actorProvider] is + * [authenticated][Actor.Authenticated], requesting the authentication process to be performed if + * it currently isn't. After it's finished, the [listener] is notified. + * + * @param T Value returned by the [listener]'s [onUnlock][OnUnlockListener.onUnlock]. + * @param listener [OnUnlockListener] to be notified when the [Actor] is + * [authenticated][Actor.Authenticated]. + * @throws FailedAuthenticationException If authentication fails. + */ + @Throws(FailedAuthenticationException::class) + private suspend fun requestUnlockWithProvidedActor(listener: OnUnlockListener): Unlock { return when (val actor = actorProvider.provide()) { - is Actor.Unauthenticated -> authenticateAndNotify(listener) - is Actor.Authenticated -> listener.onUnlock(actor) + is Actor.Unauthenticated -> authenticateAndUnlock(listener) + is Actor.Authenticated -> Unlock(actor, listener.onUnlock(actor)) } } @@ -77,16 +141,50 @@ class AuthenticationLock( * [authenticated][Actor.Authenticated]. * * @param T Value returned by the [listener]'s [onUnlock][OnUnlockListener.onUnlock]. - * @param listener [OnUnlockListener] to be notified if the [Actor] is + * @param listener [OnUnlockListener] to be notified when the [Actor] is * [authenticated][Actor.Authenticated]. * @throws FailedAuthenticationException If authentication fails. */ - private suspend fun authenticateAndNotify(listener: OnUnlockListener): T { + @Throws(FailedAuthenticationException::class) + private suspend fun authenticateAndUnlock(listener: OnUnlockListener): Unlock { val actor = authenticator.authenticate() - return if (actor is Actor.Authenticated) { - listener.onUnlock(actor) - } else { - throw FailedAuthenticationException() + val returned = + if (actor is Actor.Authenticated) { + listener.onUnlock(actor) + } else { + throw FailedAuthenticationException() + } + return Unlock(actor, returned) + } + + /** + * Requests scheduled unlocks to be performed, resuming their associated [Continuation]s with the + * value returned by their [OnUnlockListener]. + * + * Also prevents an [Actor] from having to be retrieved multiple times by propagating the one + * obtained from the first ongoing unlock to those that have been scheduled to be performed after + * it consecutively. + * + * @param actor [Actor] to be provided to the scheduled unlocks. + */ + private suspend fun requestScheduledUnlocks(actor: Actor.Authenticated) { + schedule.forEach { (listener, continuation) -> + val value = activate { listener.onUnlock(actor) } + @Suppress("UNCHECKED_CAST") (continuation as Continuation).resume(value) + schedule.remove(listener) } } + + /** + * Considers this [AuthenticationLock] to be active while the given [action] is being performed. + * + * @param action Operation to be performed while in an active state. + * @see activenessFlow + */ + private inline fun activate(action: () -> T): T { + activenessFlow.value = true + return action().also { activenessFlow.value = false } + } + + companion object } diff --git a/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/DeletablePost.kt b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/DeletablePost.kt new file mode 100644 index 000000000..3498c65af --- /dev/null +++ b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/DeletablePost.kt @@ -0,0 +1,43 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.feed.profile.post + +/** + * [Post] that can be deleted. + * + * @param delegate [Post] to which this [DeletablePost]'s functionality (apart from its deletion) + * will be delegated. + * @see delete + */ +abstract class DeletablePost(private val delegate: Post) : Post() { + override val id = delegate.id + override val author = delegate.author + override val content = delegate.content + override val publicationDateTime = delegate.publicationDateTime + override val comment = delegate.comment + override val favorite = delegate.favorite + override val repost = delegate.repost + override val url = delegate.url + + final override fun asDeletable(): DeletablePost { + return this + } + + /** Deletes this [DeletablePost]. */ + abstract suspend fun delete() + + companion object +} diff --git a/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/Post.kt b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/Post.kt index 4e55f0398..4c02a0e1f 100644 --- a/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/Post.kt +++ b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/Post.kt @@ -15,6 +15,9 @@ package com.jeanbarrossilva.orca.core.feed.profile.post +import com.jeanbarrossilva.orca.core.auth.AuthenticationLock +import com.jeanbarrossilva.orca.core.auth.SomeAuthenticationLock +import com.jeanbarrossilva.orca.core.auth.actor.Actor import com.jeanbarrossilva.orca.core.feed.profile.Profile import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.feed.profile.post.stat.Stat @@ -49,5 +52,25 @@ abstract class Post : Serializable { /** [URL] that leads to this [Post]. */ abstract val url: URL + /** Creates a [DeletablePost] from this [Post]. */ + abstract fun asDeletable(): DeletablePost + + /** + * Creates a [DeletablePost] from this [Post] if its [author] is identified by the + * [authenticated][Actor.Authenticated] [Actor]'s ID; otherwise, returns this one. + * + * @param authenticationLock [AuthenticationLock] by which the ID of the [Actor] will be provided + * and compared to the [author]'s. + * @see Actor.Authenticated.id + */ + internal suspend fun asDeletableOrThis(authenticationLock: SomeAuthenticationLock): Post { + /** + * TODO: Not require the actor to be authenticated in order to return either this post or a + * deletable one from it. May be troublesome when allowing unauthenticated ones to browse + * through the federated feed. + */ + return authenticationLock.scheduleUnlock { if (it.id == author.id) asDeletable() else this } + } + companion object } diff --git a/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/PostProvider.kt b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/PostProvider.kt index f7796feab..f4bf5c5d3 100644 --- a/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/PostProvider.kt +++ b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/PostProvider.kt @@ -15,15 +15,34 @@ package com.jeanbarrossilva.orca.core.feed.profile.post +import com.jeanbarrossilva.orca.core.auth.AuthenticationLock +import com.jeanbarrossilva.orca.core.auth.SomeAuthenticationLock import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** Provides [Post]s. */ -interface PostProvider { +abstract class PostProvider { + /** + * [AuthenticationLock] for distinguishing standard [Post]s from those that can be deleted when + * providing them. + */ + protected abstract val authenticationLock: SomeAuthenticationLock + /** * Provides the [Post] identified as [id]. * * @param id ID of the [Post] to be provided. * @see Post.id */ - suspend fun provide(id: String): Flow + suspend fun provide(id: String): Flow { + return onProvide(id).map { it.asDeletableOrThis(authenticationLock) } + } + + /** + * Callback to be called when a [Post] identified as [id] is requested to be provided. + * + * @param id ID of the [Post] to be provided. + * @see Post.id + */ + protected abstract suspend fun onProvide(id: String): Flow } diff --git a/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/repost/Repost.extensions.kt b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/repost/Repost.extensions.kt index 2e887ceb1..0ed34b523 100644 --- a/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/repost/Repost.extensions.kt +++ b/core/src/main/java/com/jeanbarrossilva/orca/core/feed/profile/post/repost/Repost.extensions.kt @@ -17,6 +17,7 @@ package com.jeanbarrossilva.orca.core.feed.profile.post.repost import com.jeanbarrossilva.orca.core.feed.profile.Profile import com.jeanbarrossilva.orca.core.feed.profile.post.Author +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.feed.profile.post.stat.Stat @@ -41,7 +42,9 @@ fun Repost(original: Post, reblogger: Author): Repost { original.favorite, original.repost, original.url - ) + ) { + original.asDeletable() + } } /** @@ -56,6 +59,7 @@ fun Repost(original: Post, reblogger: Author): Repost { * @param favorite [Stat] for the [Post]'s favorites. * @param reblog [Stat] for the [Post]'s reblogs. * @param url [URL] that leads to the [Post]. + * @param asDeletable Creates a [DeletablePost] from this [Repost]. */ fun Repost( id: String, @@ -66,7 +70,8 @@ fun Repost( comment: Stat, favorite: ToggleableStat, reblog: ToggleableStat, - url: URL + url: URL, + asDeletable: (Repost) -> DeletablePost ): Repost { return object : Repost() { override val id = id @@ -78,5 +83,9 @@ fun Repost( override val favorite = favorite override val repost = reblog override val url = url + + override fun asDeletable(): DeletablePost { + return asDeletable(this) + } } } diff --git a/core/src/test/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLockTests.kt b/core/src/test/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLockTests.kt index d9a706f30..eeece8bae 100644 --- a/core/src/test/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLockTests.kt +++ b/core/src/test/java/com/jeanbarrossilva/orca/core/auth/AuthenticationLockTests.kt @@ -1,47 +1,52 @@ -/* - * Copyright © 2023 Orca - * - * This program is free software: you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without - * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program. If - * not, see https://www.gnu.org/licenses. - */ - package com.jeanbarrossilva.orca.core.auth +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isTrue import com.jeanbarrossilva.orca.core.test.TestActorProvider import com.jeanbarrossilva.orca.core.test.TestAuthenticationLock import com.jeanbarrossilva.orca.core.test.TestAuthenticator import kotlin.test.Test -import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest internal class AuthenticationLockTests { @Test - fun `GIVEN an unauthenticated actor WHEN unlocking THEN it's authenticated`() { + fun authenticatesWhenUnlockingWithUnauthenticatedActor() { var hasBeenAuthenticated = false val authenticator = TestAuthenticator { hasBeenAuthenticated = true } - runTest { TestAuthenticationLock(authenticator = authenticator).requestUnlock {} } - assertTrue(hasBeenAuthenticated) + runTest { TestAuthenticationLock(authenticator = authenticator).scheduleUnlock {} } + assertThat(hasBeenAuthenticated).isTrue() } @Test - fun `GIVEN an authenticated actor WHEN unlocking THEN the listener is notified`() { + fun unlocksWhenActorIsAuthenticated() { val actorProvider = TestActorProvider() val authenticator = TestAuthenticator(actorProvider = actorProvider) var hasListenerBeenNotified = false runTest { authenticator.authenticate() - TestAuthenticationLock(actorProvider, authenticator).requestUnlock { + TestAuthenticationLock(actorProvider, authenticator).scheduleUnlock { hasListenerBeenNotified = true } } - assertTrue(hasListenerBeenNotified) + assertThat(hasListenerBeenNotified).isTrue() + } + + @Test + fun schedulesUnlocksForWhenOngoingOnesAreFinished() { + val lock = TestAuthenticationLock() + runTest { + lock.scheduleUnlock { delay(32.seconds) } + repeat(1_024) { lock.scheduleUnlock { delay(8.seconds) } } + assertThat( + @OptIn(ExperimentalCoroutinesApi::class) + testScheduler.currentTime.milliseconds.inWholeSeconds + ) + .isEqualTo(8_224) + } } } diff --git a/core/src/test/java/com/jeanbarrossilva/orca/core/feed/FeedProviderTests.kt b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/FeedProviderTests.kt index d77ced561..78a4fa1ad 100644 --- a/core/src/test/java/com/jeanbarrossilva/orca/core/feed/FeedProviderTests.kt +++ b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/FeedProviderTests.kt @@ -17,8 +17,9 @@ package com.jeanbarrossilva.orca.core.feed import app.cash.turbine.test import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.SampleTermMuter -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.samples +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertFailsWith @@ -74,14 +75,16 @@ internal class FeedProviderTests { override val termMuter = SampleTermMuter() override suspend fun onProvide(userID: String, page: Int): Flow> { - return flowOf(Post.samples) + return flowOf(Posts.withSamples) } override suspend fun containsUser(userID: String): Boolean { return true } } - runTest { assertContentEquals(Post.samples, provider.provide(userID = "🥸", page = 0).first()) } + runTest { + assertContentEquals(Posts.withSamples, provider.provide(userID = "🥸", page = 0).first()) + } } @Test @@ -92,7 +95,7 @@ internal class FeedProviderTests { override val termMuter = termMuter override suspend fun onProvide(userID: String, page: Int): Flow> { - return flowOf(Post.samples.take(1)) + return flowOf(Posts.withSamples.take(1)) } override suspend fun containsUser(userID: String): Boolean { @@ -100,7 +103,7 @@ internal class FeedProviderTests { } } runTest { - Post.samples.first().content.text.split(' ').take(2).forEach { termMuter.mute(it) } + Posts.withSamples.first().content.text.split(' ').take(2).forEach { termMuter.mute(it) } provider.provide(userID = "😶‍🌫️", page = 0).test { assertContentEquals(emptyList(), awaitItem()) awaitComplete() diff --git a/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/account/reblog/RepostTests.kt b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/account/reblog/RepostTests.kt index 83517b78c..163ff3f6e 100644 --- a/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/account/reblog/RepostTests.kt +++ b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/account/reblog/RepostTests.kt @@ -15,42 +15,59 @@ package com.jeanbarrossilva.orca.core.feed.profile.account.reblog +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost import com.jeanbarrossilva.orca.core.feed.profile.post.repost.Repost -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.repost.sample +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples import kotlin.test.Test import kotlin.test.assertEquals internal class RepostTests { + private val sampleRepost + get() = Posts.withSamples.filterIsInstance().first() + @Test fun createsRepost() { assertEquals( object : Repost() { - override val id = Repost.sample.id - override val author = Repost.sample.author - override val reposter = Repost.sample.reposter - override val content = Repost.sample.content - override val publicationDateTime = Repost.sample.publicationDateTime - override val comment = Repost.sample.comment - override val favorite = Repost.sample.favorite - override val repost = Repost.sample.repost - override val url = Repost.sample.url + override val id = sampleRepost.id + override val author = sampleRepost.author + override val reposter = sampleRepost.reposter + override val content = sampleRepost.content + override val publicationDateTime = sampleRepost.publicationDateTime + override val comment = sampleRepost.comment + override val favorite = sampleRepost.favorite + override val repost = sampleRepost.repost + override val url = sampleRepost.url + + override fun asDeletable(): DeletablePost { + return let { + object : DeletablePost(it) { + override suspend fun delete() {} + } + } + } }, Repost( - Repost.sample.id, - Repost.sample.author, - Repost.sample.reposter, - Repost.sample.content, - Repost.sample.publicationDateTime, - Repost.sample.comment, - Repost.sample.favorite, - Repost.sample.repost, - Repost.sample.url - ) + sampleRepost.id, + sampleRepost.author, + sampleRepost.reposter, + sampleRepost.content, + sampleRepost.publicationDateTime, + sampleRepost.comment, + sampleRepost.favorite, + sampleRepost.repost, + sampleRepost.url + ) { + object : DeletablePost(it) { + override suspend fun delete() {} + } + } ) } @Test fun createsRepostFromOriginalPost() { - assertEquals(Repost.sample, Repost(Repost.sample, Repost.sample.reposter)) + assertEquals(sampleRepost, Repost(sampleRepost, sampleRepost.reposter)) } } diff --git a/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/post/DeletablePostTests.kt b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/post/DeletablePostTests.kt new file mode 100644 index 000000000..1a643ec0a --- /dev/null +++ b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/post/DeletablePostTests.kt @@ -0,0 +1,62 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.core.feed.profile.post + +import assertk.assertThat +import assertk.assertions.isSameAs +import assertk.assertions.isTrue +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSample +import com.jeanbarrossilva.orca.core.test.TestAuthenticationLock +import com.jeanbarrossilva.testing.hasPropertiesEqualToThoseOf +import kotlin.test.Test +import kotlinx.coroutines.test.runTest + +internal class DeletablePostTests { + @Test + fun delegatesNonDeletionFunctionalityToDelegate() { + assertThat( + object : DeletablePost(Posts.withSample.single()) { + override suspend fun delete() {} + } + ) + .hasPropertiesEqualToThoseOf(Posts.withSample) + } + + @Test + fun returnsItselfWhenConvertingItIntoDeletablePost() { + val authenticationLock = TestAuthenticationLock() + val post = + object : DeletablePost(Posts.withSample.single()) { + override suspend fun delete() {} + } + runTest { assertThat(post.asDeletableOrThis(authenticationLock)).isSameAs(post) } + } + + @Test + fun isDeleted() { + var hasBeenDeleted = false + runTest { + object : DeletablePost(Posts.withSample.single()) { + override suspend fun delete() { + hasBeenDeleted = true + } + } + .delete() + } + assertThat(hasBeenDeleted).isTrue() + } +} diff --git a/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/post/test/TestPost.kt b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/post/test/TestPost.kt index 6ea8d22c6..b6abf0006 100644 --- a/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/post/test/TestPost.kt +++ b/core/src/test/java/com/jeanbarrossilva/orca/core/feed/profile/post/test/TestPost.kt @@ -17,22 +17,33 @@ package com.jeanbarrossilva.orca.core.feed.profile.post.test import com.jeanbarrossilva.orca.core.feed.profile.Profile import com.jeanbarrossilva.orca.core.feed.profile.post.Author +import com.jeanbarrossilva.orca.core.feed.profile.post.DeletablePost import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.feed.profile.post.stat.Stat import com.jeanbarrossilva.orca.core.feed.profile.post.stat.toggleable.ToggleableStat -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSample import java.net.URL import java.time.ZonedDateTime -/** Local [Post] that defaults its properties' values to [Post.Companion.sample]'s. */ +/** Local [Post] that defaults its properties' values to [Posts.Companion.withSample]'s sample. */ internal class TestPost( - override val id: String = Post.sample.id, - override val author: Author = Post.sample.author, - override val content: Content = Post.sample.content, - override val publicationDateTime: ZonedDateTime = Post.sample.publicationDateTime, - override val comment: Stat = Post.sample.comment, - override val favorite: ToggleableStat = Post.sample.favorite, - override val repost: ToggleableStat = Post.sample.repost, - override val url: URL = Post.sample.url -) : Post() + override val id: String = delegate.id, + override val author: Author = delegate.author, + override val content: Content = delegate.content, + override val publicationDateTime: ZonedDateTime = delegate.publicationDateTime, + override val comment: Stat = delegate.comment, + override val favorite: ToggleableStat = delegate.favorite, + override val repost: ToggleableStat = delegate.repost, + override val url: URL = delegate.url +) : Post() { + override fun asDeletable(): DeletablePost { + return delegate.asDeletable() + } + + companion object { + /** [Post] to which a [TestPost]'s functionality is delegated. */ + private val delegate = Posts.withSample.single() + } +} diff --git a/ext/coroutines/src/main/java/com/jeanbarrossilva/orca/ext/coroutines/Flow.extensions.kt b/ext/coroutines/src/main/java/com/jeanbarrossilva/orca/ext/coroutines/Flow.extensions.kt new file mode 100644 index 000000000..3b0285026 --- /dev/null +++ b/ext/coroutines/src/main/java/com/jeanbarrossilva/orca/ext/coroutines/Flow.extensions.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.ext.coroutines + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Maps each element of the emitted [Collection]s to the result of [transform]. + * + * @param transform Transformation to be made to the currently iterated element. + */ +fun Flow>.mapEach(transform: suspend (I) -> O): Flow> { + return map { elements -> elements.map { element -> transform(element) } } +} diff --git a/ext/coroutines/src/test/java/com/jeanbarrossilva/orca/ext/coroutines/FlowExtensionsTests.kt b/ext/coroutines/src/test/java/com/jeanbarrossilva/orca/ext/coroutines/FlowExtensionsTests.kt new file mode 100644 index 000000000..4d802db2b --- /dev/null +++ b/ext/coroutines/src/test/java/com/jeanbarrossilva/orca/ext/coroutines/FlowExtensionsTests.kt @@ -0,0 +1,37 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.ext.coroutines + +import app.cash.turbine.test +import assertk.assertThat +import assertk.assertions.containsExactly +import kotlin.test.Test +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest + +internal class FlowExtensionsTests { + @Test + fun mapsEach() { + runTest { + flowOf(listOf(2, 4)) + .mapEach { it * it } + .test { + assertThat(awaitItem()).containsExactly(4, 16) + awaitComplete() + } + } + } +} diff --git a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingBoundary.kt b/ext/testing/build.gradle.kts similarity index 76% rename from feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingBoundary.kt rename to ext/testing/build.gradle.kts index 31c8207a8..333710ed0 100644 --- a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingBoundary.kt +++ b/ext/testing/build.gradle.kts @@ -13,8 +13,17 @@ * not, see https://www.gnu.org/licenses. */ -package com.jeanbarrossilva.orca.feature.settings.termmuting +plugins { + alias(libs.plugins.kotlin.jvm) -interface TermMutingBoundary { - fun pop() + `java-library` +} + +dependencies { + api(libs.assertk) + + implementation(libs.kotlin.reflect) + implementation(libs.openTest4J) + + testImplementation(libs.kotlin.test) } diff --git a/ext/testing/src/main/java/com/jeanbarrossilva/testing/Assert.extensions.kt b/ext/testing/src/main/java/com/jeanbarrossilva/testing/Assert.extensions.kt new file mode 100644 index 000000000..482ef2e9a --- /dev/null +++ b/ext/testing/src/main/java/com/jeanbarrossilva/testing/Assert.extensions.kt @@ -0,0 +1,79 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.testing + +import assertk.Assert +import assertk.assertions.support.expected +import assertk.assertions.support.show +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.full.declaredMemberProperties +import org.opentest4j.AssertionFailedError + +/** + * Asserts that the value's properties have the same name and hold data equal to [other]'s. It will + * not fail if it has ones that aren't present in [other], vice-versa or none at all, only if the + * ones that are named equally have been assigned differently. + * + * @param F Value on which the assertion will be performed. + * @param S Value to be compared to [F]. + * @param other Object whose properties will be compared to the value's. + * @throws AssertionFailedError If a property of the value has a counterpart in [other] but has + * different data assigned to it. + */ +@Throws(AssertionFailedError::class) +inline fun Assert.hasPropertiesEqualToThoseOf(other: S): Assert { + given { value -> + value::class + .declaredMemberProperties + .associateWithEquallyNamedDeclaredProperties<_, S>() + .mapKeys { (expectedProperty, _) -> expectedProperty.get(value) } + .forEach { (expected, actualProperty) -> + expected( + expected, + actual = actualProperty.get(other), + actualProperty.name, + actualParentClass = other::class + ) + } + } + return this +} + +/** + * Fails the assertion if [expected] isn't equal to [actual], exposing [actual]'s [KProperty] name + * and its parent [KClass] in the error message for a more detailed description of what was + * mismatched. + * + * @param expected Value that [actual] is expected to equal to. + * @param actual Data held by the [KProperty] that should equal to the [expected] one. + * @param actualName Name of [actual]'s [KProperty]. + * @param actualParentClass Name of the [KClass] in which [actual]'s [KProperty] is declared. + * @throws AssertionFailedError If [expected] isn't equal to [actual]. + */ +@PublishedApi +internal fun Assert<*>.expected( + expected: Any?, + actual: Any?, + actualName: String, + actualParentClass: KClass<*> +) { + if (expected != actual) { + expected( + "${actualParentClass.simpleName}.$actualName to be:${show(expected)} but was:${show(actual)}" + ) + } +} diff --git a/ext/testing/src/main/java/com/jeanbarrossilva/testing/Collection.extensions.kt b/ext/testing/src/main/java/com/jeanbarrossilva/testing/Collection.extensions.kt new file mode 100644 index 000000000..41a31fa1c --- /dev/null +++ b/ext/testing/src/main/java/com/jeanbarrossilva/testing/Collection.extensions.kt @@ -0,0 +1,41 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.testing + +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties + +/** + * Associates each of these properties with the declared ones of [S] that have the same name as + * them. + * + * @param F Value whose properties will be associated to those of [S] with the same name. + * @param S Value whose properties will be associated to those of [F] with the same name. + */ +@PublishedApi +internal inline fun Collection> + .associateWithEquallyNamedDeclaredProperties(): Map, KProperty1> { + return associateWith { firstsProperty -> + S::class.declaredMemberProperties.firstOrNull { secondsProperty -> + secondsProperty.name == firstsProperty.name + } + } + .filterValues { it != null } + .let { + @Suppress("UNCHECKED_CAST") + it as Map, KProperty1> + } +} diff --git a/ext/testing/src/test/java/com/jeanbarrossilva/orca/ext/testing/AssertExtensionsTests.kt b/ext/testing/src/test/java/com/jeanbarrossilva/orca/ext/testing/AssertExtensionsTests.kt new file mode 100644 index 000000000..cec7dc23a --- /dev/null +++ b/ext/testing/src/test/java/com/jeanbarrossilva/orca/ext/testing/AssertExtensionsTests.kt @@ -0,0 +1,71 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.ext.testing + +import assertk.assertThat +import com.jeanbarrossilva.testing.hasPropertiesEqualToThoseOf +import kotlin.test.Test +import org.opentest4j.AssertionFailedError + +internal class AssertExtensionsTests { + @Test + fun passesWhenAssertingThatAnObjectHasPropertiesEqualToThoseOfAnotherOneWhenItDoes() { + assertThat( + object { + @Suppress("unused") val msg = "🇮🇹" + } + ) + .hasPropertiesEqualToThoseOf( + object { + @Suppress("unused") val msg = "🇮🇹" + } + ) + } + + @Test + fun passesWhenAssertingThatAnObjectHasPropertiesEqualToThoseOfAnotherOneWhenTheFormerDoesNotHaveProperties() { + assertThat(object {}) + .hasPropertiesEqualToThoseOf( + object { + @Suppress("unused") val msg = "🇮🇹" + } + ) + } + + @Test + fun passesWhenAssertingThatAnObjectHasPropertiesEqualToThoseOfAnotherOneWhenTheLatterDoesNotHaveProperties() { + assertThat(object {}) + .hasPropertiesEqualToThoseOf( + object { + @Suppress("unused") val msg = "🇮🇹" + } + ) + } + + @Test(expected = AssertionFailedError::class) + fun failsWhenAssertingThatAnObjectHasPropertiesEqualToThoseOfAnotherOneWhenItDoesNot() { + assertThat( + object { + @Suppress("unused") val msg = "🇮🇹" + } + ) + .hasPropertiesEqualToThoseOf( + object { + @Suppress("unused") val msg = "🇳🇴" + } + ) + } +} diff --git a/feature/composer/src/main/java/com/jeanbarrossilva/orca/feature/composer/Composer.kt b/feature/composer/src/main/java/com/jeanbarrossilva/orca/feature/composer/Composer.kt index 83d1666ab..6ab49d7e2 100644 --- a/feature/composer/src/main/java/com/jeanbarrossilva/orca/feature/composer/Composer.kt +++ b/feature/composer/src/main/java/com/jeanbarrossilva/orca/feature/composer/Composer.kt @@ -56,7 +56,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp -import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.feature.composer.ui.Toolbar import com.jeanbarrossilva.orca.platform.autos.colors.asColor import com.jeanbarrossilva.orca.platform.autos.iconography.asImageVector @@ -70,8 +70,8 @@ import com.jeanbarrossilva.orca.platform.autos.overlays.asPaddingValues import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme import com.jeanbarrossilva.orca.platform.autos.theme.MultiThemePreview import com.jeanbarrossilva.orca.platform.ui.core.requestFocusWithDelay -import com.jeanbarrossilva.orca.platform.ui.core.sample import com.jeanbarrossilva.orca.platform.ui.core.style.toAnnotatedString +import com.jeanbarrossilva.orca.platform.ui.core.withSample internal const val COMPOSER_FIELD = "composer-field" @@ -243,7 +243,7 @@ private fun EmptyWithToolbarComposerPreview() { private fun PopulatedWithoutToolbarComposerPreview() { AutosTheme { Composer( - TextFieldValue(Post.sample.content.text.toAnnotatedString()), + TextFieldValue(Posts.withSample.single().content.text.toAnnotatedString()), isInitiallyFocused = false ) } @@ -254,7 +254,7 @@ private fun PopulatedWithoutToolbarComposerPreview() { private fun PopulatedWithToolbarComposerPreview() { AutosTheme { Composer( - TextFieldValue(Post.sample.content.text.toAnnotatedString()), + TextFieldValue(Posts.withSample.single().content.text.toAnnotatedString()), isInitiallyFocused = true ) } diff --git a/feature/feed/build.gradle.kts b/feature/feed/build.gradle.kts index 251498ee5..ff4dad51b 100644 --- a/feature/feed/build.gradle.kts +++ b/feature/feed/build.gradle.kts @@ -27,6 +27,7 @@ android { dependencies { androidTestImplementation(project(":core:sample-test")) + androidTestImplementation(project(":platform:testing")) androidTestImplementation(project(":platform:ui-test")) androidTestImplementation(project(":std:injector-test")) androidTestImplementation(libs.android.compose.ui.test.junit) diff --git a/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/FeedFragmentTests.kt b/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/FeedFragmentTests.kt index 01e81fe5f..94afaf127 100644 --- a/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/FeedFragmentTests.kt +++ b/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/FeedFragmentTests.kt @@ -28,13 +28,13 @@ import androidx.compose.ui.test.performScrollTo import androidx.test.core.app.launchActivity import com.jeanbarrossilva.orca.core.auth.actor.Actor import com.jeanbarrossilva.orca.core.feed.profile.Profile -import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.instance.Instance import com.jeanbarrossilva.orca.core.sample.auth.actor.sample +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts +import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.withSamples import com.jeanbarrossilva.orca.core.sample.test.instance.SampleInstanceTestRule import com.jeanbarrossilva.orca.feature.feed.test.FeedActivity import com.jeanbarrossilva.orca.feature.feed.test.TestFeedModule -import com.jeanbarrossilva.orca.feature.feed.test.samples import com.jeanbarrossilva.orca.platform.ui.component.stat.favorite.FAVORITE_STAT_TAG import com.jeanbarrossilva.orca.platform.ui.core.sample import com.jeanbarrossilva.orca.platform.ui.test.component.timeline.onTimeline @@ -62,7 +62,7 @@ internal class FeedFragmentTests { .performScrollToBottom() .onChildren() .filter(isPostPreview()) - .assertCountEquals(Post.samples.size) + .assertCountEquals(Posts.withSamples.size) } } diff --git a/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/test/FeedActivity.kt b/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/test/FeedActivity.kt index 76f4f8010..fc17a2dcb 100644 --- a/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/test/FeedActivity.kt +++ b/feature/feed/src/androidTest/java/com/jeanbarrossilva/orca/feature/feed/test/FeedActivity.kt @@ -17,8 +17,8 @@ package com.jeanbarrossilva.orca.feature.feed.test import android.content.Intent import androidx.navigation.NavGraphBuilder -import androidx.test.platform.app.InstrumentationRegistry import com.jeanbarrossilva.orca.feature.feed.FeedFragment +import com.jeanbarrossilva.orca.platform.testing.context import com.jeanbarrossilva.orca.platform.ui.core.Intent import com.jeanbarrossilva.orca.platform.ui.test.core.SingleFragmentActivity @@ -31,7 +31,6 @@ internal class FeedActivity : SingleFragmentActivity() { companion object { fun getIntent(userID: String): Intent { - val context = InstrumentationRegistry.getInstrumentation().context return Intent(context, FeedFragment.USER_ID_KEY to userID) } } diff --git a/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/Feed.kt b/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/Feed.kt index c6caeff1e..e6ce44423 100644 --- a/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/Feed.kt +++ b/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/Feed.kt @@ -49,6 +49,7 @@ import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.PostPreview import com.jeanbarrossilva.orca.platform.ui.component.timeline.refresh.Refresh import java.net.URL +const val FEED_SEARCH_ACTION_TAG = "feed-search-action-tag" const val FEED_FLOATING_ACTION_BUTTON_TAG = "feed-floating-action-button" @Composable @@ -130,7 +131,7 @@ private fun Feed( TopAppBar( title = { AutoSizeText(stringResource(R.string.feature_feed)) }, actions = { - HoverableIconButton(onClick = onSearch) { + HoverableIconButton(onClick = onSearch, Modifier.testTag(FEED_SEARCH_ACTION_TAG)) { Icon( AutosTheme.iconography.search.asImageVector, contentDescription = stringResource(R.string.feature_feed_search) diff --git a/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/FeedFragment.kt b/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/FeedFragment.kt index d8f9a4037..926394150 100644 --- a/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/FeedFragment.kt +++ b/feature/feed/src/main/java/com/jeanbarrossilva/orca/feature/feed/FeedFragment.kt @@ -56,5 +56,7 @@ class FeedFragment internal constructor() : ComposableFragment(), ContextProvide companion object { internal const val USER_ID_KEY = "user-id" + + const val ROUTE = "feed" } } diff --git a/feature/gallery-test/build.gradle.kts b/feature/gallery-test/build.gradle.kts new file mode 100644 index 000000000..ac1ddb37e --- /dev/null +++ b/feature/gallery-test/build.gradle.kts @@ -0,0 +1,35 @@ +import com.jeanbarrossilva.orca.namespaceFor + +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + buildFeatures.compose = true + composeOptions.kotlinCompilerExtensionVersion = libs.versions.android.compose.compiler.get() + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + namespace = namespaceFor("feature.gallery.test") +} + +dependencies { + implementation(project(":feature:gallery")) + implementation(project(":platform:autos")) + implementation(libs.android.compose.ui.test.junit) + implementation(libs.android.compose.ui.test.manifest) +} diff --git a/feature/gallery-test/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/ui/SemanticsNodeInteractionsProviderExtensionsTests.kt b/feature/gallery-test/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/ui/SemanticsNodeInteractionsProviderExtensionsTests.kt new file mode 100644 index 000000000..f552887b1 --- /dev/null +++ b/feature/gallery-test/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/ui/SemanticsNodeInteractionsProviderExtensionsTests.kt @@ -0,0 +1,35 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.feature.gallery.test.ui + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import com.jeanbarrossilva.orca.feature.gallery.ui.Actions +import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme +import org.junit.Rule +import org.junit.Test + +internal class SemanticsNodeInteractionsProviderExtensionsTests { + @get:Rule val composeRule = createComposeRule() + + @Test + fun findsCloseActionButton() { + composeRule + .apply { setContent { AutosTheme { Actions() } } } + .onCloseActionButton() + .assertIsDisplayed() + } +} diff --git a/feature/gallery-test/src/main/AndroidManifest.xml b/feature/gallery-test/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0a88e5709 --- /dev/null +++ b/feature/gallery-test/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/feature/gallery-test/src/main/java/com/jeanbarrossilva/orca/feature/gallery/test/ui/SemanticsNodeInteractionsProvider.extensions.kt b/feature/gallery-test/src/main/java/com/jeanbarrossilva/orca/feature/gallery/test/ui/SemanticsNodeInteractionsProvider.extensions.kt new file mode 100644 index 000000000..f1216169a --- /dev/null +++ b/feature/gallery-test/src/main/java/com/jeanbarrossilva/orca/feature/gallery/test/ui/SemanticsNodeInteractionsProvider.extensions.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.feature.gallery.test.ui + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.onNodeWithTag +import com.jeanbarrossilva.orca.feature.gallery.ui.Actions +import com.jeanbarrossilva.orca.feature.gallery.ui.GALLERY_ACTIONS_CLOSE_BUTTON_TAG +import com.jeanbarrossilva.orca.platform.autos.kit.action.button.icon.HoverableIconButton + +/** [SemanticsNodeInteraction] of a gallery's [Actions]' close [HoverableIconButton]. */ +fun SemanticsNodeInteractionsProvider.onCloseActionButton(): SemanticsNodeInteraction { + return onNodeWithTag(GALLERY_ACTIONS_CLOSE_BUTTON_TAG) +} diff --git a/feature/gallery/build.gradle.kts b/feature/gallery/build.gradle.kts index 5559d5529..7ec5434cb 100644 --- a/feature/gallery/build.gradle.kts +++ b/feature/gallery/build.gradle.kts @@ -28,6 +28,8 @@ android { dependencies { androidTestImplementation(project(":core:sample-test")) + androidTestImplementation(project(":feature:gallery-test")) + androidTestImplementation(project(":platform:testing")) androidTestImplementation(project(":platform:ui-test")) androidTestImplementation(project(":std:injector-test")) androidTestImplementation(libs.android.compose.ui.test.junit) diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivityTests.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivityTests.kt index d2d522537..b54f8f282 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivityTests.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivityTests.kt @@ -24,12 +24,13 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.compose.ui.test.onRoot import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.test.platform.app.InstrumentationRegistry import com.jeanbarrossilva.orca.feature.gallery.test.activity.TestGalleryModule import com.jeanbarrossilva.orca.feature.gallery.test.launchGalleryActivity import com.jeanbarrossilva.orca.feature.gallery.ui.test.onPager import com.jeanbarrossilva.orca.feature.gallery.ui.test.performScrollToEachPage +import com.jeanbarrossilva.orca.platform.testing.asString +import com.jeanbarrossilva.orca.platform.testing.context +import com.jeanbarrossilva.orca.platform.testing.screen.screen import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.formatted import com.jeanbarrossilva.orca.std.injector.module.binding.boundTo import com.jeanbarrossilva.orca.std.injector.test.InjectorTestRule @@ -37,9 +38,6 @@ import org.junit.Rule import org.junit.Test internal class GalleryActivityTests { - private val context - get() = InstrumentationRegistry.getInstrumentation().context - @get:Rule val injectorRule = InjectorTestRule { register(TestGalleryModule.boundTo()) } @@ -55,7 +53,6 @@ internal class GalleryActivityTests { .also { context.theme.resolveAttribute(android.R.attr.actionBarSize, it, true) } .let { resources.getDimensionPixelSize(it.resourceId) } .let { with(density) { it.toDp() } } - val configuration = resources.configuration launchGalleryActivity().use { scenario -> scenario.onActivity { activity -> activity.windowManager @@ -66,10 +63,8 @@ internal class GalleryActivityTests { } composeRule .onRoot() - .assertWidthIsEqualTo(configuration.screenWidthDp.dp) - .assertHeightIsEqualTo( - configuration.screenHeightDp.dp + (systemBarsHeight - actionBarHeight) - ) + .assertWidthIsEqualTo(screen.width.inDps) + .assertHeightIsEqualTo(screen.height.inDps + (systemBarsHeight - actionBarHeight)) } } @@ -78,9 +73,7 @@ internal class GalleryActivityTests { launchGalleryActivity().use { with(composeRule) { onPager().performScrollToEachPage { - assertContentDescriptionEquals( - context.getString(R.string.feature_gallery_attachment, it.formatted) - ) + assertContentDescriptionEquals(R.string.feature_gallery_attachment.asString(it.formatted)) } } } diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/ActivityScenario.extensions.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/ActivityScenario.extensions.kt index 04e164dc8..40ea7c71b 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/ActivityScenario.extensions.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/ActivityScenario.extensions.kt @@ -22,16 +22,17 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.test.core.app.ActivityScenario import androidx.test.core.app.launchActivity -import androidx.test.platform.app.InstrumentationRegistry import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Attachment +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.samples import com.jeanbarrossilva.orca.core.sample.image.CoverImageSource -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample import com.jeanbarrossilva.orca.feature.gallery.GalleryActivity import com.jeanbarrossilva.orca.feature.gallery.ui.Gallery +import com.jeanbarrossilva.orca.platform.testing.context import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.formatted import com.jeanbarrossilva.orca.platform.ui.core.image.createSample +import com.jeanbarrossilva.orca.platform.ui.core.withSample import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader /** @@ -43,7 +44,7 @@ import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader * @param entrypoint Entrypoint page of the [Gallery]. */ internal fun launchGalleryActivity( - postID: String = Post.sample.id, + postID: String = Posts.withSample.single().id, entrypointIndex: Int = 0, secondary: List = Attachment.samples, entrypoint: @Composable (Modifier) -> Unit = { @@ -58,7 +59,6 @@ internal fun launchGalleryActivity( ) } ): ActivityScenario { - val context = InstrumentationRegistry.getInstrumentation().context val intent = GalleryActivity.getIntent(context, postID, entrypointIndex, secondary) return launchActivity(intent).onActivity { it.setEntrypoint(entrypoint) } } diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/activity/TestGalleryBoundary.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/activity/TestGalleryBoundary.kt index b8e319a2e..9bb12cc39 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/activity/TestGalleryBoundary.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/test/activity/TestGalleryBoundary.kt @@ -19,6 +19,4 @@ import com.jeanbarrossilva.orca.feature.gallery.GalleryBoundary internal object TestGalleryBoundary : GalleryBoundary { override fun navigateToPostDetails(id: String) {} - - override fun pop() {} } diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/GalleryTests.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/GalleryTests.kt index a71b3f065..6ec26bed6 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/GalleryTests.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/GalleryTests.kt @@ -24,8 +24,8 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import assertk.assertThat import assertk.assertions.isTrue +import com.jeanbarrossilva.orca.feature.gallery.test.ui.onCloseActionButton import com.jeanbarrossilva.orca.feature.gallery.ui.test.onActions -import com.jeanbarrossilva.orca.feature.gallery.ui.test.onCloseActionButton import com.jeanbarrossilva.orca.feature.gallery.ui.test.onDownloadItem import com.jeanbarrossilva.orca.feature.gallery.ui.test.onOptionsButton import com.jeanbarrossilva.orca.feature.gallery.ui.test.onOptionsMenu diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionExtensionsTests.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionExtensionsTests.kt index 8a545edd3..82199bbda 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionExtensionsTests.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionExtensionsTests.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.test.isRoot import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot -import androidx.test.platform.app.InstrumentationRegistry import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse @@ -39,6 +38,7 @@ import com.jeanbarrossilva.orca.feature.gallery.ui.test.onPager import com.jeanbarrossilva.orca.feature.gallery.ui.test.performScrollToEachPage import com.jeanbarrossilva.orca.feature.gallery.ui.test.performScrollToPageAt import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme +import com.jeanbarrossilva.orca.platform.testing.asString import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.formatted import org.junit.Rule import org.junit.Test @@ -73,30 +73,24 @@ internal class SemanticsNodeInteractionExtensionsTests { @Test fun scrollsToPageOfGalleryPager() { - val context = InstrumentationRegistry.getInstrumentation().context composeRule .apply { setContent { AutosTheme { Gallery() } } } .run { onPager().performScrollToPageAt(0) onPage() } - .assertContentDescriptionEquals( - context.getString(R.string.feature_gallery_attachment, 2.formatted) - ) + .assertContentDescriptionEquals(R.string.feature_gallery_attachment.asString(2.formatted)) } @Test fun scrollsToEachPageOfGalleryPager() { - val context = InstrumentationRegistry.getInstrumentation().context var positions = IntRange.EMPTY composeRule .apply { setContent { AutosTheme { Gallery() } } } .run { onPager().performScrollToEachPage { positions = 1..it - assertContentDescriptionEquals( - context.getString(R.string.feature_gallery_attachment, it.formatted) - ) + assertContentDescriptionEquals(R.string.feature_gallery_attachment.asString(it.formatted)) } } assertThat(positions).isEqualTo(1..Attachment.samples.size.inc()) diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionsProviderExtensionsTests.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionsProviderExtensionsTests.kt index 4624123a5..eac8e433e 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionsProviderExtensionsTests.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/SemanticsNodeInteractionsProviderExtensionsTests.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.performClick import com.jeanbarrossilva.orca.feature.gallery.ui.test.onActions -import com.jeanbarrossilva.orca.feature.gallery.ui.test.onCloseActionButton import com.jeanbarrossilva.orca.feature.gallery.ui.test.onDownloadItem import com.jeanbarrossilva.orca.feature.gallery.ui.test.onOptionsButton import com.jeanbarrossilva.orca.feature.gallery.ui.test.onOptionsMenu @@ -36,14 +35,6 @@ internal class SemanticsNodeInteractionsProviderExtensionsTests { composeRule.apply { setContent { AutosTheme { Gallery() } } }.onActions().assertIsDisplayed() } - @Test - fun findsCloseActionButton() { - composeRule - .apply { setContent { AutosTheme { Actions() } } } - .onCloseActionButton() - .assertIsDisplayed() - } - @Test fun findsOptionsButton() { composeRule diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/SemanticsNodeInteractionsProvider.extensions.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/SemanticsNodeInteractionsProvider.extensions.kt index c485f2b3e..d71ba987d 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/SemanticsNodeInteractionsProvider.extensions.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/SemanticsNodeInteractionsProvider.extensions.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.onNodeWithTag import com.jeanbarrossilva.orca.feature.gallery.ui.Actions -import com.jeanbarrossilva.orca.feature.gallery.ui.GALLERY_ACTIONS_CLOSE_BUTTON_TAG import com.jeanbarrossilva.orca.feature.gallery.ui.GALLERY_ACTIONS_OPTIONS_BUTTON_TAG import com.jeanbarrossilva.orca.feature.gallery.ui.GALLERY_ACTIONS_OPTIONS_DOWNLOADS_ITEM_TAG import com.jeanbarrossilva.orca.feature.gallery.ui.GALLERY_ACTIONS_OPTIONS_MENU_TAG @@ -35,15 +34,6 @@ internal fun SemanticsNodeInteractionsProvider.onActions(): SemanticsNodeInterac return onNodeWithTag(GALLERY_ACTIONS_TAG) } -/** - * [SemanticsNodeInteraction] of a [Gallery]'s [Actions]' close [HoverableIconButton]. - * - * @see onActions - */ -internal fun SemanticsNodeInteractionsProvider.onCloseActionButton(): SemanticsNodeInteraction { - return onNodeWithTag(GALLERY_ACTIONS_CLOSE_BUTTON_TAG) -} - /** * [SemanticsNodeInteraction] of a [Gallery]'s [Actions]' download option [DropdownMenuItem]. * diff --git a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/zoom/SemanticsMatcher.extensions.kt b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/zoom/SemanticsMatcher.extensions.kt index 1d430dc24..07eaa7c6e 100644 --- a/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/zoom/SemanticsMatcher.extensions.kt +++ b/feature/gallery/src/androidTest/java/com/jeanbarrossilva/orca/feature/gallery/ui/test/zoom/SemanticsMatcher.extensions.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.getUnclippedBoundsInRoot import androidx.compose.ui.test.onParent import androidx.compose.ui.unit.Density -import androidx.test.platform.app.InstrumentationRegistry +import com.jeanbarrossilva.orca.platform.testing.context /** * [SemanticsMatcher] that matches a [SemanticsNode] whose [predicate] is `true`. @@ -38,7 +38,6 @@ internal fun unclippedBoundsMatcher( predicate: (parentBounds: Rect, bounds: Rect) -> Boolean ): SemanticsMatcher { return SemanticsMatcher(description) { - val context = InstrumentationRegistry.getInstrumentation().context val density = Density(context) val bounds = with(density) { getUnclippedBoundsInRoot().toRect() } val parentBounds = with(density) { onParent().getUnclippedBoundsInRoot().toRect() } diff --git a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivity.kt b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivity.kt index 644ed6a40..9046e4a57 100644 --- a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivity.kt +++ b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryActivity.kt @@ -54,7 +54,9 @@ class GalleryActivity internal constructor() : ComposableActivity() { @Composable override fun Content() { - Gallery(viewModel, module.boundary(), entrypointIndex, secondary) { entrypoint?.invoke(it) } + Gallery(viewModel, module.boundary(), entrypointIndex, secondary, onClose = ::finish) { + entrypoint?.invoke(it) + } } fun setEntrypoint(entrypoint: @Composable (Modifier) -> Unit) { diff --git a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryBoundary.kt b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryBoundary.kt index b4333ffd3..ae09e33e3 100644 --- a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryBoundary.kt +++ b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/GalleryBoundary.kt @@ -17,6 +17,4 @@ package com.jeanbarrossilva.orca.feature.gallery interface GalleryBoundary { fun navigateToPostDetails(id: String) - - fun pop() } diff --git a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Actions.kt b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Actions.kt index 810c509eb..8a59853e1 100644 --- a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Actions.kt +++ b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Actions.kt @@ -15,6 +15,7 @@ package com.jeanbarrossilva.orca.feature.gallery.ui +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -42,13 +43,31 @@ import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme import com.jeanbarrossilva.orca.platform.ui.component.stat.Stats import com.jeanbarrossilva.orca.platform.ui.component.stat.StatsDetails -internal const val GALLERY_ACTIONS_CLOSE_BUTTON_TAG = "gallery-actions-close-button" internal const val GALLERY_ACTIONS_OPTIONS_BUTTON_TAG = "gallery-actions-options-button" internal const val GALLERY_ACTIONS_OPTIONS_DOWNLOADS_ITEM_TAG = "gallery-actions-options-download-action" internal const val GALLERY_ACTIONS_OPTIONS_MENU_TAG = "gallery-actions-options-menu" internal const val GALLERY_ACTIONS_TAG = "gallery-actions" +const val GALLERY_ACTIONS_CLOSE_BUTTON_TAG = "gallery-actions-close-button" + +@Composable +@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) +fun Actions(modifier: Modifier = Modifier, areOptionsVisible: Boolean = false) { + Actions( + areOptionsVisible, + onOptionsVisibilityToggle = {}, + onDownload = {}, + StatsDetails.sample, + onComment = {}, + onFavorite = {}, + onRepost = {}, + onShare = {}, + onClose = {}, + modifier + ) +} + @Composable internal fun Actions( areOptionsVisible: Boolean, @@ -122,22 +141,6 @@ internal fun Actions( } } -@Composable -internal fun Actions(modifier: Modifier = Modifier, areOptionsVisible: Boolean = false) { - Actions( - areOptionsVisible, - onOptionsVisibilityToggle = {}, - onDownload = {}, - StatsDetails.sample, - onComment = {}, - onFavorite = {}, - onRepost = {}, - onShare = {}, - onClose = {}, - modifier - ) -} - @Composable @Preview private fun ActionsPreview() { diff --git a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Gallery.kt b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Gallery.kt index 585652a7d..1723e1b62 100644 --- a/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Gallery.kt +++ b/feature/gallery/src/main/java/com/jeanbarrossilva/orca/feature/gallery/ui/Gallery.kt @@ -52,6 +52,7 @@ internal fun Gallery( boundary: GalleryBoundary, entrypointIndex: Int, secondary: List, + onClose: () -> Unit, modifier: Modifier = Modifier, entrypoint: @Composable (Modifier) -> Unit ) { @@ -66,7 +67,7 @@ internal fun Gallery( onFavorite = viewModel::toggleFavorite, onRepost = viewModel::toggleRepost, onShare = viewModel::share, - onClose = boundary::pop, + onClose, modifier, entrypoint ) diff --git a/feature/post-details/src/main/java/com/jeanbarrossilva/orca/feature/postdetails/PostDetails.kt b/feature/post-details/src/main/java/com/jeanbarrossilva/orca/feature/postdetails/PostDetails.kt index 8851497a9..3c5672765 100644 --- a/feature/post-details/src/main/java/com/jeanbarrossilva/orca/feature/postdetails/PostDetails.kt +++ b/feature/post-details/src/main/java/com/jeanbarrossilva/orca/feature/postdetails/PostDetails.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.text.AnnotatedString import com.jeanbarrossilva.loadable.Loadable import com.jeanbarrossilva.loadable.list.ListLoadable import com.jeanbarrossilva.orca.core.feed.profile.account.Account -import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.feature.postdetails.ui.header.Header import com.jeanbarrossilva.orca.feature.postdetails.ui.header.formatted import com.jeanbarrossilva.orca.feature.postdetails.viewmodel.PostDetailsViewModel @@ -49,7 +49,7 @@ import com.jeanbarrossilva.orca.platform.ui.component.timeline.Timeline import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.PostPreview import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.Figure import com.jeanbarrossilva.orca.platform.ui.component.timeline.refresh.Refresh -import com.jeanbarrossilva.orca.platform.ui.core.sample +import com.jeanbarrossilva.orca.platform.ui.core.withSample import com.jeanbarrossilva.orca.std.image.SomeImageLoader import java.io.Serializable import java.net.URL @@ -72,7 +72,7 @@ internal data class PostDetails( companion object { val sample - @Composable get() = Post.sample.toPostDetails() + @Composable get() = Posts.withSample.single().toPostDetails() } } diff --git a/feature/profile-details/build.gradle.kts b/feature/profile-details/build.gradle.kts index c4d42b943..ea7343c8f 100644 --- a/feature/profile-details/build.gradle.kts +++ b/feature/profile-details/build.gradle.kts @@ -30,6 +30,7 @@ android { dependencies { androidTestImplementation(project(":core:sample-test")) + androidTestImplementation(project(":platform:testing")) androidTestImplementation(project(":platform:ui-test")) androidTestImplementation(project(":std:injector-test")) androidTestImplementation(libs.android.compose.ui.test.junit) diff --git a/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/ProfileDetailsTests.kt b/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/ProfileDetailsTests.kt index 9cd0be049..7af02b080 100644 --- a/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/ProfileDetailsTests.kt +++ b/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/ProfileDetailsTests.kt @@ -19,10 +19,10 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performScrollToIndex -import androidx.test.platform.app.InstrumentationRegistry import com.jeanbarrossilva.loadable.list.toListLoadable import com.jeanbarrossilva.loadable.list.toSerializableList import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme +import com.jeanbarrossilva.orca.platform.testing.screen.screen import com.jeanbarrossilva.orca.platform.ui.component.timeline.TIMELINE_TAG import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.PostPreview import com.jeanbarrossilva.orca.platform.ui.test.component.timeline.onTimeline @@ -36,13 +36,13 @@ internal class ProfileDetailsTests { @Test fun showsUsernameWhenScrollingPastHeader() { - val screenHeightInPx = - InstrumentationRegistry.getInstrumentation().context.resources.displayMetrics.heightPixels composeRule.setContent { AutosTheme { ProfileDetails( postPreviewsLoadable = - List(size = screenHeightInPx) { PostPreview.sample.copy(id = "${UUID.randomUUID()}") } + List(size = screen.height.inPixels) { + PostPreview.sample.copy(id = "${UUID.randomUUID()}") + } .toSerializableList() .toListLoadable(), relativeTimeProvider = TestRelativeTimeProvider diff --git a/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/FollowableProfile.extensions.kt b/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/FollowableProfile.extensions.kt index e073e9b3b..72318360e 100644 --- a/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/FollowableProfile.extensions.kt +++ b/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/FollowableProfile.extensions.kt @@ -21,9 +21,10 @@ import com.jeanbarrossilva.orca.core.sample.feed.profile.type.followable.createS import com.jeanbarrossilva.orca.core.sample.test.feed.profile.type.sample import com.jeanbarrossilva.orca.platform.ui.core.image.sample import com.jeanbarrossilva.orca.platform.ui.core.sample +import com.jeanbarrossilva.orca.platform.ui.core.withSample import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader -/** [FollowableProfile] returned by [sample]. */ +/** [FollowableProfile] returned by [withSample]. */ private val sampleFollowableProfile = FollowableProfile.createSample( Instance.sample.profileWriter, diff --git a/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/ProfileDetailsActivity.kt b/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/ProfileDetailsActivity.kt index 87dbc505d..7acfaec1c 100644 --- a/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/ProfileDetailsActivity.kt +++ b/feature/profile-details/src/androidTest/java/com/jeanbarrossilva/orca/feature/profiledetails/test/ProfileDetailsActivity.kt @@ -17,9 +17,9 @@ package com.jeanbarrossilva.orca.feature.profiledetails.test import android.content.Intent import androidx.navigation.NavGraphBuilder -import androidx.test.platform.app.InstrumentationRegistry import com.jeanbarrossilva.orca.feature.profiledetails.ProfileDetailsFragment import com.jeanbarrossilva.orca.feature.profiledetails.navigation.BackwardsNavigationState +import com.jeanbarrossilva.orca.platform.testing.context import com.jeanbarrossilva.orca.platform.ui.core.Intent import com.jeanbarrossilva.orca.platform.ui.test.core.SingleFragmentActivity @@ -32,7 +32,6 @@ internal class ProfileDetailsActivity : SingleFragmentActivity() { companion object { fun getIntent(backwardsNavigationState: BackwardsNavigationState, id: String): Intent { - val context = InstrumentationRegistry.getInstrumentation().context return Intent( context, ProfileDetailsFragment.BACKWARDS_NAVIGATION_STATE_KEY to backwardsNavigationState, diff --git a/feature/search/src/main/AndroidManifest.xml b/feature/search/src/main/AndroidManifest.xml index 0a88e5709..ed064452d 100644 --- a/feature/search/src/main/AndroidManifest.xml +++ b/feature/search/src/main/AndroidManifest.xml @@ -14,4 +14,10 @@ ~ not, see https://www.gnu.org/licenses. --> - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/Search.kt b/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/Search.kt index c62eb69c7..e9b272ae3 100644 --- a/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/Search.kt +++ b/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/Search.kt @@ -70,21 +70,21 @@ internal object SearchDefaults { internal fun Search( viewModel: SearchViewModel, boundary: SearchBoundary, + onBackwardsNavigation: () -> Unit, modifier: Modifier = Modifier ) { val query by viewModel.queryFlow.collectAsState() val resultsLoadable by viewModel.resultsFlow.collectAsState() when (@Suppress("NAME_SHADOWING") val resultsLoadable = resultsLoadable) { - is Loadable.Loading -> - Search(query, onQueryChange = viewModel::setQuery, onBackwardsNavigation = boundary::pop) + is Loadable.Loading -> Search(query, onQueryChange = viewModel::setQuery, onBackwardsNavigation) is Loadable.Loaded -> Search( query, onQueryChange = viewModel::setQuery, resultsLoadable.content, onNavigateToProfileDetails = boundary::navigateToProfileDetails, - onBackwardsNavigation = boundary::pop, + onBackwardsNavigation, modifier ) is Loadable.Failed -> Unit diff --git a/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchFragment.kt b/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchActivity.kt similarity index 72% rename from feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchFragment.kt rename to feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchActivity.kt index 1cd8b2b2f..3e6aca38e 100644 --- a/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchFragment.kt +++ b/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Orca + * Copyright © 2024 Orca * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 of the @@ -15,26 +15,26 @@ package com.jeanbarrossilva.orca.feature.search +import android.content.Context +import androidx.activity.viewModels import androidx.compose.runtime.Composable -import androidx.fragment.app.viewModels -import com.jeanbarrossilva.orca.platform.ui.core.composable.ComposableFragment -import com.jeanbarrossilva.orca.platform.ui.core.navigation.Navigator -import com.jeanbarrossilva.orca.platform.ui.core.navigation.transition.opening +import com.jeanbarrossilva.orca.platform.ui.core.composable.ComposableActivity +import com.jeanbarrossilva.orca.platform.ui.core.on import com.jeanbarrossilva.orca.std.injector.Injector -class SearchFragment : ComposableFragment() { +class SearchActivity internal constructor() : ComposableActivity() { private val module by lazy { Injector.from() } private val viewModel by viewModels { SearchViewModel.createFactory(module.searcher()) } @Composable override fun Content() { - Search(viewModel, module.boundary()) + Search(viewModel, module.boundary(), onBackwardsNavigation = ::finish) } companion object { - fun navigate(navigator: Navigator) { - navigator.navigate(opening()) { to("search", ::SearchFragment) } + fun start(context: Context) { + context.on().start() } } } diff --git a/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchBoundary.kt b/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchBoundary.kt index 16f829dc7..01beff267 100644 --- a/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchBoundary.kt +++ b/feature/search/src/main/java/com/jeanbarrossilva/orca/feature/search/SearchBoundary.kt @@ -17,6 +17,4 @@ package com.jeanbarrossilva.orca.feature.search interface SearchBoundary { fun navigateToProfileDetails(id: String) - - fun pop() } diff --git a/feature/settings/term-muting/src/androidTest/java/com/jeanbarrossilva/orca/feature/settings/termmuting/test/TestTermMuting.kt b/feature/settings/term-muting/src/androidTest/java/com/jeanbarrossilva/orca/feature/settings/termmuting/test/TestTermMuting.kt index 1134bc890..180f0834b 100644 --- a/feature/settings/term-muting/src/androidTest/java/com/jeanbarrossilva/orca/feature/settings/termmuting/test/TestTermMuting.kt +++ b/feature/settings/term-muting/src/androidTest/java/com/jeanbarrossilva/orca/feature/settings/termmuting/test/TestTermMuting.kt @@ -27,5 +27,5 @@ import com.jeanbarrossilva.orca.feature.settings.termmuting.TermMuting @Suppress("TestFunctionName") internal fun TestTermMuting(modifier: Modifier = Modifier) { var term by remember { mutableStateOf("") } - TermMuting(modifier, term, onTermChange = { term = it }, onMute = {}, onPop = {}) + TermMuting(modifier, term, onTermChange = { term = it }, onMute = {}, onBackwardsNavigation = {}) } diff --git a/feature/settings/term-muting/src/main/AndroidManifest.xml b/feature/settings/term-muting/src/main/AndroidManifest.xml index 0a88e5709..33d2be02c 100644 --- a/feature/settings/term-muting/src/main/AndroidManifest.xml +++ b/feature/settings/term-muting/src/main/AndroidManifest.xml @@ -14,4 +14,10 @@ ~ not, see https://www.gnu.org/licenses. --> - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMuting.kt b/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMuting.kt index d17d2bee4..f6c69ca4d 100644 --- a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMuting.kt +++ b/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMuting.kt @@ -59,7 +59,7 @@ internal const val SETTINGS_TERM_MUTING_MUTE_BUTTON = "settings-term-muting-mute @Composable internal fun TermMuting( viewModel: TermMutingViewModel, - boundary: TermMutingBoundary, + onBackwardsNavigation: () -> Unit, modifier: Modifier = Modifier ) { val term by viewModel.termFlow.collectAsState() @@ -69,7 +69,7 @@ internal fun TermMuting( term, onTermChange = viewModel::setTerm, onMute = viewModel::mute, - onPop = boundary::pop + onBackwardsNavigation ) } @@ -80,7 +80,7 @@ internal fun TermMuting( term: String, onTermChange: (term: String) -> Unit, onMute: () -> Unit, - onPop: () -> Unit, + onBackwardsNavigation: () -> Unit, ) { val context = LocalContext.current val topAppBarScrollBehavior = TopAppBarDefaults.scrollBehavior @@ -95,7 +95,7 @@ internal fun TermMuting( errorDispatcher.dispatch() if (!containsErrors) { onMute() - onPop() + onBackwardsNavigation() } } @@ -110,7 +110,7 @@ internal fun TermMuting( modifier, topAppBar = { TopAppBarWithBackNavigation( - onNavigation = onPop, + onBackwardsNavigation, title = { Text(stringResource(R.string.feature_settings_term_muting)) }, subtitle = { Text(stringResource(R.string.feature_settings_term_muting_settings)) }, scrollBehavior = topAppBarScrollBehavior @@ -158,5 +158,5 @@ internal fun TermMuting( @Composable @MultiThemePreview private fun TermMutingPreview() { - AutosTheme { TermMuting(term = "🐛", onTermChange = {}, onMute = {}, onPop = {}) } + AutosTheme { TermMuting(term = "🐛", onTermChange = {}, onMute = {}, onBackwardsNavigation = {}) } } diff --git a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingFragment.kt b/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingActivity.kt similarity index 70% rename from feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingFragment.kt rename to feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingActivity.kt index 7e03bd57b..aa1156692 100644 --- a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingFragment.kt +++ b/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingActivity.kt @@ -1,5 +1,5 @@ /* - * Copyright © 2023 Orca + * Copyright © 2024 Orca * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU General Public License as published by the Free Software Foundation, either version 3 of the @@ -15,26 +15,26 @@ package com.jeanbarrossilva.orca.feature.settings.termmuting +import android.content.Context +import androidx.activity.viewModels import androidx.compose.runtime.Composable -import androidx.fragment.app.viewModels -import com.jeanbarrossilva.orca.platform.ui.core.composable.ComposableFragment -import com.jeanbarrossilva.orca.platform.ui.core.navigation.Navigator -import com.jeanbarrossilva.orca.platform.ui.core.navigation.transition.opening +import com.jeanbarrossilva.orca.platform.ui.core.composable.ComposableActivity +import com.jeanbarrossilva.orca.platform.ui.core.on import com.jeanbarrossilva.orca.std.injector.Injector -class TermMutingFragment internal constructor() : ComposableFragment() { +class TermMutingActivity internal constructor() : ComposableActivity() { private val module by lazy { Injector.from() } private val viewModel by viewModels { TermMutingViewModel.createFactory(module.termMuter()) } @Composable override fun Content() { - TermMuting(viewModel, module.boundary()) + TermMuting(viewModel, onBackwardsNavigation = ::finish) } companion object { - fun navigate(navigator: Navigator) { - navigator.navigate(opening()) { to("settings/term-muting", ::TermMutingFragment) } + fun start(context: Context) { + context.on().start() } } } diff --git a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingModule.kt b/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingModule.kt index 605d57289..d9c842ae4 100644 --- a/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingModule.kt +++ b/feature/settings/term-muting/src/main/java/com/jeanbarrossilva/orca/feature/settings/termmuting/TermMutingModule.kt @@ -19,7 +19,4 @@ import com.jeanbarrossilva.orca.core.feed.profile.post.content.TermMuter import com.jeanbarrossilva.orca.std.injector.module.Inject import com.jeanbarrossilva.orca.std.injector.module.Module -abstract class TermMutingModule( - @Inject internal val termMuter: Module.() -> TermMuter, - @Inject internal val boundary: Module.() -> TermMutingBoundary -) : Module() +abstract class TermMutingModule(@Inject internal val termMuter: Module.() -> TermMuter) : Module() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 261600be2..059b33c07 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,8 @@ android-constraintlayout-compose = { group = "androidx.constraintlayout", name = android-core = { group = "androidx.core", name = "core-ktx", version = "1.12.0" } android-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "android-fragment" } android-fragment-testing = { group = "androidx.fragment", name = "fragment-testing", version.ref = "android-fragment" } -android-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version = "2.6.2" } +android-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "android-lifecycle" } +android-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "android-lifecycle" } android-material = { group = "com.google.android.material", name = "material", version = "1.9.0" } android-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version = "2.7.3" } android-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "android-plugin" } @@ -54,6 +55,7 @@ loadable-list = { group = "com.jeanbarrossilva.loadable", name = "loadable-list" loadable-placeholder = { group = "com.jeanbarrossilva.loadable", name = "loadable-placeholder", version.ref = "loadable" } loadable-placeholder-test = { group = "com.jeanbarrossilva.loadable", name = "loadable-placeholder-test", version.ref = "loadable" } mockk = { group = "io.mockk", name = "mockk-android", version = "1.13.7" } +openTest4J = { group = "org.opentest4j", name = "opentest4j", version = "1.3.0" } orbital = { group = "com.github.skydoves", name = "orbital", version = "0.3.3" } paginate = { group = "com.chrynan.paginate", name = "paginate-core", version = "0.3.0" } slf4j = { group = "org.slf4j", name = "slf4j-api", version = "2.0.7" } @@ -86,7 +88,8 @@ android-activity = "1.8.2" android-compose = "1.6.0-beta03" android-compose-compiler = "1.5.7" android-fragment = "1.6.1" -android-plugin = "8.2.0" +android-lifecycle = "2.6.2" +android-plugin = "8.2.1" android-room = "2.5.2" android-sdk-min = "28" android-sdk-target = "34" diff --git a/platform/testing/build.gradle.kts b/platform/testing/build.gradle.kts new file mode 100644 index 000000000..e0f5aee54 --- /dev/null +++ b/platform/testing/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + packagingOptions.resources.excludes += + arrayOf("META-INF/LICENSE.md", "META-INF/LICENSE-notice.md") +} + +dependencies { + androidTestImplementation(libs.android.test.runner) + androidTestImplementation(libs.assertk) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.kotlin.reflect) + androidTestImplementation(libs.mockk) + + implementation(libs.android.compose.ui) + implementation(libs.android.test.core) +} diff --git a/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/InstrumentationExtensionsTests.kt b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/InstrumentationExtensionsTests.kt new file mode 100644 index 000000000..2272ae379 --- /dev/null +++ b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/InstrumentationExtensionsTests.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing + +import androidx.test.platform.app.InstrumentationRegistry +import assertk.assertThat +import assertk.assertions.isSameAs +import org.junit.Test + +internal class InstrumentationExtensionsTests { + @Test + fun contextIsThatOfInstrumentationFromRegistry() { + assertThat(context).isSameAs(InstrumentationRegistry.getInstrumentation().context) + } +} diff --git a/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/IntExtensionsTests.kt b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/IntExtensionsTests.kt new file mode 100644 index 000000000..f83f7232d --- /dev/null +++ b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/IntExtensionsTests.kt @@ -0,0 +1,28 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jeanbarrossilva.orca.platform.testing.test.R +import org.junit.Test + +internal class IntExtensionsTests { + @Test + fun getsStringFromResourceID() { + assertThat(R.string.string.asString()).isEqualTo("5️⃣👍🏽🏞️👩🏻‍💻⏩🎇") + } +} diff --git a/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/DimensionTests.kt b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/DimensionTests.kt new file mode 100644 index 000000000..fb4deac21 --- /dev/null +++ b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/DimensionTests.kt @@ -0,0 +1,38 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing.screen + +import androidx.compose.ui.unit.dp +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jeanbarrossilva.orca.platform.testing.context +import org.junit.Test + +internal class DimensionTests { + @Test + fun createsWidthFromResources() { + val width = Screen.Dimension.width(context.resources) + assertThat(width.inPixels).isEqualTo(context.resources.displayMetrics.widthPixels) + assertThat(width.inDps).isEqualTo(context.resources.configuration.screenWidthDp.dp) + } + + @Test + fun createsHeightFromResources() { + val height = Screen.Dimension.height(context.resources) + assertThat(height.inPixels).isEqualTo(context.resources.displayMetrics.heightPixels) + assertThat(height.inDps).isEqualTo(context.resources.configuration.screenHeightDp.dp) + } +} diff --git a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/termmuting/NavigatorTermMutingBoundary.kt b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/ScreenExtensionsTests.kt similarity index 65% rename from app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/termmuting/NavigatorTermMutingBoundary.kt rename to platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/ScreenExtensionsTests.kt index c89aedad3..f7c1328e1 100644 --- a/app/src/main/java/com/jeanbarrossilva/orca/app/module/feature/settings/termmuting/NavigatorTermMutingBoundary.kt +++ b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/ScreenExtensionsTests.kt @@ -13,13 +13,19 @@ * not, see https://www.gnu.org/licenses. */ -package com.jeanbarrossilva.orca.app.module.feature.settings.termmuting +package com.jeanbarrossilva.orca.platform.testing.screen -import com.jeanbarrossilva.orca.feature.settings.termmuting.TermMutingBoundary -import com.jeanbarrossilva.orca.platform.ui.core.navigation.Navigator +import com.jeanbarrossilva.orca.platform.testing.context +import io.mockk.mockkObject +import io.mockk.verify +import org.junit.Test -internal class NavigatorTermMutingBoundary(private val navigator: Navigator) : TermMutingBoundary { - override fun pop() { - navigator.pop() +internal class ScreenExtensionsTests { + @Test + fun getsScreenFromContext() { + mockkObject(Screen.Companion) { + screen + verify { Screen.from(context) } + } } } diff --git a/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/ScreenTests.kt b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/ScreenTests.kt new file mode 100644 index 000000000..368484573 --- /dev/null +++ b/platform/testing/src/androidTest/java/com/jeanbarrossilva/orca/platform/testing/screen/ScreenTests.kt @@ -0,0 +1,72 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing.screen + +import android.content.res.AssetManager +import android.content.res.Configuration +import android.content.res.Resources +import android.util.DisplayMetrics +import androidx.compose.ui.unit.dp +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.prop +import com.jeanbarrossilva.orca.platform.testing.context +import io.mockk.mockkObject +import io.mockk.verify +import kotlin.reflect.KFunction +import kotlin.reflect.full.staticFunctions +import org.junit.Test + +internal class ScreenTests { + @Test + fun createsScreenFromResources() { + assertThat( + Screen.from( + @Suppress("DEPRECATION") + Resources( + AssetManager::class + .staticFunctions + .filterIsInstance>() + .single { it.name == "getSystem" } + .call(), + DisplayMetrics(), + Configuration() + ) + .apply { + displayMetrics.widthPixels = 2 + displayMetrics.heightPixels = 8 + configuration.screenWidthDp = 4 + configuration.screenHeightDp = 16 + } + ) + ) + .all { + prop("width.inPixels") { it.width.inPixels }.isEqualTo(2) + prop("width.inDps") { it.width.inDps }.isEqualTo(4.dp) + prop("height.inPixels") { it.height.inPixels }.isEqualTo(8) + prop("height.inDps") { it.height.inDps }.isEqualTo(16.dp) + } + } + + @Test + fun creatingScreenFromContextDelegatesToDoingSoWithItsResources() { + mockkObject(Screen.Companion) { + Screen.from(context) + verify { Screen.from(context.resources) } + } + } +} diff --git a/platform/testing/src/androidTest/res/values/strings.xml b/platform/testing/src/androidTest/res/values/strings.xml new file mode 100644 index 000000000..dc314757b --- /dev/null +++ b/platform/testing/src/androidTest/res/values/strings.xml @@ -0,0 +1,18 @@ + + + + 5️⃣👍🏽🏞️👩🏻‍💻⏩🎇 + \ No newline at end of file diff --git a/platform/testing/src/main/AndroidManifest.xml b/platform/testing/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0a88e5709 --- /dev/null +++ b/platform/testing/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/Instrumentation.extensions.kt b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/Instrumentation.extensions.kt new file mode 100644 index 000000000..1b94fdb67 --- /dev/null +++ b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/Instrumentation.extensions.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing + +import android.app.Instrumentation +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry + +/** + * [InstrumentationRegistry]'s [Instrumentation]'s [Context]. + * + * @see InstrumentationRegistry.getInstrumentation + * @see Instrumentation.getContext + */ +val context: Context + get() = InstrumentationRegistry.getInstrumentation().context diff --git a/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/Int.extensions.kt b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/Int.extensions.kt new file mode 100644 index 000000000..c1ec0abdb --- /dev/null +++ b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/Int.extensions.kt @@ -0,0 +1,27 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing + +import androidx.annotation.StringRes + +/** + * Gets the [String] from this resource ID. + * + * @param format [String] by which arguments in the [String] will be replaced. + */ +fun @receiver:StringRes Int.asString(vararg format: String): String { + return context.getString(this, *format) +} diff --git a/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/screen/Screen.extensions.kt b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/screen/Screen.extensions.kt new file mode 100644 index 000000000..840391286 --- /dev/null +++ b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/screen/Screen.extensions.kt @@ -0,0 +1,22 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing.screen + +import com.jeanbarrossilva.orca.platform.testing.context + +/** Test-tailored information about the display of the device. */ +val screen + get() = Screen.from(context) diff --git a/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/screen/Screen.kt b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/screen/Screen.kt new file mode 100644 index 000000000..fa7a7cb96 --- /dev/null +++ b/platform/testing/src/main/java/com/jeanbarrossilva/orca/platform/testing/screen/Screen.kt @@ -0,0 +1,102 @@ +/* + * Copyright © 2023 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.testing.screen + +import android.content.Context +import android.content.res.Resources +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import java.util.Objects + +/** + * Relevant information about the display of the device in which the testing is taking place. + * + * @param width [Dimension] for how wide the display is. + * @param height [Dimension] for how tall the display is. + */ +class Screen private constructor(val width: Dimension, val height: Dimension) { + override fun equals(other: Any?): Boolean { + return other is Screen && width == other.width && height == other.height + } + + override fun hashCode(): Int { + return Objects.hash(width, height) + } + + /** + * Offers the value that represents the size of one of the axes of the [Screen] in multiple units. + * + * @param inPixels Value of this [Dimension] in pixels. + * @param inDps Value of this [Dimension] in [Dp]s. + */ + class Dimension private constructor(val inPixels: Int, val inDps: Dp) { + override fun equals(other: Any?): Boolean { + return other is Dimension && inPixels == other.inPixels && inDps == other.inDps + } + + override fun hashCode(): Int { + return Objects.hash(inPixels, inDps) + } + + companion object { + /** + * Creates a width [Dimension]. + * + * @param resources [Resources] from which the value in pixels and in [Dp]s will be obtained. + */ + internal fun width(resources: Resources): Dimension { + return Dimension( + resources.displayMetrics.widthPixels, + resources.configuration.screenWidthDp.dp + ) + } + + /** + * Creates a height [Dimension]. + * + * @param resources [Resources] from which the value in pixels and in [Dp]s will be obtained. + */ + internal fun height(resources: Resources): Dimension { + return Dimension( + resources.displayMetrics.heightPixels, + resources.configuration.screenHeightDp.dp + ) + } + } + } + + companion object { + /** + * Obtains test-tailored information about the display of the device. + * + * @param context [Context] whose [Resources] will provide the dimensions. + */ + internal fun from(context: Context): Screen { + return from(context.resources) + } + + /** + * Obtains test-tailored information about the display of the device. + * + * @param resources [Resources] by which the dimensions will be provided. + */ + internal fun from(resources: Resources): Screen { + val width = Dimension.width(resources) + val height = Dimension.height(resources) + return Screen(width, height) + } + } +} diff --git a/platform/ui/build.gradle.kts b/platform/ui/build.gradle.kts index 3bc732a5f..f1a49f1d8 100644 --- a/platform/ui/build.gradle.kts +++ b/platform/ui/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation(project(":core")) implementation(project(":core:sample")) + implementation(project(":ext:coroutines")) implementation(project(":platform:autos")) implementation(project(":std:image:compose")) implementation(libs.android.activity.compose) @@ -65,3 +66,5 @@ dependencies { testImplementation(libs.kotlin.coroutines.test) testImplementation(libs.kotlin.test) } + +kotlin.compilerOptions.freeCompilerArgs.add("-Xcontext-receivers") diff --git a/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreviewTests.kt b/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreviewTests.kt index cf1ca9864..799b9cf52 100644 --- a/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreviewTests.kt +++ b/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreviewTests.kt @@ -25,10 +25,10 @@ import com.jeanbarrossilva.loadable.placeholder.test.assertIsLoading import com.jeanbarrossilva.loadable.placeholder.test.assertIsNotLoading import com.jeanbarrossilva.orca.core.feed.profile.post.Author import com.jeanbarrossilva.orca.core.instance.Instance -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample import com.jeanbarrossilva.orca.core.sample.test.instance.SampleInstanceTestRule import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme import com.jeanbarrossilva.orca.platform.ui.R +import com.jeanbarrossilva.orca.platform.ui.component.avatar.sample import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.test.onPostPreviewBody import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.test.onPostPreviewMetadata import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.test.onPostPreviewName diff --git a/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreviewTests.kt b/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreviewTests.kt index d6ac74a0d..da25b77d4 100644 --- a/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreviewTests.kt +++ b/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreviewTests.kt @@ -25,9 +25,9 @@ import com.jeanbarrossilva.orca.autos.forms.Forms import com.jeanbarrossilva.orca.core.feed.profile.post.Author import com.jeanbarrossilva.orca.core.feed.profile.post.content.Attachment import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.samples -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme import com.jeanbarrossilva.orca.platform.ui.R +import com.jeanbarrossilva.orca.platform.ui.component.avatar.sample import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.disposition.Disposition import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.disposition.test.assertAspectRatioEquals import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.disposition.test.onOverCount diff --git a/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/thumbnail/ThumbnailTests.kt b/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/thumbnail/ThumbnailTests.kt index 8311d3bf2..44fce21dc 100644 --- a/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/thumbnail/ThumbnailTests.kt +++ b/platform/ui/src/androidTest/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/thumbnail/ThumbnailTests.kt @@ -22,8 +22,8 @@ import assertk.assertions.isTrue import com.jeanbarrossilva.orca.core.feed.profile.post.Author import com.jeanbarrossilva.orca.core.feed.profile.post.content.Attachment import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.sample -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme +import com.jeanbarrossilva.orca.platform.ui.component.avatar.sample import com.jeanbarrossilva.orca.platform.ui.test.component.timeline.post.figure.gallery.thumbnail.onThumbnail import org.junit.Rule import org.junit.Test diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/stat/StatsDetails.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/stat/StatsDetails.kt index 58ef14024..2beb5730a 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/stat/StatsDetails.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/stat/StatsDetails.kt @@ -19,10 +19,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.stat.Stat -import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSample +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.formatted -import com.jeanbarrossilva.orca.platform.ui.core.image.sample -import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader +import com.jeanbarrossilva.orca.platform.ui.core.withSample /** * Details of a [Post]'s [Stat]s. @@ -64,6 +63,6 @@ internal constructor( /** Sample [StatsDetails]. */ val sample - @Composable get() = Post.createSample(ComposableImageLoader.Provider.sample).asStatsDetails() + @Composable get() = Posts.withSample.single().asStatsDetails() } } diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/Post.extensions.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/Post.extensions.kt index 5abe74a4f..05e12c93c 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/Post.extensions.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/Post.extensions.kt @@ -19,17 +19,25 @@ import androidx.compose.runtime.Composable import com.jeanbarrossilva.orca.autos.colors.Colors import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.repost.Repost +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSamples import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme import com.jeanbarrossilva.orca.platform.ui.component.stat.asStatsDetails import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.Figure import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.disposition.Disposition import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.thumbnail.Thumbnail +import com.jeanbarrossilva.orca.platform.ui.core.image.sample import com.jeanbarrossilva.orca.platform.ui.core.style.toAnnotatedString +import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader import com.jeanbarrossilva.orca.std.image.compose.SomeComposableImageLoader import java.net.URL import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +/** Sample [Posts] whose images are loaded by a [ComposableImageLoader]. */ +internal val Posts.Companion.withSamples + get() = Posts { addAll { Post.createSamples(ComposableImageLoader.Provider.sample) } } + /** * Converts this [Post] into a [Flow] of [PostPreview]. * diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreview.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreview.kt index 2adecc21e..76a932e78 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreview.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/PostPreview.kt @@ -55,7 +55,7 @@ import com.jeanbarrossilva.orca.core.feed.profile.account.Account import com.jeanbarrossilva.orca.core.feed.profile.post.Author import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.stat.Stat -import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSamples +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.platform.autos.colors.asColor import com.jeanbarrossilva.orca.platform.autos.iconography.asImageVector import com.jeanbarrossilva.orca.platform.autos.kit.action.button.icon.IgnoringMutableInteractionSource @@ -69,10 +69,8 @@ import com.jeanbarrossilva.orca.platform.ui.component.stat.StatsDetails import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.Figure import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.time.RelativeTimeProvider import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.time.rememberRelativeTimeProvider -import com.jeanbarrossilva.orca.platform.ui.core.image.sample -import com.jeanbarrossilva.orca.platform.ui.core.sample +import com.jeanbarrossilva.orca.platform.ui.core.withSample import com.jeanbarrossilva.orca.std.image.ImageLoader -import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader import com.jeanbarrossilva.orca.std.image.compose.SomeComposableImageLoader import java.io.Serializable import java.net.URL @@ -148,8 +146,7 @@ internal constructor( /** [PostPreview] samples. */ val samples - @Composable - get() = Post.createSamples(ComposableImageLoader.Provider.sample).map { it.toPostPreview() } + @Composable get() = Posts.withSamples.map { it.toPostPreview() } /** * Gets a sample [PostPreview]. @@ -158,7 +155,7 @@ internal constructor( * colored. */ fun getSample(colors: Colors): PostPreview { - return Post.sample.toPostPreview(colors) + return Posts.withSample.single().toPostPreview(colors) } } } diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreview.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreview.kt index 92a963815..58c020fba 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreview.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/GalleryPreview.kt @@ -23,15 +23,14 @@ import androidx.compose.ui.semantics.semantics import com.jeanbarrossilva.orca.core.feed.profile.post.Author import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Attachment +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.samples -import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSample import com.jeanbarrossilva.orca.platform.autos.theme.AutosTheme import com.jeanbarrossilva.orca.platform.autos.theme.MultiThemePreview import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.disposition.Disposition import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.disposition.disposition import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.thumbnail.Thumbnail -import com.jeanbarrossilva.orca.platform.ui.core.image.sample -import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader +import com.jeanbarrossilva.orca.platform.ui.core.withSample /** Tag that identifies a [GalleryPreview] for testing purposes. */ internal const val GALLERY_PREVIEW_TAG = "gallery-preview" @@ -50,8 +49,7 @@ data class GalleryPreview( ) { companion object { /** Sample [GalleryPreview]. */ - internal val sample = - Post.createSample(ComposableImageLoader.Provider.sample).asGalleryPreview() + internal val sample = Posts.withSample.single().asGalleryPreview() } } diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Flow.extensions.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Flow.extensions.kt index 9494ed2de..ee829e83e 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Flow.extensions.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Flow.extensions.kt @@ -15,13 +15,13 @@ package com.jeanbarrossilva.orca.platform.ui.core +import com.jeanbarrossilva.orca.ext.coroutines.mapEach import com.jeanbarrossilva.orca.platform.ui.core.replacement.emptyReplacementList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.runningFold @@ -69,12 +69,3 @@ fun Flow>.flatMapEach( } } } - -/** - * Maps each element of the emitted [Collection]s to the result of [transform]. - * - * @param transform Transformation to be made to the currently iterated element. - */ -fun Flow>.mapEach(transform: suspend (I) -> O): Flow> { - return map { elements -> elements.map { element -> transform(element) } } -} diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Instance.extensions.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Instance.extensions.kt index cf41cc41b..44494fb36 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Instance.extensions.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Instance.extensions.kt @@ -20,7 +20,7 @@ import com.jeanbarrossilva.orca.core.sample.instance.createSample import com.jeanbarrossilva.orca.platform.ui.core.image.sample import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader -/** [Instance] returned by [sample]. */ +/** [Instance] returned by [withSample]. */ private val sampleInstance = Instance.createSample(ComposableImageLoader.Provider.sample) /** Sample [Instance] whose images are loaded by a sample [ComposableImageLoader]. */ diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Post.extensions.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Post.extensions.kt index f6b78c773..437d3913c 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Post.extensions.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Post.extensions.kt @@ -16,13 +16,16 @@ package com.jeanbarrossilva.orca.platform.ui.core import com.jeanbarrossilva.orca.core.feed.profile.post.Post +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.createSample import com.jeanbarrossilva.orca.platform.ui.core.image.sample import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader -/** [Post] returned by [sample]. */ -private val samplePost = Post.createSample(ComposableImageLoader.Provider.sample) +/** [Posts] returned by [withSample]. */ +private val postsWithSample = Posts { + add { Post.createSample(ComposableImageLoader.Provider.sample) } +} -/** [Post] whose images are loaded by a sample [ComposableImageLoader]. */ -val Post.Companion.sample - get() = samplePost +/** [Posts] whose sample's images are loaded by a sample [ComposableImageLoader]. */ +val Posts.Companion.withSample + get() = postsWithSample diff --git a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Profile.extensions.kt b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Profile.extensions.kt index 393af24d2..61f031436 100644 --- a/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Profile.extensions.kt +++ b/platform/ui/src/main/java/com/jeanbarrossilva/orca/platform/ui/core/Profile.extensions.kt @@ -18,11 +18,10 @@ package com.jeanbarrossilva.orca.platform.ui.core import com.jeanbarrossilva.orca.core.feed.profile.Profile import com.jeanbarrossilva.orca.core.instance.Instance import com.jeanbarrossilva.orca.core.sample.feed.profile.createSample -import com.jeanbarrossilva.orca.core.sample.instance.createSample import com.jeanbarrossilva.orca.platform.ui.core.image.sample import com.jeanbarrossilva.orca.std.image.compose.ComposableImageLoader -/** [Profile] returned by [sample]. */ +/** [Profile] returned by [withSample]. */ private val sampleProfile = Profile.createSample(Instance.sample.postProvider, ComposableImageLoader.Provider.sample) diff --git a/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/FigureTests.kt b/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/FigureTests.kt index eedd6d633..46da2a46c 100644 --- a/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/FigureTests.kt +++ b/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/FigureTests.kt @@ -20,18 +20,19 @@ import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull import com.jeanbarrossilva.orca.core.feed.profile.post.Author -import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Attachment import com.jeanbarrossilva.orca.core.feed.profile.post.content.Content import com.jeanbarrossilva.orca.core.feed.profile.post.content.highlight.Headline import com.jeanbarrossilva.orca.core.feed.profile.post.content.highlight.Highlight import com.jeanbarrossilva.orca.core.instance.domain.Domain +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.sample import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.samples import com.jeanbarrossilva.orca.core.sample.instance.domain.sample import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.content.highlight.sample -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample +import com.jeanbarrossilva.orca.platform.ui.component.avatar.sample import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.GalleryPreview +import com.jeanbarrossilva.orca.platform.ui.core.withSample import com.jeanbarrossilva.orca.std.styledstring.StyledString import com.jeanbarrossilva.orca.std.styledstring.buildStyledString import java.net.URL @@ -40,7 +41,7 @@ import kotlin.test.Test internal class FigureTests { @Test fun createsGalleryFromContentWithHighlightAndAttachments() { - assertThat(Figure.of(Post.sample.id, Author.sample.name, Content.sample)) + assertThat(Figure.of(Posts.withSample.single().id, Author.sample.name, Content.sample)) .isEqualTo(Figure.Gallery(GalleryPreview.sample)) } @@ -48,7 +49,7 @@ internal class FigureTests { fun createsGalleryFromContentWithAttachmentsAndWithoutHighlight() { assertThat( Figure.of( - Post.sample.id, + Posts.withSample.single().id, Author.sample.name, Content.from(Domain.sample, text = StyledString(""), Attachment.samples) { null }, onLinkClick = {} @@ -62,7 +63,7 @@ internal class FigureTests { val onLinkClick = { _: URL -> } assertThat( Figure.of( - Post.sample.id, + Posts.withSample.single().id, Author.sample.name, Content.from( Domain.sample, diff --git a/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/disposition/DispositionTests.kt b/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/disposition/DispositionTests.kt index 4f5e18cc1..dff68fba1 100644 --- a/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/disposition/DispositionTests.kt +++ b/platform/ui/src/test/java/com/jeanbarrossilva/orca/platform/ui/component/timeline/post/figure/gallery/disposition/DispositionTests.kt @@ -18,18 +18,23 @@ package com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gall import assertk.assertThat import assertk.assertions.isEqualTo import com.jeanbarrossilva.orca.core.feed.profile.post.Author -import com.jeanbarrossilva.orca.core.feed.profile.post.Post import com.jeanbarrossilva.orca.core.feed.profile.post.content.Attachment +import com.jeanbarrossilva.orca.core.sample.feed.profile.post.Posts import com.jeanbarrossilva.orca.core.sample.feed.profile.post.content.samples -import com.jeanbarrossilva.orca.core.sample.test.feed.profile.post.sample +import com.jeanbarrossilva.orca.platform.ui.component.avatar.sample import com.jeanbarrossilva.orca.platform.ui.component.timeline.post.figure.gallery.GalleryPreview +import com.jeanbarrossilva.orca.platform.ui.core.withSample import kotlin.test.Test internal class DispositionTests { @Test(expected = IllegalArgumentException::class) fun throwsWhenGettingDispositionWithoutAttachments() { Disposition.of( - GalleryPreview.sample.copy(Post.sample.id, Author.sample.name, attachments = emptyList()), + GalleryPreview.sample.copy( + Posts.withSample.single().id, + Author.sample.name, + attachments = emptyList() + ), Disposition.OnThumbnailClickListener.empty ) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 05f978f7c..deed38a2b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,9 +34,11 @@ include( ":core-test", ":ext:coroutines", ":ext:processing", + ":ext:testing", ":feature:composer", ":feature:feed", ":feature:gallery", + ":feature:gallery-test", ":feature:post-details", ":feature:profile-details", ":feature:search", @@ -46,6 +48,7 @@ include( ":platform:cache", ":platform:autos-test", ":platform:intents", + ":platform:testing", ":platform:ui", ":platform:ui-test", ":std:buildable",