diff --git a/.changeset/fix-remove-settings.bundle-and-add-it-again.md b/.changeset/fix-remove-settings.bundle-and-add-it-again.md new file mode 100644 index 0000000..8ccf521 --- /dev/null +++ b/.changeset/fix-remove-settings.bundle-and-add-it-again.md @@ -0,0 +1,5 @@ +--- +'react-native-legal': patch +--- + +RN Legal plugin will remove Settings.bundle from XCode project, and add it again diff --git a/.github/actions/installMaestro/action.yaml b/.github/actions/installMaestro/action.yaml new file mode 100644 index 0000000..3eb6f39 --- /dev/null +++ b/.github/actions/installMaestro/action.yaml @@ -0,0 +1,31 @@ +name: Install Maestro +description: Install Maestro + +inputs: + target-platform: + description: 'Target platform like ios or android' + required: true + +runs: + using: composite + steps: + - name: Install Maestro CLI + shell: bash + run: | + export MAESTRO_VERSION=1.40.3 + curl -Ls "https://get.maestro.mobile.dev" | bash + + - name: Conditionally install brew packages for iOS + shell: bash + run: | + if [[ "${{ inputs.target-platform }}" == "ios" ]]; then + echo "Installing brew packages for iOS..." + brew tap facebook/fb + brew install facebook/fb/idb-companion + else + echo "Skipping brew install because target platform is ${{ inputs.target-platform }}" + fi + + - name: Add Maestro to path + shell: bash + run: echo "${HOME}/.maestro/bin" >> $GITHUB_PATH diff --git a/.github/actions/uploadMaestroTestResults/action.yaml b/.github/actions/uploadMaestroTestResults/action.yaml new file mode 100644 index 0000000..13f8ec7 --- /dev/null +++ b/.github/actions/uploadMaestroTestResults/action.yaml @@ -0,0 +1,35 @@ +name: Upload Maestro test results +description: Upload Maestro test results as artifacts + +inputs: + id: + description: 'artifact ID' + required: true + +runs: + using: composite + steps: + - name: Test Report + uses: dorny/test-reporter@v2 + continue-on-error: true + with: + name: Maestro E2E test report ${{ inputs.id }} + path: ${{ github.workspace }}/examples/bare-example/e2e_results/report.xml + reporter: java-junit + + - name: Upload report + uses: actions/upload-artifact@v4 + continue-on-error: true + with: + name: Maestro E2E recordings and screenshots ${{ inputs.id }} + path: | + ${{ github.workspace }}/examples/bare-example/e2e_results/*.mp4 + ${{ github.workspace }}/examples/bare-example/e2e_results/*.png + + - name: Upload report + uses: actions/upload-artifact@v4 + continue-on-error: true + with: + name: Maestro E2E test logs API ${{ inputs.id }} + path: | + ~/.maestro/tests/** diff --git a/.github/workflows/test-e2e-android.yaml b/.github/workflows/test-e2e-android.yaml new file mode 100644 index 0000000..5816069 --- /dev/null +++ b/.github/workflows/test-e2e-android.yaml @@ -0,0 +1,99 @@ +name: E2E tests - Android + +on: + pull_request: + branches: + - main + paths: + - '.github/workflows/test-e2e-android.yaml' + - 'packages/react-native-legal/**/*.[tj]sx?' + - 'packages/react-native-legal/android/**' + - 'examples/bare-example/**/*.[tj]sx?' + - 'examples/bare-example/android/**' + + push: + branches: + - main + + workflow_dispatch: + +concurrency: + group: e2e_tests-android-${{ github.ref }} + cancel-in-progress: true + +env: + MAESTRO_CLI_NO_ANALYTICS: true + MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED: true + MAESTRO_DISABLE_UPDATE_CHECK: true + +jobs: + e2e-android: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + api-level: [ 30, 34, 35 ] + steps: + - name: Enable KVM group perms + 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: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup + uses: ./.github/actions/setup + + - name: Install Maestro + uses: ./.github/actions/installMaestro + with: + target-platform: android + + - name: Cache Build + id: cache-build + uses: actions/cache@v4 + env: + cache-name: cached-android-build + with: + path: | + examples/bare-example/android/build + examples/bare-example/android/app/build + examples/bare-example/android/app/.cxx + key: bare-example-android-build + + - uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + cache: 'gradle' + + - name: Gradle cache + uses: gradle/actions/setup-gradle@v3 + + - name: Install example dependencies + run: yarn workspace react-native-legal-bare-example install --frozen-lockfile --immutable + + - name: Bundle app + run: yarn workspace react-native-legal-bare-example build:android + + - name: Create AVD and generate snapshot for caching + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: 'x86_64' + target: 'google_apis' + force-avd-creation: true + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: false + script: ./scripts/e2e_android_emulator_build_and_test.sh + + - name: Upload report + if: always() + continue-on-error: true + uses: ./.github/actions/uploadMaestroTestResults + with: + id: API ${{ matrix.api-level }} diff --git a/.github/workflows/test-e2e-ios.yaml b/.github/workflows/test-e2e-ios.yaml new file mode 100644 index 0000000..78be5b0 --- /dev/null +++ b/.github/workflows/test-e2e-ios.yaml @@ -0,0 +1,91 @@ +name: E2E tests - iOS + +on: + pull_request: + branches: + - main + paths: + - '.github/workflows/test-e2e-ios.yaml' + - 'packages/react-native-legal/**/*.[tj]sx?' + - 'packages/react-native-legal/ios/**' + - 'examples/bare-example/**/*.[tj]sx?' + - 'examples/bare-example/ios/**' + + push: + branches: + - main + + workflow_dispatch: + +env: + MAESTRO_CLI_NO_ANALYTICS: true + MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED: true + MAESTRO_DISABLE_UPDATE_CHECK: true + +concurrency: + group: e2e_tests-ios-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e-ios: + runs-on: macos-15 + strategy: + fail-fast: false + matrix: + simulator: [ 'iPhone 16 Pro (18.5)', 'iPhone SE (3rd generation) (18.0)' ] + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup + uses: ./.github/actions/setup + + - name: Install Maestro + uses: ./.github/actions/installMaestro + with: + target-platform: ios + + - name: Install example dependencies + run: yarn workspace react-native-legal-bare-example install --frozen-lockfile --immutable + + - name: Cache Pods + id: cache-pods + uses: actions/cache@v4 + env: + cache-name: cached-ios-pods-deps + with: + path: examples/bare-example/ios/Pods + key: bare-example-pods + + - name: Cache Build + id: cache-build + uses: actions/cache@v4 + env: + cache-name: cached-ios-build + with: + path: examples/bare-example/ios/build + key: bare-example-ios-build + + - name: Install example Pods + run: yarn workspace react-native-legal-bare-example pods + + - name: Bundle app + run: yarn workspace react-native-legal-bare-example build:ios + + - name: List simulators + run: xcrun simctl list + + - name: Build iOS App + run: yarn workspace react-native-legal-bare-example ios:release --simulator="${{ matrix.simulator }}" + + - name: Run tests + run: yarn workspace react-native-legal-bare-example e2e:ios + + - name: Upload report + if: always() + continue-on-error: true + uses: ./.github/actions/uploadMaestroTestResults + with: + id: ${{ matrix.simulator }} diff --git a/examples/bare-example/.gitignore b/examples/bare-example/.gitignore index f13c9ea..023d8ef 100644 --- a/examples/bare-example/.gitignore +++ b/examples/bare-example/.gitignore @@ -64,3 +64,5 @@ yarn-error.log # testing /coverage + +/e2e_results diff --git a/examples/bare-example/e2e/checkLicenses/android.yaml b/examples/bare-example/e2e/checkLicenses/android.yaml new file mode 100644 index 0000000..795fc89 --- /dev/null +++ b/examples/bare-example/e2e/checkLicenses/android.yaml @@ -0,0 +1,60 @@ +appId: com.reactnativelegalbareexample +name: "[Android] Check React Native entry in OSS libraries list" +tags: + - pull-request + - android +--- +- launchApp: + clearState: true + stopApp: true +- startRecording: 'e2e_results/checkLicenses' +- tapOn: 'Tap to see list of OSS libraries' +- assertVisible: 'OSS Notice' +- scrollUntilVisible: + element: + containsChild: 'react-native' + index: 0 + direction: 'DOWN' + timeout: 120000 + centerElement: true + speed: 70 +- takeScreenshot: 'e2e_results/react-native_list_element' +- assertVisible: + text: 'Facebook' + index: 0 +- assertVisible: + text: '\d+\.\d+\.\d+' + index: 0 +- assertVisible: + text: 'MIT License' + index: 0 +- tapOn: + text: 'MIT License' + index: 0 +- takeScreenshot: 'e2e_results/license_modal' +- assertVisible: '.*MIT License.*' +- assertVisible: '.*Copyright \(c\).*' +- assertVisible: '.*Permission is hereby granted, free of charge.*' +- back +- tapOn: + containsChild: 'react-native' + waitToSettleTimeoutMs: 3500 + index: 0 + +- runFlow: + when: + visible: + text: 'Use without an account' + file: '../common/acceptChromeTerms-UseWithoutAnAccountVariant.yaml' + +- runFlow: + when: + visible: + text: 'Accept & continue' + file: '../common/acceptChromeTerms-AcceptContinueVariant.yaml' + +- assertVisible: A framework for building native applications using React +- takeScreenshot: 'e2e_results/github_page' +- back +- stopRecording +- killApp diff --git a/examples/bare-example/e2e/checkLicenses/ios.yaml b/examples/bare-example/e2e/checkLicenses/ios.yaml new file mode 100644 index 0000000..a929714 --- /dev/null +++ b/examples/bare-example/e2e/checkLicenses/ios.yaml @@ -0,0 +1,29 @@ +appId: org.reactjs.native.example.ReactNativeLegalBareExample +name: '[iOS] Check React Native entry in OSS libraries list' +tags: + - pull-request + - ios +--- +- launchApp: + clearState: true + stopApp: true +- startRecording: 'e2e_results/checkLicenses' +- tapOn: 'Tap to see list of OSS libraries' +- assertVisible: 'OSS Notice' +- scrollUntilVisible: + label: Scroll to React Native library + element: "react-native" + direction: 'DOWN' + timeout: 60000 + speed: 80 +- takeScreenshot: 'e2e_results/react-native_list_element' +- tapOn: "react-native" +- takeScreenshot: 'e2e_results/react-native_entry' +- assertVisible: "react-native" +- assertVisible: "MIT License" +- assertVisible: "OSS Notice" +- tapOn: "OSS Notice" +- takeScreenshot: 'e2e_results/react-native_back_to_list' +- stopRecording +- killApp + diff --git a/examples/bare-example/e2e/common/acceptChromeTerms-AcceptContinueVariant.yaml b/examples/bare-example/e2e/common/acceptChromeTerms-AcceptContinueVariant.yaml new file mode 100644 index 0000000..d281490 --- /dev/null +++ b/examples/bare-example/e2e/common/acceptChromeTerms-AcceptContinueVariant.yaml @@ -0,0 +1,9 @@ +name: "[Android] Accept Chrome T & C - Use without an account variant" +appId: com.android.chrome +--- +- tapOn: + label: Accept Chrome T & C + text: "Accept & continue" +- tapOn: + label: "Turn off Chrome sync" + text: "No thanks" diff --git a/examples/bare-example/e2e/common/acceptChromeTerms-UseWithoutAnAccountVariant.yaml b/examples/bare-example/e2e/common/acceptChromeTerms-UseWithoutAnAccountVariant.yaml new file mode 100644 index 0000000..d644198 --- /dev/null +++ b/examples/bare-example/e2e/common/acceptChromeTerms-UseWithoutAnAccountVariant.yaml @@ -0,0 +1,9 @@ +name: "[Android] Accept Chrome T & C - Accept and Continue variant" +appId: com.android.chrome +--- +- tapOn: + label: Dismiss Chrome sign in + text: "Use without an account" +- tapOn: + label: "Dismiss Chrome notifications" + id: "com.android.chrome:id/negative_button" diff --git a/examples/bare-example/e2e/config.yaml b/examples/bare-example/e2e/config.yaml new file mode 100644 index 0000000..86e2029 --- /dev/null +++ b/examples/bare-example/e2e/config.yaml @@ -0,0 +1,2 @@ +flows: + - checkLicenses/* diff --git a/examples/bare-example/package.json b/examples/bare-example/package.json index e8dd724..cb57dcb 100644 --- a/examples/bare-example/package.json +++ b/examples/bare-example/package.json @@ -4,8 +4,17 @@ "private": true, "scripts": { "android": "react-native legal-generate && react-native run-android --no-packager", + "android:release": "react-native legal-generate && react-native run-android --no-packager --mode \"Release\" --appId com.reactnativelegalbareexample", + "build:android": "yarn mkdist && react-native legal-generate && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist/res", + "build:ios": "yarn mkdist && react-native legal-generate && react-native bundle --entry-file index.js --platform ios --dev false --bundle-output dist/main.ios.jsbundle --assets-dest dist", + "e2e:android": "yarn mke2e_results && maestro test --config=e2e/config.yaml --exclude-tags=ios --format junit --output=e2e_results/report.xml e2e", + "e2e:ios": "yarn mke2e_results && maestro test --config=e2e/config.yaml --exclude-tags=android --format junit --output=e2e_results/report.xml e2e", "ios": "react-native legal-generate && react-native run-ios --no-packager", + "ios:release": "react-native legal-generate && react-native run-ios --no-packager --mode \"Release\"", "lint": "eslint .", + "mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"", + "mke2e_results": "node -e \"require('node:fs').mkdirSync('e2e_results', { recursive: true, mode: 0o755 })\"", + "pods": "pod install --project-directory=ios", "start": "react-native start --config ./metro.config.js", "test": "jest", "typecheck": "tsc --noEmit" diff --git a/packages/react-native-legal/bare-plugin/src/ios/addSettingsBundle.ts b/packages/react-native-legal/bare-plugin/src/ios/addSettingsBundle.ts index d03a56d..dbc49d7 100644 --- a/packages/react-native-legal/bare-plugin/src/ios/addSettingsBundle.ts +++ b/packages/react-native-legal/bare-plugin/src/ios/addSettingsBundle.ts @@ -11,6 +11,9 @@ export function addSettingsBundle(iosProjectPath: string) { addSettingsBundleUtil(iosProjectPath, ({ settingsBundleFilename }) => { const projectName = getIOSProjectName(iosProjectPath); const { pbxproj, pbxprojPath } = getIOSPbxProj(iosProjectPath); + + pbxproj.removeFile(settingsBundleFilename, pbxproj.findPBXGroupKey({ name: projectName })); + const settingsBundleFile = pbxproj.addFile(settingsBundleFilename, pbxproj.findPBXGroupKey({ name: projectName })); if (!settingsBundleFile) { diff --git a/packages/react-native-legal/types-definitions/xcode.d.ts b/packages/react-native-legal/types-definitions/xcode.d.ts index ae6d532..cf551c4 100644 --- a/packages/react-native-legal/types-definitions/xcode.d.ts +++ b/packages/react-native-legal/types-definitions/xcode.d.ts @@ -142,23 +142,8 @@ declare module 'xcode' { runOnlyForDeploymentPostprocessing: number; }; }; - addFile( - path: string, - group?: string, - opt?: { - plugin?: string; - target?: string; - variantGroup?: string; - lastKnownFileType?: string; - defaultEncoding?: number; - customFramework?: boolean; - explicitFileType?: number; - weak?: boolean; - compilerFlags?: string; - embed?: boolean; - sign?: boolean; - }, - ): pbxFile | null; + addFile(path: string, group?: string, opt?: FileOptions): pbxFile | null; + removeFile(path: string, group?: string, opt?: FileOptions): pbxFile | null; addToPbxBuildFileSection(file: pbxFile): void; addToPbxResourcesBuildPhase(file: pbxFile): void; buildPhase(group: string, target: string): string | undefined; @@ -171,5 +156,19 @@ declare module 'xcode' { writeSync(options?: { omitEmptyValues?: boolean }): string; } + type FileOptions = Partial<{ + plugin: string; + target: string; + variantGroup: string; + lastKnownFileType: string; + defaultEncoding: number; + customFramework: boolean; + explicitFileType: number; + weak: boolean; + compilerFlags: string; + embed: boolean; + sign: boolean; + }>; + export function project(projectPath: string): XcodeProject; } diff --git a/scripts/e2e_android_emulator_build_and_test.sh b/scripts/e2e_android_emulator_build_and_test.sh new file mode 100755 index 0000000..976dfa6 --- /dev/null +++ b/scripts/e2e_android_emulator_build_and_test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "List installed system images" +sdkmanager --list_installed | grep system-images |grep x86_64 +yarn workspace react-native-legal-bare-example android:release +yarn workspace react-native-legal-bare-example e2e:android +TEST_STATUS=$? +echo "Test run completed with status $TEST_STATUS" +exit $TEST_STATUS