Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
916f810
test: add React Native scene notification E2E
Shahroz16 Aug 25, 2026
51efeaa
test: remove scene notification timing race
Shahroz16 Aug 25, 2026
1e1dfb7
test: harden React Native scene E2E diagnostics
Shahroz16 Aug 25, 2026
8aa7d13
test: honor configured E2E simulator
Shahroz16 Aug 25, 2026
98b8f9c
test: boot resolved E2E simulator
Shahroz16 Aug 25, 2026
3cc4b07
test: add React Native remote push E2E (MBL-2233)
Shahroz16 Aug 25, 2026
4eecbe4
test: harden React Native APNs E2E identity setup
Shahroz16 Aug 25, 2026
a1dfcbd
test: isolate React Native APNs E2E campaign
Shahroz16 Aug 25, 2026
ae9a7f6
Merge branch 'main' into codex/mbl-2234-rn-maestro-e2e
Shahroz16 Aug 26, 2026
c334780
ci: run scene E2E for lifecycle changes
Shahroz16 Aug 26, 2026
5f5f16f
test: activate APNs notification through system action
Shahroz16 Aug 26, 2026
dc38554
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
d3e5255
test: pin remote push Maestro version
Shahroz16 Aug 26, 2026
5f0f97f
test: harden React Native scene notification E2E
Shahroz16 Aug 26, 2026
866357f
Merge remote-tracking branch 'origin/codex/mbl-2234-rn-maestro-e2e' i…
Shahroz16 Aug 26, 2026
4f061ee
test: harden React Native remote push release check
Shahroz16 Aug 26, 2026
3c38c7f
ci: scope React Native scene E2E triggers
Shahroz16 Aug 26, 2026
a307e06
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
5317ce2
test: guard React Native remote push interactions
Shahroz16 Aug 26, 2026
cc65309
test: pin React Native scene E2E runtime
Shahroz16 Aug 26, 2026
a1f13f1
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
81ef4d2
test: retry React Native push setup safely
Shahroz16 Aug 26, 2026
972728b
ci: cover React Native scene harness dependencies
Shahroz16 Aug 26, 2026
3881158
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
e0e9384
test: classify React Native scene E2E failures
Shahroz16 Aug 26, 2026
4f93f0c
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
f606a8b
ci: cover scene test bridge dependencies
Shahroz16 Aug 26, 2026
80dbbbd
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
39637e1
test: harden React Native scene notification E2E
Shahroz16 Aug 26, 2026
6a73be8
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
eb4f5ce
test: retain React Native device diagnostics
Shahroz16 Aug 26, 2026
c729157
Merge branch 'codex/mbl-2234-rn-maestro-e2e' into codex/mbl-2233-rn-r…
Shahroz16 Aug 26, 2026
60d90d9
Merge remote-tracking branch 'origin/main' into codex/mbl-2233-rn-rem…
Shahroz16 Aug 26, 2026
cdde691
Merge branch 'main' into codex/mbl-2233-rn-remote-push-e2e
Shahroz16 Aug 27, 2026
ebe9c6b
ci: add label-gated remote push check
Shahroz16 Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/remote-push-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: React Native remote push E2E

on:
pull_request:
types: [labeled]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: react-native-remote-push-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
remote-push:
name: APNs delivery and opened attribution
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.action == 'labeled' &&
github.event.label.name == 'run-remote-push-e2e' &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]')
runs-on: xcode-27
timeout-minutes: 60

steps:
- name: Check out React Native SDK
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Verify Xcode 27 toolchain
uses: customerio/mobile-ci-tools/github-actions/ios/report-toolchain/v1@main
with:
expected-xcode-major: '27'
expected-ios-sdk-major: '27'

- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '24'
cache: npm

- name: Set up Ruby
uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
with:
ruby-version: '3.4'
bundler-cache: true
working-directory: example

- name: Configure APNs sample
shell: bash
env:
RN_CDP_API_KEY: ${{ secrets.CUSTOMERIO_APN_WORKSPACE_CDP_API_KEY }}
RN_SITE_ID: ${{ secrets.CUSTOMERIO_APN_WORKSPACE_SITE_ID }}
run: |
set -euo pipefail
cp -R example/ios/SampleApp.xcodeproj.tracked example/ios/SampleApp.xcodeproj
cp example/src/env.ts.sample example/src/env.ts
ruby -e '
path = "example/src/env.ts"
text = File.read(path)
.gsub("<CDP_API_KEY>", ENV.fetch("RN_CDP_API_KEY"))
.gsub("<SITE_ID>", ENV.fetch("RN_SITE_ID"))
File.write(path, text)
'

- name: Install pinned Maestro
shell: bash
run: |
set -euo pipefail
maestro_zip="$RUNNER_TEMP/maestro-2.8.0.zip"
maestro_dir="$RUNNER_TEMP/maestro-2.8.0"
curl -fsSL \
https://github.com/mobile-dev-inc/Maestro/releases/download/cli-2.8.0/maestro.zip \
-o "$maestro_zip"
echo 'b3e561161904fb391875ca5834d5b22cf0b01c052dd1b408ad83e30d8f8951b3 '"$maestro_zip" \
| shasum -a 256 -c -
unzip -q "$maestro_zip" -d "$maestro_dir"
echo "$maestro_dir/maestro/bin" >> "$GITHUB_PATH"

- name: Run remote push E2E
shell: bash
env:
CIO_E2E_ARTIFACT_DIR: ${{ runner.temp }}/react-native-remote-push-e2e
MAESTRO_APP_API_KEY: ${{ secrets.MOBILE_E2E_RN_APP_API_KEY }}
run: .maestro/run_remote_push.sh

- name: Upload sanitized evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: react-native-remote-push-e2e
path: ${{ runner.temp }}/react-native-remote-push-e2e
if-no-files-found: warn
retention-days: 14
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
eslint_report.json
.maestro/.env

# Config files
env.js
Expand Down Expand Up @@ -95,4 +96,4 @@ api-extractor-output/temp/
## iOS Example app
## This file will be auto linked based
## on the target app
ios/Google-Service-Info.plist
ios/Google-Service-Info.plist
1 change: 1 addition & 0 deletions .maestro/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MAESTRO_APP_API_KEY=paste-app-api-key-here
28 changes: 27 additions & 1 deletion .maestro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,36 @@ JavaScript listener and `CustomerIO.initialize` are ready.
The checked-in payload uses `simctl push` so the destination is deterministic
and does not require changing a shared Customer.io campaign. It validates the
client routing path, not backend `sent`, `delivered`, or `opened` metrics. Those
metrics remain a separate remote E2E lane using a real workspace and APNs.
metrics are covered separately by `run_remote_push.sh`, which uses the existing
APN sample, its notification service extension, a real workspace, and APNs.

Run with Maestro 2.8.0 and a booted iPhone simulator:

```bash
.maestro/run_scene_push.sh
```

Run the remote APNs loop after configuring the APN sample's ignored
`example/src/env.ts` file. The runner gives the notification service extension
the same APN workspace key for the build and restores its ignored config when
the run ends. Put the `Mobile SDK Maestro E2E` App API key from the same
`Mobile: React Native` workspace in `.maestro/.env` as shown by
`.maestro/.env.example`. Simulator APNs tokens use the sandbox environment, so
temporarily enable **Send all push notifications to sandbox** for the workspace
and restore its previous setting after the run. Use Maestro 2.8.0, then run:

```bash
.maestro/run_remote_push.sh
```

The remote flow identifies a fresh customer in the `Mobile: React Native`
workspace, sends the `maestro_rn_apns_e2e` event used by its dedicated Maestro
automation (campaign 68), taps the real system notification, and requires
matching backend `delivered` and `opened` metrics. It is intentionally local or
trusted-CI only; pull requests do not receive the required workspace credential.

The activation gate requires an iPhone 17 Pro simulator. Notification Center
is not exposed reliably through iOS accessibility, so the flow selects the
notification and its system Open action by coordinate. The runner accepts that
interaction only when the app reopens and Customer.io records `opened` for the
exact delivered message.
16 changes: 16 additions & 0 deletions .maestro/configure_nse_env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env ruby

app_env_path, template_path, output_path = ARGV
abort 'usage: configure_nse_env.rb APP_ENV TEMPLATE OUTPUT' unless output_path

app_env = File.read(app_env_path)
primary_config = app_env.match(/primary:\s*\{(?<config>.*?)\}\s+satisfies/m)
abort 'APN sample primary config was not found' unless primary_config

api_key = primary_config[:config].match(/API_KEY:\s*'(?<value>[^']+)'/)&.[](:value)
abort 'APN sample CDP API key was not found' if api_key.nil? || api_key.empty? || api_key.include?('<')

template = File.read(template_path)
abort 'notification service extension template placeholder was not found' unless template.include?('<API_KEY>')

File.write(output_path, template.sub('<API_KEY>', api_key))
197 changes: 197 additions & 0 deletions .maestro/remote_push.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
appId: ${APP_ID}
name: React Native APNs delivery and opened attribution
---
- runScript:
file: ../scripts/setup_run.js

- launchApp:
clearState: false

- extendedWaitUntil:
visible:
id: "login_button"
timeout: 15000
- retry:
maxRetries: 2
commands:
- runFlow:
when:
visible: "Get 20% off your first order"
commands:
- tapOn: "✕"
- tapOn:
id: "first_name_input"
- eraseText
- inputText: "Maestro Campaign Tester"
- tapOn:
id: "email_input"
- eraseText
- inputText: ${output.email}
- copyTextFrom:
id: "email_input"
- assertTrue: ${maestro.copiedText === output.email}
- hideKeyboard
- runFlow:
when:
visible: "Get 20% off your first order"
commands:
- tapOn: "✕"
- tapOn:
id: "login_button"

- runFlow:
when:
visible: "Get 20% off your first order"
commands:
- tapOn: "✕"

- retry:
maxRetries: 2
commands:
- runFlow:
when:
visible: "Get 20% off your first order"
commands:
- tapOn: "✕"
- scrollUntilVisible:
element:
id: "push_permission_button"
direction: DOWN
timeout: 10000
- runFlow:
when:
visible: "Get 20% off your first order"
commands:
- tapOn: "✕"
- tapOn:
id: "push_permission_button"
- tapOn:
text: "^Allow$"
optional: true

- runScript:
file: ../scripts/assert_customer_activity.js
env:
RUN_EMAIL: ${output.email}
EXPECTED_ACTIVITY_TYPE: "device_change"
EXPECTED_PROPERTY_NAME: "devices.0.platform"
EXPECTED_PROPERTY_VALUE: "ios"
MIN_TIMESTAMP_SECONDS: ${output.started_at_seconds}
MAX_WAIT_MS: "45000"
- assertTrue: ${output.assert_ok === "true"}

- retry:
maxRetries: 1
commands:
- runFlow:
when:
visible:
id: "BackButton"
commands:
- tapOn:
id: "BackButton"
- scrollUntilVisible:
element:
id: "custom_event_button"
direction: UP
timeout: 15000
- tapOn:
id: "custom_event_button"
- extendedWaitUntil:
visible:
id: "event_name_input"
timeout: 10000
- runFlow:
when:
visible: "Get 20% off your first order"
commands:
- tapOn: "✕"
- tapOn:
id: "event_name_input"
- inputText: "maestro_rn_apns_e2e"
- tapOn:
id: "property_name_input"
- inputText: "run_id"
- tapOn:
id: "property_value_input"
- inputText: ${output.run_id}
- hideKeyboard
- scrollUntilVisible:
element:
id: "send_event_button"
direction: DOWN
timeout: 10000
- runFlow:
when:
visible: "Get 20% off your first order"
commands:
- tapOn: "✕"
- tapOn:
id: "send_event_button"

- runScript:
file: ../scripts/assert_customer_activity.js
env:
RUN_EMAIL: ${output.email}
EXPECTED_ACTIVITY_TYPE: "event"
EXPECTED_ACTIVITY_NAME: "maestro_rn_apns_e2e"
EXPECTED_PROPERTY_NAME: "run_id"
EXPECTED_PROPERTY_VALUE: ${output.run_id}
MIN_TIMESTAMP_SECONDS: ${output.started_at_seconds}
MAX_WAIT_MS: "45000"
- assertTrue: ${output.assert_ok === "true"}
- pressKey: Home

- runScript:
file: ../scripts/assert_message_delivered.js
env:
RUN_EMAIL: ${output.email}
EXPECTED_TYPE: "push"
MIN_METRIC: "delivered"
CAMPAIGN_ID: "68"
MAX_WAIT_MS: "60000"
- assertTrue: ${output.assert_ok === "true"}
- assertTrue: ${output.message_id.length > 0}

- runScript:
file: ../scripts/wait.js
env:
WAIT_MS: "5000"
- waitForAnimationToEnd:
timeout: 3000
- swipe:
start: 20%, 1%
end: 20%, 90%
duration: 900
- waitForAnimationToEnd:
timeout: 2000
- takeScreenshot: ${ARTIFACT_DIR}/remote_push_notification_center
# Notification Center is not exposed reliably through iOS accessibility.
# Select the notification and its system Open action using the proven iPhone 17
# Pro coordinates. The exact message-id assertion below prevents a false pass.
- tapOn:
point: "50%,82%"
- waitForAnimationToEnd:
timeout: 4000
- runFlow:
when:
notVisible:
id: "send_event_button"
commands:
- tapOn:
point: "15%,82%"
- extendedWaitUntil:
visible:
id: "send_event_button"
timeout: 15000

- runScript:
file: ../scripts/assert_message_delivered.js
env:
RUN_EMAIL: ${output.email}
EXPECTED_TYPE: "push"
MIN_METRIC: "opened"
CAMPAIGN_ID: "68"
EXPECTED_MESSAGE_ID: ${output.message_id}
MAX_WAIT_MS: "60000"
- assertTrue: ${output.assert_ok === "true"}
Loading
Loading