From 6d3c1cfa8fd3ff1563848885b0271123a5d2e87e Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 13:51:56 +0100 Subject: [PATCH 01/16] feat(SampleApp): add Detox e2e suite with mock server, robots and page objects --- .github/workflows/e2e-test.yml | 168 +++ .prettierignore | 4 + dotgit/hooks/pre-commit-reject-binaries.py | 2 +- eslint.config.mjs | 4 + examples/SampleApp/.detoxrc.js | 64 + examples/SampleApp/.gitignore | 4 + examples/SampleApp/App.tsx | 3 +- examples/SampleApp/android/app/build.gradle | 3 + examples/SampleApp/android/build.gradle | 2 +- examples/SampleApp/e2e/README.md | 76 + examples/SampleApp/e2e/jest.config.js | 18 + .../SampleApp/e2e/mock-server/DataTypes.ts | 25 + .../SampleApp/e2e/mock-server/MockServer.ts | 84 ++ .../SampleApp/e2e/pages/ChannelListPage.ts | 32 + examples/SampleApp/e2e/pages/LoginPage.ts | 11 + .../SampleApp/e2e/pages/MessageListPage.ts | 87 ++ examples/SampleApp/e2e/robots/BackendRobot.ts | 129 ++ .../SampleApp/e2e/robots/ParticipantRobot.ts | 161 ++ examples/SampleApp/e2e/robots/Robot.ts | 33 + examples/SampleApp/e2e/robots/UserRobot.ts | 126 ++ .../e2e/robots/UserRobotChannelListAsserts.ts | 72 + .../e2e/robots/UserRobotMessageListAsserts.ts | 105 ++ examples/SampleApp/e2e/robots/index.ts | 7 + examples/SampleApp/e2e/support/Element.ts | 146 ++ .../SampleApp/e2e/support/StreamTestCase.ts | 47 + examples/SampleApp/e2e/support/step.ts | 14 + .../e2e/tests/ChannelListTests.e2e.ts | 43 + .../e2e/tests/MessageListTests.e2e.ts | 44 + examples/SampleApp/e2e/tsconfig.json | 9 + examples/SampleApp/fastlane/Fastfile | 100 +- examples/SampleApp/ios/Podfile.lock | 16 +- examples/SampleApp/package.json | 9 +- .../SampleApp/src/components/ScreenHeader.tsx | 1 + examples/SampleApp/src/hooks/useChatClient.ts | 19 +- .../src/screens/UserSelectorScreen.tsx | 3 +- examples/SampleApp/src/utils/e2e.ts | 42 + examples/SampleApp/tsconfig.json | 3 +- .../ChannelLastMessagePreview.tsx | 2 +- .../ChannelMessagePreviewDeliveryStatus.tsx | 10 +- .../ChannelPreview/ChannelPreviewTitle.tsx | 2 +- .../Message/MessageItemView/MessageStatus.tsx | 11 + .../__tests__/MessageStatus.test.tsx | 21 + yarn.lock | 1315 ++++++++++++++++- 43 files changed, 3026 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/e2e-test.yml create mode 100644 examples/SampleApp/.detoxrc.js create mode 100644 examples/SampleApp/e2e/README.md create mode 100644 examples/SampleApp/e2e/jest.config.js create mode 100644 examples/SampleApp/e2e/mock-server/DataTypes.ts create mode 100644 examples/SampleApp/e2e/mock-server/MockServer.ts create mode 100644 examples/SampleApp/e2e/pages/ChannelListPage.ts create mode 100644 examples/SampleApp/e2e/pages/LoginPage.ts create mode 100644 examples/SampleApp/e2e/pages/MessageListPage.ts create mode 100644 examples/SampleApp/e2e/robots/BackendRobot.ts create mode 100644 examples/SampleApp/e2e/robots/ParticipantRobot.ts create mode 100644 examples/SampleApp/e2e/robots/Robot.ts create mode 100644 examples/SampleApp/e2e/robots/UserRobot.ts create mode 100644 examples/SampleApp/e2e/robots/UserRobotChannelListAsserts.ts create mode 100644 examples/SampleApp/e2e/robots/UserRobotMessageListAsserts.ts create mode 100644 examples/SampleApp/e2e/robots/index.ts create mode 100644 examples/SampleApp/e2e/support/Element.ts create mode 100644 examples/SampleApp/e2e/support/StreamTestCase.ts create mode 100644 examples/SampleApp/e2e/support/step.ts create mode 100644 examples/SampleApp/e2e/tests/ChannelListTests.e2e.ts create mode 100644 examples/SampleApp/e2e/tests/MessageListTests.e2e.ts create mode 100644 examples/SampleApp/e2e/tsconfig.json create mode 100644 examples/SampleApp/src/utils/e2e.ts diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000000..c573f95261 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,168 @@ +name: E2E Tests + +on: + pull_request: + + workflow_dispatch: + inputs: + ios: + description: "Run iOS e2e tests" + type: boolean + default: false + android: + description: "Run Android e2e tests" + type: boolean + default: true + mock_server_branch: + description: "stream-chat-test-mock-server branch" + type: string + default: "main" + +permissions: + contents: read + pull-requests: read + checks: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + MOCK_SERVER_BRANCH: ${{ inputs.mock_server_branch || 'main' }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_PR_NUM: ${{ github.event.pull_request.number }} + GITHUB_EVENT_ACTION: ${{ github.event.action }} + GITHUB_EVENT_BEFORE: ${{ github.event.before }} + GITHUB_EVENT_AFTER: ${{ github.event.after }} + +jobs: + check: + name: Check E2E Required + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + required: ${{ steps.check.outputs.required }} + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/ruby-cache + - name: Check if e2e tests are required + id: check + working-directory: examples/SampleApp + run: bundle exec fastlane e2e_check_required + + ios: + needs: check + # UNCOMMENT (replaces the `if` below): if: needs.check.outputs.required == 'true' && github.event_name == 'workflow_dispatch' && inputs.ios + if: needs.check.outputs.required == 'true' + runs-on: macos-15 + timeout-minutes: 60 + env: + DETOX_IOS_DEVICE: "iPhone 17 Pro" + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24.x + cache: "yarn" + cache-dependency-path: "yarn.lock" + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "26.2" + - uses: ./.github/actions/ruby-cache + - name: Install dependencies + uses: ./.github/actions/install-and-build-sdk + - name: Install applesimutils + run: brew tap wix/brew && brew install applesimutils + - name: Cache iOS pods + uses: actions/cache@v5 + with: + path: examples/SampleApp/ios/Pods + key: ${{ runner.os }}-pods-${{ hashFiles('examples/SampleApp/ios/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + - name: Install pods + working-directory: examples/SampleApp/ios + run: bundle exec pod install + # Yarn's `enableScripts: false` skips Detox's postinstall, which would build this. + - name: Build Detox framework cache + working-directory: examples/SampleApp + run: npx detox build-framework-cache + - name: Build app + working-directory: examples/SampleApp + run: npx detox build -c ios.sim.release + - name: Run e2e tests + working-directory: examples/SampleApp + run: bundle exec fastlane test_e2e platform:ios skip_build:true mock_server_branch:"$MOCK_SERVER_BRANCH" + - uses: actions/upload-artifact@v6 + if: failure() + with: + name: e2e-ios-logs + path: | + examples/SampleApp/stream-chat-test-mock-server/logs + examples/SampleApp/artifacts + + android: + needs: check + if: needs.check.outputs.required == 'true' && (github.event_name != 'workflow_dispatch' || inputs.android) + runs-on: ubuntu-latest + timeout-minutes: 60 + env: + ANDROID_API_LEVEL: "34" + DETOX_ADB_NAME: "emulator-5554" + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 24.x + cache: "yarn" + cache-dependency-path: "yarn.lock" + - uses: actions/setup-java@v5 + with: + distribution: "zulu" + java-version: "21" + cache: "gradle" + - uses: ./.github/actions/ruby-cache + - name: Install dependencies + uses: ./.github/actions/install-and-build-sdk + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + # Built before the emulator boots so the emulator doesn't idle through the Gradle build. + - name: Build app + working-directory: examples/SampleApp + run: npx detox build -c android.att.release + - name: Run e2e tests (Android emulator) + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.ANDROID_API_LEVEL }} + arch: x86_64 + target: google_apis + profile: pixel_6 + disable-animations: true + working-directory: examples/SampleApp + script: bundle exec fastlane test_e2e platform:android device:att skip_build:true mock_server_branch:"$MOCK_SERVER_BRANCH" + - uses: actions/upload-artifact@v6 + if: failure() + with: + name: e2e-android-logs + path: | + examples/SampleApp/stream-chat-test-mock-server/logs + examples/SampleApp/artifacts + + result: + name: Test E2E Result + runs-on: ubuntu-latest + if: always() + needs: + - check + - android + steps: + - name: Check E2E results + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: | + echo "$NEEDS" + exit 1 + env: + NEEDS: ${{ toJson(needs) }} diff --git a/.prettierignore b/.prettierignore index c4f36f8ee4..eed3166d14 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,6 +24,10 @@ examples/SampleApp/patches/ .yarn/ yarn.lock +# Detox e2e: mock server checkout cloned by `fastlane start_mock_server`, test artifacts +examples/SampleApp/stream-chat-test-mock-server/ +examples/SampleApp/artifacts/ + # Generated SDK assets package/src/components/docs/ package/src/theme/generated/ diff --git a/dotgit/hooks/pre-commit-reject-binaries.py b/dotgit/hooks/pre-commit-reject-binaries.py index efc532d2bc..014bf44ffa 100755 --- a/dotgit/hooks/pre-commit-reject-binaries.py +++ b/dotgit/hooks/pre-commit-reject-binaries.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import os import subprocess import sys diff --git a/eslint.config.mjs b/eslint.config.mjs index 97425a89b6..0db7e11ec4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -44,6 +44,10 @@ export default tsEslint.config( '**/android/', '**/fastlane/', + // Detox e2e: mock server checkout cloned by `fastlane start_mock_server`, test artifacts + 'examples/SampleApp/stream-chat-test-mock-server/', + 'examples/SampleApp/artifacts/', + // Generated SDK assets 'package/src/components/docs/', 'package/src/theme/generated/', diff --git a/examples/SampleApp/.detoxrc.js b/examples/SampleApp/.detoxrc.js new file mode 100644 index 0000000000..605ec910a0 --- /dev/null +++ b/examples/SampleApp/.detoxrc.js @@ -0,0 +1,64 @@ +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: { + args: { + $0: 'jest', + config: 'e2e/jest.config.js', + }, + jest: { + setupTimeout: 300000, + }, + }, + apps: { + 'ios.debug': { + type: 'ios.app', + binaryPath: 'ios/build/detox/Build/Products/Debug-iphonesimulator/SampleApp.app', + build: + 'xcodebuild -workspace ios/SampleApp.xcworkspace -scheme SampleApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build/detox', + }, + 'ios.release': { + type: 'ios.app', + binaryPath: 'ios/build/detox/Build/Products/Release-iphonesimulator/SampleApp.app', + build: + 'xcodebuild -workspace ios/SampleApp.xcworkspace -scheme SampleApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build/detox', + }, + 'android.debug': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', + testBinaryPath: 'android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + build: + 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..', + reversePorts: [8081], + }, + 'android.release': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', + testBinaryPath: + 'android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk', + build: + 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ..', + }, + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { type: process.env.DETOX_IOS_DEVICE || 'iPhone 16 Pro' }, + }, + emulator: { + type: 'android.emulator', + device: { avdName: process.env.DETOX_AVD_NAME || 'Pixel_API_37' }, + }, + attached: { + type: 'android.attached', + device: { adbName: process.env.DETOX_ADB_NAME || '.*' }, + }, + }, + configurations: { + 'ios.sim.debug': { app: 'ios.debug', device: 'simulator' }, + 'ios.sim.release': { app: 'ios.release', device: 'simulator' }, + 'android.emu.debug': { app: 'android.debug', device: 'emulator' }, + 'android.emu.release': { app: 'android.release', device: 'emulator' }, + 'android.att.debug': { app: 'android.debug', device: 'attached' }, + 'android.att.release': { app: 'android.release', device: 'attached' }, + }, +}; diff --git a/examples/SampleApp/.gitignore b/examples/SampleApp/.gitignore index 7388cefebf..87c3fc1e1b 100644 --- a/examples/SampleApp/.gitignore +++ b/examples/SampleApp/.gitignore @@ -77,3 +77,7 @@ yarn-error.log # Credentials credentials/ app-build/ + +# Detox e2e +/stream-chat-test-mock-server/ +/artifacts/ diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx index 14dc99a866..d56ab0bc66 100644 --- a/examples/SampleApp/App.tsx +++ b/examples/SampleApp/App.tsx @@ -59,6 +59,7 @@ import { UserSelectorScreen } from './src/screens/UserSelectorScreen'; import type { StackNavigatorParamList, UserSelectorParamList } from './src/types'; import AsyncStore from './src/utils/AsyncStore.ts'; +import { isE2eTestRun } from './src/utils/e2e'; import { navigateToChannel, RootNavigationRef } from './src/utils/RootNavigation'; import { watchLocation } from './src/utils/watchLocation'; @@ -352,7 +353,7 @@ const DrawerNavigatorWrapper: React.FC<{ > userRobot.login().openChannel()); +await step('WHEN participant sends a message', () => participantRobot.sendMessage('Hi')); +await step('THEN user receives it', () => userRobot.assertMessage('Hi').assertMessageAuthor(false)); +``` + +## How the app is pointed at the mock server + +`setUpStreamTestCase()` starts a mock server per test and relaunches the app with the launch argument +`MOCK_SERVER_URL` (`localhost` on iOS, `10.0.2.2` on the Android emulator), read in `src/utils/e2e.ts` via +`react-native-launch-arguments`. In that mode the app lists only the mock user (`luke_skywalker`), never restores a +stored login, skips push registration and permission prompts, and runs without offline storage, so every test starts +clean. + +## Running + +```bash +# Everything: clones + starts the mock server, builds, tests, stops the mock server +bundle exec fastlane test_e2e platform:ios +bundle exec fastlane test_e2e platform:android # AVD from DETOX_AVD_NAME (default Pixel_API_37) +# Options: configuration:debug, skip_build:true, local_server:, +# mock_server_branch:, test:e2e/tests/MessageListTests.e2e.ts + +# Or by hand +bundle exec fastlane start_mock_server # driver on MOCK_DRIVER_PORT (default 4569) +yarn e2e:build -c ios.sim.release # or android.emu.release +MOCK_DRIVER_PORT=4569 yarn e2e:test -c ios.sim.release +bundle exec fastlane stop_mock_server +``` + +Release configurations bundle the JS, so no Metro is needed. Debug configurations need `yarn start`. Android builds need +JDK 17 or 21 (`JAVA_HOME`). + +## CI + +`.github/workflows/e2e-test.yml` runs on pull requests and on demand (`workflow_dispatch`, with iOS/Android toggles and +a mock server branch). Like the iOS SDK, a cheap `Check E2E Required` job first runs `fastlane e2e_check_required` +(`is_check_required` over the `e2e` sources in the Fastfile's `impact_analysis`). The simulator/emulator jobs run +only when the PR touches those sources, or when the last change to them hasn't passed yet. `Test E2E Result` is the +job to require in branch protection: it passes when the e2e jobs pass or are skipped as not required. + +iOS runs on `macos-15` (Xcode 26.2, iPhone 17 Pro, `ios.sim.release`). Android runs on `ubuntu-latest` with KVM and an +API 34 x86_64 emulator (`android.att.release`). Both call `fastlane test_e2e skip_build:true`, which on CI adds +`--retries 2 --headless` and keeps failure logs and screenshots in `artifacts/`. Those and the mock server logs are +uploaded when a job fails. + +## Gotchas + +- `detox test --reuse` does not reinstall the app. After rebuilding, run without it, or you test the old binary. +- Yarn's `enableScripts: false` skips Detox's postinstall; run `npx detox build-framework-cache` once on a new machine. +- Detox replaces Jest's global `expect`. Use it for element expectations and `expect as jestExpect` from + `@jest/globals` for values. +- Fabric mounts some views that carry a `testID` (`message-list-item-*`, `message-text-container`) empty, with their + content as siblings. They are not usable as `withAncestor` scopes or visibility targets; the page objects scope message + elements to `message-content-wrapper` or the message list instead. +- Detox iOS never reports avatars as visible (their remote image renders transparent until loaded), so avatar asserts + check existence. +- A ported test keeps the native expectation. If the SDK disagrees, fix the SDK or skip the test against a ticket. diff --git a/examples/SampleApp/e2e/jest.config.js b/examples/SampleApp/e2e/jest.config.js new file mode 100644 index 0000000000..5456675188 --- /dev/null +++ b/examples/SampleApp/e2e/jest.config.js @@ -0,0 +1,18 @@ +/** @type {import('jest').Config} */ +module.exports = { + rootDir: '..', + testMatch: ['/e2e/tests/**/*.e2e.ts'], + testTimeout: 180000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, + transform: { + '\\.ts$': [ + 'babel-jest', + { babelrc: false, configFile: false, presets: ['module:@react-native/babel-preset'] }, + ], + }, +}; diff --git a/examples/SampleApp/e2e/mock-server/DataTypes.ts b/examples/SampleApp/e2e/mock-server/DataTypes.ts new file mode 100644 index 0000000000..a13549c994 --- /dev/null +++ b/examples/SampleApp/e2e/mock-server/DataTypes.ts @@ -0,0 +1,25 @@ +export const forbiddenWord = 'wth'; + +export enum AttachmentType { + IMAGE = 'image', + VIDEO = 'video', + FILE = 'file', +} + +export enum ReactionType { + LOVE = 'love', + LOL = 'haha', + WOW = 'wow', + SAD = 'sad', + LIKE = 'like', +} + +export enum MessageDeliveryStatus { + READ = 'read', + DELIVERED = 'delivered', + PENDING = 'pending', + SENT = 'sent', + FAILED = 'failed', + /** No delivery status icon is expected. */ + NIL = 'nil', +} diff --git a/examples/SampleApp/e2e/mock-server/MockServer.ts b/examples/SampleApp/e2e/mock-server/MockServer.ts new file mode 100644 index 0000000000..1d273d12cf --- /dev/null +++ b/examples/SampleApp/e2e/mock-server/MockServer.ts @@ -0,0 +1,84 @@ +declare const process: { env: Record }; + +/** + * Client for `stream-chat-test-mock-server`. The driver (`driver.rb`) spawns one mock server per + * test on its own port; `start` asks it for one and `stop` shuts it down. + * + * Unlike the native suites, the test code runs on the host, so robots always talk to `localhost` + * while the app gets `appUrl`, which is the host as the device sees it. + */ +export class MockServer { + static readonly driverPort = process.env.MOCK_DRIVER_PORT ?? '4569'; + private static readonly driverUrl = `http://localhost:${MockServer.driverPort}`; + + private port?: string; + + /** The mock server as the test runner (host) sees it. */ + get url(): string { + if (!this.port) { + throw new Error('MockServer is not started'); + } + return `http://localhost:${this.port}`; + } + + /** The mock server as the app under test sees it: the Android emulator reaches the host via 10.0.2.2. */ + get appUrl(): string { + const host = device.getPlatform() === 'android' ? '10.0.2.2' : 'localhost'; + return this.url.replace('localhost', host); + } + + /** Never retried: every call to the driver's `/start` spawns a new server on a new port. */ + async start(testName: string): Promise { + const response = await this.execute( + `${MockServer.driverUrl}/start/${encodeURIComponent(testName)}`, + ); + this.port = (await response.text()).trim(); + await this.waitUntilReady(); + } + + async stop(): Promise { + if (!this.port) { + return; + } + await this.getRequest('stop').catch(() => undefined); + this.port = undefined; + } + + async postRequest(endpoint: string, body = ''): Promise { + const response = await this.execute(`${this.url}/${endpoint}`, { + body, + headers: { 'Content-Type': 'text/plain' }, + method: 'POST', + }); + return response.text(); + } + + async getRequest(endpoint: string): Promise { + const response = await this.execute(`${this.url}/${endpoint}`); + return response.text(); + } + + private async waitUntilReady(timeoutMs = 5000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + const response = await fetch(`${this.url}/ping`); + if (response.ok) { + return; + } + } catch { + // The server process is still booting. + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`MockServer did not start within ${timeoutMs}ms on ${this.url}`); + } + + private async execute(url: string, init?: RequestInit): Promise { + const response = await fetch(url, init); + if (!response.ok) { + throw new Error(`${init?.method ?? 'GET'} ${url} failed: ${response.status}`); + } + return response; + } +} diff --git a/examples/SampleApp/e2e/pages/ChannelListPage.ts b/examples/SampleApp/e2e/pages/ChannelListPage.ts new file mode 100644 index 0000000000..4cc136b9b0 --- /dev/null +++ b/examples/SampleApp/e2e/pages/ChannelListPage.ts @@ -0,0 +1,32 @@ +export const ChannelListPage = { + ChannelList: { + get channelList() { + return by.id('channel-list-view'); + }, + get channels() { + return by.id('channel-preview-button'); + }, + + Channel: { + get avatar() { + return by.id('avatar-image').withAncestor(by.id('channel-preview-button')); + }, + get name() { + return by.id('channel-preview-title'); + }, + nameWithText(text: string) { + return by.id('channel-preview-title').and(by.text(text)); + }, + get messagePreview() { + return by.id('channel-preview-message'); + }, + /** "You:" or ":" in front of the preview text, rendered as its own text. */ + get messageSender() { + return by.id('channel-preview-sender'); + }, + get unreadCount() { + return by.id('unread-count').withAncestor(by.id('channel-preview-button')); + }, + }, + }, +}; diff --git a/examples/SampleApp/e2e/pages/LoginPage.ts b/examples/SampleApp/e2e/pages/LoginPage.ts new file mode 100644 index 0000000000..5feb125c2a --- /dev/null +++ b/examples/SampleApp/e2e/pages/LoginPage.ts @@ -0,0 +1,11 @@ +/** The mock server's app user (`E2E_USER` in `src/utils/e2e.ts`), the only one listed in e2e mode. */ +export const appUserId = 'luke_skywalker'; + +export const LoginPage = { + get userSelector() { + return by.id('user-selector-screen'); + }, + get loginButton() { + return by.id(`user-selector-button-${appUserId}`); + }, +}; diff --git a/examples/SampleApp/e2e/pages/MessageListPage.ts b/examples/SampleApp/e2e/pages/MessageListPage.ts new file mode 100644 index 0000000000..4559f48590 --- /dev/null +++ b/examples/SampleApp/e2e/pages/MessageListPage.ts @@ -0,0 +1,87 @@ +const messageCell = () => by.id(/^message-list-item-/); +// Fabric mounts `message-list-item-*` empty with its content beside it, so the list, not the cell, +// is the ancestor that scopes message elements (the channel list stays mounted underneath). +const inMessageList = (matcher: Detox.NativeMatcher) => + matcher.withAncestor(by.id(/^message-(flat|flash)-list$/)); + +export const MessageListPage = { + Header: { + get backButton() { + return by.id('back-button'); + }, + }, + + Composer: { + get inputField() { + return by.id('auto-complete-text-input'); + }, + get sendButton() { + return by.id('send-button'); + }, + get attachmentsButton() { + return by.id('attach-button'); + }, + get cooldownIndicator() { + return by.id('cooldown-timer'); + }, + }, + + MessageList: { + get messageList() { + return by.id(/^message-(flat|flash)-list$/); + }, + get messages() { + return messageCell(); + }, + get dateSeparator() { + return by.id('date-separator'); + }, + get typingIndicator() { + return by.id('typing-indicator'); + }, + get scrollToBottomButton() { + return by.id('scroll-to-bottom-button'); + }, + + Message: { + /** Wraps the author's avatar; only rendered for other users' messages. */ + get avatar() { + return by.id('message-author'); + }, + text(text: string) { + // Not under `message-text-container`: Fabric mounts that view empty, with the text beside it. + return by.text(text).withAncestor(by.id('message-content-wrapper')); + }, + get timestamp() { + return by.id('message-status-time'); + }, + get deliveryStatusIsRead() { + return inMessageList(by.id('message-status-read')); + }, + get deliveryStatusIsDelivered() { + return inMessageList(by.id('message-status-delivered')); + }, + get deliveryStatusIsPending() { + return inMessageList(by.id('message-status-sending')); + }, + get deliveryStatusIsSent() { + return inMessageList(by.id('message-status-sent')); + }, + get deliveryStatusIsFailed() { + return inMessageList(by.id('message-error')); + }, + get deletedMessage() { + return by.id('message-deleted'); + }, + get threadRepliesLabel() { + return by.id('message-replies'); + }, + + Reactions: { + get reactions() { + return by.id('reaction-list-item'); + }, + }, + }, + }, +}; diff --git a/examples/SampleApp/e2e/robots/BackendRobot.ts b/examples/SampleApp/e2e/robots/BackendRobot.ts new file mode 100644 index 0000000000..c62e3eea5c --- /dev/null +++ b/examples/SampleApp/e2e/robots/BackendRobot.ts @@ -0,0 +1,129 @@ +import { Robot } from './Robot'; + +import type { MockServer } from '../mock-server/MockServer'; + +/** Drives server-side state through the mock server's test-control endpoints. */ +export class BackendRobot extends Robot { + constructor(private readonly mockServer: MockServer) { + super(); + } + + generateChannels({ + channelsCount, + messagesCount = 0, + repliesCount = 0, + messagesText, + repliesText, + channelNames = [], + withDirectMessageChannel = false, + }: { + channelsCount: number; + messagesCount?: number; + repliesCount?: number; + messagesText?: string; + repliesText?: string; + channelNames?: string[]; + withDirectMessageChannel?: boolean; + }): this { + const params = new URLSearchParams(); + if (messagesText !== undefined) params.set('messages_text', messagesText); + if (repliesText !== undefined) params.set('replies_text', repliesText); + if (channelNames.length > 0) params.set('channel_names', channelNames.join(',')); + if (withDirectMessageChannel) params.set('dm', 'true'); + params.set('channels', `${channelsCount}`); + params.set('messages', `${messagesCount}`); + params.set('replies', `${repliesCount}`); + return this.post(`mock?${params}`); + } + + failNewMessages(): this { + return this.post('fail_messages'); + } + + freezeNewMessages(): this { + return this.post('freeze_messages'); + } + + setCooldown(enabled: boolean, duration: number): this { + return this.post(`config/cooldown?enabled=${enabled}&duration=${duration}`); + } + + /** + * Truncates the currently open channel on the server side. The app receives `channel.truncated`. + * + * @param withMessage also delivers a "Channel truncated" system message. + */ + truncateChannel(withMessage: boolean): this { + return this.post(`truncate_channel?with_message=${withMessage}`); + } + + /** Enables or disables read events on every channel. Call before the channel is opened. */ + setReadEvents(enabled: boolean): this { + return this.post(`config/read_events?value=${enabled}`); + } + + /** Adds a member to the currently open channel. The app receives `member.added` + `channel.updated`. */ + addMember(userId: string): this { + return this.post(`add_member?user_id=${encodeURIComponent(userId)}`); + } + + /** Removes a member from the currently open channel. The app receives `member.removed` + `channel.updated`. */ + removeMember(userId: string): this { + return this.post(`remove_member?user_id=${encodeURIComponent(userId)}`); + } + + /** + * Creates a reminder for the last message of the currently open channel. + * + * @param remindAtSeconds how far in the future it is due; omit for a "saved for later" reminder. + */ + createReminder(remindAtSeconds?: number): this { + return this.post( + remindAtSeconds === undefined + ? 'create_reminder' + : `create_reminder?remind_at=${remindAtSeconds}`, + ); + } + + revokeToken(duration = 5): this { + return this.post(`jwt/revoke_token?duration=${duration}`); + } + + invalidateToken(duration = 5): this { + return this.post(`jwt/invalidate_token?duration=${duration}`); + } + + invalidateTokenDate(duration = 5): this { + return this.post(`jwt/invalidate_token_date?duration=${duration}`); + } + + invalidateTokenSignature(duration = 5): this { + return this.post(`jwt/invalidate_token_signature?duration=${duration}`); + } + + breakTokenGeneration(duration = 5): this { + return this.post(`jwt/break_token_generation?duration=${duration}`); + } + + /** + * Waits until the app closes its WebSocket. The SDK keeps the socket open for a while after the + * app goes to background, so call this before server-side events that must not arrive live. + */ + waitForWebSocketDisconnection(timeoutMs = 10000): this { + return this.perform(async () => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const status = JSON.parse(await this.mockServer.getRequest('ws/status')); + if (!status.connected) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`WebSocket was not disconnected within ${timeoutMs}ms`); + }); + } + + private post(endpoint: string): this { + return this.perform(() => this.mockServer.postRequest(endpoint)); + } +} diff --git a/examples/SampleApp/e2e/robots/ParticipantRobot.ts b/examples/SampleApp/e2e/robots/ParticipantRobot.ts new file mode 100644 index 0000000000..0615c8a469 --- /dev/null +++ b/examples/SampleApp/e2e/robots/ParticipantRobot.ts @@ -0,0 +1,161 @@ +import { Robot } from './Robot'; + +import type { AttachmentType, ReactionType } from '../mock-server/DataTypes'; +import type { MockServer } from '../mock-server/MockServer'; + +const MULTIPLE_MESSAGES_INTERVAL_MS = 300; + +const quoteParam = (last: boolean) => (last ? 'quote_last=true' : 'quote_first=true'); + +/** Acts as the other channel member (`count_dooku`) through the mock server's participant endpoints. */ +export class ParticipantRobot extends Robot { + static readonly userName = 'Count Dooku'; + static readonly userId = 'count_dooku'; + + constructor(private readonly mockServer: MockServer) { + super(); + } + + startTyping(): this { + return this.post('participant/typing/start'); + } + + startTypingInThread(): this { + return this.post('participant/typing/start?thread=true'); + } + + stopTyping(): this { + return this.post('participant/typing/stop'); + } + + stopTypingInThread(): this { + return this.post('participant/typing/stop?thread=true'); + } + + readMessage(): this { + return this.post('participant/read'); + } + + /** Marks the newest message as delivered to the participant (`message.delivered`). */ + markMessagesDelivered(): this { + return this.post('participant/delivered'); + } + + sendMessage(text: string, delay = 0): this { + return this.post( + delay > 0 ? `participant/message?delay=${delay}` : 'participant/message', + text, + ); + } + + /** Sends `count` messages named `${text}-1` through `${text}-${count}`, 300ms apart. */ + sendMultipleMessages(text: string, count: number): this { + for (let index = 1; index <= count; index++) { + this.sendMessage(`${text}-${index}`).sleep(MULTIPLE_MESSAGES_INTERVAL_MS); + } + return this; + } + + sendMessageInThread(text: string, alsoSendInChannel = false): this { + return this.post( + `participant/message?thread=true&thread_and_channel=${alsoSendInChannel}`, + text, + ); + } + + editMessage(text: string): this { + return this.post('participant/message?action=edit', text); + } + + deleteMessage(hard = false): this { + return this.post(`participant/message?action=delete&hard_delete=${hard}`); + } + + quoteMessage(text: string, last = true): this { + return this.post(`participant/message?${quoteParam(last)}`, text); + } + + quoteMessageInThread(text: string, alsoSendInChannel = false, last = true): this { + return this.post( + `participant/message?${quoteParam(last)}&thread=true&thread_and_channel=${alsoSendInChannel}`, + text, + ); + } + + uploadGiphy(): this { + return this.post('participant/message?giphy=true'); + } + + uploadGiphyInThread(): this { + return this.post('participant/message?giphy=true&thread=true'); + } + + quoteMessageWithGiphy(last = true): this { + return this.post(`participant/message?giphy=true&${quoteParam(last)}`); + } + + quoteMessageWithGiphyInThread(alsoSendInChannel = false, last = true): this { + return this.post( + `participant/message?giphy=true&${quoteParam(last)}&thread=true&thread_and_channel=${alsoSendInChannel}`, + ); + } + + pinMessage(): this { + return this.post('participant/message?action=pin'); + } + + unpinMessage(): this { + return this.post('participant/message?action=unpin'); + } + + uploadAttachment(type: AttachmentType, count = 1): this { + return this.post(`participant/message?${type}=${count}`); + } + + quoteMessageWithAttachment(type: AttachmentType, count = 1, last = true): this { + return this.post(`participant/message?${quoteParam(last)}&${type}=${count}`); + } + + uploadAttachmentInThread(type: AttachmentType, count = 1, alsoSendInChannel = false): this { + return this.post( + `participant/message?${type}=${count}&thread=true&thread_and_channel=${alsoSendInChannel}`, + ); + } + + quoteMessageWithAttachmentInThread( + type: AttachmentType, + count = 1, + alsoSendInChannel = false, + last = true, + ): this { + return this.post( + `participant/message?${quoteParam(last)}&${type}=${count}&thread=true&thread_and_channel=${alsoSendInChannel}`, + ); + } + + /** Votes for `option` in the poll of the newest poll message. */ + castPollVote(option: string): this { + return this.post(`participant/poll_vote?option=${encodeURIComponent(option)}`); + } + + /** Adds an answer (comment) to the poll of the newest poll message. */ + addPollAnswer(answer: string): this { + return this.post(`participant/poll_vote?answer=${encodeURIComponent(answer)}`); + } + + addReaction(type: ReactionType, delay = 0): this { + return this.post( + delay > 0 + ? `participant/reaction?type=${type}&delay=${delay}` + : `participant/reaction?type=${type}`, + ); + } + + deleteReaction(type: ReactionType): this { + return this.post(`participant/reaction?type=${type}&delete=true`); + } + + private post(endpoint: string, body?: string): this { + return this.perform(() => this.mockServer.postRequest(endpoint, body)); + } +} diff --git a/examples/SampleApp/e2e/robots/Robot.ts b/examples/SampleApp/e2e/robots/Robot.ts new file mode 100644 index 0000000000..44dbaaecb7 --- /dev/null +++ b/examples/SampleApp/e2e/robots/Robot.ts @@ -0,0 +1,33 @@ +/** + * Base for the fluent robots. Native robots chain synchronous calls + * (`userRobot.login().openChannel()`); Detox is async, so each call queues its action and + * returns `this`, and awaiting the robot runs the queue: + * + * await userRobot.login().openChannel().assertMessage('Hi'); + * + * A failed action rejects the awaited chain and skips the rest of it. + */ +export abstract class Robot implements PromiseLike { + private queue: Promise = Promise.resolve(); + + /** Queues `action` after the previous ones. Used by the robots and their assert extensions. */ + perform(action: () => Promise): this { + this.queue = this.queue.then(async () => { + await action(); + }); + return this; + } + + sleep(ms: number): this { + return this.perform(() => new Promise((resolve) => setTimeout(resolve, ms))); + } + + then( + onfulfilled?: ((value: void) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ): PromiseLike { + const queue = this.queue; + this.queue = Promise.resolve(); + return queue.then(onfulfilled, onrejected); + } +} diff --git a/examples/SampleApp/e2e/robots/UserRobot.ts b/examples/SampleApp/e2e/robots/UserRobot.ts new file mode 100644 index 0000000000..898205ad05 --- /dev/null +++ b/examples/SampleApp/e2e/robots/UserRobot.ts @@ -0,0 +1,126 @@ +import { Robot } from './Robot'; + +import { ChannelListPage } from '../pages/ChannelListPage'; +import { LoginPage } from '../pages/LoginPage'; +import { MessageListPage } from '../pages/MessageListPage'; +import { + waitToAppear, + waitToAppearAndClick, + waitToAppearAndLongPress, + waitToDisappear, +} from '../support/Element'; + +const { ChannelList } = ChannelListPage; +const { Composer, Header, MessageList } = MessageListPage; + +/** Drives the app as the logged-in user. Asserts live in the `UserRobot*Asserts` extensions. */ +export class UserRobot extends Robot { + login(): this { + return this.perform(() => waitToAppearAndClick(LoginPage.loginButton)); + } + + waitForChannelListToLoad(): this { + return this.perform(() => waitToAppear(ChannelList.channels, { index: 0 })); + } + + waitForMessageListToLoad(): this { + return this.perform(() => waitToAppear(Composer.inputField)); + } + + openChannel(channelCellIndex = 0): this { + return this.perform(() => + waitToAppearAndClick(ChannelList.channels, { index: channelCellIndex }), + ).waitForMessageListToLoad(); + } + + openChannelWithName(channelName: string): this { + return this.perform(() => + waitToAppearAndClick(ChannelList.Channel.nameWithText(channelName)), + ).waitForMessageListToLoad(); + } + + openContextMenu(messageText: string): this { + return this.perform(() => waitToAppearAndLongPress(MessageList.Message.text(messageText))); + } + + typeText(text: string): this { + return this.perform(async () => { + const inputField = await waitToAppear(Composer.inputField); + await inputField.typeText(text); + }); + } + + clearComposer(): this { + return this.perform(async () => { + const inputField = await waitToAppear(Composer.inputField); + await inputField.clearText(); + }); + } + + tapOnSendButton(): this { + return this.perform(() => waitToAppearAndClick(Composer.sendButton)); + } + + sendMessage(text: string): this { + return this.typeText(text).tapOnSendButton(); + } + + tapOnBackButton(): this { + return this.perform(() => waitToAppearAndClick(Header.backButton)); + } + + /** Android hardware back; iOS has none, so it taps the header's back button there. */ + pressBack(): this { + return this.perform(async () => { + if (device.getPlatform() === 'android') { + await device.pressBack(); + } else { + await waitToAppearAndClick(Header.backButton); + } + }); + } + + tapOnScrollToBottomButton(): this { + return this.perform(() => waitToAppearAndClick(MessageList.scrollToBottomButton)); + } + + moveToChannelListFromMessageList(): this { + return this.tapOnBackButton() + .perform(() => waitToDisappear(Composer.inputField)) + .waitForChannelListToLoad(); + } + + scrollChannelListDown(times = 3): this { + return this.swipe(ChannelList.channelList, 'up', times); + } + + scrollChannelListUp(times = 3): this { + return this.swipe(ChannelList.channelList, 'down', times); + } + + scrollMessageListDown(times = 3): this { + // The message list is inverted: newer messages are at the bottom, reached by swiping down. + return this.swipe(MessageList.messageList, 'down', times); + } + + scrollMessageListUp(times = 3): this { + return this.swipe(MessageList.messageList, 'up', times); + } + + backgroundApp(): this { + return this.perform(() => device.sendToHome()); + } + + foregroundApp(): this { + return this.perform(() => device.launchApp({ newInstance: false })); + } + + private swipe(list: Detox.NativeMatcher, direction: Detox.Direction, times: number): this { + return this.perform(async () => { + const target = await waitToAppear(list); + for (let i = 0; i < times; i++) { + await target.swipe(direction, 'fast', 0.6); + } + }); + } +} diff --git a/examples/SampleApp/e2e/robots/UserRobotChannelListAsserts.ts b/examples/SampleApp/e2e/robots/UserRobotChannelListAsserts.ts new file mode 100644 index 0000000000..068ed8c6e6 --- /dev/null +++ b/examples/SampleApp/e2e/robots/UserRobotChannelListAsserts.ts @@ -0,0 +1,72 @@ +import { expect as jestExpect } from '@jest/globals'; + +import { ParticipantRobot } from './ParticipantRobot'; +import { UserRobot } from './UserRobot'; + +import { ChannelListPage } from '../pages/ChannelListPage'; +import { waitForCount, waitForExistence, waitForText, waitToDisappear } from '../support/Element'; + +const { ChannelList } = ChannelListPage; +const { Channel } = ChannelList; + +declare module './UserRobot' { + interface UserRobot { + assertChannelAvatar(): this; + assertChannelCount(count: number): this; + assertChannelListIsEmpty(): this; + assertChannelName(name: string, channelCellIndex?: number): this; + assertChannelUnreadCount(count: number): this; + /** + * @param fromCurrentUser `true` expects a "You: " prefix, `false` the participant's name, + * `undefined` only the text. + */ + assertMessageInChannelPreview(text: string, fromCurrentUser?: boolean): this; + } +} + +UserRobot.prototype.assertChannelAvatar = function () { + return this.perform(async () => { + jestExpect(await waitForExistence(Channel.avatar, { index: 0 })).toBe(true); + }); +}; + +UserRobot.prototype.assertChannelCount = function (count) { + return this.perform(async () => { + jestExpect(await waitForCount(ChannelList.channels, count)).toBe(count); + }); +}; + +UserRobot.prototype.assertChannelListIsEmpty = function () { + return this.perform(async () => { + await waitToDisappear(ChannelList.channels); + jestExpect(await waitForCount(ChannelList.channels, 0)).toBe(0); + }); +}; + +UserRobot.prototype.assertChannelName = function (name, channelCellIndex = 0) { + return this.perform(async () => { + jestExpect(await waitForText(Channel.name, name, { index: channelCellIndex })).toBe(name); + }); +}; + +UserRobot.prototype.assertChannelUnreadCount = function (count) { + return this.perform(async () => { + jestExpect(await waitForText(Channel.unreadCount, `${count}`)).toBe(`${count}`); + }); +}; + +UserRobot.prototype.assertMessageInChannelPreview = function (text, fromCurrentUser) { + const expectedSender = + fromCurrentUser === true + ? 'You:' + : fromCurrentUser === false + ? `${ParticipantRobot.userName}:` + : undefined; + return this.perform(async () => { + const preview = await waitForText(Channel.messagePreview, text, { mustBeEqual: false }); + jestExpect(preview.trimEnd()).toContain(text); + if (expectedSender !== undefined) { + jestExpect(await waitForText(Channel.messageSender, expectedSender)).toBe(expectedSender); + } + }); +}; diff --git a/examples/SampleApp/e2e/robots/UserRobotMessageListAsserts.ts b/examples/SampleApp/e2e/robots/UserRobotMessageListAsserts.ts new file mode 100644 index 0000000000..7596ce79c8 --- /dev/null +++ b/examples/SampleApp/e2e/robots/UserRobotMessageListAsserts.ts @@ -0,0 +1,105 @@ +import { expect as jestExpect } from '@jest/globals'; + +import { UserRobot } from './UserRobot'; + +import { MessageDeliveryStatus } from '../mock-server/DataTypes'; +import { MessageListPage } from '../pages/MessageListPage'; +import { + defaultTimeout, + isDisplayed, + waitForCount, + waitForExistence, + waitToAppear, + waitToDisappear, +} from '../support/Element'; + +const { MessageList } = MessageListPage; +const { Message } = MessageList; + +declare module './UserRobot' { + interface UserRobot { + assertMessage(text: string, isDisplayed?: boolean): this; + assertMessageAuthor(isCurrentUser: boolean): this; + assertMessageDeliveryStatus(status: MessageDeliveryStatus, count?: number): this; + assertMessageFailedIcon(isDisplayed: boolean): this; + assertTypingIndicator(isDisplayed: boolean): this; + assertScrollToBottomButton(isDisplayed: boolean): this; + } +} + +export const assertVisibility = async ( + selector: Detox.NativeMatcher, + shouldBeDisplayed: boolean, + timeout = defaultTimeout, +) => { + if (shouldBeDisplayed) { + await waitToAppear(selector, { index: 0, timeout }); + } else { + await waitToDisappear(selector, { index: 0, timeout }); + jestExpect(await isDisplayed(selector, 0)).toBe(false); + } +}; + +const assertDeliveryStatusCount = async (selector: Detox.NativeMatcher, count?: number) => { + await assertVisibility(selector, true, 30000); + if (count !== undefined) { + jestExpect(await waitForCount(selector, count)).toBe(count); + } +}; + +UserRobot.prototype.assertMessage = function (text, shouldBeDisplayed = true) { + return this.perform(async () => { + await assertVisibility(Message.text(text), shouldBeDisplayed); + if (shouldBeDisplayed) { + jestExpect(await isDisplayed(Message.timestamp, 0)).toBe(true); + } + }); +}; + +/** The current user's messages carry no avatar; other users' do. */ +UserRobot.prototype.assertMessageAuthor = function (isCurrentUser) { + return this.perform(async () => { + const exists = await waitForExistence(Message.avatar, { + index: 0, + shouldExist: !isCurrentUser, + }); + jestExpect(exists).toBe(!isCurrentUser); + }); +}; + +UserRobot.prototype.assertMessageDeliveryStatus = function (status, count) { + return this.perform(async () => { + switch (status) { + case MessageDeliveryStatus.READ: + return assertDeliveryStatusCount(Message.deliveryStatusIsRead, count); + case MessageDeliveryStatus.DELIVERED: + return assertDeliveryStatusCount(Message.deliveryStatusIsDelivered, count); + case MessageDeliveryStatus.PENDING: + return assertDeliveryStatusCount(Message.deliveryStatusIsPending, count); + case MessageDeliveryStatus.SENT: + return assertDeliveryStatusCount(Message.deliveryStatusIsSent, count); + case MessageDeliveryStatus.FAILED: + return assertDeliveryStatusCount(Message.deliveryStatusIsFailed, count); + case MessageDeliveryStatus.NIL: + await assertVisibility(Message.deliveryStatusIsRead, false); + await assertVisibility(Message.deliveryStatusIsDelivered, false); + await assertVisibility(Message.deliveryStatusIsPending, false); + await assertVisibility(Message.deliveryStatusIsSent, false); + return; + default: + throw new Error(`Unsupported delivery status: ${status}`); + } + }); +}; + +UserRobot.prototype.assertMessageFailedIcon = function (shouldBeDisplayed) { + return this.perform(() => assertVisibility(Message.deliveryStatusIsFailed, shouldBeDisplayed)); +}; + +UserRobot.prototype.assertTypingIndicator = function (shouldBeDisplayed) { + return this.perform(() => assertVisibility(MessageList.typingIndicator, shouldBeDisplayed)); +}; + +UserRobot.prototype.assertScrollToBottomButton = function (shouldBeDisplayed) { + return this.perform(() => assertVisibility(MessageList.scrollToBottomButton, shouldBeDisplayed)); +}; diff --git a/examples/SampleApp/e2e/robots/index.ts b/examples/SampleApp/e2e/robots/index.ts new file mode 100644 index 0000000000..bc92c7f124 --- /dev/null +++ b/examples/SampleApp/e2e/robots/index.ts @@ -0,0 +1,7 @@ +// The assert files extend UserRobot's prototype, so they must be loaded with it. +import './UserRobotMessageListAsserts'; +import './UserRobotChannelListAsserts'; + +export { BackendRobot } from './BackendRobot'; +export { ParticipantRobot } from './ParticipantRobot'; +export { UserRobot } from './UserRobot'; diff --git a/examples/SampleApp/e2e/support/Element.ts b/examples/SampleApp/e2e/support/Element.ts new file mode 100644 index 0000000000..680f04a112 --- /dev/null +++ b/examples/SampleApp/e2e/support/Element.ts @@ -0,0 +1,146 @@ +/** + * Element helpers mirroring the Android suite's `uiautomator` extensions (`Element.kt`, `Wait.kt`), + * so robot code reads the same: `selector.waitToAppearAndClick()` becomes + * `await waitToAppearAndClick(selector)`. + */ + +export const defaultTimeout = 15000; + +type Selector = Detox.NativeMatcher; + +type WaitOptions = { + /** Picks one match when the selector matches several elements. */ + index?: number; + timeout?: number; +}; + +const findElement = (selector: Selector, index?: number) => + index === undefined ? element(selector) : element(selector).atIndex(index); + +export const waitToAppear = async ( + selector: Selector, + { index, timeout = defaultTimeout }: WaitOptions = {}, +) => { + const target = findElement(selector, index); + await waitFor(target).toBeVisible().withTimeout(timeout); + return target; +}; + +export const waitToAppearAndClick = async (selector: Selector, options?: WaitOptions) => { + const target = await waitToAppear(selector, options); + await target.tap(); + return target; +}; + +export const waitToAppearAndLongPress = async (selector: Selector, options?: WaitOptions) => { + const target = await waitToAppear(selector, options); + await target.longPress(); + return target; +}; + +/** Waits until nothing matching `selector` is visible. Resolves (does not throw) on timeout. */ +export const waitToDisappear = async ( + selector: Selector, + { index, timeout = defaultTimeout }: WaitOptions = {}, +) => { + try { + await waitFor(findElement(selector, index)).not.toBeVisible().withTimeout(timeout); + } catch { + // The caller asserts on `isDisplayed` afterwards, like `waitToDisappear().isDisplayed()` on Android. + } +}; + +/** + * Waits until `selector` exists (or, with `shouldExist: false`, until it no longer does), and + * returns whether it exists then. For elements Detox iOS never reports as visible, such as avatars + * whose remote image has not loaded: its visibility check needs rendered pixels. + */ +export const waitForExistence = async ( + selector: Selector, + { + index, + shouldExist = true, + timeout = defaultTimeout, + }: WaitOptions & { shouldExist?: boolean } = {}, +) => { + const target = findElement(selector, index); + try { + if (shouldExist) { + await waitFor(target).toExist().withTimeout(timeout); + } else { + await waitFor(target).not.toExist().withTimeout(timeout); + } + return shouldExist; + } catch { + return !shouldExist; + } +}; + +export const isDisplayed = async (selector: Selector, index?: number) => { + try { + await expect(findElement(selector, index)).toBeVisible(); + return true; + } catch { + return false; + } +}; + +/** Polls until `predicate` holds, then returns `true`; returns `false` once `timeout` elapses. */ +export const waitUntil = async ( + predicate: () => Promise, + timeout = defaultTimeout, + interval = 500, +) => { + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + if (await predicate()) { + return true; + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } + return predicate(); +}; + +/** Number of elements currently matching `selector` (Detox has no count query of its own). */ +export const count = async (selector: Selector) => { + try { + const attributes = await element(selector).getAttributes(); + return 'elements' in attributes ? attributes.elements.length : 1; + } catch { + return 0; + } +}; + +/** Waits until `selector` matches exactly `expected` elements and returns the last count seen. */ +export const waitForCount = async (selector: Selector, expected: number, timeout?: number) => { + let actual = 0; + await waitUntil(async () => { + actual = await count(selector); + return actual === expected; + }, timeout); + return actual; +}; + +/** Waits until `selector` (or its `index`-th match) shows `text` and returns the text it shows. */ +export const waitForText = async ( + selector: Selector, + text: string, + { index = 0, mustBeEqual = true, timeout }: WaitOptions & { mustBeEqual?: boolean } = {}, +) => { + let actual = ''; + await waitUntil(async () => { + actual = await textOf(selector, index); + return mustBeEqual ? actual === text : actual.includes(text); + }, timeout); + return actual; +}; + +export const textOf = async (selector: Selector, index = 0) => { + try { + const attributes = await element(selector).getAttributes(); + const match = 'elements' in attributes ? attributes.elements[index] : attributes; + return match?.text ?? match?.label ?? ''; + } catch { + return ''; + } +}; diff --git a/examples/SampleApp/e2e/support/StreamTestCase.ts b/examples/SampleApp/e2e/support/StreamTestCase.ts new file mode 100644 index 0000000000..19231892e6 --- /dev/null +++ b/examples/SampleApp/e2e/support/StreamTestCase.ts @@ -0,0 +1,47 @@ +import { afterEach, beforeEach, expect as jestExpect } from '@jest/globals'; + +import { MockServer } from '../mock-server/MockServer'; +import { BackendRobot, ParticipantRobot, UserRobot } from '../robots'; + +/** + * Per-test fixture, the counterpart of `StreamTestCase` on iOS and Android: before each test it + * starts a fresh mock server and relaunches the app pointed at it; after each test it stops the + * server. + * + * describe('MessageListTests', () => { + * const { userRobot, participantRobot, backendRobot } = setUpStreamTestCase(); + * it('test_...', async () => { ... }); + * }); + */ +export class StreamTestCase { + readonly mockServer = new MockServer(); + readonly userRobot = new UserRobot(); + readonly backendRobot = new BackendRobot(this.mockServer); + readonly participantRobot = new ParticipantRobot(this.mockServer); +} + +/** `describe` + `it` names `MessageListTests test_x` → `test_x`, which names the mock server log. */ +const currentTestName = () => + (jestExpect.getState().currentTestName ?? 'unknown') + .split(' ') + .pop()! + .replace(/[^\w-]/g, '_'); + +export const setUpStreamTestCase = (): StreamTestCase => { + const testCase = new StreamTestCase(); + + beforeEach(async () => { + await testCase.mockServer.start(currentTestName()); + await device.launchApp({ + launchArgs: { MOCK_SERVER_URL: testCase.mockServer.appUrl }, + newInstance: true, + permissions: { notifications: 'YES', photos: 'YES' }, + }); + }); + + afterEach(async () => { + await testCase.mockServer.stop(); + }); + + return testCase; +}; diff --git a/examples/SampleApp/e2e/support/step.ts b/examples/SampleApp/e2e/support/step.ts new file mode 100644 index 0000000000..6ff6d18688 --- /dev/null +++ b/examples/SampleApp/e2e/support/step.ts @@ -0,0 +1,14 @@ +/** + * A GIVEN / WHEN / THEN step, like `step("...") { }` on Android and `GIVEN("...") { }` on iOS. + * A failure is reported with the step it happened in. + */ +export const step = async (name: string, body: () => PromiseLike) => { + try { + await body(); + } catch (error) { + if (error instanceof Error) { + error.message = `[${name}] ${error.message}`; + } + throw error; + } +}; diff --git a/examples/SampleApp/e2e/tests/ChannelListTests.e2e.ts b/examples/SampleApp/e2e/tests/ChannelListTests.e2e.ts new file mode 100644 index 0000000000..9a742a8e80 --- /dev/null +++ b/examples/SampleApp/e2e/tests/ChannelListTests.e2e.ts @@ -0,0 +1,43 @@ +import { describe, it } from '@jest/globals'; + +import { step } from '../support/step'; +import { setUpStreamTestCase } from '../support/StreamTestCase'; + +describe('ChannelListTests', () => { + const { backendRobot, participantRobot, userRobot } = setUpStreamTestCase(); + const sampleText = 'Test'; + + it('test_channelListShowsGeneratedChannels', async () => { + await step('GIVEN backend has 3 channels', () => + backendRobot.generateChannels({ channelsCount: 3, channelNames: ['Alpha', 'Beta', 'Gamma'] }), + ); + await step('WHEN user logs in', () => userRobot.login().waitForChannelListToLoad()); + await step('THEN user observes all channels in the list', () => + userRobot.assertChannelCount(3).assertChannelAvatar(), + ); + }); + + it('test_channelPreviewUpdates_whenParticipantSendsMessage', async () => { + await step('GIVEN user opens a channel', () => userRobot.login().openChannel()); + await step('WHEN participant sends a message', () => participantRobot.sendMessage(sampleText)); + await step('AND user goes back to the channel list', () => + userRobot.assertMessage(sampleText).moveToChannelListFromMessageList(), + ); + await step('THEN user observes the new message in preview', () => + userRobot.assertMessageInChannelPreview(sampleText, false).assertChannelAvatar(), + ); + }); + + it('test_channelPreviewUpdates_whenUserSendsMessage', async () => { + await step('GIVEN user opens a channel', () => userRobot.login().openChannel()); + await step('WHEN user sends a message', () => + userRobot.sendMessage(sampleText).assertMessage(sampleText), + ); + await step('AND user goes back to the channel list', () => + userRobot.moveToChannelListFromMessageList(), + ); + await step('THEN user observes the new message in preview', () => + userRobot.assertMessageInChannelPreview(sampleText, true), + ); + }); +}); diff --git a/examples/SampleApp/e2e/tests/MessageListTests.e2e.ts b/examples/SampleApp/e2e/tests/MessageListTests.e2e.ts new file mode 100644 index 0000000000..265ed0d70d --- /dev/null +++ b/examples/SampleApp/e2e/tests/MessageListTests.e2e.ts @@ -0,0 +1,44 @@ +import { describe, it } from '@jest/globals'; + +import { MessageDeliveryStatus } from '../mock-server/DataTypes'; +import { step } from '../support/step'; +import { setUpStreamTestCase } from '../support/StreamTestCase'; + +describe('MessageListTests', () => { + const { backendRobot, participantRobot, userRobot } = setUpStreamTestCase(); + const sampleText = 'Test'; + + it('test_messageListUpdates_whenUserSendsMessage', async () => { + await step('GIVEN user opens a channel', () => userRobot.login().openChannel()); + await step('WHEN user sends a message', () => userRobot.sendMessage(sampleText)); + await step('THEN message list updates', () => + userRobot + .assertMessage(sampleText) + .assertMessageAuthor(true) + .assertMessageDeliveryStatus(MessageDeliveryStatus.SENT), + ); + }); + + it('test_messageListUpdates_whenParticipantSendsMessage', async () => { + await step('GIVEN user opens a channel', () => userRobot.login().openChannel()); + await step('WHEN participant sends a message', () => participantRobot.sendMessage(sampleText)); + await step('THEN user receives a message', () => + userRobot.assertMessage(sampleText).assertMessageAuthor(false), + ); + }); + + it('test_typingIndicator', async () => { + await step('GIVEN user opens the channel', async () => { + await backendRobot.generateChannels({ channelsCount: 1, messagesCount: 1 }); + await userRobot.login().openChannel(); + }); + await step('WHEN participant starts typing', () => participantRobot.startTyping()); + await step('THEN user observes typing indicator is shown', () => + userRobot.assertTypingIndicator(true), + ); + await step('WHEN participant stops typing', () => participantRobot.stopTyping()); + await step('THEN user observes typing indicator has disappeared', () => + userRobot.assertTypingIndicator(false), + ); + }); +}); diff --git a/examples/SampleApp/e2e/tsconfig.json b/examples/SampleApp/e2e/tsconfig.json new file mode 100644 index 0000000000..2f08e6cc4d --- /dev/null +++ b/examples/SampleApp/e2e/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "lib": ["es2022", "dom"], + "types": ["detox/globals"] + }, + "include": ["./**/*.ts"], + "exclude": [] +} diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile index f2680950ee..f330c7c688 100644 --- a/examples/SampleApp/fastlane/Fastfile +++ b/examples/SampleApp/fastlane/Fastfile @@ -5,6 +5,7 @@ github_repo = ENV['GITHUB_REPOSITORY'] || 'GetStream/stream-chat-react-native' root_path = File.absolute_path('../../../') sdk_size_ext = 'KB' @force_check = false +@impact = nil build_output_directory = "./app-build" # iOS Platform Configuration @@ -20,6 +21,7 @@ apk_path = "#{build_output_directory}/#{output_apk_name}" before_all do + @impact = impact_analysis if is_ci setup_ci setup_git_config @@ -183,9 +185,87 @@ platform :android do end end -########################## -###### Common lanes ###### -########################## +####################### +###### E2E lanes ###### +####################### + +sample_app_path = File.absolute_path('../') +mock_server_repo_name = 'stream-chat-test-mock-server' +mock_server_driver_port = ENV['MOCK_DRIVER_PORT'] || '4569' + +desc 'Starts the stream-chat-test-mock-server driver. Options: local_server (path to a checkout), branch' +lane :start_mock_server do |options| + Dir.chdir(sample_app_path) do + mock_server_repo = options[:local_server] || mock_server_repo_name + stop_mock_server unless is_ci + + unless options[:local_server] + branch = options[:branch].to_s.empty? ? 'main' : options[:branch] + sh("rm -rf #{mock_server_repo}") if File.directory?(mock_server_repo) + sh("git clone --depth 1 -b #{branch} https://github.com/#{github_repo.split('/').first}/#{mock_server_repo}.git") + end + + Dir.chdir(mock_server_repo) do + FileUtils.mkdir_p('logs') + # The mock server has its own Gemfile; don't let the SampleApp bundle leak into it. + Bundler.with_unbundled_env do + sh('bundle install') + sh("bundle exec ruby driver.rb #{mock_server_driver_port} > logs/driver.log 2>&1 &") + end + end + + wait_for_mock_server_driver(port: mock_server_driver_port) + end +end + +lane :stop_mock_server do + Net::HTTP.get_response(URI("http://localhost:#{mock_server_driver_port}/stop")) rescue nil +end + +private_lane :wait_for_mock_server_driver do |options| + require 'socket' + ready = false + 60.times do + begin + TCPSocket.new('localhost', options[:port].to_i).close + ready = true + break + rescue Errno::ECONNREFUSED + sleep(1) + end + end + UI.user_error!("Mock server driver did not start on port #{options[:port]}") unless ready +end + +desc 'Writes `required=true|false` to $GITHUB_OUTPUT, so CI can skip the e2e jobs before building anything' +lane :e2e_check_required do + required = is_check_required(sources: @impact[:e2e][:sources], force_check: @force_check, required_checks: @impact[:e2e][:checks]) + File.write(ENV['GITHUB_OUTPUT'], "required=#{required}\n", mode: 'a') if ENV['GITHUB_OUTPUT'] + required +end + +desc 'Runs the Detox e2e tests. Options: platform (ios|android), configuration (release|debug), ' \ + 'device (android: emu|att), skip_build, test (a test file or -t filter), local_server, mock_server_branch' +lane :test_e2e do |options| + next unless is_check_required(sources: @impact[:e2e][:sources], force_check: @force_check, required_checks: @impact[:e2e][:checks]) + + platform = options[:platform] || 'ios' + device = platform == 'ios' ? 'sim' : (options[:device] || 'emu') + detox_config = "#{platform}.#{device}.#{options[:configuration] || 'release'}" + + start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) + + begin + Dir.chdir(sample_app_path) do + sh("npx detox build -c #{detox_config}") unless options[:skip_build] + # On CI: retry flaky tests, run the simulator headless, and keep logs + screenshots of failures in ./artifacts. + ci_flags = is_ci ? '--retries 2 --headless --record-logs failing --take-screenshots failing' : '' + sh("MOCK_DRIVER_PORT=#{mock_server_driver_port} npx detox test -c #{detox_config} --cleanup #{ci_flags} #{options[:test]}") + end + ensure + stop_mock_server + end +end lane :file_size do |options| File.size(options[:path]) / 1024.0 @@ -213,11 +293,6 @@ lane :frameworks_sizes do end lane :show_frameworks_sizes do |options| - # Besides the SDK itself, changes to how the size is measured (this lane, its plugin, the root - # `minify-*` scripts) re-run the check too. - sources = ['package/', 'package.json', 'examples/SampleApp/fastlane/', 'examples/SampleApp/Gemfile.lock'] - next unless is_check_required(sources: sources, force_check: @force_check) - sizes = options[:sizes] || frameworks_sizes show_sdk_size(branch_sizes: sizes, github_repo: github_repo, size_ext: sdk_size_ext) update_img_shields_sdk_sizes(sizes: sizes, open_pr: options[:open_pr]) if options[:update_readme] @@ -232,3 +307,12 @@ lane :update_img_shields_sdk_sizes do |options| size_ext: sdk_size_ext ) end + +lane :impact_analysis do + { + e2e: { + sources: ['package/', 'examples/SampleApp/', 'package.json'], + checks: ['Test E2E Result'] + } + } +end diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index 4a27feb1e7..e15f0420e4 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -1744,6 +1744,8 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga + - react-native-launch-arguments (4.1.1): + - React - react-native-maps (1.29.11): - react-native-maps/Maps (= 1.29.11) - react-native-maps/Generated (1.29.11): @@ -2453,7 +2455,7 @@ PODS: - ReactNativeDependencies - RNFBApp - Yoga - - RNGestureHandler (3.3.0): + - RNGestureHandler (3.4.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2815,7 +2817,7 @@ PODS: - SDWebImage/Core (~> 5.17) - SocketRocket (0.7.1): - ReactNativeDependencies - - stream-chat-react-native (9.10.0): + - stream-chat-react-native (9.10.1): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2951,6 +2953,7 @@ DEPENDENCIES: - "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)" - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - react-native-image-picker (from `../node_modules/react-native-image-picker`) + - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - react-native-maps (from `../node_modules/react-native-maps`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -3153,6 +3156,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/geolocation" react-native-image-picker: :path: "../node_modules/react-native-image-picker" + react-native-launch-arguments: + :path: "../node_modules/react-native-launch-arguments" react-native-maps: :path: "../node_modules/react-native-maps" react-native-netinfo: @@ -3347,6 +3352,7 @@ SPEC CHECKSUMS: react-native-document-picker: ca21279b19ad48ebc107c2ffca904c7aee99e40b react-native-geolocation: 2c5f29e0098223d8fa2c18813f6acbf882563424 react-native-image-picker: 7cdf342c3b12b1e4f50f6098c5feaf058ea8d350 + react-native-launch-arguments: eab68037b01ea7c78a0b3b55da3a5e967e9aaf92 react-native-maps: b39b5d5ee5ac885f2822bb124105af4042768d4b react-native-netinfo: 41575e101c171319fbcf82dfa5fa5df852d6b40d react-native-safe-area-context: 7b143af09becaf26ba247b9a82d355f91b77b9b0 @@ -3392,7 +3398,7 @@ SPEC CHECKSUMS: RNFastImage: 25f87dde869e11156daeef98b4cee2371023407e RNFBApp: 21e4d8c20656c236a818ee2eff3f859557ceff33 RNFBMessaging: e3e31aec89cd0eb986c87ec8247f598190b61c36 - RNGestureHandler: ddbdf3f293a498368803f8181ff157271ac05c48 + RNGestureHandler: abdc2d49ac3970908acd127980b429d377927ee1 RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 RNReactNativeHapticFeedback: a1bba076645e8fedcb65c8e70461b77cd5d6dd32 RNReanimated: 5c641cc430b7255eae87c8039204eb0f816ea1b8 @@ -3405,10 +3411,10 @@ SPEC CHECKSUMS: SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SocketRocket: 37aec555668fb852ec12e3c0de59a86ca58f0871 - stream-chat-react-native: dc4a04f6903f9616ed0d58757e813e96bda1972f + stream-chat-react-native: 291a3f91fef6922e89ed97bf7a790bf4a786bab9 Teleport: 8a97ac5c0d76c04e20c36843d4d8f8fe5aa1aa73 Yoga: d1c536142c5ff8ec8cd856ab2a7c227a1d875c8e PODFILE CHECKSUM: ac9682db83e42d63d399a8de4586161e86b3e751 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 9afbb82754..201b4f23d8 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -15,7 +15,9 @@ "bootstrap": "yarn install", "release": "FILTER_PATH='examples/SampleApp' TAG_FORMAT=$npm_package_name'@v${version}' node ../../release/prod", "release-next": "echo \"Skipping next release for SampleApp\"", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit && tsc --noEmit -p e2e", + "e2e:build": "detox build", + "e2e:test": "detox test", "clean": "watchman watch-del-all && yarn cache clean && rm -rf ios/build && pod cache clean --all && rm -rf android/build && cd android && ./gradlew clean && cd -", "clean-all": "yarn clean && rm -rf node_modules && rm -rf ios/Pods && rm -rf vendor && bundle install && yarn install && cd ios && bundle exec pod install && cd -", "fastlane:android-build": "bundle exec fastlane android firebase_build_and_upload deploy:false", @@ -53,6 +55,7 @@ "react-native-gesture-handler": "^3.4.0", "react-native-haptic-feedback": "^3.0.0", "react-native-image-picker": "^8.2.1", + "react-native-launch-arguments": "^4.1.1", "react-native-maps": "^1.29.11", "react-native-nitro-modules": "0.36.5", "react-native-nitro-sound": "0.2.17", @@ -76,6 +79,7 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/runtime": "^7.29.2", + "@jest/globals": "^30.4.1", "@react-native-community/cli": "20.2.0", "@react-native-community/cli-platform-android": "20.2.0", "@react-native-community/cli-platform-ios": "20.2.0", @@ -86,7 +90,10 @@ "@stream-io/typescript-config": "workspace:^", "@types/lodash.mergewith": "^4.6.9", "@types/react": "^19.3.0", + "babel-jest": "30.0.0", "babel-plugin-react-compiler": "^1.0.0", + "detox": "^20.51.4", + "jest": "^30.4.2", "typescript": "6.0.3" }, "engines": { diff --git a/examples/SampleApp/src/components/ScreenHeader.tsx b/examples/SampleApp/src/components/ScreenHeader.tsx index 4a5bd2c2e0..6962765526 100644 --- a/examples/SampleApp/src/components/ScreenHeader.tsx +++ b/examples/SampleApp/src/components/ScreenHeader.tsx @@ -87,6 +87,7 @@ export const BackButton: React.FC<{ } }} style={styles.backButton} + testID='back-button' > {!!showUnreadCountBadge && ( diff --git a/examples/SampleApp/src/hooks/useChatClient.ts b/examples/SampleApp/src/hooks/useChatClient.ts index 1ced3e1d4e..a2d755ffd2 100644 --- a/examples/SampleApp/src/hooks/useChatClient.ts +++ b/examples/SampleApp/src/hooks/useChatClient.ts @@ -14,6 +14,7 @@ import { SqliteClient } from 'stream-chat-react-native'; import { USER_TOKENS, USERS } from '../ChatUsers'; import type { LoginConfig } from '../types'; import AsyncStore from '../utils/AsyncStore'; +import { E2E_LOGIN_CONFIG, isE2eTestRun, mockServerUrl } from '../utils/e2e'; const messaging = getMessaging(); @@ -73,6 +74,7 @@ export const useChatClient = () => { // unsubscribe from previous push listeners unsubscribePushListenersRef.current?.(); const client = StreamChat.getInstance(config.apiKey, { + baseURL: mockServerUrl, timeout: 6000, // logger: (type, msg) => console.log(type, msg) }); @@ -86,6 +88,12 @@ export const useChatClient = () => { await client.connectUser(user, config.userToken); await AsyncStore.setItem('@stream-rn-sampleapp-login-config', config); + // The mock server has no push endpoints, and a permission prompt would cover the app. + if (isE2eTestRun) { + setChatClient(client); + return; + } + const permissionAuthStatus = await messaging.hasPermission(); let isEnabled = permissionAuthStatus === AuthorizationStatus.AUTHORIZED || @@ -148,7 +156,12 @@ export const useChatClient = () => { setIsConnecting(true); try { - if (userId) { + if (isE2eTestRun) { + // e2e tests always log in through the user selector, so a stored login is never restored. + if (userId) { + await loginUser(E2E_LOGIN_CONFIG); + } + } else if (userId) { await loginUser({ apiKey: 'yjrt5yxw77ev', userId: USERS[userId].id, @@ -181,7 +194,9 @@ export const useChatClient = () => { useEffect(() => { const run = async () => { - await requestNotificationPermission(); + if (!isE2eTestRun) { + await requestNotificationPermission(); + } await switchUser(); }; run(); diff --git a/examples/SampleApp/src/screens/UserSelectorScreen.tsx b/examples/SampleApp/src/screens/UserSelectorScreen.tsx index 4842132230..b8beddc837 100644 --- a/examples/SampleApp/src/screens/UserSelectorScreen.tsx +++ b/examples/SampleApp/src/screens/UserSelectorScreen.tsx @@ -22,6 +22,7 @@ import { useLegacyColors } from '../theme/useLegacyColors'; import type { UserSelectorParamList } from '../types'; import AsyncStore from '../utils/AsyncStore'; +import { E2E_USER, isE2eTestRun } from '../utils/e2e'; const styles = StyleSheet.create({ avatarImage: { @@ -119,7 +120,7 @@ export const UserSelectorScreen: React.FC = ({ navigation }) => { - {Object.values(USERS).map((u, index) => ( + {(isE2eTestRun ? [E2E_USER] : Object.values(USERS)).map((u, index) => ( { diff --git a/examples/SampleApp/src/utils/e2e.ts b/examples/SampleApp/src/utils/e2e.ts new file mode 100644 index 0000000000..7e2657e468 --- /dev/null +++ b/examples/SampleApp/src/utils/e2e.ts @@ -0,0 +1,42 @@ +import { LaunchArguments } from 'react-native-launch-arguments'; + +import type { UserResponse } from 'stream-chat'; + +import type { LoginConfig } from '../types'; + +/** + * Launch arguments passed by the Detox e2e suite (`examples/SampleApp/e2e`). + * The counterpart of `USE_MOCK_SERVER` + `MOCK_SERVER_*` on iOS and the `BASE_URL` intent extra on Android. + */ +type E2ELaunchArguments = { + /** Base URL of the per-test `stream-chat-test-mock-server` instance, e.g. `http://localhost:54321`. */ + MOCK_SERVER_URL?: string; +}; + +const launchArguments = LaunchArguments.value(); + +export const mockServerUrl = launchArguments.MOCK_SERVER_URL; + +/** + * True when the app was launched by the e2e suite. Push registration, notification + * permission prompts, the stored login and offline storage are all skipped in this mode, + * so every test starts from the same clean state. + */ +export const isE2eTestRun = !!mockServerUrl; + +/** The app user every mock server fixture is recorded for. */ +export const E2E_USER: UserResponse = { + id: 'luke_skywalker', + image: 'https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg', + name: 'Luke Skywalker', +}; + +export const E2E_LOGIN_CONFIG: LoginConfig = { + // The mock server accepts any key; this one matches the native SDKs' e2e apps. + apiKey: '8br4watad788', + userId: E2E_USER.id, + userImage: E2E_USER.image, + userName: E2E_USER.name, + userToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibHVrZV9za3l3YWxrZXIifQ.kFSLHRB5X62t0Zlc7nwczWUfsQMwfkpylC6jCUZ6Mc0', +}; diff --git a/examples/SampleApp/tsconfig.json b/examples/SampleApp/tsconfig.json index 5a1531949c..d1be171213 100644 --- a/examples/SampleApp/tsconfig.json +++ b/examples/SampleApp/tsconfig.json @@ -2,5 +2,6 @@ "extends": ["@react-native/typescript-config", "@stream-io/typescript-config/base.json"], "compilerOptions": { "types": [] - } + }, + "exclude": ["node_modules", "e2e"] } diff --git a/package/src/components/ChannelPreview/ChannelLastMessagePreview.tsx b/package/src/components/ChannelPreview/ChannelLastMessagePreview.tsx index 30d323c839..e005c42649 100644 --- a/package/src/components/ChannelPreview/ChannelLastMessagePreview.tsx +++ b/package/src/components/ChannelPreview/ChannelLastMessagePreview.tsx @@ -29,7 +29,7 @@ export const ChannelLastMessagePreview = ({ message }: ChannelLastMessagePreview width={16} /> ) : null} - + {messagePreviewTitle} diff --git a/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx b/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx index 96ea6dc072..0a08077eda 100644 --- a/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx +++ b/package/src/components/ChannelPreview/ChannelMessagePreviewDeliveryStatus.tsx @@ -81,7 +81,11 @@ export const ChannelMessagePreviewDeliveryStatus = ({ } if (!isLastMessageByCurrentUser) { - return {message?.user?.name || message?.user?.id}:; + return ( + + {message?.user?.name || message?.user?.id}: + + ); } return ( @@ -107,7 +111,9 @@ export const ChannelMessagePreviewDeliveryStatus = ({ ) : status === MessageDeliveryStatus.SENT ? ( ) : null} - {t('You')}: + + {t('You')}: + ); diff --git a/package/src/components/ChannelPreview/ChannelPreviewTitle.tsx b/package/src/components/ChannelPreview/ChannelPreviewTitle.tsx index e91d33b41f..28e1b7982c 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewTitle.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewTitle.tsx @@ -21,7 +21,7 @@ export const ChannelPreviewTitle = ({ channel, title }: ChannelPreviewTitleProps const displayName = useChannelPreviewDisplayName(channel); return ( - + {title ?? displayName} ); diff --git a/package/src/components/Message/MessageItemView/MessageStatus.tsx b/package/src/components/Message/MessageItemView/MessageStatus.tsx index db86a63105..4095fd1deb 100644 --- a/package/src/components/Message/MessageItemView/MessageStatus.tsx +++ b/package/src/components/Message/MessageItemView/MessageStatus.tsx @@ -61,6 +61,16 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { return null; } + const testID = read + ? 'message-status-read' + : delivered + ? 'message-status-delivered' + : sending + ? 'message-status-sending' + : sent + ? 'message-status-sent' + : undefined; + return ( <> @@ -68,6 +78,7 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { accessibilityElementsHidden={!!accessibilityLabel} importantForAccessibility={accessibilityLabel ? 'no-hide-descendants' : undefined} style={[styles.container, container]} + testID={testID} > {read ? ( { }); }, ); + + it.each([ + [1, 1, 'sending', 'message-status-sending'], + [2, 2, 'received', 'message-status-read'], + [1, 1, 'received', 'message-status-sent'], + [2, 1, 'received', 'message-status-delivered'], + ] as [number, number, string, string][])( + 'should expose a status testID when deliveredToCount is %s, readBy is %s and status is %s', + async (deliveredToCount, readBy, status, testID) => { + const message = generateMessage({ user: generateUser() }); + const { getByTestId } = renderMessageStatus({ + deliveredToCount, + message: { ...message, status }, + readBy, + }); + // The icon row is hidden from screen readers in favour of the hidden text label. + await waitFor(() => { + expect(getByTestId(testID, { includeHiddenElements: true })).toBeTruthy(); + }); + }, + ); }); diff --git a/yarn.lock b/yarn.lock index 6d8e94f376..a54672135a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1610,6 +1610,20 @@ __metadata: languageName: node linkType: hard +"@colors/colors@npm:1.6.0": + version: 1.6.0 + resolution: "@colors/colors@npm:1.6.0" + checksum: 10c0/9328a0778a5b0db243af54455b79a69e3fb21122d6c15ef9e9fcc94881d8d17352d8b2b2590f9bdd46fac5c2d6c1636dcfc14358a20c70e22daf89e1a759b629 + languageName: node + linkType: hard + +"@colors/colors@npm:^1.6.0": + version: 1.6.1 + resolution: "@colors/colors@npm:1.6.1" + checksum: 10c0/8fb995f2421210284305fd07b4b70e9873a417cda8b0d66ba3f7beccd2e0f4ebbe0b2ba3afbef8f07740ad707c379d94e836cffd6a8df394387e210b4338b4c9 + languageName: node + linkType: hard + "@commitlint/cli@npm:^21.0.0": version: 21.0.0 resolution: "@commitlint/cli@npm:21.0.0" @@ -1839,6 +1853,17 @@ __metadata: languageName: node linkType: hard +"@dabh/diagnostics@npm:^2.0.8": + version: 2.0.9 + resolution: "@dabh/diagnostics@npm:2.0.9" + dependencies: + "@so-ric/colorspace": "npm:^1.1.6" + enabled: "npm:2.0.x" + kuler: "npm:^2.0.0" + checksum: 10c0/3d341a52fbf2277883c4aa1153ce092932d9ecad79a54749553206b83e568cd563f66c6e1cc1416a403bd5662335193a19e8bcc1082f047f28b6402eeb16c01d + languageName: node + linkType: hard + "@emnapi/core@npm:^1.4.3": version: 1.10.0 resolution: "@emnapi/core@npm:1.10.0" @@ -3060,6 +3085,13 @@ __metadata: languageName: node linkType: hard +"@flatten-js/interval-tree@npm:^1.1.2": + version: 1.1.4 + resolution: "@flatten-js/interval-tree@npm:1.1.4" + checksum: 10c0/e4055e99b1b5958e0a1adea7413d9602f3d7ebed0dd47fcac1ad995a97151e02187fe1aa778c14be56c7faa3a29b2a71ece84be43312eff6850ebf316b3490b3 + languageName: node + linkType: hard + "@gorhom/bottom-sheet@npm:^5.2.14": version: 5.2.14 resolution: "@gorhom/bottom-sheet@npm:5.2.14" @@ -3544,6 +3576,13 @@ __metadata: languageName: node linkType: hard +"@jest/diff-sequences@npm:30.5.0": + version: 30.5.0 + resolution: "@jest/diff-sequences@npm:30.5.0" + checksum: 10c0/a8d4fef9b266cb025b2f1c1be9ae2e807077def93f36e34a628aff383910fa7a26ba0921a43744841231ead54ea87469fb2acf89de47c12ad4fead802e0ccfc3 + languageName: node + linkType: hard + "@jest/environment@npm:30.4.1": version: 30.4.1 resolution: "@jest/environment@npm:30.4.1" @@ -3556,6 +3595,18 @@ __metadata: languageName: node linkType: hard +"@jest/environment@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/environment@npm:30.5.2" + dependencies: + "@jest/fake-timers": "npm:30.5.2" + "@jest/types": "npm:30.5.1" + "@types/node": "npm:*" + jest-mock: "npm:30.5.2" + checksum: 10c0/f060f2d94dfccd7de4890a419e803735921d8358a3ecc8e424e5f6a632da80fc37372f6e54f1397876b721cec5bae2da515e6dce2ac6558bd90f0c1cbeb5c105 + languageName: node + linkType: hard + "@jest/environment@npm:^29.7.0": version: 29.7.0 resolution: "@jest/environment@npm:29.7.0" @@ -3577,6 +3628,15 @@ __metadata: languageName: node linkType: hard +"@jest/expect-utils@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/expect-utils@npm:30.5.2" + dependencies: + "@jest/get-type": "npm:30.5.0" + checksum: 10c0/0db00595907f6705c5acc8d10ee9d11d74960cd459beaae66bd7a1d6845c7df1d123295e957bb56b6882b380f2e7d55d277ced7646e0229fc4a67abeb713c048 + languageName: node + linkType: hard + "@jest/expect@npm:30.4.1": version: 30.4.1 resolution: "@jest/expect@npm:30.4.1" @@ -3587,6 +3647,16 @@ __metadata: languageName: node linkType: hard +"@jest/expect@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/expect@npm:30.5.2" + dependencies: + expect: "npm:30.5.2" + jest-snapshot: "npm:30.5.2" + checksum: 10c0/ceebb291be34707e3d2ac99bc5e4a5039927e01d28650b1dc51888dfa3963a3a75a3c72f0abe4df9aa243dc3b31af72e467072d75b52bd11b80a84ebe765d8f9 + languageName: node + linkType: hard + "@jest/fake-timers@npm:30.4.1": version: 30.4.1 resolution: "@jest/fake-timers@npm:30.4.1" @@ -3601,6 +3671,20 @@ __metadata: languageName: node linkType: hard +"@jest/fake-timers@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/fake-timers@npm:30.5.2" + dependencies: + "@jest/types": "npm:30.5.1" + "@sinonjs/fake-timers": "npm:^15.4.0" + "@types/node": "npm:*" + jest-message-util: "npm:30.5.1" + jest-mock: "npm:30.5.2" + jest-util: "npm:30.5.1" + checksum: 10c0/c8ff08f541743210abaf07b75bfe6224458c816a500d5be954690fd1661cd14b054201dfe2a6ebdc0083290d4c03d73bbac4f3c1fbef5c3b16d1981642dfff57 + languageName: node + linkType: hard + "@jest/fake-timers@npm:^29.7.0": version: 29.7.0 resolution: "@jest/fake-timers@npm:29.7.0" @@ -3622,6 +3706,13 @@ __metadata: languageName: node linkType: hard +"@jest/get-type@npm:30.5.0": + version: 30.5.0 + resolution: "@jest/get-type@npm:30.5.0" + checksum: 10c0/820a2af9902522d169ae4ede17a99249baad59ff6aead02f8100acbc937b082090b04d5f32ec0d9938491cf89c9fc99930ed469202700c110c0239c7f7ac67a2 + languageName: node + linkType: hard + "@jest/globals@npm:30.4.1": version: 30.4.1 resolution: "@jest/globals@npm:30.4.1" @@ -3634,6 +3725,18 @@ __metadata: languageName: node linkType: hard +"@jest/globals@npm:^30.4.1": + version: 30.5.2 + resolution: "@jest/globals@npm:30.5.2" + dependencies: + "@jest/environment": "npm:30.5.2" + "@jest/expect": "npm:30.5.2" + "@jest/types": "npm:30.5.1" + jest-mock: "npm:30.5.2" + checksum: 10c0/e60738080d4866350b2152d98ddf99728cc1ac19983b80bea5d5ab219f82ef271fadd102b0cee2c01808367d6e054bea1c4ec4aa7c69e45b4e14b79a30f76538 + languageName: node + linkType: hard + "@jest/pattern@npm:30.0.0": version: 30.0.0 resolution: "@jest/pattern@npm:30.0.0" @@ -3654,6 +3757,30 @@ __metadata: languageName: node linkType: hard +"@jest/pattern@npm:30.5.0": + version: 30.5.0 + resolution: "@jest/pattern@npm:30.5.0" + dependencies: + "@types/node": "npm:*" + jest-regex-util: "npm:30.5.0" + checksum: 10c0/3e960b25c7a32fee97a2cdf9ea810ada41f676c84db3c1afab138ee089e0e1cf4849372dd576acb18ac5219309695991c38e23ddacd9b95802ee3f76b4caaea4 + languageName: node + linkType: hard + +"@jest/react-is-18@npm:react-is@^18.3.1, react-is-18@npm:react-is@^18.3.1, react-is@npm:^18.0.0": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 + languageName: node + linkType: hard + +"@jest/react-is-19@npm:react-is@^19.2.5": + version: 19.3.1 + resolution: "react-is@npm:19.3.1" + checksum: 10c0/428e02d986d56f1859c57756084489f6d2900fd686905003c405b32f1168738b78772bfa4bb6c706f5ac821199171522c28bf6434281f524f901459390a19606 + languageName: node + linkType: hard + "@jest/reporters@npm:30.4.1": version: 30.4.1 resolution: "@jest/reporters@npm:30.4.1" @@ -3708,6 +3835,15 @@ __metadata: languageName: node linkType: hard +"@jest/schemas@npm:30.5.0": + version: 30.5.0 + resolution: "@jest/schemas@npm:30.5.0" + dependencies: + "@sinclair/typebox": "npm:^0.34.0" + checksum: 10c0/632c45a06a0984051c908fa9b3c9445bac7be8b7a6d1cc7ce2ecae7d4e44c612509ad41fca16449607c4ea49a333e0d3f10bb1510bafb4b6d9051ca15d9b4b14 + languageName: node + linkType: hard + "@jest/schemas@npm:^29.6.3": version: 29.6.3 resolution: "@jest/schemas@npm:29.6.3" @@ -3729,6 +3865,18 @@ __metadata: languageName: node linkType: hard +"@jest/snapshot-utils@npm:30.5.1": + version: 30.5.1 + resolution: "@jest/snapshot-utils@npm:30.5.1" + dependencies: + "@jest/types": "npm:30.5.1" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + natural-compare: "npm:^1.4.0" + checksum: 10c0/0e31aa45c4be7b6e7dabcccc7316458f53bda262ab42f23b749430110d0ac908d4905db993708723a4f6457bc9208c636fd7b886e3ffc26974d55126cf597d6e + languageName: node + linkType: hard + "@jest/source-map@npm:30.0.1": version: 30.0.1 resolution: "@jest/source-map@npm:30.0.1" @@ -3809,6 +3957,28 @@ __metadata: languageName: node linkType: hard +"@jest/transform@npm:30.5.2": + version: 30.5.2 + resolution: "@jest/transform@npm:30.5.2" + dependencies: + "@babel/core": "npm:^7.27.4" + "@jest/types": "npm:30.5.1" + "@jridgewell/trace-mapping": "npm:^0.3.31" + babel-plugin-istanbul: "npm:^8.0.0" + chalk: "npm:^4.1.2" + convert-source-map: "npm:^2.0.0" + fast-json-stable-stringify: "npm:^2.1.0" + graceful-fs: "npm:^4.2.11" + jest-haste-map: "npm:30.5.1" + jest-regex-util: "npm:30.5.0" + jest-util: "npm:30.5.1" + pirates: "npm:^4.0.7" + slash: "npm:^3.0.0" + write-file-atomic: "npm:^5.0.1" + checksum: 10c0/be3f9aa812a442402ed41a37d0f11d174a0a9037038dc1326c32d1d55e22fc7bc5f5146284c5a18db3da572afb81739f0a93d3f1d519e811d25dacaeb380c647 + languageName: node + linkType: hard + "@jest/transform@npm:^29.7.0": version: 29.7.0 resolution: "@jest/transform@npm:29.7.0" @@ -3862,6 +4032,21 @@ __metadata: languageName: node linkType: hard +"@jest/types@npm:30.5.1": + version: 30.5.1 + resolution: "@jest/types@npm:30.5.1" + dependencies: + "@jest/pattern": "npm:30.5.0" + "@jest/schemas": "npm:30.5.0" + "@types/istanbul-lib-coverage": "npm:^2.0.6" + "@types/istanbul-reports": "npm:^3.0.4" + "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.33" + chalk: "npm:^4.1.2" + checksum: 10c0/da9e46449371ddd43b89627c93fb7088fdad01e9f57dce376c65914a6f4617a558f54ec51a917ca10c9b9794a1e76c3cb450308eca303f830bb21e9529b1719a + languageName: node + linkType: hard + "@jest/types@npm:^29.6.3": version: 29.6.3 resolution: "@jest/types@npm:29.6.3" @@ -4399,6 +4584,140 @@ __metadata: languageName: node linkType: hard +"@parcel/watcher-android-arm64@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-android-arm64@npm:2.6.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-arm64@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-darwin-arm64@npm:2.6.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-darwin-x64@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-darwin-x64@npm:2.6.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-freebsd-x64@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-freebsd-x64@npm:2.6.0" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-glibc@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-linux-arm-glibc@npm:2.6.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm-musl@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-linux-arm-musl@npm:2.6.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-glibc@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-linux-arm64-glibc@npm:2.6.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-arm64-musl@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-linux-arm64-musl@npm:2.6.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-glibc@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-linux-x64-glibc@npm:2.6.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@parcel/watcher-linux-x64-musl@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-linux-x64-musl@npm:2.6.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@parcel/watcher-win32-arm64@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-win32-arm64@npm:2.6.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@parcel/watcher-win32-x64@npm:2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher-win32-x64@npm:2.6.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@parcel/watcher@npm:^2.6.0": + version: 2.6.0 + resolution: "@parcel/watcher@npm:2.6.0" + dependencies: + "@parcel/watcher-android-arm64": "npm:2.6.0" + "@parcel/watcher-darwin-arm64": "npm:2.6.0" + "@parcel/watcher-darwin-x64": "npm:2.6.0" + "@parcel/watcher-freebsd-x64": "npm:2.6.0" + "@parcel/watcher-linux-arm-glibc": "npm:2.6.0" + "@parcel/watcher-linux-arm-musl": "npm:2.6.0" + "@parcel/watcher-linux-arm64-glibc": "npm:2.6.0" + "@parcel/watcher-linux-arm64-musl": "npm:2.6.0" + "@parcel/watcher-linux-x64-glibc": "npm:2.6.0" + "@parcel/watcher-linux-x64-musl": "npm:2.6.0" + "@parcel/watcher-win32-arm64": "npm:2.6.0" + "@parcel/watcher-win32-x64": "npm:2.6.0" + detect-libc: "npm:^2.0.3" + is-glob: "npm:^4.0.3" + node-addon-api: "npm:^7.0.0" + node-gyp: "npm:latest" + picomatch: "npm:^4.0.4" + dependenciesMeta: + "@parcel/watcher-android-arm64": + optional: true + "@parcel/watcher-darwin-arm64": + optional: true + "@parcel/watcher-darwin-x64": + optional: true + "@parcel/watcher-freebsd-x64": + optional: true + "@parcel/watcher-linux-arm-glibc": + optional: true + "@parcel/watcher-linux-arm-musl": + optional: true + "@parcel/watcher-linux-arm64-glibc": + optional: true + "@parcel/watcher-linux-arm64-musl": + optional: true + "@parcel/watcher-linux-x64-glibc": + optional: true + "@parcel/watcher-linux-x64-musl": + optional: true + "@parcel/watcher-win32-arm64": + optional: true + "@parcel/watcher-win32-x64": + optional: true + checksum: 10c0/0d6799bdf7d59a1ab5e4a79859035f387a66078586d9543eef0b9bd01c9d61cebe3b64eb96f0bd1f8bf50f07ed7266a1628604b66dc63025a3cee19be2e60d65 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -6056,6 +6375,16 @@ __metadata: languageName: node linkType: hard +"@so-ric/colorspace@npm:^1.1.6": + version: 1.1.6 + resolution: "@so-ric/colorspace@npm:1.1.6" + dependencies: + color: "npm:^5.0.2" + text-hex: "npm:1.0.x" + checksum: 10c0/f3ad26afefbb8d6101ea7c385cd5f402d4291c2ffc9cabe37030d5fdb8bda980ee534a0d7c250f8233fc3a59b99272410177cd98b219f6b3770f91a0fdb6eb3e + languageName: node + linkType: hard + "@stream-io/state-store@npm:^1.1.6": version: 1.1.6 resolution: "@stream-io/state-store@npm:1.1.6" @@ -6569,6 +6898,13 @@ __metadata: languageName: node linkType: hard +"@types/triple-beam@npm:^1.3.2": + version: 1.3.5 + resolution: "@types/triple-beam@npm:1.3.5" + checksum: 10c0/d5d7f25da612f6d79266f4f1bb9c1ef8f1684e9f60abab251e1261170631062b656ba26ff22631f2760caeafd372abc41e64867cde27fba54fafb73a35b9056a + languageName: node + linkType: hard + "@types/ungap__structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@types/ungap__structured-clone@npm:1.2.0" @@ -6920,6 +7256,33 @@ __metadata: languageName: node linkType: hard +"@wix-pilot/core@npm:^3.4.2": + version: 3.4.2 + resolution: "@wix-pilot/core@npm:3.4.2" + dependencies: + chalk: "npm:^4.1.0" + pngjs: "npm:^7.0.0" + winston: "npm:^3.17.0" + peerDependencies: + expect: "*" + peerDependenciesMeta: + expect: + optional: true + checksum: 10c0/606dcd0951cfdfb6bda69718512e6daedda22b15dcc4b2029739e3d02b2d676b30d5d97a6d6558d12a8cfae332b6beb48e32ac620eae4ca5eefa43543dd9e14a + languageName: node + linkType: hard + +"@wix-pilot/detox@npm:^1.0.13": + version: 1.0.13 + resolution: "@wix-pilot/detox@npm:1.0.13" + peerDependencies: + "@wix-pilot/core": ^3.4.1 + detox: ">=20.33.0" + expect: 29.x.x || 28.x.x || ^27.2.5 + checksum: 10c0/210b1834ce628cfacdc7417ed8596455045bd6c87bc4ef01b99558ddeeab00648d8ed73abdd212f7cb3d96110b0d36bf8812fd31e1d61a4d3b8f7cad41ab69bf + languageName: node + linkType: hard + "@xmldom/xmldom@npm:^0.8.8": version: 0.8.13 resolution: "@xmldom/xmldom@npm:0.8.13" @@ -7090,7 +7453,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.11.0": +"ajv@npm:^8.11.0, ajv@npm:^8.6.3": version: 8.20.0 resolution: "ajv@npm:8.20.0" dependencies: @@ -7442,6 +7805,13 @@ __metadata: languageName: node linkType: hard +"async@npm:^3.2.3": + version: 3.2.6 + resolution: "async@npm:3.2.6" + checksum: 10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70 + languageName: node + linkType: hard + "asynckit@npm:^0.4.0": version: 0.4.0 resolution: "asynckit@npm:0.4.0" @@ -7565,6 +7935,19 @@ __metadata: languageName: node linkType: hard +"babel-plugin-istanbul@npm:^8.0.0": + version: 8.0.2 + resolution: "babel-plugin-istanbul@npm:8.0.2" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.0.0" + "@istanbuljs/load-nyc-config": "npm:^1.0.0" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-instrument: "npm:^6.0.2" + test-exclude: "npm:^7.0.1" + checksum: 10c0/795577336196114ea19d2cd9efae41cde66b08bc8b7a326ed35bfe7da85b5756ebe19d9fcf3c98d3bd0e194d1258229178414a79fe3a623a66a449e468cf7af5 + languageName: node + linkType: hard + "babel-plugin-jest-hoist@npm:30.0.0": version: 30.0.0 resolution: "babel-plugin-jest-hoist@npm:30.0.0" @@ -7926,6 +8309,13 @@ __metadata: languageName: node linkType: hard +"bluebird@npm:^3.5.4": + version: 3.7.2 + resolution: "bluebird@npm:3.7.2" + checksum: 10c0/680de03adc54ff925eaa6c7bb9a47a0690e8b5de60f4792604aae8ed618c65e6b63a7893b57ca924beaf53eee69c5af4f8314148c08124c550fe1df1add897d2 + languageName: node + linkType: hard + "body-parser@npm:^2.2.2": version: 2.3.0 resolution: "body-parser@npm:2.3.0" @@ -8021,6 +8411,13 @@ __metadata: languageName: node linkType: hard +"browser-process-hrtime@npm:^1.0.0": + version: 1.0.0 + resolution: "browser-process-hrtime@npm:1.0.0" + checksum: 10c0/65da78e51e9d7fa5909147f269c54c65ae2e03d1cf797cc3cfbbe49f475578b8160ce4a76c36c1a2ffbff26c74f937d73096c508057491ddf1a6dfd11143f72d + languageName: node + linkType: hard + "browserslist@npm:^4.20.4, browserslist@npm:^4.24.0, browserslist@npm:^4.25.0, browserslist@npm:^4.28.1": version: 4.28.2 resolution: "browserslist@npm:4.28.2" @@ -8069,6 +8466,87 @@ __metadata: languageName: node linkType: hard +"bunyamin@npm:^1.5.2": + version: 1.6.3 + resolution: "bunyamin@npm:1.6.3" + dependencies: + "@flatten-js/interval-tree": "npm:^1.1.2" + multi-sort-stream: "npm:^1.0.4" + stream-json: "npm:^1.7.5" + trace-event-lib: "npm:^1.3.1" + peerDependencies: + "@types/bunyan": ^1.8.8 + bunyan: ^1.8.15 || ^2.0.0 + peerDependenciesMeta: + "@types/bunyan": + optional: true + bunyan: + optional: true + checksum: 10c0/150a93cbf0ea83ff38f73de42742fc162e24d87551d9b5454769a4c2966b900a4e2a63dbd3d1f616a1178e65f0966cd76b2a48981764de7ae93b55de79410c83 + languageName: node + linkType: hard + +"bunyan-debug-stream@npm:^3.1.0": + version: 3.1.1 + resolution: "bunyan-debug-stream@npm:3.1.1" + dependencies: + chalk: "npm:^4.1.2" + peerDependencies: + bunyan: "*" + peerDependenciesMeta: + bunyan: + optional: true + checksum: 10c0/3682454c08910ffbe4b9d9c3e4ba97d36ca5d78e9bac5d71f639c12e35cfa921df76dd736d08c061c673c5cb232fcc3ecb05cbcc4868cd60c816edc4e6f06028 + languageName: node + linkType: hard + +"bunyan@npm:^1.8.12": + version: 1.8.15 + resolution: "bunyan@npm:1.8.15" + dependencies: + dtrace-provider: "npm:~0.8" + moment: "npm:^2.19.3" + mv: "npm:~2" + safe-json-stringify: "npm:~1" + dependenciesMeta: + dtrace-provider: + optional: true + moment: + optional: true + mv: + optional: true + safe-json-stringify: + optional: true + bin: + bunyan: bin/bunyan + checksum: 10c0/c7b3adc07a4db3256f857dcba42b97dd6c35ab054cb26766643aae2b90e1b614795cdf231774ddaf374572d952f52ef4f4205047e15414e155e478aa0672e041 + languageName: node + linkType: hard + +"bunyan@npm:^2.0.5": + version: 2.0.5 + resolution: "bunyan@npm:2.0.5" + dependencies: + dtrace-provider: "npm:~0.8" + exeunt: "npm:1.1.0" + moment: "npm:^2.19.3" + mv: "npm:~2" + safe-json-stringify: "npm:~1" + dependenciesMeta: + dtrace-provider: + optional: true + moment: + optional: true + mv: + optional: true + safe-json-stringify: + optional: true + bin: + bunyan: bin/bunyan + checksum: 10c0/b3a2f0ccc4473c7a83d94f64805dd4ae70e4f7afff8658262a782a8a41ad71e062749e67ec7be57ab3ae724d3d199247ef4b6976ac6c0c67a114f442224977aa + languageName: node + linkType: hard + "bytes@npm:3.1.2, bytes@npm:^3.1.2, bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" @@ -8095,6 +8573,13 @@ __metadata: languageName: node linkType: hard +"caf@npm:^15.0.1": + version: 15.0.1 + resolution: "caf@npm:15.0.1" + checksum: 10c0/9a852c3c93c44499ab1e2cc58c264dd99bb2eaeb17b46df84e6fa5302d6829f776e84a1c13da5bbc2830ba274087eddbde9fe23334f7eb00ce89da9333d1a5cc + languageName: node + linkType: hard + "call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" @@ -8166,7 +8651,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": +"camelcase@npm:^6.0.0, camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 10c0/0d701658219bd3116d12da3eab31acddb3f9440790c0792e0d398f0a520a6a4058018e546862b6fba89d7ae990efaeb97da71e1913e9ebf5a8b5621a3d55c710 @@ -8533,6 +9018,15 @@ __metadata: languageName: node linkType: hard +"color-convert@npm:^3.1.3": + version: 3.1.3 + resolution: "color-convert@npm:3.1.3" + dependencies: + color-name: "npm:^2.0.0" + checksum: 10c0/427648b442c6ea6dab5ba03f4962201ee59f128c80b25d5a0f7d9aab0ef52519a9db8a9bb3cf40b73f86eb19b5ca6aeb0ab930665f3d14973ce776d7d0448a15 + languageName: node + linkType: hard + "color-name@npm:1.1.3": version: 1.1.3 resolution: "color-name@npm:1.1.3" @@ -8547,6 +9041,13 @@ __metadata: languageName: node linkType: hard +"color-name@npm:^2.0.0": + version: 2.1.1 + resolution: "color-name@npm:2.1.1" + checksum: 10c0/6a1096d761b84ad18bdf1464d449800dbb1e613c914aafd9551514884f5e0cc47b5478fea32048188ec7a28424c6ded55fd4a81f39ca351e2ca4dd7729e05128 + languageName: node + linkType: hard + "color-string@npm:^1.9.0": version: 1.9.1 resolution: "color-string@npm:1.9.1" @@ -8557,6 +9058,15 @@ __metadata: languageName: node linkType: hard +"color-string@npm:^2.1.3": + version: 2.1.4 + resolution: "color-string@npm:2.1.4" + dependencies: + color-name: "npm:^2.0.0" + checksum: 10c0/18a9fefec153d885e0dbfb076f3a65cdcd19f52d96c719f2f261e90e5b7dafd13c51baac399d7099eac290f004d340045ab9467312dcc8afefe6f877ec5c4428 + languageName: node + linkType: hard + "color@npm:^4.2.3": version: 4.2.3 resolution: "color@npm:4.2.3" @@ -8567,6 +9077,16 @@ __metadata: languageName: node linkType: hard +"color@npm:^5.0.2": + version: 5.0.3 + resolution: "color@npm:5.0.3" + dependencies: + color-convert: "npm:^3.1.3" + color-string: "npm:^2.1.3" + checksum: 10c0/f08a03c5113ae4aa36dba9d2438596b194b897e18b961310643cb63872add1da507cd238df264eb434bbdbe3a377ec41f90d877531acca611523cfcd365db1b6 + languageName: node + linkType: hard + "colorette@npm:^1.0.7": version: 1.4.0 resolution: "colorette@npm:1.4.0" @@ -9155,6 +9675,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^4.0.0": + version: 4.0.0 + resolution: "decamelize@npm:4.0.0" + checksum: 10c0/e06da03fc05333e8cd2778c1487da67ffbea5b84e03ca80449519b8fa61f888714bbc6f459ea963d5641b4aa98832130eb5cd193d90ae9f0a27eee14be8e278d + languageName: node + linkType: hard + "decode-named-character-reference@npm:^1.0.0": version: 1.3.0 resolution: "decode-named-character-reference@npm:1.3.0" @@ -9309,6 +9836,58 @@ __metadata: languageName: node linkType: hard +"detox@npm:^20.51.4": + version: 20.51.4 + resolution: "detox@npm:20.51.4" + dependencies: + "@wix-pilot/core": "npm:^3.4.2" + "@wix-pilot/detox": "npm:^1.0.13" + ajv: "npm:^8.6.3" + bunyan: "npm:^1.8.12" + bunyan-debug-stream: "npm:^3.1.0" + caf: "npm:^15.0.1" + chalk: "npm:^4.0.0" + execa: "npm:^5.1.1" + find-up: "npm:^5.0.0" + fs-extra: "npm:^11.0.0" + funpermaproxy: "npm:^1.1.0" + glob: "npm:^8.0.3" + ini: "npm:^1.3.4" + jest-environment-emit: "npm:^1.2.0" + json-cycle: "npm:^1.3.0" + lodash: "npm:^4.17.11" + multi-sort-stream: "npm:^1.0.3" + multipipe: "npm:^4.0.0" + node-ipc: "npm:9.2.1" + promisify-child-process: "npm:^4.1.2" + proper-lockfile: "npm:^3.0.2" + resolve-from: "npm:^5.0.0" + sanitize-filename: "npm:^1.6.1" + semver: "npm:^7.0.0" + serialize-error: "npm:^8.0.1" + shell-quote: "npm:^1.7.2" + signal-exit: "npm:^3.0.3" + stream-json: "npm:^1.7.4" + strip-ansi: "npm:^6.0.1" + telnet-client: "npm:1.2.8" + tmp: "npm:^0.2.1" + trace-event-lib: "npm:^1.3.1" + which: "npm:^1.3.1" + ws: "npm:^7.0.0" + yargs: "npm:^17.0.0" + yargs-parser: "npm:^21.0.0" + yargs-unparser: "npm:^2.0.0" + peerDependencies: + jest: 30.x.x || 29.x.x || 28.x.x || ^27.2.5 + peerDependenciesMeta: + jest: + optional: true + bin: + detox: local-cli/cli.js + checksum: 10c0/4be674954c6ca60d8efb5d396848eefd5241018c78f64b503a462b0a66b9e7805d0af984318546ee8810063a95f9d92892d3e1e3d9b589ce703d2210da80fb16 + languageName: node + linkType: hard + "diff-sequences@npm:^29.6.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -9402,6 +9981,16 @@ __metadata: languageName: node linkType: hard +"dtrace-provider@npm:~0.8": + version: 0.8.8 + resolution: "dtrace-provider@npm:0.8.8" + dependencies: + nan: "npm:^2.14.0" + node-gyp: "npm:latest" + checksum: 10c0/33bfc18462dd59ae1de094c64b7b093d2f7f67dec48f138df3a7507c09aaed2a964a245e7bdf2bde7d1a6cc467b11d7396e0fb13a6b882642d42a44cc08c61da + languageName: node + linkType: hard + "dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" @@ -9413,7 +10002,7 @@ __metadata: languageName: node linkType: hard -"duplexer2@npm:~0.1.0": +"duplexer2@npm:^0.1.2, duplexer2@npm:~0.1.0": version: 0.1.4 resolution: "duplexer2@npm:0.1.4" dependencies: @@ -9429,6 +10018,13 @@ __metadata: languageName: node linkType: hard +"easy-stack@npm:^1.0.1": + version: 1.0.1 + resolution: "easy-stack@npm:1.0.1" + checksum: 10c0/1eaf066169a20f6cc3cafd2bb36b00baacd60b6414c8d8bf51bfd50bc6f1c487140c8af86bbb8e1ff9ded2faea5e138c55a37867fc79cbbc985bf5a5ebe4b109 + languageName: node + linkType: hard + "ecdsa-sig-formatter@npm:1.0.11": version: 1.0.11 resolution: "ecdsa-sig-formatter@npm:1.0.11" @@ -9494,6 +10090,13 @@ __metadata: languageName: node linkType: hard +"enabled@npm:2.0.x": + version: 2.0.0 + resolution: "enabled@npm:2.0.0" + checksum: 10c0/3b2c2af9bc7f8b9e291610f2dde4a75cf6ee52a68f4dd585482fbdf9a55d65388940e024e56d40bb03e05ef6671f5f53021fa8b72a20e954d7066ec28166713f + languageName: node + linkType: hard + "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -10189,6 +10792,13 @@ __metadata: languageName: node linkType: hard +"event-pubsub@npm:4.3.0": + version: 4.3.0 + resolution: "event-pubsub@npm:4.3.0" + checksum: 10c0/47fa4fb5b55b3ed08b912862cc913e03603fa063cd3ec5cf3dfeb39a19314d3ca327e938a2cf70685254ab3a71af8178969963c705a030c6081d625bec835114 + languageName: node + linkType: hard + "execa@npm:^4.0.3": version: 4.1.0 resolution: "execa@npm:4.1.0" @@ -10260,6 +10870,13 @@ __metadata: languageName: node linkType: hard +"exeunt@npm:1.1.0": + version: 1.1.0 + resolution: "exeunt@npm:1.1.0" + checksum: 10c0/dc2012d9016c01ec321e4d642d2f165ce7dc4a4d141c523a818416035bcfe11bd1d9abc142f6f608d6a7719824295131a1b8c89006eb308aafd3aba385bf40d2 + languageName: node + linkType: hard + "exit-x@npm:^0.2.2": version: 0.2.2 resolution: "exit-x@npm:0.2.2" @@ -10288,6 +10905,20 @@ __metadata: languageName: node linkType: hard +"expect@npm:30.5.2": + version: 30.5.2 + resolution: "expect@npm:30.5.2" + dependencies: + "@jest/expect-utils": "npm:30.5.2" + "@jest/get-type": "npm:30.5.0" + jest-matcher-utils: "npm:30.5.2" + jest-message-util: "npm:30.5.1" + jest-mock: "npm:30.5.2" + jest-util: "npm:30.5.1" + checksum: 10c0/31bf2307499008f267c84992a8ea27f76e97f207f685075439aa12f60b93dd81d024e5fcb0142755ed499233056f27fa51d01843d04cba437dfe7588387c352f + languageName: node + linkType: hard + "expo-asset@npm:~58.0.14": version: 58.0.14 resolution: "expo-asset@npm:58.0.14" @@ -10972,6 +11603,13 @@ __metadata: languageName: node linkType: hard +"fecha@npm:^4.2.0": + version: 4.2.3 + resolution: "fecha@npm:4.2.3" + checksum: 10c0/0e895965959cf6a22bb7b00f0bf546f2783836310f510ddf63f463e1518d4c96dec61ab33fdfd8e79a71b4856a7c865478ce2ee8498d560fe125947703c9b1cf + languageName: node + linkType: hard + "fetch-nodeshim@npm:^0.4.10": version: 0.4.10 resolution: "fetch-nodeshim@npm:0.4.10" @@ -11147,6 +11785,15 @@ __metadata: languageName: node linkType: hard +"flat@npm:^5.0.2": + version: 5.0.2 + resolution: "flat@npm:5.0.2" + bin: + flat: cli.js + checksum: 10c0/f178b13482f0cd80c7fede05f4d10585b1f2fdebf26e12edc138e32d3150c6ea6482b7f12813a1091143bad52bb6d3596bca51a162257a21163c0ff438baa5fe + languageName: node + linkType: hard + "flatted@npm:^3.2.9": version: 3.4.2 resolution: "flatted@npm:3.4.2" @@ -11193,6 +11840,13 @@ __metadata: languageName: node linkType: hard +"fn.name@npm:1.x.x": + version: 1.1.0 + resolution: "fn.name@npm:1.1.0" + checksum: 10c0/8ad62aa2d4f0b2a76d09dba36cfec61c540c13a0fd72e5d94164e430f987a7ce6a743112bbeb14877c810ef500d1f73d7f56e76d029d2e3413f20d79e3460a9a + languageName: node + linkType: hard + "follow-redirects@npm:^1.16.0": version: 1.16.0 resolution: "follow-redirects@npm:1.16.0" @@ -11369,6 +12023,13 @@ __metadata: languageName: node linkType: hard +"funpermaproxy@npm:^1.1.0": + version: 1.1.0 + resolution: "funpermaproxy@npm:1.1.0" + checksum: 10c0/058d5f56499dac0a275d7115c74230e339f7481618b96c21e8cafa8260a6db7c16f1e4127f5be2aeb167876ff5f33846503a07148279b2f68ca45d04771d666e + languageName: node + linkType: hard + "generator-function@npm:^2.0.0": version: 2.0.1 resolution: "generator-function@npm:2.0.1" @@ -11544,7 +12205,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.5.0": +"glob@npm:^10.4.1, glob@npm:^10.5.0": version: 10.5.0 resolution: "glob@npm:10.5.0" dependencies: @@ -11571,6 +12232,19 @@ __metadata: languageName: node linkType: hard +"glob@npm:^6.0.1": + version: 6.0.4 + resolution: "glob@npm:6.0.4" + dependencies: + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:2 || 3" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 10c0/520146ebce0f4594b8357338f86281b38ee14214debce398a2902176a28f18e0f98911ea48516d85022de64fbbaa57f074aa13715d1daa5d70e21b82cea22183 + languageName: node + linkType: hard + "glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -11671,7 +12345,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -13166,6 +13840,18 @@ __metadata: languageName: node linkType: hard +"jest-diff@npm:30.5.2": + version: 30.5.2 + resolution: "jest-diff@npm:30.5.2" + dependencies: + "@jest/diff-sequences": "npm:30.5.0" + "@jest/get-type": "npm:30.5.0" + chalk: "npm:^4.1.2" + pretty-format: "npm:30.5.1" + checksum: 10c0/b11524e4b7a0c7b83c5b6a3775f705b1002bf086c211e9bd2e481686e3fa0ee1a87e61fa7453232aef2808f085f254f4f8fcbb41b69681d1b9384ec321cf6002 + languageName: node + linkType: hard + "jest-diff@npm:^29.0.1, jest-diff@npm:^29.7.0": version: 29.7.0 resolution: "jest-diff@npm:29.7.0" @@ -13200,6 +13886,39 @@ __metadata: languageName: node linkType: hard +"jest-environment-emit@npm:^1.2.0": + version: 1.2.0 + resolution: "jest-environment-emit@npm:1.2.0" + dependencies: + bunyamin: "npm:^1.5.2" + bunyan: "npm:^2.0.5" + bunyan-debug-stream: "npm:^3.1.0" + funpermaproxy: "npm:^1.1.0" + lodash.merge: "npm:^4.6.2" + node-ipc: "npm:9.2.1" + strip-ansi: "npm:^6.0.0" + tslib: "npm:^2.5.3" + peerDependencies: + "@jest/environment": ">=27.2.5" + "@jest/types": ">=27.2.5" + jest: ">=27.2.5" + jest-environment-jsdom: ">=27.2.5" + jest-environment-node: ">=27.2.5" + peerDependenciesMeta: + "@jest/environment": + optional: true + "@jest/types": + optional: true + jest: + optional: true + jest-environment-jsdom: + optional: true + jest-environment-node: + optional: true + checksum: 10c0/4e1230b447b54c54eecbc6e52782e2133a07b0556a43d34b9f7733b4be8d3a390be7a444863abed31cef638a259815f65b41b588dabc9f9ee478af9ea0ab419b + languageName: node + linkType: hard + "jest-environment-node@npm:30.4.1": version: 30.4.1 resolution: "jest-environment-node@npm:30.4.1" @@ -13280,6 +13999,25 @@ __metadata: languageName: node linkType: hard +"jest-haste-map@npm:30.5.1": + version: 30.5.1 + resolution: "jest-haste-map@npm:30.5.1" + dependencies: + "@jest/types": "npm:30.5.1" + "@parcel/watcher": "npm:^2.6.0" + "@types/node": "npm:*" + anymatch: "npm:^3.1.3" + fb-watchman: "npm:^2.0.2" + fdir: "npm:^6.5.0" + graceful-fs: "npm:^4.2.11" + jest-regex-util: "npm:30.5.0" + jest-util: "npm:30.5.1" + jest-worker: "npm:30.5.1" + picomatch: "npm:^4.0.3" + checksum: 10c0/a9838762f84c54b1d551650e3c956f8d72370a15b2e9291d4b0c9aab0b4d4f3d1758bbc619ffb028af567d5dc53a73c5e72af078c56c5fa47cd73a2a309ca89a + languageName: node + linkType: hard + "jest-haste-map@npm:^29.7.0": version: 29.7.0 resolution: "jest-haste-map@npm:29.7.0" @@ -13325,6 +14063,18 @@ __metadata: languageName: node linkType: hard +"jest-matcher-utils@npm:30.5.2": + version: 30.5.2 + resolution: "jest-matcher-utils@npm:30.5.2" + dependencies: + "@jest/get-type": "npm:30.5.0" + chalk: "npm:^4.1.2" + jest-diff: "npm:30.5.2" + pretty-format: "npm:30.5.1" + checksum: 10c0/30f075c6f897335415817ad25e6caae8994de87c31ad59ae8f501e2c8bf6c925635abb481c562436b6add42a784902f105ee2fa0eca3357bd26e038f7e004cb8 + languageName: node + linkType: hard + "jest-matcher-utils@npm:^29.0.1, jest-matcher-utils@npm:^29.7.0": version: 29.7.0 resolution: "jest-matcher-utils@npm:29.7.0" @@ -13355,6 +14105,24 @@ __metadata: languageName: node linkType: hard +"jest-message-util@npm:30.5.1": + version: 30.5.1 + resolution: "jest-message-util@npm:30.5.1" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@jest/types": "npm:30.5.1" + "@types/stack-utils": "npm:^2.0.3" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + jest-util: "npm:30.5.1" + picomatch: "npm:^4.0.3" + pretty-format: "npm:30.5.1" + slash: "npm:^3.0.0" + stack-utils: "npm:^2.0.6" + checksum: 10c0/f542d07f8ceba19da26c547aa86abc51e6fad518999a18853e7c2acc2a55845ad759aa94cbe650d0453b036b81fcafee710d467b55e1e509f9939db033e76919 + languageName: node + linkType: hard + "jest-message-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-message-util@npm:29.7.0" @@ -13383,6 +14151,18 @@ __metadata: languageName: node linkType: hard +"jest-mock@npm:30.5.2": + version: 30.5.2 + resolution: "jest-mock@npm:30.5.2" + dependencies: + "@jest/expect-utils": "npm:30.5.2" + "@jest/types": "npm:30.5.1" + "@types/node": "npm:*" + jest-util: "npm:30.5.1" + checksum: 10c0/24cc9ac6449c536ef5009c531e14ee6b145f5d68db6e4c5616a8f775c5c81173e83e2c6c47922d413da4dbf804eab555b91ab424b0bf918495ee1b29d8a8ebaa + languageName: node + linkType: hard + "jest-mock@npm:^29.7.0": version: 29.7.0 resolution: "jest-mock@npm:29.7.0" @@ -13420,6 +14200,13 @@ __metadata: languageName: node linkType: hard +"jest-regex-util@npm:30.5.0": + version: 30.5.0 + resolution: "jest-regex-util@npm:30.5.0" + checksum: 10c0/f0e5ef257b906862821e1fb58c1d3d7bb3c4fc1fb11915673f04d3693673e0656b8f502c9871900e13e58329039c73e5b5c59a2622cbd6ba5c7fc7463b60ce48 + languageName: node + linkType: hard + "jest-regex-util@npm:^29.6.3": version: 29.6.3 resolution: "jest-regex-util@npm:29.6.3" @@ -13542,6 +14329,35 @@ __metadata: languageName: node linkType: hard +"jest-snapshot@npm:30.5.2": + version: 30.5.2 + resolution: "jest-snapshot@npm:30.5.2" + dependencies: + "@babel/core": "npm:^7.27.4" + "@babel/generator": "npm:^7.27.5" + "@babel/plugin-syntax-jsx": "npm:^7.27.1" + "@babel/plugin-syntax-typescript": "npm:^7.27.1" + "@babel/types": "npm:^7.27.3" + "@jest/expect-utils": "npm:30.5.2" + "@jest/get-type": "npm:30.5.0" + "@jest/snapshot-utils": "npm:30.5.1" + "@jest/transform": "npm:30.5.2" + "@jest/types": "npm:30.5.1" + babel-preset-current-node-syntax: "npm:^1.2.0" + chalk: "npm:^4.1.2" + expect: "npm:30.5.2" + graceful-fs: "npm:^4.2.11" + jest-diff: "npm:30.5.2" + jest-matcher-utils: "npm:30.5.2" + jest-message-util: "npm:30.5.1" + jest-util: "npm:30.5.1" + pretty-format: "npm:30.5.1" + semver: "npm:^7.7.2" + synckit: "npm:^0.11.8" + checksum: 10c0/cef6b102f743cbefd5d8ad0cf15b5694c09522904c43ace56537990475805b7c6eaebf82e6c5564e225e8db7617ebdda77340acc9a47d9909f338fbb84fca345 + languageName: node + linkType: hard + "jest-util@npm:30.0.0": version: 30.0.0 resolution: "jest-util@npm:30.0.0" @@ -13566,7 +14382,21 @@ __metadata: ci-info: "npm:^4.2.0" graceful-fs: "npm:^4.2.11" picomatch: "npm:^4.0.3" - checksum: 10c0/3efe1f25e5a172d04c6af8612d82867ab603b7c1bd8cb89073ff834679b44eba178793cf3af162cf5e25be13aa736ebd23a7826683acc85bddc5873f305b1f6e + checksum: 10c0/3efe1f25e5a172d04c6af8612d82867ab603b7c1bd8cb89073ff834679b44eba178793cf3af162cf5e25be13aa736ebd23a7826683acc85bddc5873f305b1f6e + languageName: node + linkType: hard + +"jest-util@npm:30.5.1": + version: 30.5.1 + resolution: "jest-util@npm:30.5.1" + dependencies: + "@jest/types": "npm:30.5.1" + "@types/node": "npm:*" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + graceful-fs: "npm:^4.2.11" + picomatch: "npm:^4.0.3" + checksum: 10c0/5f3b250f1ada29b877cab75db5808f1318af99ba7f68032e0966ddb6d853186ad2dbaeac679842684c4676dfd35083470f2d26eee65a28161fcd9e43c7146fab languageName: node linkType: hard @@ -13654,6 +14484,19 @@ __metadata: languageName: node linkType: hard +"jest-worker@npm:30.5.1": + version: 30.5.1 + resolution: "jest-worker@npm:30.5.1" + dependencies: + "@types/node": "npm:*" + "@ungap/structured-clone": "npm:^1.3.0" + jest-util: "npm:30.5.1" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.1.1" + checksum: 10c0/fe3806769014920c5256ef01e2e9a5aba91a7ded90d7c86a82196f3da03070087c8c6b591f8bbb0b500c184c7cc8c5f22191e51cce504163b24a8b42c4cadd8b + languageName: node + linkType: hard + "jest-worker@npm:^29.6.3, jest-worker@npm:^29.7.0": version: 29.7.0 resolution: "jest-worker@npm:29.7.0" @@ -13723,6 +14566,22 @@ __metadata: languageName: node linkType: hard +"js-message@npm:1.0.7": + version: 1.0.7 + resolution: "js-message@npm:1.0.7" + checksum: 10c0/2dc2ff5a594613a9690c157a921999237164a3c213523f406dee23985ed81c92be9afdc7b34e67a2838d447d7d79cbf1662048effe8367bedcd71a950e4e292e + languageName: node + linkType: hard + +"js-queue@npm:2.0.2": + version: 2.0.2 + resolution: "js-queue@npm:2.0.2" + dependencies: + easy-stack: "npm:^1.0.1" + checksum: 10c0/cdadc65576c6d46817f47c27d7a57a1f3acbe3a4da2a4bc7303218f06d6ee7971358db28ec0d6b6835b4cd93332c52792b535064fee706453ef9d2bfe03b4a1a + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -13787,6 +14646,13 @@ __metadata: languageName: node linkType: hard +"json-cycle@npm:^1.3.0": + version: 1.5.0 + resolution: "json-cycle@npm:1.5.0" + checksum: 10c0/43ba321cf186b22573826ee76d64601bdfe57fe787d74162626c453bd860595c4c62ebfc338b5a64994fb4e4d7a947e6c2ef5f202e3a2ee4b01ca23ebc0cd710 + languageName: node + linkType: hard + "json-parse-better-errors@npm:^1.0.1": version: 1.0.2 resolution: "json-parse-better-errors@npm:1.0.2" @@ -13983,6 +14849,13 @@ __metadata: languageName: node linkType: hard +"kuler@npm:^2.0.0": + version: 2.0.0 + resolution: "kuler@npm:2.0.0" + checksum: 10c0/0a4e99d92ca373f8f74d1dc37931909c4d0d82aebc94cf2ba265771160fc12c8df34eaaac80805efbda367e2795cb1f1dd4c3d404b6b1cf38aec94035b503d2d + languageName: node + linkType: hard + "lan-network@npm:^0.2.1": version: 0.2.1 resolution: "lan-network@npm:0.2.1" @@ -14440,7 +15313,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.18.1, lodash@npm:^4.17.15, lodash@npm:^4.17.4": +"lodash@npm:4.18.1, lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.4": version: 4.18.1 resolution: "lodash@npm:4.18.1" checksum: 10c0/757228fc68805c59789e82185135cf85f05d0b2d3d54631d680ca79ec21944ec8314d4533639a14b8bcfbd97a517e78960933041a5af17ecb693ec6eecb99a27 @@ -14476,6 +15349,20 @@ __metadata: languageName: node linkType: hard +"logform@npm:^2.7.0": + version: 2.7.0 + resolution: "logform@npm:2.7.0" + dependencies: + "@colors/colors": "npm:1.6.0" + "@types/triple-beam": "npm:^1.3.2" + fecha: "npm:^4.2.0" + ms: "npm:^2.1.1" + safe-stable-stringify: "npm:^2.3.1" + triple-beam: "npm:^1.3.0" + checksum: 10c0/4789b4b37413c731d1835734cb799240d31b865afde6b7b3e06051d6a4127bfda9e88c99cfbf296d084a315ccbed2647796e6a56b66e725bcb268c586f57558f + languageName: node + linkType: hard + "logkitty@npm:^0.7.1": version: 0.7.1 resolution: "logkitty@npm:0.7.1" @@ -15281,6 +16168,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:2 || 3, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2, minimatch@npm:^3.1.5": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 + languageName: node + linkType: hard + "minimatch@npm:^10.0.3, minimatch@npm:^10.1.1, minimatch@npm:^10.2.2, minimatch@npm:^10.2.5": version: 10.2.5 resolution: "minimatch@npm:10.2.5" @@ -15290,15 +16186,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2, minimatch@npm:^3.1.5": - version: 3.1.5 - resolution: "minimatch@npm:3.1.5" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/2ecbdc0d33f07bddb0315a8b5afbcb761307a8778b48f0b312418ccbced99f104a2d17d8aca7573433c70e8ccd1c56823a441897a45e384ea76ef401a26ace70 - languageName: node - linkType: hard - "minimatch@npm:^5.0.1": version: 5.1.9 resolution: "minimatch@npm:5.1.9" @@ -15423,6 +16310,17 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:~0.5.1": + version: 0.5.6 + resolution: "mkdirp@npm:0.5.6" + dependencies: + minimist: "npm:^1.2.6" + bin: + mkdirp: bin/cmd.js + checksum: 10c0/e2e2be789218807b58abced04e7b49851d9e46e88a2f9539242cc8a92c9b5c3a0b9bab360bd3014e02a140fc4fbc58e31176c408b493f8a2a6f4986bd7527b01 + languageName: node + linkType: hard + "moment-timezone@npm:^0.6.0": version: 0.6.2 resolution: "moment-timezone@npm:0.6.2" @@ -15432,6 +16330,13 @@ __metadata: languageName: node linkType: hard +"moment@npm:^2.19.3": + version: 2.31.0 + resolution: "moment@npm:2.31.0" + checksum: 10c0/3755b186b8ff62c565685ce69dde55bf46003c0329139be5c8dd7845cd567bfd7d02be3a13e379a25acb9e98f6b87136c4776745e440615a4f5ca8dd9a8815b9 + languageName: node + linkType: hard + "moment@npm:^2.29.4": version: 2.30.1 resolution: "moment@npm:2.30.1" @@ -15453,6 +16358,23 @@ __metadata: languageName: node linkType: hard +"multi-sort-stream@npm:^1.0.3, multi-sort-stream@npm:^1.0.4": + version: 1.0.4 + resolution: "multi-sort-stream@npm:1.0.4" + checksum: 10c0/74820fd8f4287798f56f5c0b390d52d0fa0efd46b22a880390c6182c4b2ff6bd608dc90974fe7b0981c9243ae4f857d3389e74ebf6d071d7465056c29772196d + languageName: node + linkType: hard + +"multipipe@npm:^4.0.0": + version: 4.0.0 + resolution: "multipipe@npm:4.0.0" + dependencies: + duplexer2: "npm:^0.1.2" + object-assign: "npm:^4.1.0" + checksum: 10c0/60b547f85a3c692fe24c4e15988bdbb6c7985665fd7c4f6228bbf2630f699badeefd8d492bff5b295ea02a284e12917ff21e324a2f660e3d5667bb656a01792d + languageName: node + linkType: hard + "multitars@npm:^1.0.2": version: 1.0.2 resolution: "multitars@npm:1.0.2" @@ -15467,6 +16389,17 @@ __metadata: languageName: node linkType: hard +"mv@npm:~2": + version: 2.1.1 + resolution: "mv@npm:2.1.1" + dependencies: + mkdirp: "npm:~0.5.1" + ncp: "npm:~2.0.0" + rimraf: "npm:~2.4.0" + checksum: 10c0/5da59a9f4ec16da0867289b5018c81c25c59b06bb9da717bc7bd0b40363d6653dc88d6da32a9434fd7416bfc3f67184c306ea44d3856ff97f3214cc96960efcd + languageName: node + linkType: hard + "mz@npm:^2.4.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -15478,6 +16411,15 @@ __metadata: languageName: node linkType: hard +"nan@npm:^2.14.0": + version: 2.29.0 + resolution: "nan@npm:2.29.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/c90586afb3f101f64c2997e7481f7c2c5528e196399a1c2653e4fff7354a01f35009e10fd43aa567a4b68be0ac8ed45a60794f5ca0a863120b74aecf4a45ad2c + languageName: node + linkType: hard + "nanoid@npm:^3.3.1, nanoid@npm:^3.3.11": version: 3.3.12 resolution: "nanoid@npm:3.3.12" @@ -15519,6 +16461,15 @@ __metadata: languageName: node linkType: hard +"ncp@npm:~2.0.0": + version: 2.0.0 + resolution: "ncp@npm:2.0.0" + bin: + ncp: ./bin/ncp + checksum: 10c0/d515babf9d3205ab9252e7d640af7c3e1a880317016d41f2fce2e6b9c8f60eb8bb6afde30e8c4f8e1e3fa551465f094433c3f364b25a85d6a28ec52c1ad6e067 + languageName: node + linkType: hard + "negotiator@npm:0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" @@ -15584,6 +16535,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.0.0": + version: 7.1.1 + resolution: "node-addon-api@npm:7.1.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/fb32a206276d608037fa1bcd7e9921e177fe992fc610d098aa3128baca3c0050fc1e014fa007e9b3874cf865ddb4f5bd9f43ccb7cbbbe4efaff6a83e920b17e9 + languageName: node + linkType: hard + "node-emoji@npm:^2.2.0": version: 2.2.0 resolution: "node-emoji@npm:2.2.0" @@ -15656,6 +16616,17 @@ __metadata: languageName: node linkType: hard +"node-ipc@npm:9.2.1": + version: 9.2.1 + resolution: "node-ipc@npm:9.2.1" + dependencies: + event-pubsub: "npm:4.3.0" + js-message: "npm:1.0.7" + js-queue: "npm:2.0.2" + checksum: 10c0/ed6448e2c643a831b9795a52de727658417a2cbedd3a59988b9be7654383982577dc507e0ec13211d10ed90377124366e3869a137b05093a424caf647eba0523 + languageName: node + linkType: hard + "node-releases@npm:^2.0.36": version: 2.0.44 resolution: "node-releases@npm:2.0.44" @@ -16118,6 +17089,15 @@ __metadata: languageName: node linkType: hard +"one-time@npm:^1.0.0": + version: 1.0.0 + resolution: "one-time@npm:1.0.0" + dependencies: + fn.name: "npm:1.x.x" + checksum: 10c0/6e4887b331edbb954f4e915831cbec0a7b9956c36f4feb5f6de98c448ac02ff881fd8d9b55a6b1b55030af184c6b648f340a76eb211812f4ad8c9b4b8692fdaa + languageName: node + linkType: hard + "onetime@npm:^2.0.0": version: 2.0.1 resolution: "onetime@npm:2.0.1" @@ -16721,6 +17701,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^7.0.0": + version: 7.0.0 + resolution: "pngjs@npm:7.0.0" + checksum: 10c0/0d4c7a0fd476a9c33df7d0a2a73e1d56537628a668841f6995c2bca070cf30819f9254a64363266bc14ef2fee47659dd3b4f2b18eec7ab65143015139f497b38 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.0.0 resolution: "possible-typed-array-names@npm:1.0.0" @@ -16815,6 +17802,18 @@ __metadata: languageName: node linkType: hard +"pretty-format@npm:30.5.1": + version: 30.5.1 + resolution: "pretty-format@npm:30.5.1" + dependencies: + "@jest/react-is-18": "npm:react-is@^18.3.1" + "@jest/react-is-19": "npm:react-is@^19.2.5" + "@jest/schemas": "npm:30.5.0" + ansi-styles: "npm:^5.2.0" + checksum: 10c0/bdef1790b9c93d1aa329c93885b0d1b8b6a3273bb680377531c70151b2e1b0a465210cae100661fc19b0bf35f507587f78b1a2f1afcf08791b9fe5630c23c076 + languageName: node + linkType: hard + "pretty-format@npm:^29.0.3, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" @@ -16933,6 +17932,13 @@ __metadata: languageName: node linkType: hard +"promisify-child-process@npm:^4.1.2": + version: 4.1.2 + resolution: "promisify-child-process@npm:4.1.2" + checksum: 10c0/e5347137f1c333af4b7b4d2272a091d31007b30320af4b852f30f41776db378e22b3db1e67e40026a18301c243d9c9780e41c211cfce76ba26aedba17e3a385c + languageName: node + linkType: hard + "prompts@npm:^2.3.2, prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -16963,6 +17969,17 @@ __metadata: languageName: node linkType: hard +"proper-lockfile@npm:^3.0.2": + version: 3.2.0 + resolution: "proper-lockfile@npm:3.2.0" + dependencies: + graceful-fs: "npm:^4.1.11" + retry: "npm:^0.12.0" + signal-exit: "npm:^3.0.2" + checksum: 10c0/b3a0bbf87f1b842ab57d91bee2e8c73d6cb2e90558aad259e83561cf5467f20885a1eb127343ecd9db214c78aaa196867ef8edb76e3681dc07ee43a489fd53bd + languageName: node + linkType: hard + "property-information@npm:^7.0.0": version: 7.2.0 resolution: "property-information@npm:7.2.0" @@ -17155,13 +18172,6 @@ __metadata: languageName: node linkType: hard -"react-is-18@npm:react-is@^18.3.1, react-is@npm:^18.0.0": - version: 18.3.1 - resolution: "react-is@npm:18.3.1" - checksum: 10c0/f2f1e60010c683479e74c63f96b09fb41603527cd131a9959e2aee1e5a8b0caf270b365e5ca77d4a6b18aae659b60a86150bb3979073528877029b35aecd2072 - languageName: node - linkType: hard - "react-is-19@npm:react-is@^19.2.5, react-is@npm:^19.1.0": version: 19.2.6 resolution: "react-is@npm:19.2.6" @@ -17317,6 +18327,16 @@ __metadata: languageName: node linkType: hard +"react-native-launch-arguments@npm:^4.1.1": + version: 4.1.1 + resolution: "react-native-launch-arguments@npm:4.1.1" + peerDependencies: + react: ">=16.8.1" + react-native: ">=0.60.0-rc.0 <1.0.x" + checksum: 10c0/94a88b3c6ba583b20108200c794f2c132a5b94383f0eb22797ff21194d140904bd62510ee9a01ea464ba831ddd69b3c49fb9b2799728d1d7fbaebf0ed683edd2 + languageName: node + linkType: hard + "react-native-lightbox@npm:^0.7.0": version: 0.7.0 resolution: "react-native-lightbox@npm:0.7.0" @@ -17758,7 +18778,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.2": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -18084,6 +19104,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:~2.4.0": + version: 2.4.5 + resolution: "rimraf@npm:2.4.5" + dependencies: + glob: "npm:^6.0.1" + bin: + rimraf: ./bin.js + checksum: 10c0/5251a36053165d23248efec5077f9addc13ad7f742a02dcd9ac7adda9e208cbf7523901e96a9ca6c33059bd0b573b97eab3334cf1d9976cc5ddc8b3c24d9ddd7 + languageName: node + linkType: hard + "root@workspace:.": version: 0.0.0-use.local resolution: "root@workspace:." @@ -18177,6 +19208,13 @@ __metadata: languageName: node linkType: hard +"safe-json-stringify@npm:~1": + version: 1.2.0 + resolution: "safe-json-stringify@npm:1.2.0" + checksum: 10c0/9c21c7b63a35a9e52d248eea2ad7bc9e790dde5aa418f0d4eed3c0b4c866e15337425b0d973173d30dd70a9e422271619f17e13574e0c8371d0c240cf72b871f + languageName: node + linkType: hard + "safe-push-apply@npm:^1.0.0": version: 1.0.0 resolution: "safe-push-apply@npm:1.0.0" @@ -18198,6 +19236,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.3.1": + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: 10c0/baea14971858cadd65df23894a40588ed791769db21bafb7fd7608397dbdce9c5aac60748abae9995e0fc37e15f2061980501e012cd48859740796bea2987f49 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -18214,6 +19259,7 @@ __metadata: "@d11/react-native-fast-image": "npm:^8.13.0" "@emoji-mart/data": "npm:^1.2.1" "@gorhom/bottom-sheet": "npm:^5.2.14" + "@jest/globals": "npm:^30.4.1" "@notifee/react-native": "npm:^9.1.8" "@op-engineering/op-sqlite": "npm:^18.2.5" "@react-native-async-storage/async-storage": "npm:^3.1.1" @@ -18241,9 +19287,12 @@ __metadata: "@stream-io/typescript-config": "workspace:^" "@types/lodash.mergewith": "npm:^4.6.9" "@types/react": "npm:^19.3.0" + babel-jest: "npm:30.0.0" babel-plugin-react-compiler: "npm:^1.0.0" dayjs: "npm:1.11.13" + detox: "npm:^20.51.4" emoji-mart: "npm:^5.6.0" + jest: "npm:^30.4.2" lodash.mergewith: "npm:^4.6.2" react: "npm:19.3.0" react-native: "npm:0.88.0" @@ -18251,6 +19300,7 @@ __metadata: react-native-gesture-handler: "npm:^3.4.0" react-native-haptic-feedback: "npm:^3.0.0" react-native-image-picker: "npm:^8.2.1" + react-native-launch-arguments: "npm:^4.1.1" react-native-maps: "npm:^1.29.11" react-native-nitro-modules: "npm:0.36.5" react-native-nitro-sound: "npm:0.2.17" @@ -18278,6 +19328,15 @@ __metadata: languageName: node linkType: hard +"sanitize-filename@npm:^1.6.1": + version: 1.6.4 + resolution: "sanitize-filename@npm:1.6.4" + dependencies: + truncate-utf8-bytes: "npm:^1.0.0" + checksum: 10c0/b56415a95e4f90dc992cd126b5f45c7b39d178662fbd0dc48f03203e35c58ab8e9eb3f5cebfaabc46f1438093d65a3a3cc96875e2b036a1474e19593bee9a540 + languageName: node + linkType: hard + "sax@npm:>=0.6.0": version: 1.6.0 resolution: "sax@npm:1.6.0" @@ -18353,6 +19412,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.0.0": + version: 7.8.5 + resolution: "semver@npm:7.8.5" + bin: + semver: bin/semver.js + checksum: 10c0/b1f3127a5be8125a94f37188b361c212466c292c6910adce3ec106cff5dc211ccaedc4739c11bb70fda59d6fc1f040a9bca289f4e093451521a2372e5231fe0c + languageName: node + linkType: hard + "semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.7.2, semver@npm:^7.7.3, semver@npm:^7.7.4": version: 7.8.1 resolution: "semver@npm:7.8.1" @@ -18390,6 +19458,15 @@ __metadata: languageName: node linkType: hard +"serialize-error@npm:^8.0.1": + version: 8.1.0 + resolution: "serialize-error@npm:8.1.0" + dependencies: + type-fest: "npm:^0.20.2" + checksum: 10c0/8cfd89f43ca93e283c5f1d16178a536bdfac9bc6029f4a9df988610cc399bc4f2478d1f10ce40b9dff66b863a5158a19b438fbec929045c96d92174f6bca1e88 + languageName: node + linkType: hard + "serve-static@npm:^1.13.1, serve-static@npm:^1.16.2": version: 1.16.3 resolution: "serve-static@npm:1.16.3" @@ -18497,6 +19574,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.7.2": + version: 1.12.0 + resolution: "shell-quote@npm:1.12.0" + checksum: 10c0/1605a4ed6a758bf13de7cb8197a4184d8015f7a4dbdcd6cd4a717d5b30b3ae7666300cc4ae7f3155cf80ace9739e9e82061ee5aa1a648f53f5fab31a220816d0 + languageName: node + linkType: hard + "side-channel-list@npm:^1.0.0, side-channel-list@npm:^1.0.1": version: 1.0.1 resolution: "side-channel-list@npm:1.0.1" @@ -18837,6 +19921,13 @@ __metadata: languageName: node linkType: hard +"stack-trace@npm:0.0.x": + version: 0.0.10 + resolution: "stack-trace@npm:0.0.10" + checksum: 10c0/9ff3dabfad4049b635a85456f927a075c9d0c210e3ea336412d18220b2a86cbb9b13ec46d6c37b70a302a4ea4d49e30e5d4944dd60ae784073f1cde778ac8f4b + languageName: node + linkType: hard + "stack-utils@npm:^2.0.3, stack-utils@npm:^2.0.6": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" @@ -18916,6 +20007,13 @@ __metadata: languageName: node linkType: hard +"stream-chain@npm:^2.2.5": + version: 2.2.5 + resolution: "stream-chain@npm:2.2.5" + checksum: 10c0/c512f50190d7c92d688fa64e7af540c51b661f9c2b775fc72bca38ea9bca515c64c22c2197b1be463741daacbaaa2dde8a8ea24ebda46f08391224f15249121a + languageName: node + linkType: hard + "stream-chat-expo@workspace:^, stream-chat-expo@workspace:package/expo-package": version: 0.0.0-use.local resolution: "stream-chat-expo@workspace:package/expo-package" @@ -19155,6 +20253,15 @@ __metadata: languageName: node linkType: hard +"stream-json@npm:^1.7.4, stream-json@npm:^1.7.5": + version: 1.9.1 + resolution: "stream-json@npm:1.9.1" + dependencies: + stream-chain: "npm:^2.2.5" + checksum: 10c0/0521e5cb3fb6b0e2561d715975e891bd81fa77d0239c8d0b1756846392bc3c7cdd7f1ddb0fe7ed77e6fdef58daab9e665d3b39f7d677bd0859e65a2bff59b92c + languageName: node + linkType: hard + "string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2" @@ -19536,6 +20643,15 @@ __metadata: languageName: node linkType: hard +"telnet-client@npm:1.2.8": + version: 1.2.8 + resolution: "telnet-client@npm:1.2.8" + dependencies: + bluebird: "npm:^3.5.4" + checksum: 10c0/f6700268a9734305655e305540b21e18f0ab05923d4917fcf8c3192ced596ce4675c61e8b8d6ad12c89bf7eb2d38f2792ead5eb721c7055c4e924f9312913a34 + languageName: node + linkType: hard + "temp-dir@npm:^3.0.0": version: 3.0.0 resolution: "temp-dir@npm:3.0.0" @@ -19590,6 +20706,24 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.2 + resolution: "test-exclude@npm:7.0.2" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^10.4.1" + minimatch: "npm:^10.2.2" + checksum: 10c0/b79b855af9168c6a362146015ccf40f5e3a25e307304ba9bea930818507f6319d230380d5d7b5baa659c981ccc11f1bd21b6f012f85606353dec07e02dee67c9 + languageName: node + linkType: hard + +"text-hex@npm:1.0.x": + version: 1.0.0 + resolution: "text-hex@npm:1.0.0" + checksum: 10c0/57d8d320d92c79d7c03ffb8339b825bb9637c2cbccf14304309f51d8950015c44464b6fd1b6820a3d4821241c68825634f09f5a2d9d501e84f7c6fd14376860d + languageName: node + linkType: hard + "text-table@npm:~0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -19665,6 +20799,13 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.1": + version: 0.2.7 + resolution: "tmp@npm:0.2.7" + checksum: 10c0/59eb55584f2f07210d3231b6a1f6b5c2b9794d8a7b509c8ee867ed2acad6d2245ee2448b7937b676ffbff3155a70077edde8a69f9d7cf0f90c86a62e8910c357 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -19702,6 +20843,15 @@ __metadata: languageName: node linkType: hard +"trace-event-lib@npm:^1.3.1": + version: 1.4.1 + resolution: "trace-event-lib@npm:1.4.1" + dependencies: + browser-process-hrtime: "npm:^1.0.0" + checksum: 10c0/301faf760c8d956d9879f1eaf8a949d480126a1af0d88d33299f1580401e32358d48726750ac8903a4e9be1d0c5f68bdc13453e067ca2bc93565ea381123c4bd + languageName: node + linkType: hard + "traverse@npm:0.6.8": version: 0.6.8 resolution: "traverse@npm:0.6.8" @@ -19716,6 +20866,22 @@ __metadata: languageName: node linkType: hard +"triple-beam@npm:^1.3.0": + version: 1.4.1 + resolution: "triple-beam@npm:1.4.1" + checksum: 10c0/4bf1db71e14fe3ff1c3adbe3c302f1fdb553b74d7591a37323a7badb32dc8e9c290738996cbb64f8b10dc5a3833645b5d8c26221aaaaa12e50d1251c9aba2fea + languageName: node + linkType: hard + +"truncate-utf8-bytes@npm:^1.0.0": + version: 1.0.2 + resolution: "truncate-utf8-bytes@npm:1.0.2" + dependencies: + utf8-byte-length: "npm:^1.0.1" + checksum: 10c0/af2b431fc4314f119b551e5fccfad49d4c0ef82e13ba9ca61be6567801195b08e732ce9643542e8ad1b3df44f3df2d7345b3dd34f723954b6bb43a14584d6b3c + languageName: node + linkType: hard + "ts-api-utils@npm:^2.5.0": version: 2.5.0 resolution: "ts-api-utils@npm:2.5.0" @@ -19737,7 +20903,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.4.0": +"tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.5.3": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -19787,6 +20953,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.20.2": + version: 0.20.2 + resolution: "type-fest@npm:0.20.2" + checksum: 10c0/dea9df45ea1f0aaa4e2d3bed3f9a0bfe9e5b2592bddb92eb1bf06e50bcf98dbb78189668cd8bc31a0511d3fc25539b4cd5c704497e53e93e2d40ca764b10bfc3 + languageName: node + linkType: hard + "type-fest@npm:^0.21.3": version: 0.21.3 resolution: "type-fest@npm:0.21.3" @@ -20235,6 +21408,13 @@ __metadata: languageName: node linkType: hard +"utf8-byte-length@npm:^1.0.1": + version: 1.0.5 + resolution: "utf8-byte-length@npm:1.0.5" + checksum: 10c0/e69bda3299608f4cc75976da9fb74ac94801a58b9ca29fdad03a20ec952e7477d7f226c12716b5f36bd4cff8151d1d152d02ee1df3752f017d4b2c725ce3e47a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -20532,6 +21712,17 @@ __metadata: languageName: node linkType: hard +"which@npm:^1.3.1": + version: 1.3.1 + resolution: "which@npm:1.3.1" + dependencies: + isexe: "npm:^2.0.0" + bin: + which: ./bin/which + checksum: 10c0/e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 + languageName: node + linkType: hard + "which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -20554,6 +21745,36 @@ __metadata: languageName: node linkType: hard +"winston-transport@npm:^4.9.0": + version: 4.9.0 + resolution: "winston-transport@npm:4.9.0" + dependencies: + logform: "npm:^2.7.0" + readable-stream: "npm:^3.6.2" + triple-beam: "npm:^1.3.0" + checksum: 10c0/e2990a172e754dbf27e7823772214a22dc8312f7ec9cfba831e5ef30a5d5528792e5ea8f083c7387ccfc5b2af20e3691f64738546c8869086110a26f98671095 + languageName: node + linkType: hard + +"winston@npm:^3.17.0": + version: 3.19.0 + resolution: "winston@npm:3.19.0" + dependencies: + "@colors/colors": "npm:^1.6.0" + "@dabh/diagnostics": "npm:^2.0.8" + async: "npm:^3.2.3" + is-stream: "npm:^2.0.0" + logform: "npm:^2.7.0" + one-time: "npm:^1.0.0" + readable-stream: "npm:^3.4.0" + safe-stable-stringify: "npm:^2.3.1" + stack-trace: "npm:0.0.x" + triple-beam: "npm:^1.3.0" + winston-transport: "npm:^4.9.0" + checksum: 10c0/341a8ccfb726120209d34e2466040e2ca72cadb1a3402c4fc90425facad002b81275675b4ab9b4432a624311bc47ef7c9fb7652c86fca454d2be2f2ee1882226 + languageName: node + linkType: hard + "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -20673,6 +21894,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^7.0.0": + version: 7.5.13 + resolution: "ws@npm:7.5.13" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10c0/e223726bbda5768ed58ec9252d04eda7018ef380ea122bb94e595a4d7a02b5baeb423933936576fddb8fa51b642375f7ae814bf0f6f5d9a3ce290566578a8c5a + languageName: node + linkType: hard + "ws@npm:^8.12.1, ws@npm:^8.20.1": version: 8.21.0 resolution: "ws@npm:8.21.0" @@ -20797,7 +22033,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^21.1.1": +"yargs-parser@npm:^21.0.0, yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" checksum: 10c0/f84b5e48169479d2f402239c59f084cfd1c3acc197a05c59b98bab067452e6b3ea46d4dd8ba2985ba7b3d32a343d77df0debd6b343e5dae3da2aab2cdf5886b2 @@ -20811,6 +22047,18 @@ __metadata: languageName: node linkType: hard +"yargs-unparser@npm:^2.0.0": + version: 2.0.0 + resolution: "yargs-unparser@npm:2.0.0" + dependencies: + camelcase: "npm:^6.0.0" + decamelize: "npm:^4.0.0" + flat: "npm:^5.0.2" + is-plain-obj: "npm:^2.1.0" + checksum: 10c0/a5a7d6dc157efa95122e16780c019f40ed91d4af6d2bac066db8194ed0ec5c330abb115daa5a79ff07a9b80b8ea80c925baacf354c4c12edd878c0529927ff03 + languageName: node + linkType: hard + "yargs@npm:^15.1.0": version: 15.4.1 resolution: "yargs@npm:15.4.1" @@ -20845,6 +22093,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^17.0.0": + version: 17.7.3 + resolution: "yargs@npm:17.7.3" + dependencies: + cliui: "npm:^8.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + require-directory: "npm:^2.1.1" + string-width: "npm:^4.2.3" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^21.1.1" + checksum: 10c0/7a28572f7e785a57886e34fdbddb9b28756dec552e1453d5f6e7cdd00ad8721a4e8c4321d33683f5e61cacb36ad43258adbb48396b71ec4ed14abee0fc0d0c1f + languageName: node + linkType: hard + "yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" From bcd7e375c584e3078037e20e455cb57e1ec081c4 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 14:16:00 +0100 Subject: [PATCH 02/16] ci: trust the applesimutils formula before installing it --- .github/workflows/e2e-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index c573f95261..b3bf4b6f29 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -72,7 +72,8 @@ jobs: - name: Install dependencies uses: ./.github/actions/install-and-build-sdk - name: Install applesimutils - run: brew tap wix/brew && brew install applesimutils + # Homebrew refuses formulae from non-official taps until they are trusted. + run: brew tap wix/brew && brew trust --formula wix/brew/applesimutils && brew install wix/brew/applesimutils - name: Cache iOS pods uses: actions/cache@v5 with: From e121dd1cfce1c359a70e03235b1d37e81289bd82 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 15:22:49 +0100 Subject: [PATCH 03/16] ci(e2e): run Detox on a dedicated E2ETestApp and cache builds --- .github/workflows/e2e-test.yml | 222 +- .prettierignore | 4 +- AGENTS.md | 8 +- eslint.config.mjs | 4 +- .../{SampleApp => E2ETestApp}/.detoxrc.js | 8 +- examples/E2ETestApp/.gitignore | 79 + examples/E2ETestApp/.watchmanconfig | 1 + examples/E2ETestApp/App.tsx | 133 + examples/E2ETestApp/android/app/build.gradle | 124 + .../E2ETestApp/android/app/debug.keystore | Bin 0 -> 2257 bytes .../E2ETestApp/android/app/proguard-rules.pro | 10 + .../reactnative/e2etestapp/DetoxTest.kt | 27 + .../android/app/src/main/AndroidManifest.xml | 28 + .../reactnative/e2etestapp/MainActivity.kt | 22 + .../reactnative/e2etestapp/MainApplication.kt | 27 + .../res/drawable/rn_edit_text_material.xml | 37 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + .../main/res/xml/network_security_config.xml | 8 + examples/E2ETestApp/android/build.gradle | 34 + examples/E2ETestApp/android/gradle.properties | 54 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48966 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + examples/E2ETestApp/android/gradlew | 248 ++ examples/E2ETestApp/android/gradlew.bat | 98 + examples/E2ETestApp/android/settings.gradle | 6 + examples/E2ETestApp/app.json | 4 + examples/E2ETestApp/babel.config.js | 4 + .../{SampleApp => E2ETestApp}/e2e/README.md | 48 +- .../e2e/jest.config.js | 0 .../e2e/mock-server/DataTypes.ts | 0 .../e2e/mock-server/MockServer.ts | 0 .../e2e/pages/ChannelListPage.ts | 0 .../e2e/pages/LoginPage.ts | 2 +- .../e2e/pages/MessageListPage.ts | 0 .../e2e/robots/BackendRobot.ts | 0 .../e2e/robots/ParticipantRobot.ts | 0 .../e2e/robots/Robot.ts | 0 .../e2e/robots/UserRobot.ts | 0 .../e2e/robots/UserRobotChannelListAsserts.ts | 0 .../e2e/robots/UserRobotMessageListAsserts.ts | 0 .../e2e/robots/index.ts | 0 .../e2e/support/Element.ts | 22 +- .../e2e/support/StreamTestCase.ts | 9 +- .../e2e/support/step.ts | 0 .../e2e/tests/ChannelListTests.e2e.ts | 0 .../e2e/tests/MessageListTests.e2e.ts | 0 .../e2e/tsconfig.json | 0 examples/E2ETestApp/index.js | 7 + examples/E2ETestApp/ios/.xcode.env | 11 + .../ios/E2ETestApp.xcodeproj/project.pbxproj | 498 ++++ .../xcschemes/E2ETestApp.xcscheme | 88 + .../contents.xcworkspacedata | 10 + .../ios/E2ETestApp/AppDelegate.swift | 11 + .../AppIcon.appiconset/Contents.json | 53 + .../E2ETestApp/Images.xcassets/Contents.json | 6 + examples/E2ETestApp/ios/E2ETestApp/Info.plist | 78 + .../ios/E2ETestApp/LaunchScreen.storyboard | 47 + .../ios/E2ETestApp/PrivacyInfo.xcprivacy | 37 + .../ios/E2ETestApp/SceneDelegate.swift | 56 + examples/E2ETestApp/ios/Podfile | 45 + examples/E2ETestApp/ios/Podfile.lock | 2554 +++++++++++++++++ examples/E2ETestApp/metro.config.js | 81 + examples/E2ETestApp/package.json | 50 + examples/E2ETestApp/src/config.ts | 26 + examples/E2ETestApp/tsconfig.json | 7 + examples/SampleApp/.gitignore | 4 - examples/SampleApp/App.tsx | 3 +- examples/SampleApp/android/app/build.gradle | 3 - examples/SampleApp/android/build.gradle | 2 +- examples/SampleApp/fastlane/Fastfile | 23 +- examples/SampleApp/ios/Podfile.lock | 16 +- examples/SampleApp/package.json | 9 +- .../SampleApp/src/components/ScreenHeader.tsx | 1 - examples/SampleApp/src/hooks/useChatClient.ts | 19 +- .../src/screens/UserSelectorScreen.tsx | 3 +- examples/SampleApp/src/utils/e2e.ts | 42 - examples/SampleApp/tsconfig.json | 3 +- package.json | 3 +- yarn.lock | 40 +- 90 files changed, 4971 insertions(+), 155 deletions(-) rename examples/{SampleApp => E2ETestApp}/.detoxrc.js (85%) create mode 100644 examples/E2ETestApp/.gitignore create mode 100644 examples/E2ETestApp/.watchmanconfig create mode 100644 examples/E2ETestApp/App.tsx create mode 100644 examples/E2ETestApp/android/app/build.gradle create mode 100644 examples/E2ETestApp/android/app/debug.keystore create mode 100644 examples/E2ETestApp/android/app/proguard-rules.pro create mode 100644 examples/E2ETestApp/android/app/src/androidTest/java/io/getstream/reactnative/e2etestapp/DetoxTest.kt create mode 100644 examples/E2ETestApp/android/app/src/main/AndroidManifest.xml create mode 100644 examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainActivity.kt create mode 100644 examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainApplication.kt create mode 100644 examples/E2ETestApp/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 examples/E2ETestApp/android/app/src/main/res/values/strings.xml create mode 100644 examples/E2ETestApp/android/app/src/main/res/values/styles.xml create mode 100644 examples/E2ETestApp/android/app/src/main/res/xml/network_security_config.xml create mode 100644 examples/E2ETestApp/android/build.gradle create mode 100644 examples/E2ETestApp/android/gradle.properties create mode 100644 examples/E2ETestApp/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 examples/E2ETestApp/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 examples/E2ETestApp/android/gradlew create mode 100644 examples/E2ETestApp/android/gradlew.bat create mode 100644 examples/E2ETestApp/android/settings.gradle create mode 100644 examples/E2ETestApp/app.json create mode 100644 examples/E2ETestApp/babel.config.js rename examples/{SampleApp => E2ETestApp}/e2e/README.md (64%) rename examples/{SampleApp => E2ETestApp}/e2e/jest.config.js (100%) rename examples/{SampleApp => E2ETestApp}/e2e/mock-server/DataTypes.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/mock-server/MockServer.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/pages/ChannelListPage.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/pages/LoginPage.ts (68%) rename examples/{SampleApp => E2ETestApp}/e2e/pages/MessageListPage.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/robots/BackendRobot.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/robots/ParticipantRobot.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/robots/Robot.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/robots/UserRobot.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/robots/UserRobotChannelListAsserts.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/robots/UserRobotMessageListAsserts.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/robots/index.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/support/Element.ts (85%) rename examples/{SampleApp => E2ETestApp}/e2e/support/StreamTestCase.ts (73%) rename examples/{SampleApp => E2ETestApp}/e2e/support/step.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/tests/ChannelListTests.e2e.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/tests/MessageListTests.e2e.ts (100%) rename examples/{SampleApp => E2ETestApp}/e2e/tsconfig.json (100%) create mode 100644 examples/E2ETestApp/index.js create mode 100644 examples/E2ETestApp/ios/.xcode.env create mode 100644 examples/E2ETestApp/ios/E2ETestApp.xcodeproj/project.pbxproj create mode 100644 examples/E2ETestApp/ios/E2ETestApp.xcodeproj/xcshareddata/xcschemes/E2ETestApp.xcscheme create mode 100644 examples/E2ETestApp/ios/E2ETestApp.xcworkspace/contents.xcworkspacedata create mode 100644 examples/E2ETestApp/ios/E2ETestApp/AppDelegate.swift create mode 100644 examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/Contents.json create mode 100644 examples/E2ETestApp/ios/E2ETestApp/Info.plist create mode 100644 examples/E2ETestApp/ios/E2ETestApp/LaunchScreen.storyboard create mode 100644 examples/E2ETestApp/ios/E2ETestApp/PrivacyInfo.xcprivacy create mode 100644 examples/E2ETestApp/ios/E2ETestApp/SceneDelegate.swift create mode 100644 examples/E2ETestApp/ios/Podfile create mode 100644 examples/E2ETestApp/ios/Podfile.lock create mode 100644 examples/E2ETestApp/metro.config.js create mode 100644 examples/E2ETestApp/package.json create mode 100644 examples/E2ETestApp/src/config.ts create mode 100644 examples/E2ETestApp/tsconfig.json delete mode 100644 examples/SampleApp/src/utils/e2e.ts diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index b3bf4b6f29..d94dce58eb 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -3,6 +3,12 @@ name: E2E Tests on: pull_request: + # Runs on develop too: GitHub only shares caches from the base branch, so these runs fill the + # caches every PR restores from. + push: + branches: + - develop + workflow_dispatch: inputs: ios: @@ -34,6 +40,15 @@ env: GITHUB_EVENT_ACTION: ${{ github.event.action }} GITHUB_EVENT_BEFORE: ${{ github.event.before }} GITHUB_EVENT_AFTER: ${{ github.event.after }} + # node_modules of the e2e app and the workspaces it depends on (nmHoistingLimits: workspaces + # keeps them apart); `yarn workspaces focus` installs nothing else. + NODE_MODULES_PATHS: | + node_modules + configs/*/node_modules + package/node_modules + package/native-package/node_modules + examples/E2ETestApp/node_modules + .yarn/install-state.gz jobs: check: @@ -58,6 +73,9 @@ jobs: timeout-minutes: 60 env: DETOX_IOS_DEVICE: "iPhone 17 Pro" + XCODE_VERSION: "26.2" + APP_PATH: examples/E2ETestApp/ios/build/detox/Build/Products/Release-iphonesimulator/E2ETestApp.app + MOCK_SERVER_BUNDLE_PATH: ${{ github.workspace }}/../mock-server-gems steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 @@ -67,30 +85,99 @@ jobs: cache-dependency-path: "yarn.lock" - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "26.2" + xcode-version: ${{ env.XCODE_VERSION }} - uses: ./.github/actions/ruby-cache + + # --- JS dependencies --- + - name: Restore node_modules + id: node-modules + uses: actions/cache/restore@v5 + with: + path: ${{ env.NODE_MODULES_PATHS }} + key: ${{ runner.os }}-e2e-node-modules-${{ hashFiles('yarn.lock') }} + # Only the e2e app's dependency tree, not every example app. - name: Install dependencies - uses: ./.github/actions/install-and-build-sdk - - name: Install applesimutils - # Homebrew refuses formulae from non-official taps until they are trusted. - run: brew tap wix/brew && brew trust --formula wix/brew/applesimutils && brew install wix/brew/applesimutils - - name: Cache iOS pods + run: yarn workspaces focus e2etestapp + - name: Save node_modules + if: steps.node-modules.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ env.NODE_MODULES_PATHS }} + key: ${{ steps.node-modules.outputs.cache-primary-key }} + + # --- App binary: rebuilt only when the app or SDK sources change, not for test-only changes --- + - name: Restore app + id: app + uses: actions/cache/restore@v5 + with: + path: ${{ env.APP_PATH }} + key: e2e-ios-app-xcode${{ env.XCODE_VERSION }}-${{ hashFiles('yarn.lock', 'package/package.json', 'package/src/**', 'package/shared-native/**', 'package/native-package/package.json', 'package/native-package/src/**', 'package/native-package/ios/**', 'package/native-package/*.podspec', 'examples/SampleApp/Gemfile.lock', 'examples/E2ETestApp/*.js', 'examples/E2ETestApp/*.json', 'examples/E2ETestApp/*.tsx', 'examples/E2ETestApp/src/**', 'examples/E2ETestApp/ios/**', '!examples/E2ETestApp/ios/build/**', '!examples/E2ETestApp/ios/Pods/**') }} + - name: Set up ccache + if: steps.app.outputs.cache-hit != 'true' + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: e2e-ios-xcode${{ env.XCODE_VERSION }} + max-size: 1500M + - name: Point the RN ccache wrappers at ccache + if: steps.app.outputs.cache-hit != 'true' + # RN's ccache-clang.sh runs `$CCACHE_BINARY clang`, but Xcode doesn't export that build + # setting to the compiler, so without this every compile silently skips ccache. + run: echo "CCACHE_BINARY=$(which ccache)" >> "$GITHUB_ENV" + - name: Cache pods + if: steps.app.outputs.cache-hit != 'true' uses: actions/cache@v5 with: - path: examples/SampleApp/ios/Pods - key: ${{ runner.os }}-pods-${{ hashFiles('examples/SampleApp/ios/Podfile.lock') }} + path: examples/E2ETestApp/ios/Pods + key: ${{ runner.os }}-e2e-pods-${{ hashFiles('examples/E2ETestApp/ios/Podfile.lock') }} restore-keys: | - ${{ runner.os }}-pods- + ${{ runner.os }}-e2e-pods- - name: Install pods - working-directory: examples/SampleApp/ios - run: bundle exec pod install - # Yarn's `enableScripts: false` skips Detox's postinstall, which would build this. - - name: Build Detox framework cache + if: steps.app.outputs.cache-hit != 'true' working-directory: examples/SampleApp - run: npx detox build-framework-cache + run: USE_CCACHE=1 bundle exec fastlane e2e_pod_install - name: Build app - working-directory: examples/SampleApp - run: npx detox build -c ios.sim.release + if: steps.app.outputs.cache-hit != 'true' + working-directory: examples/E2ETestApp + run: yarn e2e:build -c ios.sim.release + - name: Save app + if: steps.app.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ env.APP_PATH }} + key: ${{ steps.app.outputs.cache-primary-key }} + + # --- Test tooling --- + - name: Install applesimutils + # Homebrew refuses formulae from non-official taps until they are trusted. + run: brew tap wix/brew && brew trust --formula wix/brew/applesimutils && brew install wix/brew/applesimutils + - name: Restore Detox framework + id: detox-framework + uses: actions/cache/restore@v5 + with: + path: ~/Library/Detox + key: ${{ runner.os }}-detox-framework-xcode${{ env.XCODE_VERSION }}-${{ hashFiles('yarn.lock') }} + # Yarn's `enableScripts: false` skips Detox's postinstall, which would build this. + - name: Build Detox framework + if: steps.detox-framework.outputs.cache-hit != 'true' + working-directory: examples/E2ETestApp + run: yarn detox build-framework-cache + - name: Save Detox framework + if: steps.detox-framework.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ~/Library/Detox + key: ${{ steps.detox-framework.outputs.cache-primary-key }} + - name: Resolve mock server revision + id: mock-server + run: echo "sha=$(git ls-remote https://github.com/GetStream/stream-chat-test-mock-server "refs/heads/$MOCK_SERVER_BRANCH" | cut -f1)" >> "$GITHUB_OUTPUT" + - name: Cache mock server gems + uses: actions/cache@v5 + with: + path: ${{ env.MOCK_SERVER_BUNDLE_PATH }} + key: ${{ runner.os }}-mock-server-gems-${{ steps.mock-server.outputs.sha }} + restore-keys: | + ${{ runner.os }}-mock-server-gems- + - name: Run e2e tests working-directory: examples/SampleApp run: bundle exec fastlane test_e2e platform:ios skip_build:true mock_server_branch:"$MOCK_SERVER_BRANCH" @@ -99,8 +186,8 @@ jobs: with: name: e2e-ios-logs path: | - examples/SampleApp/stream-chat-test-mock-server/logs - examples/SampleApp/artifacts + examples/E2ETestApp/stream-chat-test-mock-server/logs + examples/E2ETestApp/artifacts android: needs: check @@ -109,7 +196,14 @@ jobs: timeout-minutes: 60 env: ANDROID_API_LEVEL: "34" + ANDROID_EMULATOR_OPTIONS: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none DETOX_ADB_NAME: "emulator-5554" + APK_PATHS: | + examples/E2ETestApp/android/app/build/outputs/apk/release/app-release.apk + examples/E2ETestApp/android/app/build/outputs/apk/androidTest/release/app-release-androidTest.apk + MOCK_SERVER_BUNDLE_PATH: ${{ github.workspace }}/../mock-server-gems + # The emulator is x86_64: skip compiling native code for the other ABIs. + ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64 steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 @@ -121,19 +215,93 @@ jobs: with: distribution: "zulu" java-version: "21" - cache: "gradle" + # Caches Gradle's dependencies, wrapper and build cache; written on develop, read on PRs. + - uses: gradle/actions/setup-gradle@v4 - uses: ./.github/actions/ruby-cache + + # --- JS dependencies --- + - name: Restore node_modules + id: node-modules + uses: actions/cache/restore@v5 + with: + path: ${{ env.NODE_MODULES_PATHS }} + key: ${{ runner.os }}-e2e-node-modules-${{ hashFiles('yarn.lock') }} + # Only the e2e app's dependency tree, not every example app. - name: Install dependencies - uses: ./.github/actions/install-and-build-sdk + run: yarn workspaces focus e2etestapp + - name: Save node_modules + if: steps.node-modules.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ env.NODE_MODULES_PATHS }} + key: ${{ steps.node-modules.outputs.cache-primary-key }} + + # --- App binaries: rebuilt only when the app or SDK sources change, not for test-only changes --- + - name: Restore APKs + id: apk + uses: actions/cache/restore@v5 + with: + path: ${{ env.APK_PATHS }} + key: e2e-android-apk-${{ hashFiles('yarn.lock', 'package/package.json', 'package/src/**', 'package/shared-native/**', 'package/native-package/package.json', 'package/native-package/src/**', 'package/native-package/android/**', '!package/native-package/android/build/**', 'examples/E2ETestApp/*.js', 'examples/E2ETestApp/*.json', 'examples/E2ETestApp/*.tsx', 'examples/E2ETestApp/src/**', 'examples/E2ETestApp/android/**', '!examples/E2ETestApp/android/**/build/**', '!examples/E2ETestApp/android/.gradle/**') }} + # Built before the emulator boots so the emulator doesn't idle through the Gradle build. + - name: Build app + if: steps.apk.outputs.cache-hit != 'true' + working-directory: examples/E2ETestApp + run: yarn e2e:build -c android.att.release + - name: Save APKs + if: steps.apk.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ${{ env.APK_PATHS }} + key: ${{ steps.apk.outputs.cache-primary-key }} + + # --- Emulator: boots from a cached snapshot --- - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - # Built before the emulator boots so the emulator doesn't idle through the Gradle build. - - name: Build app - working-directory: examples/SampleApp - run: npx detox build -c android.att.release + - name: Restore AVD + id: avd + uses: actions/cache/restore@v5 + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ env.ANDROID_API_LEVEL }}-x86_64-google_apis-pixel_6 + - name: Create AVD snapshot + if: steps.avd.outputs.cache-hit != 'true' + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ env.ANDROID_API_LEVEL }} + arch: x86_64 + target: google_apis + profile: pixel_6 + force-avd-creation: false + emulator-options: ${{ env.ANDROID_EMULATOR_OPTIONS }} + disable-animations: false + script: echo "Generated AVD snapshot for caching." + - name: Save AVD + if: steps.avd.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: ${{ steps.avd.outputs.cache-primary-key }} + + # --- Test tooling --- + - name: Resolve mock server revision + id: mock-server + run: echo "sha=$(git ls-remote https://github.com/GetStream/stream-chat-test-mock-server "refs/heads/$MOCK_SERVER_BRANCH" | cut -f1)" >> "$GITHUB_OUTPUT" + - name: Cache mock server gems + uses: actions/cache@v5 + with: + path: ${{ env.MOCK_SERVER_BUNDLE_PATH }} + key: ${{ runner.os }}-mock-server-gems-${{ steps.mock-server.outputs.sha }} + restore-keys: | + ${{ runner.os }}-mock-server-gems- + - name: Run e2e tests (Android emulator) uses: reactivecircus/android-emulator-runner@v2 with: @@ -141,6 +309,8 @@ jobs: arch: x86_64 target: google_apis profile: pixel_6 + force-avd-creation: false + emulator-options: -no-snapshot-save ${{ env.ANDROID_EMULATOR_OPTIONS }} disable-animations: true working-directory: examples/SampleApp script: bundle exec fastlane test_e2e platform:android device:att skip_build:true mock_server_branch:"$MOCK_SERVER_BRANCH" @@ -149,8 +319,8 @@ jobs: with: name: e2e-android-logs path: | - examples/SampleApp/stream-chat-test-mock-server/logs - examples/SampleApp/artifacts + examples/E2ETestApp/stream-chat-test-mock-server/logs + examples/E2ETestApp/artifacts result: name: Test E2E Result diff --git a/.prettierignore b/.prettierignore index eed3166d14..55d7ebb190 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,8 +25,8 @@ examples/SampleApp/patches/ yarn.lock # Detox e2e: mock server checkout cloned by `fastlane start_mock_server`, test artifacts -examples/SampleApp/stream-chat-test-mock-server/ -examples/SampleApp/artifacts/ +examples/E2ETestApp/stream-chat-test-mock-server/ +examples/E2ETestApp/artifacts/ # Generated SDK assets package/src/components/docs/ diff --git a/AGENTS.md b/AGENTS.md index 2b9e6679c3..f2d2a43f30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Targets iOS and Android. - **Languages:** TypeScript + React Native - **Runtime:** Node 24 (`.nvmrc` is `v24`; root `engines.node` is `>=20.19.4`; CI runs `24.x`) - **Package manager:** Yarn 4.15.0 (Berry). The binary lives at `.yarn/releases/yarn-4.15.0.cjs` and is activated via `yarnPath` in `.yarnrc.yml`. Any globally installed `yarn` (even the Homebrew classic 1.x) acts only as a launcher — no Corepack required. -- **Workspaces:** single root `yarn.lock`; workspaces are `configs/typescript-config`, `package`, `package/native-package`, `package/expo-package`, `examples/SampleApp`, `examples/ExpoMessaging`. **No Lerna.** +- **Workspaces:** single root `yarn.lock`; workspaces are `configs/typescript-config`, `package`, `package/native-package`, `package/expo-package`, `examples/SampleApp`, `examples/E2ETestApp`, `examples/ExpoMessaging`. **No Lerna.** - **Testing:** Jest with the `@react-native/jest-preset` + `@testing-library/react-native`. - **Build:** `react-native-builder-bob` → CommonJS (`lib/commonjs`), ESM (`lib/module`), types (`lib/typescript`) - **Lint/format:** ESLint 9 flat config + Prettier, strict (`--max-warnings 0`) @@ -41,7 +41,7 @@ Respect repo-specific rules. Do not suppress lint rules broadly; justify and sco - `native-package/` — bare RN wrapper (`stream-chat-react-native`) - `expo-package/` — Expo wrapper (`stream-chat-expo`) - `shared-native/{ios,android}` — native source shared by both wrappers; synced into them, never edited in place -- `examples/` — `SampleApp` (full-featured), `ExpoMessaging` +- `examples/` — `SampleApp` (full-featured), `E2ETestApp` (minimal app for the Detox e2e suite), `ExpoMessaging` - `configs/typescript-config/` — shared `tsconfig` presets - `ai-docs/` — agent-facing deep dives (see [References](#references)) - `perf/` — on-device performance toolkit (see [Accessibility, RTL & performance](#accessibility-rtl--performance)) @@ -244,6 +244,8 @@ Native pieces live in `package/shared-native/`: the Android-only `PerfText` Fabr To run one test file, prefer `cd package && TZ=UTC npx jest path/to/file.test.tsx`. The `testRegex` array in `jest.config.js` also accepts a temporary path — revert it before committing. +**E2E:** Detox suite in `examples/E2ETestApp/e2e/` against the shared `stream-chat-test-mock-server`, with robots and page objects mirroring the native iOS/Android suites. Run it with `bundle exec fastlane test_e2e platform:ios|android` from `examples/SampleApp`; see `examples/E2ETestApp/e2e/README.md`. Elements it needs get a `testID` in the SDK (kebab-case, additive). + **Mock builders** (`package/src/mock-builders/`): - `api/initiateClientWithChannels` — creates a test client + channels in one call (fastest path) @@ -369,7 +371,7 @@ Follow `PULL_REQUEST_TEMPLATE.md`. Keep PRs small and focused. ### CI expectations -`.github/workflows/check-pr.yml` (Node 24): `yarn install --immutable` → `yarn build` → `yarn lint` → `yarn typecheck` → `yarn test:coverage`. Other workflows: `changelog-preview`, `lint-pr-title`, `release`, `sample-distribution`, `sdk-size-metrics`. +`.github/workflows/check-pr.yml` (Node 24): `yarn install --immutable` → `yarn build` → `yarn lint` → `yarn typecheck` → `yarn test:coverage`. Other workflows: `changelog-preview`, `e2e-test` (Detox on `examples/E2ETestApp`, gated by `is_check_required`; require its `Test E2E Result` check), `lint-pr-title`, `release`, `sample-distribution`, `sdk-size-metrics`. Failing or flaky tests: fix them, or quarantine with a justification comment and a follow-up. diff --git a/eslint.config.mjs b/eslint.config.mjs index 0db7e11ec4..468d782eab 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -45,8 +45,8 @@ export default tsEslint.config( '**/fastlane/', // Detox e2e: mock server checkout cloned by `fastlane start_mock_server`, test artifacts - 'examples/SampleApp/stream-chat-test-mock-server/', - 'examples/SampleApp/artifacts/', + 'examples/E2ETestApp/stream-chat-test-mock-server/', + 'examples/E2ETestApp/artifacts/', // Generated SDK assets 'package/src/components/docs/', diff --git a/examples/SampleApp/.detoxrc.js b/examples/E2ETestApp/.detoxrc.js similarity index 85% rename from examples/SampleApp/.detoxrc.js rename to examples/E2ETestApp/.detoxrc.js index 605ec910a0..7bc7b438c3 100644 --- a/examples/SampleApp/.detoxrc.js +++ b/examples/E2ETestApp/.detoxrc.js @@ -12,15 +12,15 @@ module.exports = { apps: { 'ios.debug': { type: 'ios.app', - binaryPath: 'ios/build/detox/Build/Products/Debug-iphonesimulator/SampleApp.app', + binaryPath: 'ios/build/detox/Build/Products/Debug-iphonesimulator/E2ETestApp.app', build: - 'xcodebuild -workspace ios/SampleApp.xcworkspace -scheme SampleApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build/detox', + 'xcodebuild -workspace ios/E2ETestApp.xcworkspace -scheme E2ETestApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build/detox', }, 'ios.release': { type: 'ios.app', - binaryPath: 'ios/build/detox/Build/Products/Release-iphonesimulator/SampleApp.app', + binaryPath: 'ios/build/detox/Build/Products/Release-iphonesimulator/E2ETestApp.app', build: - 'xcodebuild -workspace ios/SampleApp.xcworkspace -scheme SampleApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build/detox', + 'xcodebuild -workspace ios/E2ETestApp.xcworkspace -scheme E2ETestApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build/detox', }, 'android.debug': { type: 'android.apk', diff --git a/examples/E2ETestApp/.gitignore b/examples/E2ETestApp/.gitignore new file mode 100644 index 0000000000..9be6361de6 --- /dev/null +++ b/examples/E2ETestApp/.gitignore @@ -0,0 +1,79 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +**/.xcode.env.local + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore +.kotlin/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +# Bundle artifact +*.jsbundle + +# Ruby / CocoaPods +**/Pods/ +/vendor/bundle/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# testing +/coverage + +# Yarn +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Detox e2e: mock server checkout cloned by `fastlane start_mock_server`, test artifacts +/stream-chat-test-mock-server/ +/artifacts/ diff --git a/examples/E2ETestApp/.watchmanconfig b/examples/E2ETestApp/.watchmanconfig new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/examples/E2ETestApp/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/examples/E2ETestApp/App.tsx b/examples/E2ETestApp/App.tsx new file mode 100644 index 0000000000..653961dc5f --- /dev/null +++ b/examples/E2ETestApp/App.tsx @@ -0,0 +1,133 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { BackHandler, Pressable, StyleSheet, Text, View } from 'react-native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { SafeAreaProvider, SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { Channel as ChannelType, ChannelFilters, ChannelSort, StreamChat } from 'stream-chat'; +import { + Channel, + ChannelList, + Chat, + MessageComposer, + MessageList, + OverlayProvider, +} from 'stream-chat-react-native'; + +import { apiKey, appUser, appUserToken, mockServerUrl } from './src/config'; + +// Same opt-in as the SampleApp, so the e2e suite sees the same accessibility tree. +const accessibility = { enabled: true }; +const sort: ChannelSort = { last_message_at: -1 }; + +/** Lists the mock server's app user, like the SampleApp's user selector. */ +const LoginScreen = ({ onLogin }: { onLogin: () => void }) => ( + + E2E Test App + {mockServerUrl ? ( + + {appUser.name} + + ) : ( + Launch with the MOCK_SERVER_URL argument (see e2e/README.md). + )} + +); + +const ChannelScreen = ({ channel, onBack }: { channel: ChannelType; onBack: () => void }) => { + const { top } = useSafeAreaInsets(); + + useEffect(() => { + const subscription = BackHandler.addEventListener('hardwareBackPress', () => { + onBack(); + return true; + }); + return () => subscription.remove(); + }, [onBack]); + + // Laid out like the SampleApp's channel screen: the header lives inside , which sizes + // itself to the screen and handles the bottom inset and keyboard for the composer. + return ( + + + + + ‹ Back + + + {channel.id} + + + + + + + ); +}; + +const ChatScreens = ({ client }: { client: StreamChat }) => { + const [channel, setChannel] = useState(); + const filters = useMemo( + () => ({ members: { $in: [client.userID ?? appUser.id] } }), + [client.userID], + ); + const closeChannel = useCallback(() => setChannel(undefined), []); + + return ( + + {/* The list stays mounted under the channel, as in a navigation stack. */} + + + + {channel ? : null} + + ); +}; + +const App = () => { + const [client, setClient] = useState(); + + const login = useCallback(async () => { + const chatClient = StreamChat.getInstance(apiKey, { baseURL: mockServerUrl, timeout: 6000 }); + await chatClient.connectUser(appUser, appUserToken); + setClient(chatClient); + }, []); + + return ( + + + + {client ? : } + + + + ); +}; + +const styles = StyleSheet.create({ + back: { fontSize: 17, paddingRight: 12 }, + container: { backgroundColor: '#FFFFFF', flex: 1 }, + flex: { flex: 1 }, + header: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 12, + }, + headerTitle: { flex: 1, fontSize: 17, fontWeight: '600' }, + hint: { padding: 16 }, + title: { fontSize: 22, fontWeight: '700', padding: 16 }, + userName: { fontSize: 17 }, + userRow: { borderBottomColor: '#E5E5E5', borderBottomWidth: 1, padding: 16 }, +}); + +export default App; diff --git a/examples/E2ETestApp/android/app/build.gradle b/examples/E2ETestApp/android/app/build.gradle new file mode 100644 index 0000000000..879f64489c --- /dev/null +++ b/examples/E2ETestApp/android/app/build.gradle @@ -0,0 +1,124 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js + // cliFile = file("../../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. Default is "debug", "debugOptimized". + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "liteDebugOptimized", "prodDebug", "prodDebugOptimized"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + // + // The command to run when bundling. By default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' + +android { + ndkVersion rootProject.ext.ndkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace "io.getstream.reactnative.e2etestapp" + defaultConfig { + applicationId "io.getstream.reactnative.e2etestapp" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + // Detox: builds the instrumentation APK against the variant under test + testBuildType System.getProperty('testBuildType', 'debug') + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } + + androidTestImplementation('com.wix:detox:+') +} diff --git a/examples/E2ETestApp/android/app/debug.keystore b/examples/E2ETestApp/android/app/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..364e105ed39fbfd62001429a68140672b06ec0de GIT binary patch literal 2257 zcmchYXEfYt8;7T1^dLH$VOTZ%2NOdOH5j5LYLtZ0q7x-V8_6gU5)#7dkq{HTmsfNq zB3ZqcAxeY^G10@?efK?Q&)M(qInVv!xjx+IKEL}p*K@LYvIzo#AZG>st5|P)KF1_Z;y){W{<7K{nl!CPuE z_^(!C(Ol0n8 zK13*rzAtW>(wULKPRYLd7G18F8#1P`V*9`(Poj26eOXYyBVZPno~Cvvhx7vPjAuZo zF?VD!zB~QG(!zbw#qsxT8%BSpqMZ4f70ZPn-3y$L8{EVbbN9$H`B&Z1quk9tgp5FM zuxp3pJ0b8u|3+#5bkJ4SRnCF2l7#DyLYXYY8*?OuAwK4E6J{0N=O3QNVzQ$L#FKkR zi-c@&!nDvezOV$i$Lr}iF$XEcwnybQ6WZrMKuw8gCL^U#D;q3t&HpTbqyD%vG=TeDlzCT~MXUPC|Leb-Uk+ z=vnMd(|>ld?Fh>V8poP;q;;nc@en$|rnP0ytzD&fFkCeUE^kG9Kx4wUh!!rpjwKDP zyw_e|a^x_w3E zP}}@$g>*LLJ4i0`Gx)qltL}@;mDv}D*xR^oeWcWdPkW@Uu)B^X&4W1$p6}ze!zudJ zyiLg@uggoMIArBr*27EZV7djDg@W1MaL+rcZ-lrANJQ%%>u8)ZMWU@R2qtnmG(acP z0d_^!t>}5W zpT`*2NR+0+SpTHb+6Js4b;%LJB;B_-ChhnU5py}iJtku*hm5F0!iql8Hrpcy1aYbT z1*dKC5ua6pMX@@iONI?Hpr%h;&YaXp9n!ND7-=a%BD7v&g zOO41M6EbE24mJ#S$Ui0-brR5ML%@|ndz^)YLMMV1atna{Fw<;TF@>d&F|!Z>8eg>>hkFrV)W+uv=`^F9^e zzzM2*oOjT9%gLoub%(R57p-`TXFe#oh1_{&N-YN z<}artH|m=d8TQuKSWE)Z%puU|g|^^NFwC#N=@dPhasyYjoy(fdEVfKR@cXKHZV-`06HsP`|Ftx;8(YD$fFXumLWbGnu$GMqRncXYY9mwz9$ap zQtfZB^_BeNYITh^hA7+(XNFox5WMeG_LtJ%*Q}$8VKDI_p8^pqX)}NMb`0e|wgF7D zuQACY_Ua<1ri{;Jwt@_1sW9zzdgnyh_O#8y+C;LcZq6=4e^cs6KvmK@$vVpKFGbQ= z$)Eux5C|Fx;Gtmv9^#Y-g@7Rt7*eLp5n!gJmn7&B_L$G?NCN`AP>cXQEz}%F%K;vUs{+l4Q{}eWW;ATe2 zqvXzxoIDy(u;F2q1JH7Sf;{jy_j})F+cKlIOmNfjBGHoG^CN zM|Ho&&X|L-36f}Q-obEACz`sI%2f&k>z5c$2TyTSj~vmO)BW~+N^kt`Jt@R|s!){H ze1_eCrlNaPkJQhL$WG&iRvF*YG=gXd1IyYQ9ew|iYn7r~g!wOnw;@n42>enAxBv*A zEmV*N#sxdicyNM=A4|yaOC5MByts}s_Hpfj|y<6G=o=!3S@eIFKDdpR7|FY>L&Wat&oW&cm&X~ z5Bt>Fcq(fgnvlvLSYg&o6>&fY`ODg4`V^lWWD=%oJ#Kbad2u~! zLECFS*??>|vDsNR&pH=Ze0Eo`sC_G`OjoEKVHY|wmwlX&(XBE<@sx3Hd^gtd-fNwUHsylg06p`U2y_={u}Bc + + + + + + + + + + + + diff --git a/examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainActivity.kt b/examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainActivity.kt new file mode 100644 index 0000000000..b59b475ece --- /dev/null +++ b/examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainActivity.kt @@ -0,0 +1,22 @@ +package io.getstream.reactnative.e2etestapp + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +class MainActivity : ReactActivity() { + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "E2ETestApp" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate = + DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) +} diff --git a/examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainApplication.kt b/examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainApplication.kt new file mode 100644 index 0000000000..dd573a42aa --- /dev/null +++ b/examples/E2ETestApp/android/app/src/main/java/io/getstream/reactnative/e2etestapp/MainApplication.kt @@ -0,0 +1,27 @@ +package io.getstream.reactnative.e2etestapp + +import android.app.Application +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost + +class MainApplication : Application(), ReactApplication { + + override val reactHost: ReactHost by lazy { + getDefaultReactHost( + context = applicationContext, + packageList = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + }, + ) + } + + override fun onCreate() { + super.onCreate() + loadReactNative(this) + } +} diff --git a/examples/E2ETestApp/android/app/src/main/res/drawable/rn_edit_text_material.xml b/examples/E2ETestApp/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000000..5c25e728ea --- /dev/null +++ b/examples/E2ETestApp/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/examples/E2ETestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/E2ETestApp/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj zwslyNbxW4-gAj;v!J{u#G1>?8h`uw{1?o<0nB+tYjKOW@kQM}bUbgE7^CRD4K zgurXDRXWsX-Q$uVZ0o5KpKdOl5?!YGV|1Cict&~YiG*r%TU43m2Hf99&})mPEvepe z0_$L1e8*kL@h2~YPCajw6Kkw%Bh1Pp)6B|t06|1rR3xRYjBxjSEUmZk@7wX+2&-~! z!V&EdUw!o7hqZI=T4a)^N1D|a=2scW6oZU|Q=}_)gz4pu#43{muRW1cW2WC&m-ik? zskL0dHaVZ5X4PN*v4ZEAB9m;^6r-#eJH?TnU#SN&MO`Aj%)ybFYE+Pf8Vg^T3ybTl zu50EU=3Q60vA7xg@YQ$UKD-7(jf%}8gWS$_9%)wD1O2xB!_VxzcJdN!_qQ9j8#o^Kb$2+XTKxM8p>Ve{O8LcI(e2O zeg{tPSvIFaM+_Ivk&^FEk!WiV^;s?v8fmLglKG<7EO3ezShZ_0J-`(fM;C#i5~B@w zzx;4Hu{-SKq1{ftxbjc(dX3rj46zWzu02-kR>tAoFYDaylWMJ`>FO2QR%cfi+*^9A z54;@nFhVJEQ{88Q7n&mUvLn33icX`a355bQ=TDRS4Uud|cnpZ?a5X|cXgeBhYN7btgj zfrwP+iKdz4?L7PUDFA_HqCI~GMy`trF@g!KZ#+y6U%p5#-nm5{bUh>vhr^77p~ zq~UTK6@uhDVAQcL4g#8p-`vS4CnD9M_USvfi(M-;7nXjlk)~pr>zOI`{;$VXt;?VTNcCePv4 zgZm`^)VCx8{D=H2c!%Y*Sj3qbx z3Bcvv7qRAl|BGZCts{+>FZrE;#w(Yo2zD#>s3a*Bm!6{}vF_;i)6sl_+)pUj?b%BL!T1ELx|Q*Gi=7{Z_>n0I(uv>N^kh|~nJfab z-B6Q6i-x>YYa_42Hv&m>NNuPj31wOaHZ2`_8f~BtbXc@`9CZpHzaE@9sme%_D-HH! z_+C&VZ5tjE65?}X&u-D4AHRJ|7M{hR!}PYPpANP?7wnur`Z(&LFwzUmDz}m6%m#_` zN1ihq8f|zZ&zTL92M2b-hMpPyjp;j(qwgP9x)qI?EZx@<$g#>i7(MC}@*J1VGXm6J ztz1=RK@?%Qz^vmWNydd0K7oyrXw`TLb`z;fP6eV|NZ@9kKH zIyMqzZ9Y_)PZnC#UgW6&o7RiGXSCtSQvnrvJ07P9WCuE5TE27za*L6r1qX7pIDFiP znSaHYJF8sl^n0|3j!i{?fD%?fpQ8-}VX4%STy1t@8)G-8??Fy}j}~2_iJ79Y<9BW~ z!~)T{3Y|lwcVD5s4z^GP5M=~t`V?*Wng7gTvC9%p>ErZpM)pQVx57>AIcf1j4QFg^w>YYB%MypIj2syoXw9$K!N8%s=iPIw!LE-+6v6*Rm zvCqdN&kwI+@pEX0FTb&P)ujD9Td-sLBVV=A$;?RiFOROnT^LC^+PZR*u<3yl z7b%>viF-e48L=c`4Yhgb^U=+w7snP$R-gzx379%&q-0#fsMgvQlo>14~`1YOv{?^ z*^VYyiSJO8fE65P0FORgqSz#mi#9@40VO@TaPOT7pJq3WTK9*n;Niogu+4zte1FUa zyN7rIFbaQxeK{^RC3Iu@_J~ii&CvyWn^W}4wpexHwV9>GKO$zR3a&*L9&AgL=QfA$ z+G-YMq;1D{;N38`jTdN}Pw77sDCR|$2s+->;9gh-ObE_muwxq>sEpX)ywtgCHKIATY}p&%F4bRV>R9rYpeWbT(xnE7}?(HDXFgNDdC^@gUdK& zk=MolYT3>rpR*$Ell2!`c zjrIZftl&PUxlH2EgV+3VfQy&FjhL&5*Zg&R8xrSx?WgB?YuLO-JDaP3jr*I~qiywy z`-52AwB_6L#X ztms{{yRkRfQLbsb#Ov%`)acN(OCewI3Ex__xed17hg#g4c1blx?sK}UQg%PM@N;5d zsg{y6(|`H1Xfbz@5x{1688tu7TGkzFEBhOPDdFK(H_NQIFf|(>)ltFd!WdnkrY&mp z0y@5yU2;u1_enx%+U9tyY-LNWrd4^Wi?x<^r`QbaLBngWL`HzX@G550 zrdyNjhPTknrrJn#jT0WD0Z)WJRi&3FKJ#Sa&|883%QxM-?S%4niK{~k81<(c11sLk|!_7%s zH>c$`*nP-wA8Dx-K(HE~JG_@Yxxa;J+2yr+*iVlh;2Eiw?e`D1vu6*qY1+XTe8RVu z?RV%L|Mk!wO}j^S)p4H%?G37StD0Rx{_Y00%3a+V^SyOkfV@ZuFlEc;vR9r-D>cYU&plUkXL|M%1AYBQ3DI;;hF%_X@m*cTQAMZ4+FO74@AQB{A*_HtoXT@}l=8awaa7{RHC>07s?E%G{iSeRbh z?h#NM)bP`z`zdp5lij!N*df;4+sgz&U_JEr?N9#1{+UG3^11oQUOvU4W%tD1Cie3; z4zcz0SIrK-PG0(mp9gTYr(4ngx;ieH{NLq{* z;Pd=vS6KZYPV?DLbo^)~2dTpiKVBOh?|v2XNA)li)4V6B6PA!iq#XV5eO{{vL%OmU z0z3ZE2kcEkZ`kK(g^#s)#&#Zn5zw!R93cW^4+g0D=ydf&j4o_ti<@2WbzC>{(QhCL z(=%Zb;Ax8U=sdec9pkk|cW)1Ko;gK{-575HsDZ!w@WOQ^Up)GGorc38cGxe<$8O!6 zmQ`=@;TG{FjWq(s0eBn5I~vVgoE}un8+#YuR$Asq?lobvVAO-`SBs3!&;QEKT>gZ0T)jG^Foo~J2YkV&mi-axlvC}-(J4S2 z;opuO)+FIV#}&4;wwisb>{XU+FJ~tyK7UaG@ZD^C1^brazu7Xkh5Od}&P)GufW=u# zMxOwfWJ3a^MZha>9OmQ)@!Y;v*4@+dg~s~NQ;q@hV~l>lw`P)d`4XF9rE?aEFe(JV zI>11}Ny%^CkO=VN>wCV?P!-?VdT3vWe4zBLV*?6XPqsC%n93bQXvydh0Mo+tXHO4^ zxQ{x0?CG{fmToCyYny7>*-tNh;Sh9=THLzkS~lBiV9)IKa^C~_p8MVZWAUb)Btjt< zVZ;l7?_KnLHelj>)M1|Q_%pk5b?Bod_&86o-#36xIEag%b+8JqlDy@B^*YS*1; zGYT`@5nPgt)S^6Ap@b160C4d9do0iE;wYdn_Tr(vY{MS!ja!t*Z7G=Vz-=j5Z⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/examples/E2ETestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/E2ETestApp/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/examples/E2ETestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/E2ETestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 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 B0*L?s literal 0 HcmV?d00001 diff --git a/examples/E2ETestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/E2ETestApp/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/examples/E2ETestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/E2ETestApp/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 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 BIP(Ai literal 0 HcmV?d00001 diff --git a/examples/E2ETestApp/android/app/src/main/res/values/strings.xml b/examples/E2ETestApp/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..a0ea08d876 --- /dev/null +++ b/examples/E2ETestApp/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + E2ETestApp + diff --git a/examples/E2ETestApp/android/app/src/main/res/values/styles.xml b/examples/E2ETestApp/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..7ba83a2ad5 --- /dev/null +++ b/examples/E2ETestApp/android/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/examples/E2ETestApp/android/app/src/main/res/xml/network_security_config.xml b/examples/E2ETestApp/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..a667072fee --- /dev/null +++ b/examples/E2ETestApp/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,8 @@ + + + + + 10.0.2.2 + localhost + + diff --git a/examples/E2ETestApp/android/build.gradle b/examples/E2ETestApp/android/build.gradle new file mode 100644 index 0000000000..49baeef939 --- /dev/null +++ b/examples/E2ETestApp/android/build.gradle @@ -0,0 +1,34 @@ +buildscript { + ext { + buildToolsVersion = "37.0.0" + minSdkVersion = 24 + compileSdkVersion = 37 + targetSdkVersion = 36 + ndkVersion = "27.1.12297006" + kotlinVersion = "2.2.0" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + } +} + +ext.REACT_NATIVE_NODE_MODULES_DIR = file("$rootDir/../node_modules/react-native").absolutePath +ext.REACT_NATIVE_WORKLETS_NODE_MODULES_DIR = + file("$rootDir/../node_modules/react-native-worklets").absolutePath + +allprojects { + repositories { + // Detox's Android library ships inside its npm package + maven { + url "$rootDir/../node_modules/detox/Detox-android" + } + } +} + +apply plugin: "com.facebook.react.rootproject" diff --git a/examples/E2ETestApp/android/gradle.properties b/examples/E2ETestApp/android/gradle.properties new file mode 100644 index 0000000000..ab47cc1f7b --- /dev/null +++ b/examples/E2ETestApp/android/gradle.properties @@ -0,0 +1,54 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +# Emulator ABIs only (arm64 for Apple Silicon, x86_64 for CI); CI narrows it to x86_64 with +# ORG_GRADLE_PROJECT_reactNativeArchitectures. +reactNativeArchitectures=arm64-v8a,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + +# Use this property to enable edge-to-edge display support. +# This allows your app to draw behind system bars for an immersive UI. +# Note: Only works with ReactActivity and should not be used with custom Activity. +edgeToEdgeEnabled=true + +# Opt out of built-in kotlin and new DSL behavior that ships with AGP 9. +# Starting from AGP 10.x these opt outs will be removed. +android.builtInKotlin=false +android.newDsl=false +# Reuse task outputs across builds (and CI runs, via the Gradle cache) +org.gradle.caching=true +org.gradle.parallel=true diff --git a/examples/E2ETestApp/android/gradle/wrapper/gradle-wrapper.jar b/examples/E2ETestApp/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d997cfc60f4cff0e7451d19d49a82fa986695d07 GIT binary patch literal 48966 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

`Lem|V$A>eR0uN8Y&sdjtu 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

1bvi{DS7E@q1{$_8->K{_OWv2 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 literal 0 HcmV?d00001 diff --git a/examples/E2ETestApp/android/gradle/wrapper/gradle-wrapper.properties b/examples/E2ETestApp/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..c61a118f7d --- /dev/null +++ b/examples/E2ETestApp/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/E2ETestApp/android/gradlew b/examples/E2ETestApp/android/gradlew new file mode 100755 index 0000000000..739907dfd1 --- /dev/null +++ b/examples/E2ETestApp/android/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/E2ETestApp/android/gradlew.bat b/examples/E2ETestApp/android/gradlew.bat new file mode 100644 index 0000000000..39baf4d684 --- /dev/null +++ b/examples/E2ETestApp/android/gradlew.bat @@ -0,0 +1,98 @@ +@REM Copyright (c) Meta Platforms, Inc. and affiliates. +@REM +@REM This source code is licensed under the MIT license found in the +@REM LICENSE file in the root directory of this source tree. + +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/examples/E2ETestApp/android/settings.gradle b/examples/E2ETestApp/android/settings.gradle new file mode 100644 index 0000000000..96b171f560 --- /dev/null +++ b/examples/E2ETestApp/android/settings.gradle @@ -0,0 +1,6 @@ +pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } +rootProject.name = 'io.getstream.reactnative.e2etestapp' +include ':app' +includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/examples/E2ETestApp/app.json b/examples/E2ETestApp/app.json new file mode 100644 index 0000000000..73849fcf35 --- /dev/null +++ b/examples/E2ETestApp/app.json @@ -0,0 +1,4 @@ +{ + "name": "E2ETestApp", + "displayName": "E2ETestApp" +} diff --git a/examples/E2ETestApp/babel.config.js b/examples/E2ETestApp/babel.config.js new file mode 100644 index 0000000000..8ba8eb658c --- /dev/null +++ b/examples/E2ETestApp/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['module:@react-native/babel-preset'], + plugins: ['react-native-worklets/plugin'], +}; diff --git a/examples/SampleApp/e2e/README.md b/examples/E2ETestApp/e2e/README.md similarity index 64% rename from examples/SampleApp/e2e/README.md rename to examples/E2ETestApp/e2e/README.md index acd8c39016..1601d27f08 100644 --- a/examples/SampleApp/e2e/README.md +++ b/examples/E2ETestApp/e2e/README.md @@ -1,6 +1,6 @@ -# SampleApp E2E tests (Detox) +# E2E tests (Detox) -End-to-end tests for `examples/SampleApp`, run with [Detox](https://wix.github.io/Detox/) against the shared +End-to-end tests for the SDK, driving `examples/E2ETestApp` with [Detox](https://wix.github.io/Detox/) against the shared [`stream-chat-test-mock-server`](https://github.com/GetStream/stream-chat-test-mock-server). The layout mirrors the native iOS/Android (and Flutter) suites, so tests can be ported almost line by line: @@ -22,30 +22,42 @@ await step('WHEN participant sends a message', () => participantRobot.sendMessag await step('THEN user receives it', () => userRobot.assertMessage('Hi').assertMessageAuthor(false)); ``` -## How the app is pointed at the mock server +## The app + +`examples/E2ETestApp` exists only for this suite, like the native SDKs' `StreamChatUITestsApp`. It has the SDK and its +required peer dependencies, and nothing else: no Firebase, maps, video or navigation library. That keeps native builds +short (a clean Release build is ~2 min on an M-series Mac, versus ~15 min for the SampleApp on Android). It has three +screens: a user selector, the channel list, and a channel stacked on top of the list (`App.tsx`), laid out like the +SampleApp's, so the same SDK paths run. `setUpStreamTestCase()` starts a mock server per test and relaunches the app with the launch argument -`MOCK_SERVER_URL` (`localhost` on iOS, `10.0.2.2` on the Android emulator), read in `src/utils/e2e.ts` via -`react-native-launch-arguments`. In that mode the app lists only the mock user (`luke_skywalker`), never restores a -stored login, skips push registration and permission prompts, and runs without offline storage, so every test starts -clean. +`MOCK_SERVER_URL` (`localhost` on iOS, `10.0.2.2` on the Android emulator), read in `src/config.ts` via +`react-native-launch-arguments`. The app only lists the mock user (`luke_skywalker`), never persists a login and runs +without offline storage, so every test starts clean. ## Running +The fastlane lanes live with the repo's other lanes in `examples/SampleApp/fastlane` (run them from +`examples/SampleApp`). The e2e app has no Gemfile of its own; `e2e_pod_install` installs its pods with that bundle. + ```bash +bundle exec fastlane e2e_pod_install # once, and after native dependency changes # Everything: clones + starts the mock server, builds, tests, stops the mock server bundle exec fastlane test_e2e platform:ios bundle exec fastlane test_e2e platform:android # AVD from DETOX_AVD_NAME (default Pixel_API_37) # Options: configuration:debug, skip_build:true, local_server:, # mock_server_branch:, test:e2e/tests/MessageListTests.e2e.ts -# Or by hand +# Or by hand (yarn commands in examples/E2ETestApp) bundle exec fastlane start_mock_server # driver on MOCK_DRIVER_PORT (default 4569) yarn e2e:build -c ios.sim.release # or android.emu.release MOCK_DRIVER_PORT=4569 yarn e2e:test -c ios.sim.release bundle exec fastlane stop_mock_server ``` +Android builds only the `arm64-v8a` and `x86_64` ABIs (emulators); set `ORG_GRADLE_PROJECT_reactNativeArchitectures` to +narrow it further. + Release configurations bundle the JS, so no Metro is needed. Debug configurations need `yarn start`. Android builds need JDK 17 or 21 (`JAVA_HOME`). @@ -62,10 +74,28 @@ API 34 x86_64 emulator (`android.att.release`). Both call `fastlane test_e2e ski `--retries 2 --headless` and keeps failure logs and screenshots in `artifacts/`. Those and the mock server logs are uploaded when a job fails. +What CI caches (restored on PRs, filled by the runs on `develop`): + +- **The built app** (`.app`; release + test `.apk`), keyed on the app and SDK sources. A change that only touches + `e2e/` reuses the binary and skips pods, Gradle and Xcode entirely. +- `node_modules` of `yarn workspaces focus e2etestapp`, so only the e2e app's dependency tree is installed. +- iOS: CocoaPods, ccache (the RN ccache wrappers need `CCACHE_BINARY` in the environment, which the workflow sets), the + Detox framework. +- Android: Gradle (`gradle/actions/setup-gradle`, `org.gradle.caching`), a single ABI (`x86_64`), an AVD snapshot. +- The mock server's gems, keyed on the mock server revision. + ## Gotchas +- iOS runs with Detox's idle synchronization off (`detoxEnableSynchronization: 0` in `StreamTestCase.ts`): it can wedge + on RN's UIManager shadow queue, whose pending-work counter underflows to 2^64-1, so the app looks busy until the test + times out. Robots wait explicitly instead, and taps retry while the target is not hittable yet (e.g. under the + keyboard as it animates in). Android keeps Espresso's synchronization. - `detox test --reuse` does not reinstall the app. After rebuilding, run without it, or you test the old binary. -- Yarn's `enableScripts: false` skips Detox's postinstall; run `npx detox build-framework-cache` once on a new machine. +- Yarn's `enableScripts: false` skips Detox's postinstall; run `yarn detox build-framework-cache` once on a new machine. +- The SDK's shared native sources are synced into `stream-chat-react-native` and gitignored; `yarn e2e:build` and + `e2e_pod_install` run `yarn sync-native` first. +- Don't commit `ios/E2ETestApp.xcodeproj` after `USE_CCACHE=1 pod install`: it writes machine-specific ccache paths into + it. - Detox replaces Jest's global `expect`. Use it for element expectations and `expect as jestExpect` from `@jest/globals` for values. - Fabric mounts some views that carry a `testID` (`message-list-item-*`, `message-text-container`) empty, with their diff --git a/examples/SampleApp/e2e/jest.config.js b/examples/E2ETestApp/e2e/jest.config.js similarity index 100% rename from examples/SampleApp/e2e/jest.config.js rename to examples/E2ETestApp/e2e/jest.config.js diff --git a/examples/SampleApp/e2e/mock-server/DataTypes.ts b/examples/E2ETestApp/e2e/mock-server/DataTypes.ts similarity index 100% rename from examples/SampleApp/e2e/mock-server/DataTypes.ts rename to examples/E2ETestApp/e2e/mock-server/DataTypes.ts diff --git a/examples/SampleApp/e2e/mock-server/MockServer.ts b/examples/E2ETestApp/e2e/mock-server/MockServer.ts similarity index 100% rename from examples/SampleApp/e2e/mock-server/MockServer.ts rename to examples/E2ETestApp/e2e/mock-server/MockServer.ts diff --git a/examples/SampleApp/e2e/pages/ChannelListPage.ts b/examples/E2ETestApp/e2e/pages/ChannelListPage.ts similarity index 100% rename from examples/SampleApp/e2e/pages/ChannelListPage.ts rename to examples/E2ETestApp/e2e/pages/ChannelListPage.ts diff --git a/examples/SampleApp/e2e/pages/LoginPage.ts b/examples/E2ETestApp/e2e/pages/LoginPage.ts similarity index 68% rename from examples/SampleApp/e2e/pages/LoginPage.ts rename to examples/E2ETestApp/e2e/pages/LoginPage.ts index 5feb125c2a..069eab4a75 100644 --- a/examples/SampleApp/e2e/pages/LoginPage.ts +++ b/examples/E2ETestApp/e2e/pages/LoginPage.ts @@ -1,4 +1,4 @@ -/** The mock server's app user (`E2E_USER` in `src/utils/e2e.ts`), the only one listed in e2e mode. */ +/** The mock server's app user (`appUser` in `src/config.ts`), the only one listed in e2e mode. */ export const appUserId = 'luke_skywalker'; export const LoginPage = { diff --git a/examples/SampleApp/e2e/pages/MessageListPage.ts b/examples/E2ETestApp/e2e/pages/MessageListPage.ts similarity index 100% rename from examples/SampleApp/e2e/pages/MessageListPage.ts rename to examples/E2ETestApp/e2e/pages/MessageListPage.ts diff --git a/examples/SampleApp/e2e/robots/BackendRobot.ts b/examples/E2ETestApp/e2e/robots/BackendRobot.ts similarity index 100% rename from examples/SampleApp/e2e/robots/BackendRobot.ts rename to examples/E2ETestApp/e2e/robots/BackendRobot.ts diff --git a/examples/SampleApp/e2e/robots/ParticipantRobot.ts b/examples/E2ETestApp/e2e/robots/ParticipantRobot.ts similarity index 100% rename from examples/SampleApp/e2e/robots/ParticipantRobot.ts rename to examples/E2ETestApp/e2e/robots/ParticipantRobot.ts diff --git a/examples/SampleApp/e2e/robots/Robot.ts b/examples/E2ETestApp/e2e/robots/Robot.ts similarity index 100% rename from examples/SampleApp/e2e/robots/Robot.ts rename to examples/E2ETestApp/e2e/robots/Robot.ts diff --git a/examples/SampleApp/e2e/robots/UserRobot.ts b/examples/E2ETestApp/e2e/robots/UserRobot.ts similarity index 100% rename from examples/SampleApp/e2e/robots/UserRobot.ts rename to examples/E2ETestApp/e2e/robots/UserRobot.ts diff --git a/examples/SampleApp/e2e/robots/UserRobotChannelListAsserts.ts b/examples/E2ETestApp/e2e/robots/UserRobotChannelListAsserts.ts similarity index 100% rename from examples/SampleApp/e2e/robots/UserRobotChannelListAsserts.ts rename to examples/E2ETestApp/e2e/robots/UserRobotChannelListAsserts.ts diff --git a/examples/SampleApp/e2e/robots/UserRobotMessageListAsserts.ts b/examples/E2ETestApp/e2e/robots/UserRobotMessageListAsserts.ts similarity index 100% rename from examples/SampleApp/e2e/robots/UserRobotMessageListAsserts.ts rename to examples/E2ETestApp/e2e/robots/UserRobotMessageListAsserts.ts diff --git a/examples/SampleApp/e2e/robots/index.ts b/examples/E2ETestApp/e2e/robots/index.ts similarity index 100% rename from examples/SampleApp/e2e/robots/index.ts rename to examples/E2ETestApp/e2e/robots/index.ts diff --git a/examples/SampleApp/e2e/support/Element.ts b/examples/E2ETestApp/e2e/support/Element.ts similarity index 85% rename from examples/SampleApp/e2e/support/Element.ts rename to examples/E2ETestApp/e2e/support/Element.ts index 680f04a112..edefaf1414 100644 --- a/examples/SampleApp/e2e/support/Element.ts +++ b/examples/E2ETestApp/e2e/support/Element.ts @@ -26,15 +26,33 @@ export const waitToAppear = async ( return target; }; +/** + * Retries `action` while Detox reports the element as not hittable (e.g. covered by the keyboard + * while it animates in). iOS runs without Detox's idle synchronization, which used to absorb that. + */ +const retryWhileNotHittable = async (action: () => Promise, timeout = defaultTimeout) => { + const deadline = Date.now() + timeout; + for (;;) { + try { + return await action(); + } catch (error) { + if (Date.now() > deadline || !/not hittable/i.test(String(error))) { + throw error; + } + await new Promise((resolve) => setTimeout(resolve, 300)); + } + } +}; + export const waitToAppearAndClick = async (selector: Selector, options?: WaitOptions) => { const target = await waitToAppear(selector, options); - await target.tap(); + await retryWhileNotHittable(() => target.tap(), options?.timeout); return target; }; export const waitToAppearAndLongPress = async (selector: Selector, options?: WaitOptions) => { const target = await waitToAppear(selector, options); - await target.longPress(); + await retryWhileNotHittable(() => target.longPress(), options?.timeout); return target; }; diff --git a/examples/SampleApp/e2e/support/StreamTestCase.ts b/examples/E2ETestApp/e2e/support/StreamTestCase.ts similarity index 73% rename from examples/SampleApp/e2e/support/StreamTestCase.ts rename to examples/E2ETestApp/e2e/support/StreamTestCase.ts index 19231892e6..d0a61d85f7 100644 --- a/examples/SampleApp/e2e/support/StreamTestCase.ts +++ b/examples/E2ETestApp/e2e/support/StreamTestCase.ts @@ -33,7 +33,14 @@ export const setUpStreamTestCase = (): StreamTestCase => { beforeEach(async () => { await testCase.mockServer.start(currentTestName()); await device.launchApp({ - launchArgs: { MOCK_SERVER_URL: testCase.mockServer.appUrl }, + launchArgs: { + MOCK_SERVER_URL: testCase.mockServer.appUrl, + // iOS: Detox's idle tracking can wedge on RN's UIManager shadow queue (its pending-work + // counter underflows to 2^64-1), leaving the app "busy" until the test times out. Every + // robot action and assert waits explicitly, like the native XCUITest suites, so it runs + // without that synchronization. Android's Espresso synchronization stays on. + detoxEnableSynchronization: device.getPlatform() === 'ios' ? 0 : 1, + }, newInstance: true, permissions: { notifications: 'YES', photos: 'YES' }, }); diff --git a/examples/SampleApp/e2e/support/step.ts b/examples/E2ETestApp/e2e/support/step.ts similarity index 100% rename from examples/SampleApp/e2e/support/step.ts rename to examples/E2ETestApp/e2e/support/step.ts diff --git a/examples/SampleApp/e2e/tests/ChannelListTests.e2e.ts b/examples/E2ETestApp/e2e/tests/ChannelListTests.e2e.ts similarity index 100% rename from examples/SampleApp/e2e/tests/ChannelListTests.e2e.ts rename to examples/E2ETestApp/e2e/tests/ChannelListTests.e2e.ts diff --git a/examples/SampleApp/e2e/tests/MessageListTests.e2e.ts b/examples/E2ETestApp/e2e/tests/MessageListTests.e2e.ts similarity index 100% rename from examples/SampleApp/e2e/tests/MessageListTests.e2e.ts rename to examples/E2ETestApp/e2e/tests/MessageListTests.e2e.ts diff --git a/examples/SampleApp/e2e/tsconfig.json b/examples/E2ETestApp/e2e/tsconfig.json similarity index 100% rename from examples/SampleApp/e2e/tsconfig.json rename to examples/E2ETestApp/e2e/tsconfig.json diff --git a/examples/E2ETestApp/index.js b/examples/E2ETestApp/index.js new file mode 100644 index 0000000000..d2660dcb40 --- /dev/null +++ b/examples/E2ETestApp/index.js @@ -0,0 +1,7 @@ +import 'react-native-gesture-handler'; +import { AppRegistry } from 'react-native'; + +import App from './App'; +import { name as appName } from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/examples/E2ETestApp/ios/.xcode.env b/examples/E2ETestApp/ios/.xcode.env new file mode 100644 index 0000000000..3d5782c715 --- /dev/null +++ b/examples/E2ETestApp/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/examples/E2ETestApp/ios/E2ETestApp.xcodeproj/project.pbxproj b/examples/E2ETestApp/ios/E2ETestApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..756385091b --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp.xcodeproj/project.pbxproj @@ -0,0 +1,498 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0C80B921A6F3F58F76C31292 /* libPods-E2ETestApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-E2ETestApp.a */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; + 761780EF2CA45674006654EF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EE2CA45674006654EF /* SceneDelegate.swift */; }; + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + AE4AFD92A8A3756E73E2B1AB /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* E2ETestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = E2ETestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = E2ETestApp/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = E2ETestApp/Info.plist; sourceTree = ""; }; + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = E2ETestApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 3B4392A12AC88292D35C810B /* Pods-E2ETestApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-E2ETestApp.debug.xcconfig"; path = "Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp.debug.xcconfig"; sourceTree = ""; }; + 5709B34CF0A7D63546082F79 /* Pods-E2ETestApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-E2ETestApp.release.xcconfig"; path = "Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp.release.xcconfig"; sourceTree = ""; }; + 5DCACB8F33CDC322A6C60F78 /* libPods-E2ETestApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-E2ETestApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = E2ETestApp/AppDelegate.swift; sourceTree = ""; }; + 761780EE2CA45674006654EF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegate.swift; path = E2ETestApp/SceneDelegate.swift; sourceTree = ""; }; + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = E2ETestApp/LaunchScreen.storyboard; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0C80B921A6F3F58F76C31292 /* libPods-E2ETestApp.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* E2ETestApp */ = { + isa = PBXGroup; + children = ( + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 761780EC2CA45674006654EE /* AppDelegate.swift */, + 761780EE2CA45674006654EF /* SceneDelegate.swift */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, + ); + name = E2ETestApp; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 5DCACB8F33CDC322A6C60F78 /* libPods-E2ETestApp.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* E2ETestApp */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + BBD78D7AC51CEA395F1C20DB /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* E2ETestApp.app */, + ); + name = Products; + sourceTree = ""; + }; + BBD78D7AC51CEA395F1C20DB /* Pods */ = { + isa = PBXGroup; + children = ( + 3B4392A12AC88292D35C810B /* Pods-E2ETestApp.debug.xcconfig */, + 5709B34CF0A7D63546082F79 /* Pods-E2ETestApp.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* E2ETestApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "E2ETestApp" */; + buildPhases = ( + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = E2ETestApp; + productName = E2ETestApp; + productReference = 13B07F961A680F5B00A75B9A /* E2ETestApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1210; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1120; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "E2ETestApp" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* E2ETestApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + AE4AFD92A8A3756E73E2B1AB /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"\\\"$WITH_ENVIRONMENT\\\" \\\"$REACT_NATIVE_XCODE\\\"\"\n"; + }; + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-E2ETestApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-E2ETestApp/Pods-E2ETestApp-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, + 761780EF2CA45674006654EF /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-E2ETestApp.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = E2ETestApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.getstream.reactnative.e2etestapp; + PRODUCT_NAME = E2ETestApp; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-E2ETestApp.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = E2ETestApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.getstream.reactnative.e2etestapp; + PRODUCT_NAME = E2ETestApp; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + PODFILE_DIR = "$(SRCROOT)"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = ( + "$(inherited)", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + "-DRCT_REMOVE_LEGACY_ARCH=1", + ); + PODFILE_DIR = "$(SRCROOT)"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "E2ETestApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "E2ETestApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/examples/E2ETestApp/ios/E2ETestApp.xcodeproj/xcshareddata/xcschemes/E2ETestApp.xcscheme b/examples/E2ETestApp/ios/E2ETestApp.xcodeproj/xcshareddata/xcschemes/E2ETestApp.xcscheme new file mode 100644 index 0000000000..cd9bdbadeb --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp.xcodeproj/xcshareddata/xcschemes/E2ETestApp.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/E2ETestApp/ios/E2ETestApp.xcworkspace/contents.xcworkspacedata b/examples/E2ETestApp/ios/E2ETestApp.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..bd672bb7e5 --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/E2ETestApp/ios/E2ETestApp/AppDelegate.swift b/examples/E2ETestApp/ios/E2ETestApp/AppDelegate.swift new file mode 100644 index 0000000000..0d14d1b0ac --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp/AppDelegate.swift @@ -0,0 +1,11 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + true + } +} diff --git a/examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/AppIcon.appiconset/Contents.json b/examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..81213230de --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/Contents.json b/examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/Contents.json new file mode 100644 index 0000000000..2d92bd53fd --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/E2ETestApp/ios/E2ETestApp/Info.plist b/examples/E2ETestApp/ios/E2ETestApp/Info.plist new file mode 100644 index 0000000000..cf6c5bcfd6 --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp/Info.plist @@ -0,0 +1,78 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + E2ETestApp + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSLocationWhenInUseUsageDescription + + RCTNewArchEnabled + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/examples/E2ETestApp/ios/E2ETestApp/LaunchScreen.storyboard b/examples/E2ETestApp/ios/E2ETestApp/LaunchScreen.storyboard new file mode 100644 index 0000000000..600e8dc84b --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/E2ETestApp/ios/E2ETestApp/PrivacyInfo.xcprivacy b/examples/E2ETestApp/ios/E2ETestApp/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..fc90547a7a --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp/PrivacyInfo.xcprivacy @@ -0,0 +1,37 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/examples/E2ETestApp/ios/E2ETestApp/SceneDelegate.swift b/examples/E2ETestApp/ios/E2ETestApp/SceneDelegate.swift new file mode 100644 index 0000000000..b5c4d45824 --- /dev/null +++ b/examples/E2ETestApp/ios/E2ETestApp/SceneDelegate.swift @@ -0,0 +1,56 @@ +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider +import UIKit + +class SceneDelegate: RCTDefaultReactNativeFactoryDelegate, UIWindowSceneDelegate { + var window: UIWindow? + var reactNativeFactory: RCTReactNativeFactory? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + dependencyProvider = RCTAppDependencyProvider() + reactNativeFactory = RCTReactNativeFactory(delegate: self) + window = UIWindow(windowScene: windowScene) + + #if DEBUG + let devMenuConfiguration = RCTDevMenuConfiguration( + devMenuEnabled: true, + shakeGestureEnabled: true, + keyboardShortcutsEnabled: true + ) + reactNativeFactory?.devMenuConfiguration = devMenuConfiguration + #endif + + reactNativeFactory?.startReactNative( + withModuleName: "E2ETestApp", + in: window, + connectionOptions: connectionOptions + ) + + + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + RCTLinkingManager.scene(scene, openURLContexts: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + RCTLinkingManager.scene(scene, continue: userActivity) + } + + override func bundleURL() -> URL? { + #if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") + #else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") + #endif + } +} diff --git a/examples/E2ETestApp/ios/Podfile b/examples/E2ETestApp/ios/Podfile new file mode 100644 index 0000000000..10e8a8c808 --- /dev/null +++ b/examples/E2ETestApp/ios/Podfile @@ -0,0 +1,45 @@ +# Resolve react_native_pods.rb with node to allow for hoisting +require Pod::Executable.execute_command('node', ['-p', + 'require.resolve( + "react-native/scripts/react_native_pods.rb", + {paths: [process.argv[1]]}, + )', __dir__]).strip + +platform :ios, min_ios_version_supported +prepare_react_native_project! + +linkage = ENV['USE_FRAMEWORKS'] +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end + +target 'E2ETestApp' do + config = use_native_modules! + + use_react_native!( + :path => config[:reactNativePath], + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) + + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + # CI sets USE_CCACHE=1 so compiled objects are reused across runs. + :ccache_enabled => ENV['USE_CCACHE'] == '1', + ) + + # Some pods pin an iOS deployment target (e.g. RNSVG at 12.4) below the range supported by + # current Xcode. Clamp them up to the app's minimum, as the SampleApp does. + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < min_ios_version_supported.to_f + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = min_ios_version_supported + end + end + end + end +end diff --git a/examples/E2ETestApp/ios/Podfile.lock b/examples/E2ETestApp/ios/Podfile.lock new file mode 100644 index 0000000000..cb27ec2bdd --- /dev/null +++ b/examples/E2ETestApp/ios/Podfile.lock @@ -0,0 +1,2554 @@ +PODS: + - boost (1.84.0): + - ReactNativeDependencies + - DoubleConversion (1.1.6): + - ReactNativeDependencies + - fast_float (8.0.0): + - ReactNativeDependencies + - FBLazyVector (0.88.0): + - React-Core-prebuilt + - fmt (12.1.0): + - ReactNativeDependencies + - glog (0.3.5): + - ReactNativeDependencies + - hermes-engine (260318099.0.4): + - hermes-engine/Pre-built (= 260318099.0.4) + - hermes-engine/Pre-built (260318099.0.4) + - RCT-Folly (2024.11.18.00): + - RCT-Folly/Default (= 2024.11.18.00) + - ReactNativeDependencies + - RCT-Folly/Default (2024.11.18.00): + - ReactNativeDependencies + - RCTDeprecation (0.88.0): + - React-Core-prebuilt + - RCTRequired (0.88.0): + - React-Core-prebuilt + - RCTSwiftUI (0.88.0) + - RCTSwiftUIWrapper (0.88.0): + - RCTSwiftUI + - RCTTypeSafety (0.88.0): + - FBLazyVector (= 0.88.0) + - RCTRequired (= 0.88.0) + - React-Core (= 0.88.0) + - React (0.88.0): + - React-Core (= 0.88.0) + - React-Core/DevSupport (= 0.88.0) + - React-Core/RCTWebSocket (= 0.88.0) + - React-RCTActionSheet (= 0.88.0) + - React-RCTAnimation (= 0.88.0) + - React-RCTBlob (= 0.88.0) + - React-RCTImage (= 0.88.0) + - React-RCTLinking (= 0.88.0) + - React-RCTNetwork (= 0.88.0) + - React-RCTSettings (= 0.88.0) + - React-RCTText (= 0.88.0) + - React-RCTVibration (= 0.88.0) + - React-bridging (0.88.0): + - React-callinvoker + - React-Core-prebuilt + - React-jsi + - React-timing + - ReactNativeDependencies + - React-callinvoker (0.88.0): + - React-callinvoker/callinvokerUmbrella (= 0.88.0) + - React-cxxstableapi + - React-callinvoker/callinvokerUmbrella (0.88.0): + - React-cxxstableapi + - React-Core (0.88.0): + - React-Core-prebuilt + - React-Core/Default (= 0.88.0) + - React-Core-prebuilt (0.88.0): + - ReactNativeDependencies + - React-Core/CoreModulesHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/Default (0.88.0): + - React-Core-prebuilt + - React-Core/DevSupport (0.88.0): + - React-Core-prebuilt + - React-Core/RCTActionSheetHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTAnimationHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTBlobHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTImageHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTLinkingHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTNetworkHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTSettingsHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTTextHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTVibrationHeaders (0.88.0): + - React-Core-prebuilt + - React-Core/RCTWebSocket (0.88.0): + - React-Core-prebuilt + - React-CoreModules (0.88.0): + - RCTTypeSafety (= 0.88.0) + - React-Core-prebuilt + - React-Core/CoreModulesHeaders (= 0.88.0) + - React-debug + - React-featureflags + - React-jsi (= 0.88.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-NativeModulesApple + - React-networking + - React-RCTBlob + - React-RCTFBReactNativeSpec + - React-RCTImage (= 0.88.0) + - React-runtimeexecutor + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-cxxreact (0.88.0): + - hermes-engine + - React-callinvoker (= 0.88.0) + - React-Core-prebuilt + - React-debug (= 0.88.0) + - React-jserrorhandler (= 0.88.0) + - React-jsi (= 0.88.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-logger (= 0.88.0) + - React-perflogger (= 0.88.0) + - React-runtimeexecutor + - React-timing (= 0.88.0) + - React-utils + - ReactNativeDependencies + - React-cxxstableapi (0.88.0) + - React-debug (0.88.0): + - React-cxxstableapi + - React-debug/debugUmbrella (= 0.88.0) + - React-debug/redbox (= 0.88.0) + - React-debug/debugUmbrella (0.88.0): + - React-cxxstableapi + - React-debug/redbox (0.88.0): + - React-cxxstableapi + - React-defaultsnativemodule (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxstableapi + - React-domnativemodule + - React-Fabric/animated + - React-featureflags + - React-featureflagsnativemodule + - React-idlecallbacksnativemodule + - React-intersectionobservernativemodule + - React-jsi + - React-jsiexecutor + - React-microtasksnativemodule + - React-mutationobservernativemodule + - React-RCTFBReactNativeSpec + - React-resizeobservernativemodule + - React-viewtransitionnativemodule + - React-webperformancenativemodule + - ReactNativeDependencies + - Yoga + - React-domnativemodule (0.88.0): + - hermes-engine + - React-bridging + - React-Core-prebuilt + - React-cxxstableapi + - React-Fabric + - React-Fabric/bridging + - React-FabricComponents + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric/animated (= 0.88.0) + - React-Fabric/animationbackend (= 0.88.0) + - React-Fabric/animations (= 0.88.0) + - React-Fabric/attributedstring (= 0.88.0) + - React-Fabric/bridging (= 0.88.0) + - React-Fabric/componentregistry (= 0.88.0) + - React-Fabric/componentregistrynative (= 0.88.0) + - React-Fabric/components (= 0.88.0) + - React-Fabric/consistency (= 0.88.0) + - React-Fabric/core (= 0.88.0) + - React-Fabric/dom (= 0.88.0) + - React-Fabric/imagemanager (= 0.88.0) + - React-Fabric/leakchecker (= 0.88.0) + - React-Fabric/mounting (= 0.88.0) + - React-Fabric/observers (= 0.88.0) + - React-Fabric/scheduler (= 0.88.0) + - React-Fabric/telemetry (= 0.88.0) + - React-Fabric/uimanager (= 0.88.0) + - React-Fabric/viewtransition (= 0.88.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animated (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric/animationbackend + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animationbackend (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animations (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/attributedstring (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/bridging (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistry (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistrynative (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.88.0) + - React-Fabric/components/root (= 0.88.0) + - React-Fabric/components/scrollview (= 0.88.0) + - React-Fabric/components/view (= 0.88.0) + - React-Fabric/components/viewUmbrella (= 0.88.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/legacyviewmanagerinterop (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/root (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/scrollview (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/view (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-renderercss + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric/components/viewUmbrella (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/consistency (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/core (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/dom (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/imagemanager (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/leakchecker (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/mounting (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-jsinspectortracing + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric/observers/events (= 0.88.0) + - React-Fabric/observers/intersection (= 0.88.0) + - React-Fabric/observers/mutation (= 0.88.0) + - React-Fabric/observers/resize (= 0.88.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/events (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/intersection (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/mutation (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/resize (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/scheduler (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric/animationbackend + - React-Fabric/observers/events + - React-Fabric/viewtransition + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-performancecdpmetrics + - React-performancetimeline + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/telemetry (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric/uimanager/consistency (= 0.88.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager/consistency (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/viewtransition (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-FabricComponents (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.88.0) + - React-FabricComponents/textlayoutmanager (= 0.88.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.88.0) + - React-FabricComponents/components/iostextinput (= 0.88.0) + - React-FabricComponents/components/modal (= 0.88.0) + - React-FabricComponents/components/rncore (= 0.88.0) + - React-FabricComponents/components/safeareaview (= 0.88.0) + - React-FabricComponents/components/switch (= 0.88.0) + - React-FabricComponents/components/text (= 0.88.0) + - React-FabricComponents/components/textinput (= 0.88.0) + - React-FabricComponents/components/unimplementedview (= 0.88.0) + - React-FabricComponents/components/virtualview (= 0.88.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/inputaccessory (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/iostextinput (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/modal (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/rncore (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/safeareaview (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/switch (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/text (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/textinput (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/unimplementedview (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/virtualview (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/textlayoutmanager (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricImage (0.88.0): + - hermes-engine + - RCTRequired (= 0.88.0) + - RCTTypeSafety (= 0.88.0) + - React-Core-prebuilt + - React-cxxstableapi + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.88.0) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-featureflags (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - React-featureflags/featureflagsUmbrella (= 0.88.0) + - ReactNativeDependencies + - React-featureflags/featureflagsUmbrella (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - ReactNativeDependencies + - React-featureflagsnativemodule (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxstableapi + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-graphics (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-rendererdebug + - React-utils + - ReactNativeDependencies + - React-hermes (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.88.0) + - React-cxxstableapi + - React-jsi + - React-jsiexecutor (= 0.88.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-oscompat + - React-perflogger (= 0.88.0) + - React-runtimeexecutor + - ReactNativeDependencies + - React-idlecallbacksnativemodule (0.88.0): + - hermes-engine + - React-bridging + - React-Core-prebuilt + - React-cxxstableapi + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-ImageManager (0.88.0): + - React-Core-prebuilt + - React-Core/Default + - React-cxxstableapi + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - ReactNativeDependencies + - React-intersectionobservernativemodule (0.88.0): + - hermes-engine + - React-bridging + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-Fabric + - React-Fabric/bridging + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-jserrorhandler (0.88.0): + - hermes-engine + - React-bridging + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-featureflags + - React-jsi + - ReactNativeDependencies + - React-jsi (0.88.0): + - hermes-engine + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsiexecutor (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-jserrorhandler + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspector (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-jsinspectortracing + - React-oscompat + - React-perflogger (= 0.88.0) + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspectorcdp (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - ReactNativeDependencies + - React-jsinspectornetwork (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - React-jsinspectorcdp + - ReactNativeDependencies + - React-jsinspectortracing (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxstableapi + - React-jsi + - React-jsinspectornetwork + - React-oscompat + - React-timing + - React-utils + - ReactNativeDependencies + - React-jsitooling (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.88.0) + - React-cxxstableapi + - React-debug + - React-jsi (= 0.88.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsitracing (0.88.0): + - React-jsi + - React-logger (0.88.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-Mapbuffer (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Mapbuffer/mapbufferUmbrella (= 0.88.0) + - ReactNativeDependencies + - React-Mapbuffer/mapbufferUmbrella (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - ReactNativeDependencies + - React-microtasksnativemodule (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxstableapi + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-mutationobservernativemodule (0.88.0): + - hermes-engine + - React-bridging + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-Fabric + - React-Fabric/bridging + - React-Fabric/observers/mutation + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-launch-arguments (4.1.1): + - React + - react-native-netinfo (12.0.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context (5.10.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common (= 5.10.1) + - react-native-safe-area-context/fabric (= 5.10.1) + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context/common (5.10.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context/fabric (5.10.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-NativeModulesApple (0.88.0): + - hermes-engine + - React-bridging + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-networking (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - React-jsinspectornetwork + - React-jsinspectortracing + - React-performancetimeline + - React-timing + - ReactNativeDependencies + - React-oscompat (0.88.0): + - React-cxxstableapi + - React-perflogger (0.88.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-performancecdpmetrics (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxstableapi + - React-jsi + - React-performancetimeline + - React-runtimeexecutor + - React-timing + - ReactNativeDependencies + - React-performancetimeline (0.88.0): + - React-Core-prebuilt + - React-cxxstableapi + - React-featureflags + - React-jsinspector + - React-jsinspectortracing + - React-perflogger + - React-timing + - ReactNativeDependencies + - React-RCTActionSheet (0.88.0): + - React-Core/RCTActionSheetHeaders (= 0.88.0) + - React-RCTAnimatedModuleProvider (0.88.0): + - hermes-engine + - React-Core + - React-Core-prebuilt + - React-Fabric/animated + - React-featureflags + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-RCTAnimation (0.88.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTAnimationHeaders + - React-debug + - React-featureflags + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTAppDelegate (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-CoreModules + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsitooling + - React-NativeModulesApple + - React-RCTAnimatedModuleProvider + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTRuntime + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-RCTBlob (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTFabric (0.88.0): + - React-Core-prebuilt + - React-RCTFBReactNativeSpec (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec/components (= 0.88.0) + - ReactCommon + - ReactNativeDependencies + - React-RCTFBReactNativeSpec/components (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-RCTImage (0.88.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTLinking (0.88.0): + - React-Core/RCTLinkingHeaders (= 0.88.0) + - React-jsi (= 0.88.0) + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactCommon/turbomodule/core (= 0.88.0) + - React-RCTNetwork (0.88.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTNetworkHeaders + - React-debug + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-NativeModulesApple + - React-networking + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTRuntime (0.88.0): + - React-Core-prebuilt + - React-RCTSettings (0.88.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTText (0.88.0): + - React-Core/RCTTextHeaders (= 0.88.0) + - Yoga + - React-RCTVibration (0.88.0): + - React-Core-prebuilt + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-rendererconsistency (0.88.0) + - React-renderercss (0.88.0): + - React-debug + - React-utils + - React-rendererdebug (0.88.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-resizeobservernativemodule (0.88.0): + - hermes-engine + - React-bridging + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-Fabric + - React-Fabric/bridging + - React-Fabric/observers/resize + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-RuntimeApple (0.88.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-cxxstableapi + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-RuntimeCore (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-Fabric + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-runtimeexecutor (0.88.0): + - React-Core-prebuilt + - React-debug + - React-featureflags + - React-jsi (= 0.88.0) + - React-utils + - ReactNativeDependencies + - React-RuntimeHermes (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxstableapi + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-jsitracing + - React-RuntimeCore + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-runtimescheduler (0.88.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-debug + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsinspectortracing + - React-performancetimeline + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-timing + - React-utils + - ReactNativeDependencies + - React-timing (0.88.0): + - React-cxxstableapi + - React-debug + - React-timing/timingUmbrella (= 0.88.0) + - React-timing/timingUmbrella (0.88.0): + - React-cxxstableapi + - React-debug + - React-utils (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-debug + - React-jsi (= 0.88.0) + - ReactNativeDependencies + - React-viewtransitionnativemodule (0.88.0): + - hermes-engine + - React-Core-prebuilt + - React-Fabric + - React-Fabric/bridging + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-webperformancenativemodule (0.88.0): + - hermes-engine + - React-bridging + - React-Core-prebuilt + - React-cxxreact + - React-cxxstableapi + - React-jsi + - React-jsiexecutor + - React-performancetimeline + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactAppDependencyProvider (0.88.0): + - ReactCodegen + - ReactCodegen (0.88.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-RCTAppDelegate + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactCommon (0.88.0): + - React-Core-prebuilt + - ReactCommon/turbomodule (= 0.88.0) + - ReactNativeDependencies + - ReactCommon/turbomodule (0.88.0): + - hermes-engine + - React-callinvoker (= 0.88.0) + - React-Core-prebuilt + - React-jsi (= 0.88.0) + - React-logger (= 0.88.0) + - React-perflogger (= 0.88.0) + - ReactCommon/turbomodule/core (= 0.88.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/core (0.88.0): + - hermes-engine + - React-bridging + - React-callinvoker (= 0.88.0) + - React-Core-prebuilt + - React-cxxreact (= 0.88.0) + - React-debug (= 0.88.0) + - React-featureflags (= 0.88.0) + - React-jsi (= 0.88.0) + - React-logger (= 0.88.0) + - React-perflogger (= 0.88.0) + - React-utils (= 0.88.0) + - ReactNativeDependencies + - ReactNativeDependencies (0.88.0) + - RNGestureHandler (3.4.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets (>= 0.8.0) + - Yoga + - RNReanimated (4.7.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNReanimated/apple (= 4.7.1) + - RNReanimated/common (= 4.7.1) + - RNReanimated/view (= 4.7.1) + - RNWorklets + - Yoga + - RNReanimated/apple (4.7.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets + - Yoga + - RNReanimated/common (4.7.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets + - Yoga + - RNReanimated/view (4.7.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets + - Yoga + - RNSVG (15.15.5): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNSVG/common (= 15.15.5) + - Yoga + - RNSVG/common (15.15.5): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - RNWorklets (0.13.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNWorklets/apple (= 0.13.0) + - RNWorklets/common (= 0.13.0) + - Yoga + - RNWorklets/apple (0.13.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - RNWorklets/common (0.13.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - SocketRocket (0.7.1): + - ReactNativeDependencies + - stream-chat-react-native (9.10.1): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - Teleport (1.2.2): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Teleport/common (= 1.2.2) + - Yoga + - Teleport/common (1.2.2): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-bridging + - React-Core + - React-Core-prebuilt + - React-cxxstableapi + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - Yoga (0.0.0): + - React-Core-prebuilt + +DEPENDENCIES: + - boost (from `build/rndeps-facades/boost`) + - DoubleConversion (from `build/rndeps-facades/DoubleConversion`) + - fast_float (from `build/rndeps-facades/fast_float`) + - FBLazyVector (from `build/rncore-facades/FBLazyVector`) + - fmt (from `build/rndeps-facades/fmt`) + - glog (from `build/rndeps-facades/glog`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCT-Folly (from `build/rndeps-facades/RCT-Folly`) + - RCTDeprecation (from `build/rncore-facades/RCTDeprecation`) + - RCTRequired (from `build/rncore-facades/RCTRequired`) + - RCTSwiftUI (from `../node_modules/react-native/ReactApple/RCTSwiftUI`) + - RCTSwiftUIWrapper (from `../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-bridging (from `../node_modules/react-native/ReactCommon/react/bridging`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `build/rncore-facades/React-Core`) + - React-Core-prebuilt (from `../node_modules/react-native/React-Core-prebuilt.podspec`) + - React-Core/RCTWebSocket (from `build/rncore-facades/React-Core`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-cxxstableapi (from `../node_modules/react-native/ReactCommon/react/cxxstableapi`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-intersectionobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - React-mutationobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver`) + - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) + - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-networking (from `../node_modules/react-native/ReactCommon/react/networking`) + - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimatedModuleProvider (from `../node_modules/react-native/ReactApple/RCTAnimatedModuleProvider`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `build/rncore-facades/React-RCTFabric`) + - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `build/rncore-facades/React-RCTRuntime`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-resizeobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/resizeobserver`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - React-viewtransitionnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/viewtransition`) + - React-webperformancenativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) + - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) + - ReactCodegen (from `build/generated/ios/ReactCodegen`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNSVG (from `../node_modules/react-native-svg`) + - RNWorklets (from `../node_modules/react-native-worklets`) + - SocketRocket (from `build/rndeps-facades/SocketRocket`) + - stream-chat-react-native (from `../node_modules/stream-chat-react-native`) + - Teleport (from `../node_modules/react-native-teleport`) + - Yoga (from `build/rncore-facades/Yoga`) + +EXTERNAL SOURCES: + boost: + :path: build/rndeps-facades/boost + DoubleConversion: + :path: build/rndeps-facades/DoubleConversion + fast_float: + :path: build/rndeps-facades/fast_float + FBLazyVector: + :path: build/rncore-facades/FBLazyVector + fmt: + :path: build/rndeps-facades/fmt + glog: + :path: build/rndeps-facades/glog + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-v260318099.0.2 + RCT-Folly: + :path: build/rndeps-facades/RCT-Folly + RCTDeprecation: + :path: build/rncore-facades/RCTDeprecation + RCTRequired: + :path: build/rncore-facades/RCTRequired + RCTSwiftUI: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUI" + RCTSwiftUIWrapper: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-bridging: + :path: "../node_modules/react-native/ReactCommon/react/bridging" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Core: + :path: build/rncore-facades/React-Core + React-Core-prebuilt: + :podspec: "../node_modules/react-native/React-Core-prebuilt.podspec" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-cxxstableapi: + :path: "../node_modules/react-native/ReactCommon/react/cxxstableapi" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-intersectionobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectorcdp: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + React-jsinspectornetwork: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network" + React-jsinspectortracing: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../node_modules/react-native/ReactCommon/jsitooling" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + React-mutationobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver" + react-native-launch-arguments: + :path: "../node_modules/react-native-launch-arguments" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-networking: + :path: "../node_modules/react-native/ReactCommon/react/networking" + React-oscompat: + :path: "../node_modules/react-native/ReactCommon/oscompat" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancecdpmetrics: + :path: "../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" + React-performancetimeline: + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimatedModuleProvider: + :path: "../node_modules/react-native/ReactApple/RCTAnimatedModuleProvider" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: build/rncore-facades/React-RCTFabric + React-RCTFBReactNativeSpec: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: build/rncore-facades/React-RCTRuntime + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../node_modules/react-native/ReactCommon/react/renderer/css" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-resizeobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/resizeobserver" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../node_modules/react-native/ReactCommon/react/timing" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + React-viewtransitionnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/viewtransition" + React-webperformancenativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" + ReactAppDependencyProvider: + :path: build/generated/ios/ReactAppDependencyProvider + ReactCodegen: + :path: build/generated/ios/ReactCodegen + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + ReactNativeDependencies: + :podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNSVG: + :path: "../node_modules/react-native-svg" + RNWorklets: + :path: "../node_modules/react-native-worklets" + SocketRocket: + :path: build/rndeps-facades/SocketRocket + stream-chat-react-native: + :path: "../node_modules/stream-chat-react-native" + Teleport: + :path: "../node_modules/react-native-teleport" + Yoga: + :path: build/rncore-facades/Yoga + +SPEC CHECKSUMS: + boost: 8502bfdc46e2804408af2e163049ece181009af4 + DoubleConversion: 501a575c40c0b50c89685762dd3e415e6bb26fb0 + fast_float: 7c925d1cebf8328d4018d061f49377e9f887e3b4 + FBLazyVector: fdebac2dfe9c53bca49f8218fbdf1e24e36c8be0 + fmt: ba0886e864ebb21d14c98b2e12e164b8002f5b93 + glog: 3cf7cb46f743c4bc758ae3e6d9c15bd1b4cfd6d4 + hermes-engine: a1158910bb7e300b9e3d2f1e2d433f817ec1a4b5 + RCT-Folly: 82921e585c0400eda46fac1068297aa9dfb035e4 + RCTDeprecation: e134f2a2c096a6450f5fcbbe949531f913a2ff04 + RCTRequired: f1a68295837ee8f91e112ed1b5694d12e7ff1784 + RCTSwiftUI: 4b21ec5b1f86acb9c96bb429100ad88d4023ed76 + RCTSwiftUIWrapper: 68bd9fac4e710259cb2b202472adc5f4d113e872 + RCTTypeSafety: 77078a9c26030dd84a6bdae74ed900159f89a098 + React: 8dafd0ef058191e9e7238cfa6b3deaa24398e5dd + React-bridging: 658efeb1b4704d5f60ea2fb6e8ca826ab0ae3234 + React-callinvoker: 4e40c208c1ea46686c084a7c23da6a65cbb99482 + React-Core: a831be15fcea0e345a24cb10d6cc0b87e9913d03 + React-Core-prebuilt: 0fd2555a42f40e7770ced1c7dfc5ee1dbfe041cd + React-CoreModules: a86fbe698fd817c17b3936832dc1106a130c4694 + React-cxxreact: 48ca56bd4f26de869478a803a5544b101a57cde6 + React-cxxstableapi: f35502b4c542a0d44031b42f91c4ca813d267e16 + React-debug: 238e15d17dfded9419a6d739d4c8b60968bad02e + React-defaultsnativemodule: c028edc7e48914344b823931aec1cc4ffabe631e + React-domnativemodule: b09770fc69f5ce5759de98f8065cab900b6d87d6 + React-Fabric: a157c978603fa2796f349f3a27d17861e55804c5 + React-FabricComponents: c7c58a1deea5db215e9d5a2aec890e208d5d5075 + React-FabricImage: 234346bf5a3ef918f3873e845b57b93b2b1f31c0 + React-featureflags: a00ac5c38e63d5f86de895ca923f07f5ff76de66 + React-featureflagsnativemodule: 6b93ba0aef46757b9d54166475abc5e4fa80e70d + React-graphics: 634bc2e5884b4a29f56aa8aef719e03171a23f6b + React-hermes: 9ce7aaf6784709e196ac51bfd89dd6c551e3fb4c + React-idlecallbacksnativemodule: e02e194086278377ced48b7fd7816b6c925b9e7e + React-ImageManager: 452d4b3587f21d3384868e2abaab7c0a952b1d1c + React-intersectionobservernativemodule: 3ddbc206bc95c086f1df6eea04aa617ae02627d2 + React-jserrorhandler: a0d842e1f74cc10e597fde2a6447860483df2b13 + React-jsi: 489de0620905b88a17ccb0ac2328046af43e7ebd + React-jsiexecutor: f0e2ae6d98b587d3505ebde246599ee3ad97cee1 + React-jsinspector: 515bf11caeda5df444f39aec63e377ef77ad7696 + React-jsinspectorcdp: be9ac71a77880a6fcc95375e7e61a79e152a3203 + React-jsinspectornetwork: a4bc1475897a2301d114d7a8605af3e8760f9e52 + React-jsinspectortracing: 7d35952c944dc337d1a4167605c1338108880219 + React-jsitooling: 1886dc5058e9feded2136b0d9ba5c189c6af5faa + React-jsitracing: 76b58feee3fb0539c8967e5b9de1a40ad8014f09 + React-logger: 3d2b406a780fed50ad0bbd07452b373c2a515308 + React-Mapbuffer: a2dae4f2cb335ebb97f2240494d0c2bab84cd339 + React-microtasksnativemodule: 8b3aaa5f3b907ecf071b105589868d9f027a448d + React-mutationobservernativemodule: 11d77d3f7360578f07847843544ada23cc473049 + react-native-launch-arguments: eab68037b01ea7c78a0b3b55da3a5e967e9aaf92 + react-native-netinfo: 687391ecedb199c6d67fbc46c87cbae3277de967 + react-native-safe-area-context: b1c9f15a36e597e9cef4c5526e209ef2645bc2e0 + React-NativeModulesApple: 52f3def085f129de1307e3eb78e26729ff70f421 + React-networking: 1e6441349ac72bfd4f7a79c300c3dd76993a7058 + React-oscompat: e64123dd8199f9e99cede19fa542c3da3445481e + React-perflogger: f1466e0f46ce706aef73a559337eca85f51a40b7 + React-performancecdpmetrics: f238752e99eb7c69c782020827bff38a4b1b2ef2 + React-performancetimeline: f1b216fa02200c859cd18ef345d4230cd712137a + React-RCTActionSheet: 81662769b1f576e9c2e074deb41d6b83d1c7e29f + React-RCTAnimatedModuleProvider: a9829773197f782c19ad86c5438a2cc199203774 + React-RCTAnimation: ad9dd2c6c3b4d07e583c2b79354858c4f34481a0 + React-RCTAppDelegate: 70398a06e10ada27cdf77dc2b43f883126a91ac0 + React-RCTBlob: f3d86b491b8f28c36ced0cb750878b62f72e1380 + React-RCTFabric: 3767f97183c40ba3dadf007b17a01be2cd4e632a + React-RCTFBReactNativeSpec: d7ad5d9edc1865f38b9aac5b7091c3abb6978eed + React-RCTImage: 46ad408f777dce991d4095d7e73a568212be6baa + React-RCTLinking: 547427cc885bfa74b3cbba405971c5270defde7a + React-RCTNetwork: 52c00c0fe07327df9b9fe9881b4f5550dc5ced37 + React-RCTRuntime: 081fd457d305c3aee76c335c568e693ed107f48e + React-RCTSettings: 9cd0b2be41404574ec280b6b8e73c2208c842050 + React-RCTText: 5a6d38456eabc0a1a7c0d47d592390d35c6de57c + React-RCTVibration: c1815b4f471462954a4b7d8835155e053cec5412 + React-rendererconsistency: b7996da82c8d3f4a7928b39be3e437b72ad92012 + React-renderercss: 4c256900eba87661178771766da0171493f88848 + React-rendererdebug: 4d9df8cf2cac6eee7002a4d14358c79fdf6ad8c5 + React-resizeobservernativemodule: 750d334554e7c44bacf14568a8a0d0048cc4c278 + React-RuntimeApple: 2045a6206895ad5ce90d141d1bd384888fd06c42 + React-RuntimeCore: b051facffbc7ca038097278cabf810d6d86dd328 + React-runtimeexecutor: b06ec9bb745a97cbd3ce25e3e02be7b8058fb374 + React-RuntimeHermes: fcfa8baca49a6d37d331147568d51d5b8b0a175d + React-runtimescheduler: c2afe74d1db006bafd9270186101f5378b1bef81 + React-timing: 6a1a8cfb0caae8de243aba16fa92fa81de23efaf + React-utils: 2384e5db98378df8c0bf2c0d5489ec1b8e80b398 + React-viewtransitionnativemodule: 8505b253586f792c6c722869a664889797ecae3e + React-webperformancenativemodule: 67f32fde92ed7e8470ee1c669bdb7c2fcb760141 + ReactAppDependencyProvider: 782ee9546c9cd3b53b73f1e9e3dd728540085fc7 + ReactCodegen: 7b71f4464442e9b6eae41094d11076d56c51c0e3 + ReactCommon: 145a14b942b19fc2fe38927f261955ada56978aa + ReactNativeDependencies: d3b7423ba1e52525e42085c8470109abde583d2e + RNGestureHandler: b5512a093e6923dd1c31fbdd8e6186cd0ebb3541 + RNReanimated: 3018727dbf272754d0f31ef73f619e49ade7baf0 + RNSVG: 8b5f2fdb9f0245a863cdd6e9c48a7689821a08b0 + RNWorklets: 488806637a21eb17812a48dadca7cb8c698c8cfd + SocketRocket: 37aec555668fb852ec12e3c0de59a86ca58f0871 + stream-chat-react-native: 7f1fdf2d1ce87c0b8a6c1d581308dff0c84e8e21 + Teleport: 90f0d7e81c6b40db9b253e1ef73b2f4f4f3f6b53 + Yoga: d1c536142c5ff8ec8cd856ab2a7c227a1d875c8e + +PODFILE CHECKSUM: 84fecefd0a807b1cfa8b4184065b9bbd44fa58d1 + +COCOAPODS: 1.15.2 diff --git a/examples/E2ETestApp/metro.config.js b/examples/E2ETestApp/metro.config.js new file mode 100644 index 0000000000..c96816d4be --- /dev/null +++ b/examples/E2ETestApp/metro.config.js @@ -0,0 +1,81 @@ +const { getDefaultConfig } = require('@react-native/metro-config'); +const { resolveUniqueModule } = require('@rnx-kit/metro-config'); + +/** + * Metro configuration + * https://reactnative.dev/docs/metro + * + * @type {import('@react-native/metro-config').MetroConfig} + */ +const config = getDefaultConfig(__dirname); + +const PATH = require('path'); +const metroConfigPackageJsonPath = require.resolve('metro-config/package.json', { + paths: [__dirname], +}); +const metroExclusionList = require( + PATH.join(PATH.dirname(metroConfigPackageJsonPath), 'src/defaults/exclusionList.js'), +); +const exclusionList = metroExclusionList.default || metroExclusionList; +const packageDirPath = PATH.resolve(__dirname, '../../package'); +const nativePackageDirPath = PATH.resolve(__dirname, '../../package/native-package'); + +const symlinked = { + 'stream-chat-react-native': nativePackageDirPath, + 'stream-chat-react-native-core': packageDirPath, +}; + +// find what all modules need to be unique for the app (mainly react and react-native) +// note: we filter the symlinked modules as they are already unique +// and as they dont follow the workspace pattern the auto-generated path to the module is incorrect +const dependencyPackageNames = Object.keys(require('./package.json').dependencies); + +const uniqueModules = dependencyPackageNames.map((packageName) => { + if (symlinked[packageName]) { + const modulePath = symlinked[packageName]; + const escapedPackageName = PATH.normalize(packageName).replace(/\\/g, '\\\\'); + + // exclude the symlinked package from being resolved from node_modules + // example: .*\/node_modules\/stream-chat-react-native-core\/.* + // the above would avoid native-package to resolve core from its own node_modules + const exclusionRE = new RegExp( + `.*${PATH.sep}node_modules\\${PATH.sep}${escapedPackageName}\\${PATH.sep}.*`, + ); + + return { + packageName, // name of the package + modulePath, + blockPattern: exclusionRE, // paths that match this pattern will be blocked from being resolved + }; + } + const [modulePath, blockPattern] = resolveUniqueModule(packageName, __dirname); + // resolveUniqueModule emits a regex ending in `/` (package-dir boundary). metro-config's + // exclusionList anchors every alternation branch with `$`, which would require file paths to + // literally end with `/node_modules//`. Append `.*` so the anchor lands at end-of-file + // instead of end-of-directory, otherwise the blocklist silently matches nothing. + const fixedBlockPattern = new RegExp(blockPattern.source + '.*'); + return { + packageName, // name of the package + modulePath, // actual path to the module in the project's node modules + blockPattern: fixedBlockPattern, // paths that match this pattern will be blocked from being resolved + }; +}); + +// block the other paths for unique modules from being resolved +const blockList = uniqueModules.map(({ blockPattern }) => blockPattern); + +// provide the path for the unique modules +const extraNodeModules = uniqueModules.reduce((acc, item) => { + acc[item.packageName] = item.modulePath; + return acc; +}, {}); + +config.resolver.blockList = exclusionList(blockList); +config.resolver.extraNodeModules = extraNodeModules; + +config.resolver.nodeModulesPaths = [PATH.resolve(__dirname, 'node_modules')]; + +// add the package dir for metro to access the package folder +config.watchFolders = [packageDirPath]; + +module.exports = config; diff --git a/examples/E2ETestApp/package.json b/examples/E2ETestApp/package.json new file mode 100644 index 0000000000..3329927fda --- /dev/null +++ b/examples/E2ETestApp/package.json @@ -0,0 +1,50 @@ +{ + "name": "e2etestapp", + "version": "0.0.1", + "private": true, + "description": "Minimal app driven by the Detox e2e suite (e2e/). Only the SDK and its required peer dependencies.", + "scripts": { + "sync-native": "bash ../../package/scripts/reconcile-shared-native.sh native-package && bash ../../package/scripts/sync-shared-native.sh native-package", + "android": "yarn sync-native && react-native run-android", + "ios": "yarn sync-native && react-native run-ios", + "start": "yarn sync-native && react-native start", + "typecheck": "tsc --noEmit && tsc --noEmit -p e2e", + "e2e:build": "yarn sync-native && detox build", + "e2e:test": "detox test" + }, + "dependencies": { + "@react-native-community/netinfo": "^12.0.1", + "react": "19.3.0", + "react-native": "0.88.0", + "react-native-gesture-handler": "^3.4.0", + "react-native-launch-arguments": "^4.1.1", + "react-native-reanimated": "4.7.1", + "react-native-safe-area-context": "^5.10.1", + "react-native-svg": "^15.15.5", + "react-native-teleport": "^1.2.2", + "react-native-worklets": "^0.13.0", + "stream-chat": "^9.54.0", + "stream-chat-react-native": "workspace:^" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/runtime": "^7.29.2", + "@jest/globals": "^30.4.1", + "@react-native-community/cli": "20.2.0", + "@react-native-community/cli-platform-android": "20.2.0", + "@react-native-community/cli-platform-ios": "20.2.0", + "@react-native/babel-preset": "0.88.0", + "@react-native/metro-config": "0.88.0", + "@react-native/typescript-config": "0.88.0", + "@rnx-kit/metro-config": "^2.2.6", + "@stream-io/typescript-config": "workspace:^", + "@types/react": "^19.3.0", + "babel-jest": "30.0.0", + "detox": "^20.51.4", + "jest": "^30.4.2", + "typescript": "6.0.3" + }, + "engines": { + "node": ">= 22.11.0" + } +} diff --git a/examples/E2ETestApp/src/config.ts b/examples/E2ETestApp/src/config.ts new file mode 100644 index 0000000000..8d10c2d95b --- /dev/null +++ b/examples/E2ETestApp/src/config.ts @@ -0,0 +1,26 @@ +import { LaunchArguments } from 'react-native-launch-arguments'; + +import type { UserResponse } from 'stream-chat'; + +/** + * Launch arguments passed by the Detox suite (`e2e/support/StreamTestCase.ts`). The counterpart of + * `USE_MOCK_SERVER` + `MOCK_SERVER_*` on iOS and the `BASE_URL` intent extra on Android. + */ +type E2ELaunchArguments = { + /** Base URL of the per-test `stream-chat-test-mock-server` instance, e.g. `http://localhost:54321`. */ + MOCK_SERVER_URL?: string; +}; + +export const mockServerUrl = LaunchArguments.value().MOCK_SERVER_URL; + +/** The app user every mock server fixture is recorded for. */ +export const appUser: UserResponse = { + id: 'luke_skywalker', + image: 'https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg', + name: 'Luke Skywalker', +}; + +// The mock server accepts any key and token; these match the native SDKs' e2e apps. +export const apiKey = '8br4watad788'; +export const appUserToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibHVrZV9za3l3YWxrZXIifQ.kFSLHRB5X62t0Zlc7nwczWUfsQMwfkpylC6jCUZ6Mc0'; diff --git a/examples/E2ETestApp/tsconfig.json b/examples/E2ETestApp/tsconfig.json new file mode 100644 index 0000000000..d1be171213 --- /dev/null +++ b/examples/E2ETestApp/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": ["@react-native/typescript-config", "@stream-io/typescript-config/base.json"], + "compilerOptions": { + "types": [] + }, + "exclude": ["node_modules", "e2e"] +} diff --git a/examples/SampleApp/.gitignore b/examples/SampleApp/.gitignore index 87c3fc1e1b..7388cefebf 100644 --- a/examples/SampleApp/.gitignore +++ b/examples/SampleApp/.gitignore @@ -77,7 +77,3 @@ yarn-error.log # Credentials credentials/ app-build/ - -# Detox e2e -/stream-chat-test-mock-server/ -/artifacts/ diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx index d56ab0bc66..14dc99a866 100644 --- a/examples/SampleApp/App.tsx +++ b/examples/SampleApp/App.tsx @@ -59,7 +59,6 @@ import { UserSelectorScreen } from './src/screens/UserSelectorScreen'; import type { StackNavigatorParamList, UserSelectorParamList } from './src/types'; import AsyncStore from './src/utils/AsyncStore.ts'; -import { isE2eTestRun } from './src/utils/e2e'; import { navigateToChannel, RootNavigationRef } from './src/utils/RootNavigation'; import { watchLocation } from './src/utils/watchLocation'; @@ -353,7 +352,7 @@ const DrawerNavigatorWrapper: React.FC<{ > logs/driver.log 2>&1 &") end @@ -244,6 +247,14 @@ lane :e2e_check_required do required end +desc 'Installs the E2ETestApp pods with this bundle (the e2e app has no Gemfile of its own)' +lane :e2e_pod_install do + Dir.chdir(e2e_app_path) do + sh('yarn sync-native') + sh('bundle exec pod install --project-directory=ios') + end +end + desc 'Runs the Detox e2e tests. Options: platform (ios|android), configuration (release|debug), ' \ 'device (android: emu|att), skip_build, test (a test file or -t filter), local_server, mock_server_branch' lane :test_e2e do |options| @@ -256,11 +267,11 @@ lane :test_e2e do |options| start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) begin - Dir.chdir(sample_app_path) do - sh("npx detox build -c #{detox_config}") unless options[:skip_build] + Dir.chdir(e2e_app_path) do + sh("yarn e2e:build -c #{detox_config}") unless options[:skip_build] # On CI: retry flaky tests, run the simulator headless, and keep logs + screenshots of failures in ./artifacts. ci_flags = is_ci ? '--retries 2 --headless --record-logs failing --take-screenshots failing' : '' - sh("MOCK_DRIVER_PORT=#{mock_server_driver_port} npx detox test -c #{detox_config} --cleanup #{ci_flags} #{options[:test]}") + sh("MOCK_DRIVER_PORT=#{mock_server_driver_port} yarn detox test -c #{detox_config} --cleanup #{ci_flags} #{options[:test]}") end ensure stop_mock_server @@ -311,7 +322,7 @@ end lane :impact_analysis do { e2e: { - sources: ['package/', 'examples/SampleApp/', 'package.json'], + sources: ['package/', 'examples/E2ETestApp/', 'examples/SampleApp/fastlane/', 'package.json'], checks: ['Test E2E Result'] } } diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index e15f0420e4..4a27feb1e7 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -1744,8 +1744,6 @@ PODS: - ReactCommon/turbomodule/core - ReactNativeDependencies - Yoga - - react-native-launch-arguments (4.1.1): - - React - react-native-maps (1.29.11): - react-native-maps/Maps (= 1.29.11) - react-native-maps/Generated (1.29.11): @@ -2455,7 +2453,7 @@ PODS: - ReactNativeDependencies - RNFBApp - Yoga - - RNGestureHandler (3.4.0): + - RNGestureHandler (3.3.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2817,7 +2815,7 @@ PODS: - SDWebImage/Core (~> 5.17) - SocketRocket (0.7.1): - ReactNativeDependencies - - stream-chat-react-native (9.10.1): + - stream-chat-react-native (9.10.0): - hermes-engine - RCTRequired - RCTTypeSafety @@ -2953,7 +2951,6 @@ DEPENDENCIES: - "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)" - "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)" - react-native-image-picker (from `../node_modules/react-native-image-picker`) - - react-native-launch-arguments (from `../node_modules/react-native-launch-arguments`) - react-native-maps (from `../node_modules/react-native-maps`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -3156,8 +3153,6 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/geolocation" react-native-image-picker: :path: "../node_modules/react-native-image-picker" - react-native-launch-arguments: - :path: "../node_modules/react-native-launch-arguments" react-native-maps: :path: "../node_modules/react-native-maps" react-native-netinfo: @@ -3352,7 +3347,6 @@ SPEC CHECKSUMS: react-native-document-picker: ca21279b19ad48ebc107c2ffca904c7aee99e40b react-native-geolocation: 2c5f29e0098223d8fa2c18813f6acbf882563424 react-native-image-picker: 7cdf342c3b12b1e4f50f6098c5feaf058ea8d350 - react-native-launch-arguments: eab68037b01ea7c78a0b3b55da3a5e967e9aaf92 react-native-maps: b39b5d5ee5ac885f2822bb124105af4042768d4b react-native-netinfo: 41575e101c171319fbcf82dfa5fa5df852d6b40d react-native-safe-area-context: 7b143af09becaf26ba247b9a82d355f91b77b9b0 @@ -3398,7 +3392,7 @@ SPEC CHECKSUMS: RNFastImage: 25f87dde869e11156daeef98b4cee2371023407e RNFBApp: 21e4d8c20656c236a818ee2eff3f859557ceff33 RNFBMessaging: e3e31aec89cd0eb986c87ec8247f598190b61c36 - RNGestureHandler: abdc2d49ac3970908acd127980b429d377927ee1 + RNGestureHandler: ddbdf3f293a498368803f8181ff157271ac05c48 RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 RNReactNativeHapticFeedback: a1bba076645e8fedcb65c8e70461b77cd5d6dd32 RNReanimated: 5c641cc430b7255eae87c8039204eb0f816ea1b8 @@ -3411,10 +3405,10 @@ SPEC CHECKSUMS: SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SocketRocket: 37aec555668fb852ec12e3c0de59a86ca58f0871 - stream-chat-react-native: 291a3f91fef6922e89ed97bf7a790bf4a786bab9 + stream-chat-react-native: dc4a04f6903f9616ed0d58757e813e96bda1972f Teleport: 8a97ac5c0d76c04e20c36843d4d8f8fe5aa1aa73 Yoga: d1c536142c5ff8ec8cd856ab2a7c227a1d875c8e PODFILE CHECKSUM: ac9682db83e42d63d399a8de4586161e86b3e751 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 201b4f23d8..9afbb82754 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -15,9 +15,7 @@ "bootstrap": "yarn install", "release": "FILTER_PATH='examples/SampleApp' TAG_FORMAT=$npm_package_name'@v${version}' node ../../release/prod", "release-next": "echo \"Skipping next release for SampleApp\"", - "typecheck": "tsc --noEmit && tsc --noEmit -p e2e", - "e2e:build": "detox build", - "e2e:test": "detox test", + "typecheck": "tsc --noEmit", "clean": "watchman watch-del-all && yarn cache clean && rm -rf ios/build && pod cache clean --all && rm -rf android/build && cd android && ./gradlew clean && cd -", "clean-all": "yarn clean && rm -rf node_modules && rm -rf ios/Pods && rm -rf vendor && bundle install && yarn install && cd ios && bundle exec pod install && cd -", "fastlane:android-build": "bundle exec fastlane android firebase_build_and_upload deploy:false", @@ -55,7 +53,6 @@ "react-native-gesture-handler": "^3.4.0", "react-native-haptic-feedback": "^3.0.0", "react-native-image-picker": "^8.2.1", - "react-native-launch-arguments": "^4.1.1", "react-native-maps": "^1.29.11", "react-native-nitro-modules": "0.36.5", "react-native-nitro-sound": "0.2.17", @@ -79,7 +76,6 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/runtime": "^7.29.2", - "@jest/globals": "^30.4.1", "@react-native-community/cli": "20.2.0", "@react-native-community/cli-platform-android": "20.2.0", "@react-native-community/cli-platform-ios": "20.2.0", @@ -90,10 +86,7 @@ "@stream-io/typescript-config": "workspace:^", "@types/lodash.mergewith": "^4.6.9", "@types/react": "^19.3.0", - "babel-jest": "30.0.0", "babel-plugin-react-compiler": "^1.0.0", - "detox": "^20.51.4", - "jest": "^30.4.2", "typescript": "6.0.3" }, "engines": { diff --git a/examples/SampleApp/src/components/ScreenHeader.tsx b/examples/SampleApp/src/components/ScreenHeader.tsx index 6962765526..4a5bd2c2e0 100644 --- a/examples/SampleApp/src/components/ScreenHeader.tsx +++ b/examples/SampleApp/src/components/ScreenHeader.tsx @@ -87,7 +87,6 @@ export const BackButton: React.FC<{ } }} style={styles.backButton} - testID='back-button' > {!!showUnreadCountBadge && ( diff --git a/examples/SampleApp/src/hooks/useChatClient.ts b/examples/SampleApp/src/hooks/useChatClient.ts index a2d755ffd2..1ced3e1d4e 100644 --- a/examples/SampleApp/src/hooks/useChatClient.ts +++ b/examples/SampleApp/src/hooks/useChatClient.ts @@ -14,7 +14,6 @@ import { SqliteClient } from 'stream-chat-react-native'; import { USER_TOKENS, USERS } from '../ChatUsers'; import type { LoginConfig } from '../types'; import AsyncStore from '../utils/AsyncStore'; -import { E2E_LOGIN_CONFIG, isE2eTestRun, mockServerUrl } from '../utils/e2e'; const messaging = getMessaging(); @@ -74,7 +73,6 @@ export const useChatClient = () => { // unsubscribe from previous push listeners unsubscribePushListenersRef.current?.(); const client = StreamChat.getInstance(config.apiKey, { - baseURL: mockServerUrl, timeout: 6000, // logger: (type, msg) => console.log(type, msg) }); @@ -88,12 +86,6 @@ export const useChatClient = () => { await client.connectUser(user, config.userToken); await AsyncStore.setItem('@stream-rn-sampleapp-login-config', config); - // The mock server has no push endpoints, and a permission prompt would cover the app. - if (isE2eTestRun) { - setChatClient(client); - return; - } - const permissionAuthStatus = await messaging.hasPermission(); let isEnabled = permissionAuthStatus === AuthorizationStatus.AUTHORIZED || @@ -156,12 +148,7 @@ export const useChatClient = () => { setIsConnecting(true); try { - if (isE2eTestRun) { - // e2e tests always log in through the user selector, so a stored login is never restored. - if (userId) { - await loginUser(E2E_LOGIN_CONFIG); - } - } else if (userId) { + if (userId) { await loginUser({ apiKey: 'yjrt5yxw77ev', userId: USERS[userId].id, @@ -194,9 +181,7 @@ export const useChatClient = () => { useEffect(() => { const run = async () => { - if (!isE2eTestRun) { - await requestNotificationPermission(); - } + await requestNotificationPermission(); await switchUser(); }; run(); diff --git a/examples/SampleApp/src/screens/UserSelectorScreen.tsx b/examples/SampleApp/src/screens/UserSelectorScreen.tsx index b8beddc837..4842132230 100644 --- a/examples/SampleApp/src/screens/UserSelectorScreen.tsx +++ b/examples/SampleApp/src/screens/UserSelectorScreen.tsx @@ -22,7 +22,6 @@ import { useLegacyColors } from '../theme/useLegacyColors'; import type { UserSelectorParamList } from '../types'; import AsyncStore from '../utils/AsyncStore'; -import { E2E_USER, isE2eTestRun } from '../utils/e2e'; const styles = StyleSheet.create({ avatarImage: { @@ -120,7 +119,7 @@ export const UserSelectorScreen: React.FC = ({ navigation }) => { - {(isE2eTestRun ? [E2E_USER] : Object.values(USERS)).map((u, index) => ( + {Object.values(USERS).map((u, index) => ( { diff --git a/examples/SampleApp/src/utils/e2e.ts b/examples/SampleApp/src/utils/e2e.ts deleted file mode 100644 index 7e2657e468..0000000000 --- a/examples/SampleApp/src/utils/e2e.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { LaunchArguments } from 'react-native-launch-arguments'; - -import type { UserResponse } from 'stream-chat'; - -import type { LoginConfig } from '../types'; - -/** - * Launch arguments passed by the Detox e2e suite (`examples/SampleApp/e2e`). - * The counterpart of `USE_MOCK_SERVER` + `MOCK_SERVER_*` on iOS and the `BASE_URL` intent extra on Android. - */ -type E2ELaunchArguments = { - /** Base URL of the per-test `stream-chat-test-mock-server` instance, e.g. `http://localhost:54321`. */ - MOCK_SERVER_URL?: string; -}; - -const launchArguments = LaunchArguments.value(); - -export const mockServerUrl = launchArguments.MOCK_SERVER_URL; - -/** - * True when the app was launched by the e2e suite. Push registration, notification - * permission prompts, the stored login and offline storage are all skipped in this mode, - * so every test starts from the same clean state. - */ -export const isE2eTestRun = !!mockServerUrl; - -/** The app user every mock server fixture is recorded for. */ -export const E2E_USER: UserResponse = { - id: 'luke_skywalker', - image: 'https://vignette.wikia.nocookie.net/starwars/images/2/20/LukeTLJ.jpg', - name: 'Luke Skywalker', -}; - -export const E2E_LOGIN_CONFIG: LoginConfig = { - // The mock server accepts any key; this one matches the native SDKs' e2e apps. - apiKey: '8br4watad788', - userId: E2E_USER.id, - userImage: E2E_USER.image, - userName: E2E_USER.name, - userToken: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibHVrZV9za3l3YWxrZXIifQ.kFSLHRB5X62t0Zlc7nwczWUfsQMwfkpylC6jCUZ6Mc0', -}; diff --git a/examples/SampleApp/tsconfig.json b/examples/SampleApp/tsconfig.json index d1be171213..5a1531949c 100644 --- a/examples/SampleApp/tsconfig.json +++ b/examples/SampleApp/tsconfig.json @@ -2,6 +2,5 @@ "extends": ["@react-native/typescript-config", "@stream-io/typescript-config/base.json"], "compilerOptions": { "types": [] - }, - "exclude": ["node_modules", "e2e"] + } } diff --git a/package.json b/package.json index 73df92c8a5..ee702cd07b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "package/native-package", "package/expo-package", "examples/SampleApp", + "examples/E2ETestApp", "examples/ExpoMessaging", "examples/ExpoChatGPTSample" ], @@ -71,7 +72,7 @@ "lint-fix": "prettier --write . && eslint . --fix --max-warnings 0", "prettier": "prettier --list-different .", "prettier-fix": "prettier --write .", - "typecheck": "yarn workspaces foreach -A --parallel --include 'stream-chat-react-native-core' --include 'stream-chat-react-native' --include 'stream-chat-expo' --include 'sampleapp' --include 'ExpoMessaging' --include 'expochatgptsample' run typecheck", + "typecheck": "yarn workspaces foreach -A --parallel --include 'stream-chat-react-native-core' --include 'stream-chat-react-native' --include 'stream-chat-expo' --include 'sampleapp' --include 'e2etestapp' --include 'ExpoMessaging' --include 'expochatgptsample' run typecheck", "build": "yarn workspace stream-chat-react-native-core build", "test:coverage": "yarn workspace stream-chat-react-native-core test:coverage", "test:unit": "yarn workspace stream-chat-react-native-core test:unit", diff --git a/yarn.lock b/yarn.lock index a54672135a..95ca73ae46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10011,6 +10011,41 @@ __metadata: languageName: node linkType: hard +"e2etestapp@workspace:examples/E2ETestApp": + version: 0.0.0-use.local + resolution: "e2etestapp@workspace:examples/E2ETestApp" + dependencies: + "@babel/core": "npm:^7.29.0" + "@babel/runtime": "npm:^7.29.2" + "@jest/globals": "npm:^30.4.1" + "@react-native-community/cli": "npm:20.2.0" + "@react-native-community/cli-platform-android": "npm:20.2.0" + "@react-native-community/cli-platform-ios": "npm:20.2.0" + "@react-native-community/netinfo": "npm:^12.0.1" + "@react-native/babel-preset": "npm:0.88.0" + "@react-native/metro-config": "npm:0.88.0" + "@react-native/typescript-config": "npm:0.88.0" + "@rnx-kit/metro-config": "npm:^2.2.6" + "@stream-io/typescript-config": "workspace:^" + "@types/react": "npm:^19.3.0" + babel-jest: "npm:30.0.0" + detox: "npm:^20.51.4" + jest: "npm:^30.4.2" + react: "npm:19.3.0" + react-native: "npm:0.88.0" + react-native-gesture-handler: "npm:^3.4.0" + react-native-launch-arguments: "npm:^4.1.1" + react-native-reanimated: "npm:4.7.1" + react-native-safe-area-context: "npm:^5.10.1" + react-native-svg: "npm:^15.15.5" + react-native-teleport: "npm:^1.2.2" + react-native-worklets: "npm:^0.13.0" + stream-chat: "npm:^9.54.0" + stream-chat-react-native: "workspace:^" + typescript: "npm:6.0.3" + languageName: unknown + linkType: soft + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -19259,7 +19294,6 @@ __metadata: "@d11/react-native-fast-image": "npm:^8.13.0" "@emoji-mart/data": "npm:^1.2.1" "@gorhom/bottom-sheet": "npm:^5.2.14" - "@jest/globals": "npm:^30.4.1" "@notifee/react-native": "npm:^9.1.8" "@op-engineering/op-sqlite": "npm:^18.2.5" "@react-native-async-storage/async-storage": "npm:^3.1.1" @@ -19287,12 +19321,9 @@ __metadata: "@stream-io/typescript-config": "workspace:^" "@types/lodash.mergewith": "npm:^4.6.9" "@types/react": "npm:^19.3.0" - babel-jest: "npm:30.0.0" babel-plugin-react-compiler: "npm:^1.0.0" dayjs: "npm:1.11.13" - detox: "npm:^20.51.4" emoji-mart: "npm:^5.6.0" - jest: "npm:^30.4.2" lodash.mergewith: "npm:^4.6.2" react: "npm:19.3.0" react-native: "npm:0.88.0" @@ -19300,7 +19331,6 @@ __metadata: react-native-gesture-handler: "npm:^3.4.0" react-native-haptic-feedback: "npm:^3.0.0" react-native-image-picker: "npm:^8.2.1" - react-native-launch-arguments: "npm:^4.1.1" react-native-maps: "npm:^1.29.11" react-native-nitro-modules: "npm:0.36.5" react-native-nitro-sound: "npm:0.2.17" From fface0a04284e02023b17fb30e323270175edd32 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 15:58:24 +0100 Subject: [PATCH 04/16] ci(e2e): set composer text directly on Android and give the emulator more memory --- .github/workflows/e2e-test.yml | 9 ++++++++- examples/E2ETestApp/e2e/robots/UserRobot.ts | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index d94dce58eb..708c03bf1c 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -254,6 +254,11 @@ jobs: with: path: ${{ env.APK_PATHS }} key: ${{ steps.apk.outputs.cache-primary-key }} + # Frees the daemons' memory for the emulator, which otherwise can get killed mid-run. + - name: Stop Gradle daemons + if: steps.apk.outputs.cache-hit != 'true' + working-directory: examples/E2ETestApp/android + run: ./gradlew --stop # --- Emulator: boots from a cached snapshot --- - name: Enable KVM @@ -268,7 +273,7 @@ jobs: path: | ~/.android/avd/* ~/.android/adb* - key: avd-${{ env.ANDROID_API_LEVEL }}-x86_64-google_apis-pixel_6 + key: avd-${{ env.ANDROID_API_LEVEL }}-x86_64-google_apis-pixel_6-ram4096 - name: Create AVD snapshot if: steps.avd.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 @@ -277,6 +282,7 @@ jobs: arch: x86_64 target: google_apis profile: pixel_6 + ram-size: 4096M force-avd-creation: false emulator-options: ${{ env.ANDROID_EMULATOR_OPTIONS }} disable-animations: false @@ -309,6 +315,7 @@ jobs: arch: x86_64 target: google_apis profile: pixel_6 + ram-size: 4096M force-avd-creation: false emulator-options: -no-snapshot-save ${{ env.ANDROID_EMULATOR_OPTIONS }} disable-animations: true diff --git a/examples/E2ETestApp/e2e/robots/UserRobot.ts b/examples/E2ETestApp/e2e/robots/UserRobot.ts index 898205ad05..f8e99e6bee 100644 --- a/examples/E2ETestApp/e2e/robots/UserRobot.ts +++ b/examples/E2ETestApp/e2e/robots/UserRobot.ts @@ -43,10 +43,19 @@ export class UserRobot extends Robot { return this.perform(() => waitToAppearAndLongPress(MessageList.Message.text(messageText))); } + /** + * iOS types key by key, like XCUITest. Android sets the text, like the native suite's + * `typeText` (UiAutomator `setText`): typing through the soft keyboard leaves the text in the + * IME's composing state, which the composer doesn't pick up, so the send button never shows. + */ typeText(text: string): this { return this.perform(async () => { const inputField = await waitToAppear(Composer.inputField); - await inputField.typeText(text); + if (device.getPlatform() === 'android') { + await inputField.replaceText(text); + } else { + await inputField.typeText(text); + } }); } From 5df91f8f26ac63311dde2af43d048ae2eb0e3771 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 16:16:46 +0100 Subject: [PATCH 05/16] ci(e2e): run the Android emulator with the native SDK's setup --- .github/workflows/e2e-test.yml | 39 ++++--------------------------- examples/E2ETestApp/e2e/README.md | 4 +++- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 708c03bf1c..095bfde6f3 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -196,7 +196,7 @@ jobs: timeout-minutes: 60 env: ANDROID_API_LEVEL: "34" - ANDROID_EMULATOR_OPTIONS: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + ANDROID_EMULATOR_OPTIONS: -no-snapshot-save -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -camera-back none -camera-front none DETOX_ADB_NAME: "emulator-5554" APK_PATHS: | examples/E2ETestApp/android/app/build/outputs/apk/release/app-release.apk @@ -260,41 +260,12 @@ jobs: working-directory: examples/E2ETestApp/android run: ./gradlew --stop - # --- Emulator: boots from a cached snapshot --- + # --- Emulator: same setup as the native Android SDK's e2e jobs (AOSP image, no Play services) --- - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Restore AVD - id: avd - uses: actions/cache/restore@v5 - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-${{ env.ANDROID_API_LEVEL }}-x86_64-google_apis-pixel_6-ram4096 - - name: Create AVD snapshot - if: steps.avd.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ env.ANDROID_API_LEVEL }} - arch: x86_64 - target: google_apis - profile: pixel_6 - ram-size: 4096M - force-avd-creation: false - emulator-options: ${{ env.ANDROID_EMULATOR_OPTIONS }} - disable-animations: false - script: echo "Generated AVD snapshot for caching." - - name: Save AVD - if: steps.avd.outputs.cache-hit != 'true' - uses: actions/cache/save@v5 - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: ${{ steps.avd.outputs.cache-primary-key }} # --- Test tooling --- - name: Resolve mock server revision @@ -313,11 +284,9 @@ jobs: with: api-level: ${{ env.ANDROID_API_LEVEL }} arch: x86_64 - target: google_apis - profile: pixel_6 + profile: pixel ram-size: 4096M - force-avd-creation: false - emulator-options: -no-snapshot-save ${{ env.ANDROID_EMULATOR_OPTIONS }} + emulator-options: ${{ env.ANDROID_EMULATOR_OPTIONS }} disable-animations: true working-directory: examples/SampleApp script: bundle exec fastlane test_e2e platform:android device:att skip_build:true mock_server_branch:"$MOCK_SERVER_BRANCH" diff --git a/examples/E2ETestApp/e2e/README.md b/examples/E2ETestApp/e2e/README.md index 1601d27f08..b4423d77fd 100644 --- a/examples/E2ETestApp/e2e/README.md +++ b/examples/E2ETestApp/e2e/README.md @@ -81,7 +81,9 @@ What CI caches (restored on PRs, filled by the runs on `develop`): - `node_modules` of `yarn workspaces focus e2etestapp`, so only the e2e app's dependency tree is installed. - iOS: CocoaPods, ccache (the RN ccache wrappers need `CCACHE_BINARY` in the environment, which the workflow sets), the Detox framework. -- Android: Gradle (`gradle/actions/setup-gradle`, `org.gradle.caching`), a single ABI (`x86_64`), an AVD snapshot. +- Android: Gradle (`gradle/actions/setup-gradle`, `org.gradle.caching`), a single ABI (`x86_64`). The emulator matches the + native Android SDK's e2e jobs (API 34 AOSP image, `pixel` profile, no snapshot): the Google APIs image plus a cached + snapshot froze the emulator on the 5th app launch, two runs in a row. - The mock server's gems, keyed on the mock server revision. ## Gotchas From 508150da17218a8817b60f8d5f79d49775ba6c16 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 16:41:42 +0100 Subject: [PATCH 06/16] ci(e2e): use the default emulator RAM and log OOM kills on failure --- .github/workflows/e2e-test.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 095bfde6f3..0dd7185216 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -285,11 +285,18 @@ jobs: api-level: ${{ env.ANDROID_API_LEVEL }} arch: x86_64 profile: pixel - ram-size: 4096M emulator-options: ${{ env.ANDROID_EMULATOR_OPTIONS }} disable-animations: true working-directory: examples/SampleApp script: bundle exec fastlane test_e2e platform:android device:att skip_build:true mock_server_branch:"$MOCK_SERVER_BRANCH" + # The emulator has vanished mid-run without any output; this shows whether the kernel killed it. + - name: Diagnose emulator + if: failure() + run: | + sudo dmesg -T | grep -i -E 'out of memory|oom-kill|killed process' || echo "No OOM kills in dmesg" + free -m + df -h + ls -la /tmp/android-runner 2>/dev/null || true - uses: actions/upload-artifact@v6 if: failure() with: From f5554ccbf52e9984808838e6e675925c88a9c8a5 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 16:43:19 +0100 Subject: [PATCH 07/16] Disable iOS --- .github/workflows/e2e-test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 0dd7185216..53b0905173 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -67,8 +67,7 @@ jobs: ios: needs: check - # UNCOMMENT (replaces the `if` below): if: needs.check.outputs.required == 'true' && github.event_name == 'workflow_dispatch' && inputs.ios - if: needs.check.outputs.required == 'true' + if: needs.check.outputs.required == 'true' && github.event_name == 'workflow_dispatch' && inputs.ios runs-on: macos-15 timeout-minutes: 60 env: From cd6ab5997e46ef3e0d7adc3a1cb37b9b3094bd70 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 17:14:32 +0100 Subject: [PATCH 08/16] ci(e2e): run Android on API 36 and upload emulator crash data --- .github/workflows/e2e-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 53b0905173..8916347e8d 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -194,7 +194,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 env: - ANDROID_API_LEVEL: "34" + # Same as the native Android SDK e2e jobs, which run stably on these runners + ANDROID_API_LEVEL: "36" ANDROID_EMULATOR_OPTIONS: -no-snapshot-save -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -camera-back none -camera-front none DETOX_ADB_NAME: "emulator-5554" APK_PATHS: | @@ -303,6 +304,7 @@ jobs: path: | examples/E2ETestApp/stream-chat-test-mock-server/logs examples/E2ETestApp/artifacts + /tmp/android-runner result: name: Test E2E Result From 239e8ecfc48c69b66c7d20d362cb1f32ad7e70d3 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 17:33:54 +0100 Subject: [PATCH 09/16] Test iOS --- .github/workflows/e2e-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 8916347e8d..8ac3f88054 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -67,7 +67,7 @@ jobs: ios: needs: check - if: needs.check.outputs.required == 'true' && github.event_name == 'workflow_dispatch' && inputs.ios + # if: needs.check.outputs.required == 'true' && github.event_name == 'workflow_dispatch' && inputs.ios runs-on: macos-15 timeout-minutes: 60 env: From d97eedfbc812c7b8e6ee45d81e99816ef706c101 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 17:41:37 +0100 Subject: [PATCH 10/16] Test iOS --- examples/SampleApp/fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile index ac13c5aecb..eb0a1f11a4 100644 --- a/examples/SampleApp/fastlane/Fastfile +++ b/examples/SampleApp/fastlane/Fastfile @@ -258,7 +258,7 @@ end desc 'Runs the Detox e2e tests. Options: platform (ios|android), configuration (release|debug), ' \ 'device (android: emu|att), skip_build, test (a test file or -t filter), local_server, mock_server_branch' lane :test_e2e do |options| - next unless is_check_required(sources: @impact[:e2e][:sources], force_check: @force_check, required_checks: @impact[:e2e][:checks]) + # next unless is_check_required(sources: @impact[:e2e][:sources], force_check: @force_check, required_checks: @impact[:e2e][:checks]) platform = options[:platform] || 'ios' device = platform == 'ios' ? 'sim' : (options[:device] || 'emu') From cff6aad413572a2daac5d8b4023ed8d2531706ad Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 18:37:43 +0100 Subject: [PATCH 11/16] ci(e2e): drop iOS launch permissions and pre-boot the simulator --- .github/workflows/e2e-test.yml | 19 +++++++++++++++++++ examples/E2ETestApp/.detoxrc.js | 5 ++++- examples/E2ETestApp/e2e/README.md | 5 +++++ .../E2ETestApp/e2e/support/StreamTestCase.ts | 4 +++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 8ac3f88054..75ce10086a 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -72,6 +72,7 @@ jobs: timeout-minutes: 60 env: DETOX_IOS_DEVICE: "iPhone 17 Pro" + IOS_VERSION: "26.2" XCODE_VERSION: "26.2" APP_PATH: examples/E2ETestApp/ios/build/detox/Build/Products/Release-iphonesimulator/E2ETestApp.app MOCK_SERVER_BUNDLE_PATH: ${{ github.workspace }}/../mock-server-gems @@ -85,6 +86,18 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.XCODE_VERSION }} + # A fresh simulator takes minutes to boot on CI; boot it now so that overlaps with setup. + - name: Boot simulator + run: | + runtime="com.apple.CoreSimulator.SimRuntime.iOS-${IOS_VERSION//./-}" + udid=$(xcrun simctl list devices available -j | + jq -r --arg rt "$runtime" --arg name "$DETOX_IOS_DEVICE" '.devices[$rt][]? | select(.name == $name) | .udid' | head -1) + if [ -z "$udid" ]; then + echo "::warning::No $DETOX_IOS_DEVICE on iOS $IOS_VERSION; Detox will pick and boot a simulator itself" + exit 0 + fi + echo "DETOX_IOS_UDID=$udid" >> "$GITHUB_ENV" + nohup xcrun simctl boot "$udid" > /dev/null 2>&1 & - uses: ./.github/actions/ruby-cache # --- JS dependencies --- @@ -177,6 +190,12 @@ jobs: restore-keys: | ${{ runner.os }}-mock-server-gems- + - name: Wait for the simulator + if: env.DETOX_IOS_UDID != '' + timeout-minutes: 15 + # `-b` boots it if the background boot never started; it errors (405) when the device is + # already booted, and then the plain call returns at once. + run: xcrun simctl bootstatus "$DETOX_IOS_UDID" -b || xcrun simctl bootstatus "$DETOX_IOS_UDID" - name: Run e2e tests working-directory: examples/SampleApp run: bundle exec fastlane test_e2e platform:ios skip_build:true mock_server_branch:"$MOCK_SERVER_BRANCH" diff --git a/examples/E2ETestApp/.detoxrc.js b/examples/E2ETestApp/.detoxrc.js index 7bc7b438c3..2e090e80c7 100644 --- a/examples/E2ETestApp/.detoxrc.js +++ b/examples/E2ETestApp/.detoxrc.js @@ -42,7 +42,10 @@ module.exports = { devices: { simulator: { type: 'ios.simulator', - device: { type: process.env.DETOX_IOS_DEVICE || 'iPhone 16 Pro' }, + // CI pins the simulator it boots early (DETOX_IOS_UDID), so Detox reuses it. + device: process.env.DETOX_IOS_UDID + ? { id: process.env.DETOX_IOS_UDID } + : { type: process.env.DETOX_IOS_DEVICE || 'iPhone 16 Pro' }, }, emulator: { type: 'android.emulator', diff --git a/examples/E2ETestApp/e2e/README.md b/examples/E2ETestApp/e2e/README.md index b4423d77fd..07db76c1c0 100644 --- a/examples/E2ETestApp/e2e/README.md +++ b/examples/E2ETestApp/e2e/README.md @@ -92,6 +92,11 @@ What CI caches (restored on PRs, filled by the runs on `develop`): on RN's UIManager shadow queue, whose pending-work counter underflows to 2^64-1, so the app looks busy until the test times out. Robots wait explicitly instead, and taps retry while the target is not hittable yet (e.g. under the keyboard as it animates in). Android keeps Espresso's synchronization. +- Don't pass `permissions` to `device.launchApp` unless a test needs one: on iOS Detox applies each with + `applesimutils --restartSB`, which restarts SpringBoard on every launch. That cost 1.5–3 min per test on CI (hitting the + hook timeout) and ~15s locally. +- CI boots the simulator at the start of the job and pins it with `DETOX_IOS_UDID` (`.detoxrc.js`), so the boot + overlaps with setup instead of delaying the first test by ~7 min. - `detox test --reuse` does not reinstall the app. After rebuilding, run without it, or you test the old binary. - Yarn's `enableScripts: false` skips Detox's postinstall; run `yarn detox build-framework-cache` once on a new machine. - The SDK's shared native sources are synced into `stream-chat-react-native` and gitignored; `yarn e2e:build` and diff --git a/examples/E2ETestApp/e2e/support/StreamTestCase.ts b/examples/E2ETestApp/e2e/support/StreamTestCase.ts index d0a61d85f7..783e97d246 100644 --- a/examples/E2ETestApp/e2e/support/StreamTestCase.ts +++ b/examples/E2ETestApp/e2e/support/StreamTestCase.ts @@ -42,7 +42,9 @@ export const setUpStreamTestCase = (): StreamTestCase => { detoxEnableSynchronization: device.getPlatform() === 'ios' ? 0 : 1, }, newInstance: true, - permissions: { notifications: 'YES', photos: 'YES' }, + // No `permissions`: the app asks for none, and on iOS Detox applies each one with + // `applesimutils --restartSB`, restarting SpringBoard per permission on every launch + // (minutes per test on CI). }); }); From 40d9faad45d1f97284380d1d63e9b1bf468291c5 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 18:57:22 +0100 Subject: [PATCH 12/16] ci(e2e): keep the Detox config out of the app cache keys --- .github/workflows/e2e-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 75ce10086a..d6a6582932 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -123,7 +123,7 @@ jobs: uses: actions/cache/restore@v5 with: path: ${{ env.APP_PATH }} - key: e2e-ios-app-xcode${{ env.XCODE_VERSION }}-${{ hashFiles('yarn.lock', 'package/package.json', 'package/src/**', 'package/shared-native/**', 'package/native-package/package.json', 'package/native-package/src/**', 'package/native-package/ios/**', 'package/native-package/*.podspec', 'examples/SampleApp/Gemfile.lock', 'examples/E2ETestApp/*.js', 'examples/E2ETestApp/*.json', 'examples/E2ETestApp/*.tsx', 'examples/E2ETestApp/src/**', 'examples/E2ETestApp/ios/**', '!examples/E2ETestApp/ios/build/**', '!examples/E2ETestApp/ios/Pods/**') }} + key: e2e-ios-app-xcode${{ env.XCODE_VERSION }}-${{ hashFiles('yarn.lock', 'package/package.json', 'package/src/**', 'package/shared-native/**', 'package/native-package/package.json', 'package/native-package/src/**', 'package/native-package/ios/**', 'package/native-package/*.podspec', 'examples/SampleApp/Gemfile.lock', 'examples/E2ETestApp/*.js', '!examples/E2ETestApp/.detoxrc.js', 'examples/E2ETestApp/*.json', 'examples/E2ETestApp/*.tsx', 'examples/E2ETestApp/src/**', 'examples/E2ETestApp/ios/**', '!examples/E2ETestApp/ios/build/**', '!examples/E2ETestApp/ios/Pods/**') }} - name: Set up ccache if: steps.app.outputs.cache-hit != 'true' uses: hendrikmuhs/ccache-action@v1.2 @@ -261,7 +261,7 @@ jobs: uses: actions/cache/restore@v5 with: path: ${{ env.APK_PATHS }} - key: e2e-android-apk-${{ hashFiles('yarn.lock', 'package/package.json', 'package/src/**', 'package/shared-native/**', 'package/native-package/package.json', 'package/native-package/src/**', 'package/native-package/android/**', '!package/native-package/android/build/**', 'examples/E2ETestApp/*.js', 'examples/E2ETestApp/*.json', 'examples/E2ETestApp/*.tsx', 'examples/E2ETestApp/src/**', 'examples/E2ETestApp/android/**', '!examples/E2ETestApp/android/**/build/**', '!examples/E2ETestApp/android/.gradle/**') }} + key: e2e-android-apk-${{ hashFiles('yarn.lock', 'package/package.json', 'package/src/**', 'package/shared-native/**', 'package/native-package/package.json', 'package/native-package/src/**', 'package/native-package/android/**', '!package/native-package/android/build/**', 'examples/E2ETestApp/*.js', '!examples/E2ETestApp/.detoxrc.js', 'examples/E2ETestApp/*.json', 'examples/E2ETestApp/*.tsx', 'examples/E2ETestApp/src/**', 'examples/E2ETestApp/android/**', '!examples/E2ETestApp/android/**/build/**', '!examples/E2ETestApp/android/.gradle/**') }} # Built before the emulator boots so the emulator doesn't idle through the Gradle build. - name: Build app if: steps.apk.outputs.cache-hit != 'true' From be2d3da546eb84251fb9d2963d43fd96ca305ea3 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 19:08:49 +0100 Subject: [PATCH 13/16] Uncomment stuff --- .github/workflows/e2e-test.yml | 2 +- examples/SampleApp/fastlane/Fastfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index d6a6582932..aaea06b21a 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -67,7 +67,7 @@ jobs: ios: needs: check - # if: needs.check.outputs.required == 'true' && github.event_name == 'workflow_dispatch' && inputs.ios + if: needs.check.outputs.required == 'true' && github.event_name == 'workflow_dispatch' && inputs.ios runs-on: macos-15 timeout-minutes: 60 env: diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile index eb0a1f11a4..ac13c5aecb 100644 --- a/examples/SampleApp/fastlane/Fastfile +++ b/examples/SampleApp/fastlane/Fastfile @@ -258,7 +258,7 @@ end desc 'Runs the Detox e2e tests. Options: platform (ios|android), configuration (release|debug), ' \ 'device (android: emu|att), skip_build, test (a test file or -t filter), local_server, mock_server_branch' lane :test_e2e do |options| - # next unless is_check_required(sources: @impact[:e2e][:sources], force_check: @force_check, required_checks: @impact[:e2e][:checks]) + next unless is_check_required(sources: @impact[:e2e][:sources], force_check: @force_check, required_checks: @impact[:e2e][:checks]) platform = options[:platform] || 'ios' device = platform == 'ios' ? 'sim' : (options[:device] || 'emu') From 62684e8b884ba67c6b974d7cb51c77b3a6f8a108 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Tue, 20 Oct 2026 19:16:52 +0100 Subject: [PATCH 14/16] ci(e2e): stop running on develop pushes --- .github/workflows/e2e-test.yml | 11 ++++------- examples/E2ETestApp/e2e/README.md | 9 +++++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index aaea06b21a..e71792358f 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -3,12 +3,6 @@ name: E2E Tests on: pull_request: - # Runs on develop too: GitHub only shares caches from the base branch, so these runs fill the - # caches every PR restores from. - push: - branches: - - develop - workflow_dispatch: inputs: ios: @@ -234,8 +228,11 @@ jobs: with: distribution: "zulu" java-version: "21" - # Caches Gradle's dependencies, wrapper and build cache; written on develop, read on PRs. + # Caches Gradle's dependencies, wrapper and build cache. It only writes on the default branch by + # default, and this workflow doesn't run there, so let PR runs write: their reruns reuse it. - uses: gradle/actions/setup-gradle@v4 + with: + cache-read-only: false - uses: ./.github/actions/ruby-cache # --- JS dependencies --- diff --git a/examples/E2ETestApp/e2e/README.md b/examples/E2ETestApp/e2e/README.md index 07db76c1c0..972ec43db5 100644 --- a/examples/E2ETestApp/e2e/README.md +++ b/examples/E2ETestApp/e2e/README.md @@ -74,16 +74,17 @@ API 34 x86_64 emulator (`android.att.release`). Both call `fastlane test_e2e ski `--retries 2 --headless` and keeps failure logs and screenshots in `artifacts/`. Those and the mock server logs are uploaded when a job fails. -What CI caches (restored on PRs, filled by the runs on `develop`): +What CI caches. The workflow doesn't run on `develop`, and GitHub only lets a PR restore caches from its own branch or +the base branch, so each PR fills its own: its first run builds cold, and later pushes to it reuse the caches. - **The built app** (`.app`; release + test `.apk`), keyed on the app and SDK sources. A change that only touches `e2e/` reuses the binary and skips pods, Gradle and Xcode entirely. - `node_modules` of `yarn workspaces focus e2etestapp`, so only the e2e app's dependency tree is installed. - iOS: CocoaPods, ccache (the RN ccache wrappers need `CCACHE_BINARY` in the environment, which the workflow sets), the Detox framework. -- Android: Gradle (`gradle/actions/setup-gradle`, `org.gradle.caching`), a single ABI (`x86_64`). The emulator matches the - native Android SDK's e2e jobs (API 34 AOSP image, `pixel` profile, no snapshot): the Google APIs image plus a cached - snapshot froze the emulator on the 5th app launch, two runs in a row. +- Android: Gradle (`gradle/actions/setup-gradle` with `cache-read-only: false`, `org.gradle.caching`), a single ABI + (`x86_64`). The emulator matches the native Android SDK's e2e jobs (API 36 AOSP image, `pixel` profile, no snapshot); + on API 34 (Google APIs image, then AOSP) it died mid-run with no crash output. - The mock server's gems, keyed on the mock server revision. ## Gotchas From 109051d9eea548577b4e66d57c5782f52b99a9cf Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 22 Oct 2026 10:11:02 +0100 Subject: [PATCH 15/16] fix(fastlane): restore the SDK size gate and fix the README's API level --- examples/E2ETestApp/e2e/README.md | 2 +- examples/SampleApp/fastlane/Fastfile | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/E2ETestApp/e2e/README.md b/examples/E2ETestApp/e2e/README.md index 972ec43db5..5878cd7a7d 100644 --- a/examples/E2ETestApp/e2e/README.md +++ b/examples/E2ETestApp/e2e/README.md @@ -70,7 +70,7 @@ only when the PR touches those sources, or when the last change to them hasn't p job to require in branch protection: it passes when the e2e jobs pass or are skipped as not required. iOS runs on `macos-15` (Xcode 26.2, iPhone 17 Pro, `ios.sim.release`). Android runs on `ubuntu-latest` with KVM and an -API 34 x86_64 emulator (`android.att.release`). Both call `fastlane test_e2e skip_build:true`, which on CI adds +API 36 x86_64 emulator (`android.att.release`). Both call `fastlane test_e2e skip_build:true`, which on CI adds `--retries 2 --headless` and keeps failure logs and screenshots in `artifacts/`. Those and the mock server logs are uploaded when a job fails. diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile index ac13c5aecb..72bca085af 100644 --- a/examples/SampleApp/fastlane/Fastfile +++ b/examples/SampleApp/fastlane/Fastfile @@ -304,6 +304,8 @@ lane :frameworks_sizes do end lane :show_frameworks_sizes do |options| + next unless is_check_required(sources: @impact[:size][:sources], force_check: @force_check) + sizes = options[:sizes] || frameworks_sizes show_sdk_size(branch_sizes: sizes, github_repo: github_repo, size_ext: sdk_size_ext) update_img_shields_sdk_sizes(sizes: sizes, open_pr: options[:open_pr]) if options[:update_readme] @@ -324,6 +326,9 @@ lane :impact_analysis do e2e: { sources: ['package/', 'examples/E2ETestApp/', 'examples/SampleApp/fastlane/', 'package.json'], checks: ['Test E2E Result'] + }, + size: { + sources: ['package/', 'package.json'] } } end From 0ce18c0e200d2bcd102199420c01352e0d9c3d38 Mon Sep 17 00:00:00 2001 From: Alexey Alter-Pesotskiy Date: Thu, 22 Oct 2026 12:50:00 +0100 Subject: [PATCH 16/16] ci(e2e): dismiss Android ANR dialogs before the tests --- examples/E2ETestApp/e2e/README.md | 5 +++ .../e2e/support/AndroidSystemDialogs.ts | 43 +++++++++++++++++++ .../E2ETestApp/e2e/support/StreamTestCase.ts | 5 +++ examples/SampleApp/fastlane/Fastfile | 11 +++++ 4 files changed, 64 insertions(+) create mode 100644 examples/E2ETestApp/e2e/support/AndroidSystemDialogs.ts diff --git a/examples/E2ETestApp/e2e/README.md b/examples/E2ETestApp/e2e/README.md index 5878cd7a7d..7088447cf3 100644 --- a/examples/E2ETestApp/e2e/README.md +++ b/examples/E2ETestApp/e2e/README.md @@ -98,6 +98,11 @@ the base branch, so each PR fills its own: its first run builds cold, and later hook timeout) and ~15s locally. - CI boots the simulator at the start of the job and pins it with `DETOX_IOS_UDID` (`.detoxrc.js`), so the boot overlaps with setup instead of delaying the first test by ~7 min. +- A freshly booted CI emulator can show "Quickstep isn't responding" (a launcher ANR). The dialog takes window focus, and + every Espresso action then fails with "Waited for the root of the view hierarchy to have window focus". The + `test_e2e` lane hides error dialogs and waits up to 60s for the emulator to settle, and `StreamTestCase` dismisses + any ANR dialog before each Android launch (`support/AndroidSystemDialogs.ts`). Read the focus from + `dumpsys window`: `dumpsys window windows` no longer prints `mCurrentFocus`. - `detox test --reuse` does not reinstall the app. After rebuilding, run without it, or you test the old binary. - Yarn's `enableScripts: false` skips Detox's postinstall; run `yarn detox build-framework-cache` once on a new machine. - The SDK's shared native sources are synced into `stream-chat-react-native` and gitignored; `yarn e2e:build` and diff --git a/examples/E2ETestApp/e2e/support/AndroidSystemDialogs.ts b/examples/E2ETestApp/e2e/support/AndroidSystemDialogs.ts new file mode 100644 index 0000000000..a7d2c61dcd --- /dev/null +++ b/examples/E2ETestApp/e2e/support/AndroidSystemDialogs.ts @@ -0,0 +1,43 @@ +// Node built-ins, typed locally: the e2e project's types are limited to `detox/globals`. +declare const process: { env: Record }; +declare const require: (module: 'child_process') => { + execFileSync: (file: string, args: string[], options: { encoding: 'utf8' }) => string; +}; + +const { execFileSync } = require('child_process'); + +const sdkRoot = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT; +const adbPath = sdkRoot ? `${sdkRoot}/platform-tools/adb` : 'adb'; + +const adb = (...args: string[]) => { + const serial = process.env.DETOX_ADB_NAME; + const deviceArgs = serial && !serial.includes('*') ? ['-s', serial] : []; + return execFileSync(adbPath, [...deviceArgs, ...args], { encoding: 'utf8' }); +}; + +/** + * Dismisses an " isn't responding" dialog, if one is showing. On a slow CI emulator the + * launcher (Quickstep) can hit an ANR; its dialog takes window focus, and Espresso then fails every + * test waiting for the app's window to get it. Same handling as the `wait_android_emu_idle` fastlane + * action: Enter picks "Close app", except for System UI, where it picks "Wait". + */ +export const dismissAnrDialog = () => { + try { + const focus = adb('shell', 'dumpsys', 'window') + .split('\n') + .find( + (line) => line.includes('mCurrentFocus') && line.includes('Application Not Responding'), + ); + if (!focus) { + return; + } + console.warn(`Dismissing an ANR dialog: ${focus.trim()}`); + if (focus.includes('com.android.systemui')) { + adb('shell', 'input', 'keyevent', 'KEYCODE_DPAD_DOWN'); + } + adb('shell', 'input', 'keyevent', 'KEYCODE_ENTER'); + } catch (error) { + // Best effort: a failed check must not fail the test on its own. + console.warn(`Could not check for ANR dialogs: ${String(error)}`); + } +}; diff --git a/examples/E2ETestApp/e2e/support/StreamTestCase.ts b/examples/E2ETestApp/e2e/support/StreamTestCase.ts index 783e97d246..4a4ddb1b9a 100644 --- a/examples/E2ETestApp/e2e/support/StreamTestCase.ts +++ b/examples/E2ETestApp/e2e/support/StreamTestCase.ts @@ -1,5 +1,7 @@ import { afterEach, beforeEach, expect as jestExpect } from '@jest/globals'; +import { dismissAnrDialog } from './AndroidSystemDialogs'; + import { MockServer } from '../mock-server/MockServer'; import { BackendRobot, ParticipantRobot, UserRobot } from '../robots'; @@ -32,6 +34,9 @@ export const setUpStreamTestCase = (): StreamTestCase => { beforeEach(async () => { await testCase.mockServer.start(currentTestName()); + if (device.getPlatform() === 'android') { + dismissAnrDialog(); + } await device.launchApp({ launchArgs: { MOCK_SERVER_URL: testCase.mockServer.appUrl, diff --git a/examples/SampleApp/fastlane/Fastfile b/examples/SampleApp/fastlane/Fastfile index 72bca085af..c491a81025 100644 --- a/examples/SampleApp/fastlane/Fastfile +++ b/examples/SampleApp/fastlane/Fastfile @@ -255,6 +255,15 @@ lane :e2e_pod_install do end end +# A freshly booted CI emulator is slow enough for the launcher to hit an ANR ("Quickstep isn't +# responding"). The dialog takes window focus, and Espresso then fails every test waiting for it. +private_lane :prepare_android_emulator do + sh('adb shell settings put global hide_error_dialogs 1') + # Gives the post-boot CPU load a minute to settle, dismissing ANR dialogs meanwhile. Capped: the + # load can stay above the threshold for minutes, and each test dismisses ANR dialogs too. + wait_android_emu_idle(timeout: 60) +end + desc 'Runs the Detox e2e tests. Options: platform (ios|android), configuration (release|debug), ' \ 'device (android: emu|att), skip_build, test (a test file or -t filter), local_server, mock_server_branch' lane :test_e2e do |options| @@ -265,6 +274,8 @@ lane :test_e2e do |options| detox_config = "#{platform}.#{device}.#{options[:configuration] || 'release'}" start_mock_server(local_server: options[:local_server], branch: options[:mock_server_branch]) + # An attached emulator is already booted; Detox boots `emu` devices itself. + prepare_android_emulator if platform == 'android' && device == 'att' begin Dir.chdir(e2e_app_path) do