Skip to content

test(e2e): port the native e2e suites, add impact analysis - #3925

Merged
isekovanic merged 24 commits into
developfrom
ci/v10-e2e-tests
Nov 9, 2026
Merged

isekovanic merged 24 commits into
developfrom
ci/v10-e2e-tests

Conversation

@testableapple

@testableapple testableapple commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Ticket: https://linear.app/stream/issue/RN-449

🎯 Goal

Port the native iOS and Android e2e suites to the V10 Detox suite in examples/E2ETestApp, and bring its CI up to the native SDKs: per-test impact analysis on pull requests, a nightly run that builds the coverage map, and Allure TestOps reporting.

🛠 Implementation details

Tests

  • 19 suites, 255 tests, ported from the Android compose and iOS UIKit suites (the union of both, with the native names and expectations). Every test links an Allure case in project 3.
  • When the SDK disagrees with the native expectation, the test keeps the native expectation and is skipped with it.skip and a comment that links a ticket. That covers 72 tests, tracked in RN-489 to RN-506 and RN-510. With those skipped, 183 tests run.
  • Robots, pages and support code grew to cover the suites: offline toggling (Android), the media library, JWT asserts, and Android system dialogs.

E2E app

  • Offline support is on (@op-engineering/op-sqlite, <Chat enableOfflineSupport>). The database is wiped at login, so every test starts clean.
  • Added a JWT test screen, plus the channel details and thread screens the suites need.
  • No setupCommandUIMiddlewares call: since stream-chat 10.0.0-rc.31 (chore: stream-chat rc.31 compatibility #3919), the client installs the command middlewares itself.

SDK

  • Additive testIDs only (channel preview, reactions, link previews, unread indicators, swipe actions, and a few others). There is no behaviour change. One snapshot changed because ScrollToBottomButton got a new testID.

Impact analysis (e2e/README.md, "Impact analysis")

  • E2E_COVERAGE=1 builds add a small Babel plugin (e2e/coverage/babelPlugin.js) that records which files run code. It isn't istanbul, because worklets capture istanbul's counters and then throw on the UI runtime.
  • After each test, the app reports its coverage to a collector in the test process, which writes one lcov file per test.
  • build_e2e_coverage_map and select_e2e_tests use fastlane-plugin-stream_actions (bumped from 0.4.14 to 0.4.19).
  • A PR runs only the tests its diff can reach. Changes to the harness, native code, dependencies or lanes still run everything.
  • The Check E2E Required job now also selects the tests, and skips e2e when none are selected.
  • Android runs in batches, like the native Android SDK: Build Android builds the APKs once, Launch Allure TestOps (Android) opens one launch, and Test Android runs up to 8 batches (one per selected test when fewer than 8 are selected), each taking a round-robin share of the selection.
  • A new nightly workflow, e2e-test-cron.yml, runs everything on Android with the coverage build and uploads the e2e-coverage-map artifact. It also runs every test on iOS. Until its first run on the default branch, every PR runs all tests.

Allure

  • The Allure lanes moved into examples/SampleApp/fastlane/Allurefile, the same layout as stream-chat-swift. A new allure_testcase lane creates test cases.
  • Allure uses the ALLURE_TOKEN repository secret.

Mock server

🎨 UI Changes

None. The SDK changes are testIDs only.

🧪 Testing

  • Full Android run (debug build, emulator): 255 tests, 182 passed, 70 skipped and 3 failed. All 3 passed when rerun:
    • A mock server start timeout, which is a harness issue.
    • RN-489 (the channel actions sheet sometimes opens empty).
    • RN-506 (after jumping to a quoted message, the list stays on the jumped page when the user sends).
  • Impact analysis:
    • Coverage collection checked on debug and release builds (17 of 17 tests collected).
    • A map built from those tests gave sensible selections from changed files.
    • A selection file ran exactly the selected tests.
  • First batched CI run (Android release build, 8 batches): batches 0 to 3 passed. Of the rest:
    • The two long-thread jump tests hung in release builds only: the open thread keeps enqueuing JS timers, so the app never becomes idle. Reproduced locally, ticketed as RN-510, and skipped.
    • The multiple-votes poll test pressed Back to hide a keyboard the CI emulator never showed, which closed the sheet. It now presses Back only when the keyboard is up.
    • One batch lost its emulator mid-run (runner issue, no test failure).
  • yarn lint, E2ETestApp typecheck and the SDK unit tests pass.
  • iOS isn't tuned yet. On PRs its e2e job runs on dispatch only; the nightly runs it.

☑️ Checklist

  • I have signed the Stream CLA (required)
  • PR targets the develop branch (targets V10: the suite needs the v10 SDK)
  • Documentation is updated (examples/E2ETestApp/e2e/README.md)
  • New code is tested in main example apps, including all possible scenarios (e2e app only, no SampleApp/Expo changes)

@testableapple
testableapple marked this pull request as draft November 3, 2026 23:07
@testableapple
testableapple marked this pull request as ready for review November 5, 2026 19:24
supportedReactions={supportedReactions}
type={reaction.type}
/>
{/* Identifies each reaction type for e2e tests; adds no layout of its own. */}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A testID stops Fabric from flattening a View, so this wrapper adds up to 4 native views per message row (the default before they add the number thingy). Let's put it on the icon instead:

Suggested change
{/* Identifies each reaction type for e2e tests; adds no layout of its own. */}
<Icon
size={icon.size}
style={[styles.iconStyle, icon.style]}
supportedReactions={supportedReactions}
testID={`reaction-list-item-${reaction.type}`}
type={reaction.type}
/>

We'd then also have to forward it, so Props (above) gets 'testID', Icon passes it to ReactionIcon and so on down the chain.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread package/src/components/Reply/Reply.tsx Outdated
accessibilityLabel={title}
accessibilityRole='text'
style={[!messageFromContext?.quoted_message ? styles.wrapper : null, wrapper]}
testID={messageFromContext?.quoted_message ? 'quoted-message' : 'reply-composer-preview'}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This root View was flattened before. container is already a native view and still wraps the text:

Suggested change
testID={messageFromContext?.quoted_message ? 'quoted-message' : 'reply-composer-preview'}
>
<View
style={[styles.container, container, stylesProp?.container]}
testID={messageFromContext?.quoted_message ? 'quoted-message' : 'reply-composer-preview'}
>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return (
<CompositeAccessibilityProbe label={statusLabel}>
<View style={styles.container}>
<View style={styles.container} testID={statusTestID}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This container is layout only, so the testID adds a native view per channel row (not going to be flattened with it). The icons are SVGs, so let's put it on them:

Suggested change
<View style={styles.container} testID={statusTestID}>
<View style={styles.container}>
{messageStatus === MessageStatusTypes.SENDING ? (
<icons.Time
stroke={semantics.chatTextTimestamp}
height={16}
width={16}
testID='channel-preview-status-sending'
{...timeIcon}
/>
) : messageStatus === MessageStatusTypes.RECEIVED &&
status === MessageDeliveryStatus.READ ? (
<icons.CheckAll
stroke={semantics.accentPrimary}
height={16}
width={16}
testID='channel-preview-status-read'
{...checkAllIcon}
/>
) : status === MessageDeliveryStatus.DELIVERED ? (
<icons.CheckAll
stroke={semantics.chatTextTimestamp}
height={16}
width={16}
testID='channel-preview-status-delivered'
{...checkAllIcon}
/>
) : status === MessageDeliveryStatus.SENT ? (
<icons.Check
stroke={semantics.chatTextTimestamp}
height={16}
width={16}
testID='channel-preview-status-sent'
{...checkIcon}
/>

(similar to how we did it in the last pr)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

get messageSender() {
return by.id('channel-preview-sender');
},
deliveryStatus(status: 'sending' | 'sent' | 'delivered' | 'read' | 'none') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
deliveryStatus(status: 'sending' | 'sent' | 'delivered' | 'read' | 'none') {
deliveryStatus(status: 'sending' | 'sent' | 'delivered' | 'read') {

(since this will noop from the other comment)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


return (
<View style={styles.reactionButton}>
<View style={styles.reactionButton} testID={`reaction-button-${type}`}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This View was flattened before. Button passes the testID to its Pressable:

Suggested change
<View style={styles.reactionButton} testID={`reaction-button-${type}`}>
<View style={styles.reactionButton}>
<Button
testID={`reaction-button-${type}`}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

</View>
</View>
<View style={styles.dismissWrapper}>
<View style={styles.dismissWrapper} testID='composer-link-preview-dismiss'>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dismissWrapper was flattened before. AttachmentRemoveControl passes the testID to its Pressable:

Suggested change
<View style={styles.dismissWrapper} testID='composer-link-preview-dismiss'>
<View style={styles.dismissWrapper}>
<AttachmentRemoveControl onPress={dismissPreview} testID='composer-link-preview-dismiss' />

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@isekovanic isekovanic mentioned this pull request Nov 6, 2026
1 of 6 tasks
@isekovanic
isekovanic changed the base branch from V10 to develop November 6, 2026 14:10
@Stream-SDK-Bot

Copy link
Copy Markdown
Contributor

SDK Size

title develop branch diff status
js_bundle_size 2026 KB 2027 KB +1053 B 🟢
ai_components_size 163 KB 163 KB 0 B 🟢

@isekovanic
isekovanic merged commit 8d70426 into develop Nov 9, 2026
22 checks passed
@isekovanic
isekovanic deleted the ci/v10-e2e-tests branch November 9, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants