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

Filter by extension

Filter by extension


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

# Runs the example app's E2E suites: the Maestro flows in example/e2e/flows on
# an iOS simulator and an Android emulator, and the Playwright tests in
# example/e2e/web against the web build.
#
# The apps are release builds, so the JS bundle is embedded and no Metro server
# has to run next to the simulator or emulator.
#
# Flows tagged `sandbox` open the real PayPal flow, which needs a Braintree
# sandbox tokenization key with PayPal enabled. It comes from the
# PAYPAL_SANDBOX_TOKENIZATION_KEY secret, and those flows are skipped when the
# secret is not available, as on pull requests from forks.

on:
push:
branches:
- main
pull_request:

concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
MAESTRO_VERSION: "2.12.0"

jobs:
ios:
name: iOS (Maestro)
runs-on: macos-latest
timeout-minutes: 60
env:
EXPO_PUBLIC_PAYPAL_CLIENT_TOKEN: ${{ secrets.PAYPAL_SANDBOX_TOKENIZATION_KEY }}
HAS_SANDBOX_KEY: ${{ secrets.PAYPAL_SANDBOX_TOKENIZATION_KEY != '' }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup mise
uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5
- name: Setup pnpm
uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6
with:
cache: true
# For Maestro.
- name: Setup JDK
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6
with:
distribution: temurin
java-version: "17"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Maestro
run: |
curl -fsSL "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
# An iPhone on the newest iOS runtime the runner has, rather than a fixed
# model that a later runner image may no longer ship.
- name: Boot a simulator
run: |
UDID=$(xcrun simctl list devices available --json | node -e '
const { devices } = JSON.parse(require("fs").readFileSync(0, "utf8"));
const phones = Object.entries(devices)
.filter(([runtime]) => runtime.includes("iOS"))
.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
.flatMap(([, list]) => list.filter((d) => d.name.startsWith("iPhone")));
if (!phones.length) { console.error("No iPhone simulator available"); process.exit(1); }
console.log(phones[phones.length - 1].udid);')
xcrun simctl boot "$UDID"
echo "UDID=$UDID" >> "$GITHUB_ENV"
- name: Prebuild example app
run: pnpm exec expo prebuild --platform ios --no-install
working-directory: example
- name: Install pods
run: pod install
working-directory: example/ios
- name: Build example app
run: >-
xcodebuild
-workspace reactnativepaypalexample.xcworkspace
-scheme reactnativepaypalexample
-configuration Release
-sdk iphonesimulator
-destination "id=$UDID"
-derivedDataPath build
-quiet
working-directory: example/ios
- name: Install example app
run: xcrun simctl install "$UDID" example/ios/build/Build/Products/Release-iphonesimulator/reactnativepaypalexample.app
- name: Run E2E flows
run: pnpm e2e
- name: Run sandbox E2E flows
if: env.HAS_SANDBOX_KEY == 'true'
run: pnpm e2e:sandbox
- name: Upload Maestro results
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: maestro-ios
path: ~/.maestro/tests

android:
name: Android (Maestro)
runs-on: ubuntu-latest
timeout-minutes: 60
env:
EXPO_PUBLIC_PAYPAL_CLIENT_TOKEN: ${{ secrets.PAYPAL_SANDBOX_TOKENIZATION_KEY }}
HAS_SANDBOX_KEY: ${{ secrets.PAYPAL_SANDBOX_TOKENIZATION_KEY != '' }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup mise
uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5
- name: Setup pnpm
uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6
with:
cache: true
- name: Setup JDK
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6
with:
distribution: temurin
java-version: "17"
- name: Setup Android SDK
uses: android-actions/setup-android@be39fa834029ff78f1a44aa3bb0819b8fc2bd8fd # v4
- name: Setup Gradle
uses: gradle/actions/setup-gradle@8da46b539cd8fe6ab27ca75128919a18d757604c # v6
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Prebuild example app
run: pnpm exec expo prebuild --platform android --no-install
working-directory: example
# Only for the emulator's ABI: React Native's C++ for all four is most of
# the build time. Release builds are signed with the debug keystore.
- name: Build example app
run: ./gradlew --console=plain :app:assembleRelease -PreactNativeArchitectures=x86_64
working-directory: example/android
- name: Install Maestro
run: |
curl -fsSL "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
- 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
# Each line of `script` runs in its own shell, so the sandbox step is a
# one-line `if`.
#
# The google_apis image's setup app often hangs on a CI first boot, and
# its "isn't responding" dialog then covers the app under test, so such
# dialogs are hidden. The logcat is kept for failures.
- name: Run E2E flows
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
with:
api-level: 35
arch: x86_64
target: google_apis
profile: pixel_7
script: |
adb shell settings put global hide_error_dialogs 1
adb logcat -c && (adb logcat -v time > logcat.txt &)
adb install example/android/app/build/outputs/apk/release/app-release.apk
pnpm e2e
if [ "$HAS_SANDBOX_KEY" = "true" ]; then pnpm e2e:sandbox; fi
- name: Upload Maestro results
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: maestro-android
path: |
~/.maestro/tests
logcat.txt

web:
name: Web (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Setup mise
uses: jdx/mise-action@b44d12e7d0d8ee7461bb893e1b1993500433185b # v5
- name: Setup pnpm
uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6
with:
cache: true
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium
working-directory: example
- name: Run E2E tests
run: pnpm e2e:web
- name: Upload Playwright traces
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: playwright-traces
path: example/test-results
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ Node comes from `.tool-versions` (`mise install`); run `corepack enable` once so
- `pnpm lint`: oxlint (type-aware) and `oxfmt --check`. `pnpm lint:fix` autofixes and formats.
- `pnpm lint:kotlin`: detekt on `android/src`. Needs a JDK on the path or `JAVA_HOME`.
- `pnpm test`: Jest (`--passWithNoTests`). One file: `pnpm test plugin/__tests__/withPaypal.test.js`; one test: `pnpm test -t "<name>"`.
- `pnpm e2e`: the Maestro flows in `example/e2e/flows` against the example app on the booted simulator or emulator. `pnpm e2e:sandbox` runs the flows tagged `sandbox`, which need a Braintree sandbox key. `pnpm e2e:web`: Playwright against the web build.
- `pnpm run docs`: typedoc into `docs/`. Spell out `run`: `pnpm docs` is a built-in pnpm command.
- `pnpm example start|ios|android|web`: run the example app (`start` is `expo start --dev-client`; `ios` and `android` are `expo run:ios` and `expo run:android`).

## Testing changes

- Jest covers only the config plugin (`plugin/__tests__/withPaypal.test.js`). The Swift and Kotlin code has no automated tests.
- Jest covers only the config plugin (`plugin/__tests__/withPaypal.test.js`). Kotlin unit tests (`android/src/test`) run in the Android workflow; the Swift code has none.
- E2E: Maestro flows in `example/e2e/flows` (shared steps in `example/e2e/subflows`) and Playwright tests in `example/e2e/web`, run by `.github/workflows/e2e.yml` on release builds. The flows tap the example's buttons by `testID` and match its `status` line (`<scenario>:<outcome per request>`, e.g. `concurrent:Failed,Failed`), so keep `example/App.tsx` and the flows in sync. A behaviour change should come with a flow.
- Flows tagged `sandbox` open the real PayPal flow. They need a Braintree sandbox tokenization key with PayPal enabled, built in as `EXPO_PUBLIC_PAYPAL_CLIENT_TOKEN` (`example/.env.local` locally, the `PAYPAL_SANDBOX_TOKENIZATION_KEY` secret on CI, where they are skipped without it). Rebuild the app after changing it.
- Maestro targets whichever device is booted: if a physical phone is also connected, pass `--device`. A debug build needs `pnpm example start` running.
- The example app is the check for iOS and Android. It needs a development build, because the native module is not in Expo Go: `pnpm example ios` or `pnpm example android` builds and installs one, and `pnpm example start` serves JS to it. Metro bundles the library from `src/`, so edits there reload without a rebuild; native changes need a new `ios` or `android` run.
- PayPal does not work on web. `pnpm example web` only checks that importing the package does not break a web bundle (see the comment in `.github/workflows/web.yml`).

Expand Down
26 changes: 26 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,30 @@ Remember to add tests for your change if possible. Run the unit tests by:
pnpm test
```

### End-to-end tests

The E2E tests drive the example app. The [Maestro](https://maestro.dev/) flows in `example/e2e/flows` run on iOS and Android, and the [Playwright](https://playwright.dev/) tests in `example/e2e/web` run against the web build. CI runs them all from `.github/workflows/e2e.yml`.

To run the Maestro flows, [install Maestro](https://docs.maestro.dev/getting-started/installing-maestro) (it needs a JDK), install the example app on a simulator or emulator with `pnpm example ios` or `pnpm example android`, keep `pnpm example start` running, and run:

```sh
pnpm e2e
```

The flows tagged `sandbox` open the real PayPal flow, so they need a [Braintree sandbox](https://sandbox.braintreegateway.com/) tokenization key for a merchant with PayPal enabled. Put it in `example/.env.local` (git-ignored), rebuild the app, and run them with `pnpm e2e:sandbox`:

```sh
EXPO_PUBLIC_PAYPAL_CLIENT_TOKEN=sandbox_xxxxxxxx_xxxxxxxxxxxxxxxx
```

On CI the key comes from the `PAYPAL_SANDBOX_TOKENIZATION_KEY` repository secret, and the sandbox flows are skipped without it.

To run the web tests, install Chromium once with `pnpm example exec playwright install chromium`, then:

```sh
pnpm e2e:web
```

To edit the Swift files, run `pnpm example ios` once to generate the native project, then open `example/ios/reactnativepaypalexample.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > react-native-paypal`.

To edit the Kotlin files, run `pnpm example android` once to generate the native project, then open `example/android` in Android Studio and find the source files at `reactnativepaypal` under `Android`.
Expand Down Expand Up @@ -104,6 +128,8 @@ The `package.json` file contains various scripts for common tasks:
- `pnpm lint:fix`: fix lint and formatting errors.
- `pnpm lint:kotlin`: lint the Kotlin files with detekt.
- `pnpm test`: run unit tests with Jest.
- `pnpm e2e`: run the Maestro E2E flows against the installed example app (`pnpm e2e:sandbox` for the flows that need a Braintree sandbox key).
- `pnpm e2e:web`: run the Playwright E2E tests against the web build.
- `pnpm run docs`: generate the API reference with typedoc.
- `pnpm example start`: start the Metro server for the example app.
- `pnpm example android`: run the example app on Android.
Expand Down
3 changes: 3 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ android {
defaultConfig {
versionCode 1
versionName packageJson.version
// Applied to the app's R8 run, which Expo's template turns on for release
// builds. See the file for why.
consumerProguardFiles 'consumer-rules.pro'
}
lint {
abortOnError false
Expand Down
8 changes: 8 additions & 0 deletions android/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Braintree depends on WorkManager, which resolves to Room 2.6.1. Room 2.6's own
# rule, `-keep class * extends androidx.room.RoomDatabase`, names no members, and
# R8's full mode (the AGP default) then keeps the class but not its no-arg
# constructor. WorkManager creates its database through that constructor by
# reflection, so a minified release build crashed at startup with
# `NoSuchMethodException: androidx.work.impl.WorkDatabase_Impl.<init> []`,
# before any JavaScript ran. Later Room versions keep the constructor themselves.
-keep class * extends androidx.room.RoomDatabase { <init>(); }
5 changes: 5 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ npm-debug.*
# generated native folders
/ios
/android

# Playwright
/test-results/
/playwright-report/
/blob-report/
Loading
Loading