diff --git a/.github/workflows/e2e-android-self.yml b/.github/workflows/e2e-android-self.yml new file mode 100644 index 0000000000..113d6e1d6f --- /dev/null +++ b/.github/workflows/e2e-android-self.yml @@ -0,0 +1,44 @@ +name: Detox E2E Android (self-hosted) + +on: + push: + paths: + - packages/mobile/** + - packages/backend/** + - packages/state-manager/** + - .github/workflows/e2e-android-self.yml + +jobs: + detox-android-self-hosted: + timeout-minutes: 10 + runs-on: [self-hosted, macOS, ARM64, android] + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + npm i + npm run lerna bootstrap --scope @quiet/eslint-config,@quiet/logger,@quiet/common,@quiet/types,@quiet/state-manager,@quiet/backend,@quiet/identity,@quiet/mobile,backend-bundle + + - name: Pull binaries + run: | + git lfs install --force + git lfs pull + + - name: Pass local config + run : | + cat << EOF >> packages/mobile/android/local.properties + ndk.path=/Users/quiet/Library/Android/sdk/ndk/25.1.8937393 + EOF + + - name: Build Detox + run: | + export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home + cd packages/mobile + detox build -c android.emu.debug.ci + + - name: Run basic tests + run: | + cd packages/mobile + detox test starter -c android.emu.debug.ci \ No newline at end of file diff --git a/.github/workflows/e2e-android.yml b/.github/workflows/e2e-android.yml index 4481bdc54d..08a9ee19de 100644 --- a/.github/workflows/e2e-android.yml +++ b/.github/workflows/e2e-android.yml @@ -1,40 +1,70 @@ -name: E2E Android +name: Detox E2E Android -on: - push: - paths: - - packages/mobile/** - - packages/backend/** - - packages/state-manager/** - - .github/workflows/e2e-android.yml +on: workflow_dispatch jobs: detox-android: - timeout-minutes: 10 - runs-on: [self-hosted, macOS, ARM64, android] + timeout-minutes: 25 + runs-on: [macos-latest-xlarge] steps: - uses: actions/checkout@v4 + + - uses: actions/setup-node@master + with: + node-version: 18.12.1 - name: Install dependencies run: | - npm i - npm run lerna bootstrap --scope @quiet/eslint-config,@quiet/logger,@quiet/common,@quiet/types,@quiet/state-manager,@quiet/backend,@quiet/identity,@quiet/mobile,backend-bundle + npm ci + npm run lerna bootstrap -- --scope=\'{@quiet/eslint-config,@quiet/logger,@quiet/common,@quiet/types,@quiet/state-manager,@quiet/backend,@quiet/identity,@quiet/mobile,backend-bundle}\' - name: Pull binaries run: | - git lfs install + git lfs install --force git lfs pull - - name: Pass local config - run : | - cat << EOF >> packages/mobile/android/local.properties - ndk.path=/Users/quiet/Library/Android/sdk/ndk/25.1.8937393 - EOF + # see: https://stackoverflow.com/questions/67264212/android-emulator-crash-when-start-hvf-error-hv-error + - name: Create qemu entitlements + run: | + { + echo '' + echo '' + echo '' + echo '' + echo ' com.apple.security.hypervisor' + echo ' ' + echo '' + echo '' + } >> $ANDROID_HOME/emulator/qemu/darwin-aarch64/entitlements.xml + + - name: Re-sign qemu binary + run: | + cd $ANDROID_HOME/emulator/qemu/darwin-aarch64 + codesign -s - --entitlements entitlements.xml --force qemu-system-aarch64 --verbose + + - name: Install SDK image + run: yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install 'system-images;android-34;google_apis;arm64-v8a' + + - name: Create AVD + run: $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager create avd -n emulator_ci -k 'system-images;android-34;google_apis;arm64-v8a' -d 'pixel_7' + + - name: Boot AVD + run: $ANDROID_HOME/emulator/emulator -avd emulator_ci + + - name: Install pm2 + run: npm install pm2@latest -g + + - name: Start metro + run: | + cd packages/mobile + pm2 --name METRO start npm -- start + + - name: Install Detox CLI + run: npm install detox-cli --global - name: Build Detox run: | - export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home cd packages/mobile detox build -c android.emu.debug.ci @@ -42,3 +72,17 @@ jobs: run: | cd packages/mobile detox test starter -c android.emu.debug.ci + + - name: Stop metro + run: pm2 stop METRO + + - name: Take screenshot + if: always() + run: | # TODO + + - name: Upload screenshot + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: screenshot.png + path: screenshot.png diff --git a/.github/workflows/e2e-crossplatform.yml b/.github/workflows/e2e-crossplatform.yml index b45d5caddb..e5e629a763 100644 --- a/.github/workflows/e2e-crossplatform.yml +++ b/.github/workflows/e2e-crossplatform.yml @@ -8,6 +8,7 @@ on: - packages/state-manager/** - packages/identity/** - packages/common/** + - packages/e2e-tests/** jobs: mac: diff --git a/.github/workflows/e2e-ios.yml b/.github/workflows/e2e-ios.yml index 34ad277d79..40e4a43dc7 100644 --- a/.github/workflows/e2e-ios.yml +++ b/.github/workflows/e2e-ios.yml @@ -1,28 +1,27 @@ -name: E2E iOS +name: Detox E2E iOS -on: - push: - paths: - - packages/mobile/** - - packages/backend/** - - packages/state-manager/** +on: workflow_dispatch jobs: detox-ios: - timeout-minutes: 10 - runs-on: [self-hosted, macOS, ARM64, iOS] + timeout-minutes: 25 + runs-on: [macos-latest-xlarge] steps: - uses: actions/checkout@v4 + + - uses: actions/setup-node@master + with: + node-version: 18.12.1 - name: Install dependencies run: | - npm i - npm run lerna bootstrap --scope @quiet/eslint-config,@quiet/logger,@quiet/common,@quiet/types,@quiet/state-manager,@quiet/backend,@quiet/identity,@quiet/mobile,backend-bundle + npm ci + npm run lerna bootstrap -- --scope=\'{@quiet/eslint-config,@quiet/logger,@quiet/common,@quiet/types,@quiet/state-manager,@quiet/backend,@quiet/identity,@quiet/mobile,backend-bundle}\' - name: Pull binaries run: | - git lfs install + git lfs install --force git lfs pull - name: Install pods @@ -30,6 +29,30 @@ jobs: cd packages/mobile/ios pod install + - name: List simulator devices + run: xcrun simctl list devices + + - name: Boot simulator + run: | + UDID=$(xcrun simctl list devices | grep 'iPhone 15 (' | awk -F '[()]' '{print $2}' | awk 'NR==2') + xcrun simctl boot "$UDID" + + - name: Install pm2 + run: npm install pm2@latest -g + + - name: Start metro + run: | + cd packages/mobile + pm2 --name METRO start npm -- start + + - name: Install Detox CLI + run: npm install detox-cli --global + + - name: Install applesimutils + run: | + brew tap wix/brew + brew install applesimutils + - name: Build Detox run: | cd packages/mobile @@ -39,3 +62,18 @@ jobs: run: | cd packages/mobile detox test starter -c ios.sim.debug.ci + + - name: Stop metro + run: pm2 stop METRO + + - name: Take screenshot + if: always() + run: | + /usr/bin/xcrun simctl io booted screenshot screenshot.png + + - name: Upload screenshot + if: always() + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: screenshot.png + path: screenshot.png diff --git a/.github/workflows/e2e-win.yml b/.github/workflows/e2e-win.yml index 3c6b32f20e..541f73edb7 100644 --- a/.github/workflows/e2e-win.yml +++ b/.github/workflows/e2e-win.yml @@ -92,15 +92,7 @@ jobs: max_attempts: 3 shell: bash command: cd packages/e2e-tests && npm run test oneClient.test.ts - - - name: Run multiple clients test - uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 - with: - timeout_minutes: 30 - max_attempts: 3 - shell: bash - command: cd packages/e2e-tests && npm run test multipleClients.test.ts - + - name: Run user profile test uses: nick-fields/retry@v2 with: @@ -108,11 +100,20 @@ jobs: max_attempts: 3 shell: bash command: cd packages/e2e-tests && npm run test userProfile.test.ts - + - name: Run invitation link test - Includes 2 separate application clients uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 with: - timeout_minutes: 25 + timeout_minutes: 10 max_attempts: 3 shell: bash command: cd packages/e2e-tests && npm run test invitationLink.test.ts + + + - name: Run multiple clients test + uses: nick-fields/retry@14672906e672a08bd6eeb15720e9ed3ce869cdd4 # v2.9.0 + with: + timeout_minutes: 30 + max_attempts: 3 + shell: bash + command: cd packages/e2e-tests && npm run test multipleClients.test.ts \ No newline at end of file diff --git a/packages/e2e-tests/src/selectors.ts b/packages/e2e-tests/src/selectors.ts index ff1325c8f4..bd865d4f41 100644 --- a/packages/e2e-tests/src/selectors.ts +++ b/packages/e2e-tests/src/selectors.ts @@ -51,12 +51,12 @@ export class App { console.log('App closed', this.buildSetup.dataDir) } - async cleanup() { + async cleanup(force: boolean = false) { console.log(`Performing app cleanup`, this.buildSetup.dataDir) if (this.isOpened) { throw new Error(`App with dataDir ${this.buildSetup.dataDir} is still open, close before cleaning up!`) } - this.buildSetup.clearDataDir() + this.buildSetup.clearDataDir(force) } get saveStateButton() { diff --git a/packages/e2e-tests/src/tests/invitationLink.test.ts b/packages/e2e-tests/src/tests/invitationLink.test.ts index 472780f041..321bc77489 100644 --- a/packages/e2e-tests/src/tests/invitationLink.test.ts +++ b/packages/e2e-tests/src/tests/invitationLink.test.ts @@ -26,6 +26,9 @@ describe('New user joins using invitation link while having app opened', () => { beforeAll(async () => { ownerApp = new App() guestApp = new App({ defaultDataDir: true }) + if (process.platform === 'win32') { + await guestApp.cleanup(true) + } }) beforeEach(async () => { @@ -143,7 +146,7 @@ describe('New user joins using invitation link while having app opened', () => { const copiedCode = url.hash.substring(1) expect(() => parseInvitationCode(copiedCode)).not.toThrow() const data = parseInvitationCode(copiedCode) - const commandFull = `${command[process.platform as SupportedPlatformDesktop]} "${composeInvitationDeepUrl(data)}"` + const commandFull = `${command[process.platform as SupportedPlatformDesktop]} ${process.platform === 'win32' ? '""' : ''} "${composeInvitationDeepUrl(data)}"` console.log(`Calling ${commandFull}`) execSync(commandFull) console.log('Guest opened invitation link') diff --git a/packages/e2e-tests/src/utils.ts b/packages/e2e-tests/src/utils.ts index 363f639d8b..c167c02a3c 100644 --- a/packages/e2e-tests/src/utils.ts +++ b/packages/e2e-tests/src/utils.ts @@ -54,9 +54,9 @@ export class BuildSetup { } private getBinaryLocation() { - console.log('filename', this.fileName) switch (process.platform) { case 'linux': + console.log('filename', this.fileName) return `${__dirname}/../Quiet/${this.fileName ? this.fileName : BuildSetup.getEnvFileName()}` case 'win32': return `${process.env.LOCALAPPDATA}\\Programs\\@quietdesktop\\Quiet.exe` @@ -234,8 +234,8 @@ export class BuildSetup { await this.driver?.close() } - public clearDataDir() { - if (process.env.IS_CI === 'true') { + public clearDataDir(force: boolean = false) { + if (process.env.IS_CI === 'true' && !force) { console.warn('Not deleting data directory because we are running in CI') return } diff --git a/packages/mobile/.detoxrc.js b/packages/mobile/.detoxrc.js index a03e86f3a9..8ac7d9d0ce 100644 --- a/packages/mobile/.detoxrc.js +++ b/packages/mobile/.detoxrc.js @@ -77,7 +77,7 @@ module.exports = { emulator_ci: { type: 'android.emulator', device: { - avdName: 'Pixel_7_API_31', + avdName: 'emulator_ci', }, }, }, diff --git a/packages/mobile/e2e/utils/consts/timeouts.js b/packages/mobile/e2e/utils/consts/timeouts.js index f4bcf26440..9bdfda887e 100644 --- a/packages/mobile/e2e/utils/consts/timeouts.js +++ b/packages/mobile/e2e/utils/consts/timeouts.js @@ -1,3 +1,3 @@ export const BASIC = 5000 export const LONG = 20000 -export const STARTUP = 90000 +export const STARTUP = 120000 diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index 43c87c263b..de8795aada 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -1,6 +1,6 @@ require_relative '../node_modules/react-native/scripts/react_native_pods' -platform :ios, '17.4' +platform :ios, '17.1' target 'Quiet' do config = use_native_modules! diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index e550809b7d..01c21f974e 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -1393,6 +1393,6 @@ SPEC CHECKSUMS: Tor: 39dc71bf048312e202608eb499ca5c74e841b503 Yoga: 13c8ef87792450193e117976337b8527b49e8c03 -PODFILE CHECKSUM: 811e75c5d23ebd5b5f3e16c6dd4bae230db730d0 +PODFILE CHECKSUM: eed49772dde039b0723324c813c83dd4c1af35f7 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj index 489dc64d1f..c101495008 100644 --- a/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/Quiet.xcodeproj/project.pbxproj @@ -5300,7 +5300,7 @@ INFOPLIST_FILE = Quiet/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Quiet; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -5396,7 +5396,7 @@ INFOPLIST_FILE = Quiet/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Quiet; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; - IPHONEOS_DEPLOYMENT_TARGET = 17.4; + IPHONEOS_DEPLOYMENT_TARGET = 17.1; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/packages/mobile/ios/Quiet/AppDelegate.m b/packages/mobile/ios/Quiet/AppDelegate.m index cb92136270..ee2c2c74ac 100644 --- a/packages/mobile/ios/Quiet/AppDelegate.m +++ b/packages/mobile/ios/Quiet/AppDelegate.m @@ -51,11 +51,14 @@ - (void) initWebsocketConnection { * Delay used below can't cause any race condition as websocket won't connect until data server starts listening anyway. */ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ - NSTimeInterval delayInSeconds = 5; - dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); - dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { - [[self.bridge moduleForName:@"CommunicationModule"] sendDataPortWithPort:self.dataPort socketIOSecret:self.socketIOSecret]; - }); + NSArray *intervals = @[@5, @15, @30, @60, @90]; + for (NSNumber *interval in intervals) { + NSTimeInterval delayInSeconds = [interval doubleValue]; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { + [[self.bridge moduleForName:@"CommunicationModule"] sendDataPortWithPort:self.dataPort socketIOSecret:self.socketIOSecret]; + }); + } }); } diff --git a/packages/mobile/ios/Quiet/Info.plist b/packages/mobile/ios/Quiet/Info.plist index 51846dee44..1ba9e97f4a 100644 --- a/packages/mobile/ios/Quiet/Info.plist +++ b/packages/mobile/ios/Quiet/Info.plist @@ -36,26 +36,26 @@ CFBundleVersion 371 ITSAppUsesNonExemptEncryption - + LSRequiresIPhoneOS - + NSAppTransportSecurity NSAllowsArbitraryLoads - + NSAllowsLocalNetworking - + NSExceptionDomains localhost NSExceptionAllowsInsecureHTTPLoads - + NSLocationWhenInUseUsageDescription - + UIAppFonts Rubik-Black.ttf @@ -74,7 +74,7 @@ Rubik-SemiBoldItalic.ttf UIBackgroundModes - + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -88,6 +88,6 @@ UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance - + diff --git a/packages/mobile/src/store/init/startConnection/restoreConnection/restoreConnection.saga.ts b/packages/mobile/src/store/init/startConnection/restoreConnection/restoreConnection.saga.ts index 7b7195b2be..3a7c001360 100644 --- a/packages/mobile/src/store/init/startConnection/restoreConnection/restoreConnection.saga.ts +++ b/packages/mobile/src/store/init/startConnection/restoreConnection/restoreConnection.saga.ts @@ -2,7 +2,7 @@ import { delay, put, select } from 'typed-redux-saga' import { initSelectors } from '../../init.selectors' import { initActions } from '../../init.slice' -const WEBSOCKET_CONNECTION_DELAY = 5000 +const WEBSOCKET_CONNECTION_DELAY = 15000 export function* restoreConnectionSaga(): Generator { // Give the worker time to init websocket connection diff --git a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts index 5a4d63ce1b..6cf1f49ef3 100644 --- a/packages/mobile/src/store/init/startConnection/startConnection.saga.ts +++ b/packages/mobile/src/store/init/startConnection/startConnection.saga.ts @@ -10,6 +10,9 @@ import { eventChannel } from 'redux-saga' export function* startConnectionSaga( action: PayloadAction['payload']> ): Generator { + const isAlreadyConnected = yield* select(initSelectors.isWebsocketConnected) + if (isAlreadyConnected) return + const { dataPort, socketIOSecret } = action.payload console.log('WEBSOCKET', 'Entered start connection saga', dataPort)