Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ Prefer module-scoped tasks while iterating; PRs should pass `spotlessCheck`, `de
- For concurrency-sensitive logic (uploads, sync, message state), add deterministic tests using `runTest` + virtual time.
- Update baseline profiles (`baseline-prof.txt`) when start-up or scroll performance changes significantly.

### End-to-end (E2E) tests
E2E tests run a sample app against the mock server (`stream-chat-test-mock-server`) with UiAutomator, driven by fastlane and reported to Allure. Code is split by whether it knows the app under test, and the dependency flows one way: the sample depends on the module, never the reverse.
- `stream-chat-android-e2e-test/` holds app-agnostic infrastructure, reusable by any sample: the mock server client, the outside-world robots (`ParticipantRobot` for the second user, `BackendRobot` for server controls like tokens and cooldown), the JUnit `RetryRule`, and the low-level UiAutomator toolkit (device access, waits, selectors). Package root: `io.getstream.chat.android.e2e.test.*`.
- `{app}-sample/src/androidTest<Flavor>/` holds everything tied to that app's UI: the Page Objects (resource-id selectors), the `UserRobot` that drives the app under test plus its assertion extensions, the test classes, and the app-specific `StreamTestCase` base.
- Rule of thumb: new code that references app screens or resource ids goes in the sample; mock-server, lifecycle, or device helpers that any app could reuse go in the module.
- Today only `stream-chat-android-compose-sample` has E2E tests.

## Documentation & comments
- Update module README, `docs/`, or API docs when altering setup, themes, or sample flows.
- Log deprecations or behavioural shifts in release notes via appropriate PR labels.
Expand Down
4 changes: 3 additions & 1 deletion fastlane/Allurefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ desc 'Create launch on Allure TestOps'
lane :allure_launch do |options|
next unless is_check_required(sources: sources_matrix[:e2e], force_check: @force_check)

# Only real scheduled runs are named 'Cron checks'. A manual dispatch of the nightly
# workflow falls back to the head commit message, so it stays distinguishable in TestOps.
launch_id = allure_create_launch(
url: allure_api_url,
project_id: allure_project_id,
github_run_details: github_run_details,
cron: options[:cron]
cron: options[:cron] && ENV['GITHUB_EVENT_NAME'] == 'schedule'
)
sh("echo 'LAUNCH_ID=#{launch_id}' >> $GITHUB_ENV") if is_ci
end
Expand Down
7 changes: 6 additions & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ private_lane :batch_tests do |options|
install(tool: :test_parser)
sh("java -jar test-parser.jar #{options[:test_apk_path]} ./")
test_names = File.read('AllTests.txt').split
current_batch = test_names.each_slice((test_names.size.to_f / options[:batch_count].to_i).ceil).to_a[options[:batch].to_i]
batch = options[:batch].to_i
batch_count = options[:batch_count].to_i
# Round-robin split always yields exactly batch_count groups. Slicing by a rounded-up
# size can yield fewer groups than batch_count, making the last batch nil and crashing.
current_batch = test_names.select.with_index { |_, i| i % batch_count == batch }
UI.user_error!("Batch #{batch} is empty: #{test_names.size} tests cannot fill #{batch_count} batches") if current_batch.empty?
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"-e class #{current_batch.join(',')}"
else
''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,21 +135,9 @@ open class MessageListPage {

companion object {
val reactions get() = By.res("Stream_MessageReaction")
private val like get() = By.res("Stream_MessageReaction_${ReactionType.LIKE.reaction}")
private val love get() = By.res("Stream_MessageReaction_${ReactionType.LOVE.reaction}")
private val lol get() = By.res("Stream_MessageReaction_${ReactionType.LOL.reaction}")
private val wow get() = By.res("Stream_MessageReaction_${ReactionType.WOW.reaction}")
private val sad get() = By.res("Stream_MessageReaction_${ReactionType.SAD.reaction}")

fun reaction(type: ReactionType): BySelector {
return when (type) {
ReactionType.LIKE -> like
ReactionType.LOVE -> love
ReactionType.LOL -> lol
ReactionType.SAD -> sad
ReactionType.WOW -> wow
}
}

fun reaction(type: ReactionType): BySelector =
By.res("Stream_MessageReaction_${type.reaction}")
}
}

Expand Down Expand Up @@ -182,21 +170,8 @@ open class MessageListPage {
class ReactionsView {

companion object {
private val like get() = By.res("Stream_Reaction_${ReactionType.LIKE.reaction}")
private val love get() = By.res("Stream_Reaction_${ReactionType.LOVE.reaction}")
private val lol get() = By.res("Stream_Reaction_${ReactionType.LOL.reaction}")
private val wow get() = By.res("Stream_Reaction_${ReactionType.WOW.reaction}")
private val sad get() = By.res("Stream_Reaction_${ReactionType.SAD.reaction}")

fun reaction(type: ReactionType): BySelector {
return when (type) {
ReactionType.LIKE -> like
ReactionType.LOVE -> love
ReactionType.LOL -> lol
ReactionType.SAD -> sad
ReactionType.WOW -> wow
}
}
fun reaction(type: ReactionType): BySelector =
By.res("Stream_Reaction_${type.reaction}")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,28 @@ import io.getstream.chat.android.compose.pages.MessageListPage.MessageList
import io.getstream.chat.android.compose.pages.MessageListPage.MessageList.Message
import io.getstream.chat.android.compose.pages.MessageListPage.MessageList.Message.ContextMenu
import io.getstream.chat.android.compose.pages.ThreadPage
import io.getstream.chat.android.compose.uiautomator.defaultTimeout
import io.getstream.chat.android.compose.uiautomator.device
import io.getstream.chat.android.compose.uiautomator.findObject
import io.getstream.chat.android.compose.uiautomator.findObjects
import io.getstream.chat.android.compose.uiautomator.isDisplayed
import io.getstream.chat.android.compose.uiautomator.longPress
import io.getstream.chat.android.compose.uiautomator.swipeDown
import io.getstream.chat.android.compose.uiautomator.swipeUp
import io.getstream.chat.android.compose.uiautomator.tapOnScreenCenter
import io.getstream.chat.android.compose.uiautomator.typeText
import io.getstream.chat.android.compose.uiautomator.wait
import io.getstream.chat.android.compose.uiautomator.waitToAppear
import io.getstream.chat.android.compose.uiautomator.waitToAppearBottomUp
import io.getstream.chat.android.compose.uiautomator.waitToDisappear
import io.getstream.chat.android.e2e.test.mockserver.AttachmentType
import io.getstream.chat.android.e2e.test.mockserver.ReactionType
import io.getstream.chat.android.e2e.test.robots.ParticipantRobot
import io.getstream.chat.android.e2e.test.uiautomator.defaultTimeout
import io.getstream.chat.android.e2e.test.uiautomator.device
import io.getstream.chat.android.e2e.test.uiautomator.findObject
import io.getstream.chat.android.e2e.test.uiautomator.findObjects
import io.getstream.chat.android.e2e.test.uiautomator.isDisplayed
import io.getstream.chat.android.e2e.test.uiautomator.longPress
import io.getstream.chat.android.e2e.test.uiautomator.swipeDown
import io.getstream.chat.android.e2e.test.uiautomator.swipeUp
import io.getstream.chat.android.e2e.test.uiautomator.tapOnScreenCenter
import io.getstream.chat.android.e2e.test.uiautomator.typeText
import io.getstream.chat.android.e2e.test.uiautomator.wait
import io.getstream.chat.android.e2e.test.uiautomator.waitToAppear
import io.getstream.chat.android.e2e.test.uiautomator.waitToAppearBottomUp
import io.getstream.chat.android.e2e.test.uiautomator.waitToDisappear

class UserRobot {

fun sleep(timeOutMillis: Long = defaultTimeout): UserRobot {
io.getstream.chat.android.compose.uiautomator.sleep(timeOutMillis)
io.getstream.chat.android.e2e.test.uiautomator.sleep(timeOutMillis)
return this
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
package io.getstream.chat.android.compose.robots

import io.getstream.chat.android.compose.pages.ChannelListPage.ChannelList.Channel
import io.getstream.chat.android.compose.uiautomator.isDisplayed
import io.getstream.chat.android.compose.uiautomator.waitForText
import io.getstream.chat.android.compose.uiautomator.waitToAppear
import io.getstream.chat.android.compose.uiautomator.waitToDisappear
import io.getstream.chat.android.e2e.test.robots.ParticipantRobot
import io.getstream.chat.android.e2e.test.uiautomator.isDisplayed
import io.getstream.chat.android.e2e.test.uiautomator.waitForText
import io.getstream.chat.android.e2e.test.uiautomator.waitToAppear
import io.getstream.chat.android.e2e.test.uiautomator.waitToDisappear
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,47 @@ package io.getstream.chat.android.compose.robots

import android.annotation.SuppressLint
import androidx.test.uiautomator.By
import androidx.test.uiautomator.BySelector
import io.getstream.chat.android.compose.R
import io.getstream.chat.android.compose.pages.MessageListPage
import io.getstream.chat.android.compose.pages.MessageListPage.Composer
import io.getstream.chat.android.compose.pages.MessageListPage.MessageList.Message
import io.getstream.chat.android.compose.pages.ThreadPage
import io.getstream.chat.android.compose.uiautomator.appContext
import io.getstream.chat.android.compose.uiautomator.device
import io.getstream.chat.android.compose.uiautomator.findObject
import io.getstream.chat.android.compose.uiautomator.findObjects
import io.getstream.chat.android.compose.uiautomator.height
import io.getstream.chat.android.compose.uiautomator.isDisplayed
import io.getstream.chat.android.compose.uiautomator.isEnabled
import io.getstream.chat.android.compose.uiautomator.retryOnStaleObjectException
import io.getstream.chat.android.compose.uiautomator.seconds
import io.getstream.chat.android.compose.uiautomator.wait
import io.getstream.chat.android.compose.uiautomator.waitForCount
import io.getstream.chat.android.compose.uiautomator.waitForText
import io.getstream.chat.android.compose.uiautomator.waitToAppear
import io.getstream.chat.android.compose.uiautomator.waitToAppearBottomUp
import io.getstream.chat.android.compose.uiautomator.waitToDisappear
import io.getstream.chat.android.e2e.test.mockserver.MessageDeliveryStatus
import io.getstream.chat.android.e2e.test.mockserver.ReactionType
import io.getstream.chat.android.e2e.test.robots.ParticipantRobot
import io.getstream.chat.android.e2e.test.uiautomator.appContext
import io.getstream.chat.android.e2e.test.uiautomator.device
import io.getstream.chat.android.e2e.test.uiautomator.findObject
import io.getstream.chat.android.e2e.test.uiautomator.findObjects
import io.getstream.chat.android.e2e.test.uiautomator.height
import io.getstream.chat.android.e2e.test.uiautomator.isDisplayed
import io.getstream.chat.android.e2e.test.uiautomator.isEnabled
import io.getstream.chat.android.e2e.test.uiautomator.retryOnStaleObjectException
import io.getstream.chat.android.e2e.test.uiautomator.seconds
import io.getstream.chat.android.e2e.test.uiautomator.wait
import io.getstream.chat.android.e2e.test.uiautomator.waitForCount
import io.getstream.chat.android.e2e.test.uiautomator.waitForText
import io.getstream.chat.android.e2e.test.uiautomator.waitToAppear
import io.getstream.chat.android.e2e.test.uiautomator.waitToAppearBottomUp
import io.getstream.chat.android.e2e.test.uiautomator.waitToDisappear
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue

/**
* Asserts the selector's visibility with a bounded wait: waits for it to appear when
* [isDisplayed] is `true`, or to disappear when `false`, then asserts the final state.
*/
private fun assertVisibility(selector: BySelector, isDisplayed: Boolean) {
if (isDisplayed) {
assertTrue(selector.waitToAppear().isDisplayed())
} else {
assertFalse(selector.waitToDisappear().isDisplayed())
}
}

fun UserRobot.assertMessage(
text: String,
isDisplayed: Boolean = true,
Expand Down Expand Up @@ -190,20 +203,12 @@ fun UserRobot.assertComposerSize(isChangeable: Boolean): UserRobot {
}

fun UserRobot.assertTypingIndicator(isDisplayed: Boolean): UserRobot {
if (isDisplayed) {
assertTrue(MessageListPage.MessageList.typingIndicator.waitToAppear().isDisplayed())
} else {
assertFalse(MessageListPage.MessageList.typingIndicator.waitToDisappear().isDisplayed())
}
assertVisibility(MessageListPage.MessageList.typingIndicator, isDisplayed)
return this
}

fun UserRobot.assertAttachmentsMenu(isDisplayed: Boolean): UserRobot {
if (isDisplayed) {
assertTrue(MessageListPage.AttachmentPicker.view.waitToAppear().isDisplayed())
} else {
assertFalse(MessageListPage.AttachmentPicker.view.waitToDisappear().isDisplayed())
}
assertVisibility(MessageListPage.AttachmentPicker.view, isDisplayed)
return this
}

Expand All @@ -219,11 +224,7 @@ fun UserRobot.assertComposerCommandsMenu(isDisplayed: Boolean): UserRobot {
}

fun UserRobot.assertComposerMentionsMenu(isDisplayed: Boolean): UserRobot {
if (isDisplayed) {
assertTrue(Composer.userSuggestion.waitToAppear().isDisplayed())
} else {
assertFalse(Composer.userSuggestion.waitToDisappear().isDisplayed())
}
assertVisibility(Composer.userSuggestion, isDisplayed)
return this
}

Expand Down Expand Up @@ -259,11 +260,7 @@ fun UserRobot.assertComposerIsDisabledInSlowMode(): UserRobot {
}

fun UserRobot.assertScrollToBottomButton(isDisplayed: Boolean): UserRobot {
if (isDisplayed) {
assertTrue(MessageListPage.MessageList.scrollToBottomButton.waitToAppear().isDisplayed())
} else {
assertFalse(MessageListPage.MessageList.scrollToBottomButton.waitToDisappear().isDisplayed())
}
assertVisibility(MessageListPage.MessageList.scrollToBottomButton, isDisplayed)
return this
}

Expand Down Expand Up @@ -326,11 +323,7 @@ fun UserRobot.assertGiphyButtons(areDisplayed: Boolean = true): UserRobot {
}

fun UserRobot.assertSystemMessage(text: String, isDisplayed: Boolean = true): UserRobot {
if (isDisplayed) {
By.text(text).waitToAppear().isDisplayed()
} else {
By.text(text).waitToDisappear().isDisplayed()
}
assertVisibility(By.text(text), isDisplayed)
return this
}

Expand All @@ -343,11 +336,7 @@ fun UserRobot.assertInvalidCommandMessage(text: String, isDisplayed: Boolean = t
}

fun UserRobot.assertReaction(type: ReactionType, isDisplayed: Boolean): UserRobot {
if (isDisplayed) {
assertTrue(Message.Reactions.reaction(type).waitToAppear().isDisplayed())
} else {
assertFalse(Message.Reactions.reaction(type).waitToDisappear().isDisplayed())
}
assertVisibility(Message.Reactions.reaction(type), isDisplayed)
return this
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ package io.getstream.chat.android.compose.tests
import io.getstream.chat.android.compose.pages.JwtPage
import io.getstream.chat.android.compose.robots.UserRobot
import io.getstream.chat.android.compose.sample.ui.InitTestActivity
import io.getstream.chat.android.compose.uiautomator.device
import io.getstream.chat.android.compose.uiautomator.disableInternetConnection
import io.getstream.chat.android.compose.uiautomator.enableInternetConnection
import io.getstream.chat.android.compose.uiautomator.goToBackground
import io.getstream.chat.android.compose.uiautomator.goToForeground
import io.getstream.chat.android.compose.uiautomator.isDisplayed
import io.getstream.chat.android.compose.uiautomator.seconds
import io.getstream.chat.android.compose.uiautomator.waitToAppear
import io.getstream.chat.android.e2e.test.uiautomator.device
import io.getstream.chat.android.e2e.test.uiautomator.disableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.enableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.goToBackground
import io.getstream.chat.android.e2e.test.uiautomator.goToForeground
import io.getstream.chat.android.e2e.test.uiautomator.isDisplayed
import io.getstream.chat.android.e2e.test.uiautomator.seconds
import io.getstream.chat.android.e2e.test.uiautomator.waitToAppear
import io.qameta.allure.kotlin.Allure.step
import io.qameta.allure.kotlin.AllureId
import org.junit.Assert.assertTrue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import io.getstream.chat.android.compose.robots.assertMessageDeliveryStatus
import io.getstream.chat.android.compose.robots.assertMessageInChannelPreview
import io.getstream.chat.android.compose.robots.assertMessagePreviewTimestamp
import io.getstream.chat.android.compose.sample.ui.InitTestActivity
import io.getstream.chat.android.compose.uiautomator.device
import io.getstream.chat.android.compose.uiautomator.disableInternetConnection
import io.getstream.chat.android.compose.uiautomator.enableInternetConnection
import io.getstream.chat.android.compose.uiautomator.seconds
import io.getstream.chat.android.e2e.test.mockserver.MessageDeliveryStatus
import io.getstream.chat.android.e2e.test.uiautomator.device
import io.getstream.chat.android.e2e.test.uiautomator.disableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.enableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.seconds
import io.qameta.allure.kotlin.Allure.step
import io.qameta.allure.kotlin.AllureId
import org.junit.Ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ package io.getstream.chat.android.compose.tests
import io.getstream.chat.android.compose.robots.assertComposerText
import io.getstream.chat.android.compose.robots.assertMessageInChannelPreview
import io.getstream.chat.android.compose.sample.ui.InitTestActivity
import io.getstream.chat.android.compose.uiautomator.device
import io.getstream.chat.android.compose.uiautomator.disableInternetConnection
import io.getstream.chat.android.compose.uiautomator.enableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.device
import io.getstream.chat.android.e2e.test.uiautomator.disableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.enableInternetConnection
import io.qameta.allure.kotlin.Allure.step
import io.qameta.allure.kotlin.AllureId
import org.junit.Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ import io.getstream.chat.android.compose.robots.assertThreadMessage
import io.getstream.chat.android.compose.robots.assertThreadReplyLabelOnParentMessage
import io.getstream.chat.android.compose.robots.assertTypingIndicator
import io.getstream.chat.android.compose.sample.ui.InitTestActivity
import io.getstream.chat.android.compose.uiautomator.device
import io.getstream.chat.android.compose.uiautomator.disableInternetConnection
import io.getstream.chat.android.compose.uiautomator.enableInternetConnection
import io.getstream.chat.android.compose.uiautomator.goToBackground
import io.getstream.chat.android.compose.uiautomator.goToForeground
import io.getstream.chat.android.e2e.test.mockserver.MessageDeliveryStatus
import io.getstream.chat.android.e2e.test.mockserver.forbiddenWord
import io.getstream.chat.android.e2e.test.uiautomator.device
import io.getstream.chat.android.e2e.test.uiautomator.disableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.enableInternetConnection
import io.getstream.chat.android.e2e.test.uiautomator.goToBackground
import io.getstream.chat.android.e2e.test.uiautomator.goToForeground
import io.qameta.allure.kotlin.Allure.step
import io.qameta.allure.kotlin.AllureId
import org.junit.Ignore
Expand Down
Loading
Loading