Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Kotlin
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287package social.grain
import androidx.compose.ui.test.junit4.v2.createComposeRuleimport androidx.compose.ui.test.onNodeWithContentDescriptionimport androidx.compose.ui.test.onNodeWithTextimport androidx.compose.ui.test.onRootimport androidx.compose.ui.test.performClickimport androidx.compose.ui.test.performTouchInputimport androidx.compose.ui.test.swipeDownimport androidx.compose.ui.test.swipeLeftimport androidx.lifecycle.SavedStateHandleimport androidx.test.ext.junit.runners.AndroidJUnit4import org.junit.Afterimport org.junit.Assert.assertEqualsimport org.junit.Beforeimport org.junit.Ruleimport org.junit.Testimport org.junit.runner.RunWithimport social.grain.data.model.GrainProfileimport social.grain.feature.stories.StoryCommentsViewModelimport social.grain.feature.stories.StoryViewerScreenimport social.grain.feature.stories.StoryViewerViewModelimport social.grain.support.Fixturesimport social.grain.support.Harnessimport social.grain.support.MainDispatcherRuleimport social.grain.support.awaitContentDescriptionimport social.grain.support.awaitTextimport social.grain.support.jsonimport social.grain.ui.theme.GrainTheme
/** * The story viewer as it appears: a full-bleed photo with controls over it. * * Almost everything here is drawn in white over an unknown photograph, and the * whole screen has no chrome to fall back on — a control that fails to compose * leaves a reader tapping a black rectangle with no way out. */@RunWith(AndroidJUnit4::class)class StoryViewerScreenTest {
@get:Rule val mainDispatcher = MainDispatcherRule()
@get:Rule val composeRule = createComposeRule()
private lateinit var harness: Harness
private val alice = Fixtures.profile(did = "did:plc:alice", handle = "alice.test")
private fun stories(creator: GrainProfile, vararg rkeys: String) = """{"stories":[${rkeys.joinToString(",") { json(Fixtures.story(rkey = it, creator = creator)) }}]}"""
@Before fun setUp() { harness = Harness() harness.server .on("dev.hatk.describeLabels", """{"definitions":[]}""") .on("social.grain.unspecced.getStories", stories(alice, "a1", "a2")) .on("social.grain.unspecced.getCommentThread", """{"comments":[],"totalCount":0}""") }
@After fun tearDown() = harness.tearDown()
private lateinit var viewer: StoryViewerViewModel
private fun show( did: String = alice.did, story: String = "", solo: Boolean = true, onClose: () -> Unit = {}, onOpenProfile: (String) -> Unit = {}, ) { viewer = StoryViewerViewModel( client = harness.client, auth = harness.auth, viewedStore = harness.viewedStories, statusCache = harness.storyStatus, labelDefsCache = harness.labelDefinitions, savedStateHandle = SavedStateHandle( mapOf("did" to did, "story" to story, "solo" to solo), ), ) val comments = StoryCommentsViewModel(harness.client, harness.auth) composeRule.setContent { GrainTheme { StoryViewerScreen( onClose = onClose, onOpenProfile = onOpenProfile, viewModel = viewer, commentsViewModel = comments, ) } } composeRule.waitForIdle() }
@Test fun theViewerNamesTheAuthorAndOffersAWayOut() { show()
composeRule.awaitText("Alice") composeRule.awaitContentDescription("Close") }
@Test fun closingReportsItsTap() { var closed = 0 show(onClose = { closed += 1 }) composeRule.awaitContentDescription("Close")
composeRule.onNodeWithContentDescription("Close").performClick()
// Leaving plays a short animation before it hands back. composeRule.waitUntil(10_000) { closed == 1 } }
@Test fun aStoryOffersToBeFavoritedAndCommentedOn() { show()
composeRule.awaitContentDescription("Favorite") composeRule.awaitContentDescription("Comments") }
@Test fun favoritingWritesTheRecord() { harness.server.on("dev.hatk.createRecord", """{"uri":"at://did:plc:me/fav/1"}""") show() composeRule.awaitContentDescription("Favorite")
composeRule.onNodeWithContentDescription("Favorite").performClick()
composeRule.waitUntil(10_000) { harness.server.countOf("dev.hatk.createRecord") >= 1 } }
@Test fun anAlreadyFavoritedStoryOffersTheUndo() { harness.server.on( "social.grain.unspecced.getStories", """{"stories":[${json( Fixtures.story(rkey = "a1", creator = alice, favUri = "at://fav/1"), )}]}""", ) show()
composeRule.awaitContentDescription("Unfavorite") }
@Test fun somebodyElsesStoryOffersReportingRatherThanDeleting() { show()
composeRule.awaitContentDescription("Report story") composeRule.onNodeWithContentDescription("Delete story").assertDoesNotExist() }
@Test fun theViewersOwnStoryOffersDeletingBehindAConfirmation() { val me = Fixtures.profile(did = harness.did, handle = harness.handle) harness.server.on("social.grain.unspecced.getStories", stories(me, "m1")) show(did = harness.did) composeRule.awaitContentDescription("Delete story")
composeRule.onNodeWithContentDescription("Delete story").performClick() composeRule.awaitText("Delete story?") composeRule.onNodeWithText("This will permanently delete this story.").assertExists()
composeRule.onNodeWithText("Cancel").performClick() composeRule.waitForIdle()
assertEquals(0, harness.server.countOf("dev.hatk.deleteRecord")) }
@Test fun confirmingTheDeleteWritesIt() { val me = Fixtures.profile(did = harness.did, handle = harness.handle) harness.server.on("social.grain.unspecced.getStories", stories(me, "m1")) show(did = harness.did) composeRule.awaitContentDescription("Delete story")
composeRule.onNodeWithContentDescription("Delete story").performClick() composeRule.awaitText("Delete story?") composeRule.onNodeWithText("Delete").performClick()
composeRule.waitUntil(10_000) { harness.server.countOf("dev.hatk.deleteRecord") >= 1 } }
@Test fun tappingTheAuthorOpensTheirProfile() { var opened: String? = null show(onOpenProfile = { opened = it }) composeRule.awaitText("Alice")
composeRule.onNodeWithText("Alice").performClick()
composeRule.waitUntil(10_000) { opened == "did:plc:alice" } }
// MARK: - Gestures
private val bob = Fixtures.profile(did = "did:plc:bob", handle = "bob.test", displayName = "Bob")
/** Two authors in the strip, alice first — the strip is newest-first. */ private fun seedStrip() { harness.storyStatus.update( listOf( Fixtures.storyAuthor( profile = alice, storyCount = 2, latestAt = Fixtures.recentIso(minutesAgo = 1), ), Fixtures.storyAuthor( profile = bob, storyCount = 1, latestAt = Fixtures.recentIso(minutesAgo = 30), ), ), ) harness.server.on("social.grain.unspecced.getStories") { call -> when (call.params["actor"]) { bob.did -> stories(bob, "b1") else -> stories(alice, "a1", "a2") } } }
@Test fun draggingSidewaysHandsOverToTheNextAuthor() { seedStrip() show(solo = false) composeRule.awaitText("Alice")
composeRule.onRoot().performTouchInput { swipeLeft(startX = right * 0.9f, endX = left + 1f, durationMillis = 200) }
composeRule.waitUntil(10_000) { viewer.state.value.author?.did == bob.did } }
@Test fun draggingDownThrowsTheViewerAway() { seedStrip() var closed = 0 show(solo = false, onClose = { closed += 1 }) composeRule.awaitText("Alice")
composeRule.onRoot().performTouchInput { swipeDown(startY = top + 1f, endY = bottom - 1f, durationMillis = 200) }
composeRule.waitUntil(10_000) { closed == 1 || viewer.state.value.isFinished } }
@Test fun aDragThatGoesNowhereLeavesTheStoryWhereItWas() { seedStrip() show(solo = false) composeRule.awaitText("Alice")
composeRule.onRoot().performTouchInput { down(center) moveTo(center + androidx.compose.ui.geometry.Offset(20f, 0f)) up() } composeRule.waitForIdle()
assertEquals(alice.did, viewer.state.value.author?.did) }
@Test fun anArchivedStoryOpensOnItsOwn() { harness.server.on( "social.grain.unspecced.getStory", """{"story":${json(Fixtures.story(rkey = "old", creator = alice, expired = true))}}""", ) show(story = "at://did:plc:alice/social.grain.story/old", solo = false)
composeRule.awaitText("Alice") assertEquals(1, viewer.state.value.stories.size) }}