diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..5deb92f1e2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,180 @@ +name: Test Suite + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + GOPRIVATE: github.com/getlantern + +jobs: + test-suite: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest-16-cores, windows-latest ] + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + lfs: true + + - name: Pull LFS objects + run: git lfs pull + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + + - name: Granting private modules access + run: git config --global url."https://${{ secrets.GH_TOKEN }}:x-oauth-basic@github.com/".insteadOf "https://github.com/" + + - name: GCloud Auth + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_CREDENTIALS }} + + - name: 'Set up Cloud SDK' + uses: google-github-actions/setup-gcloud@v2 + + - name: Setup protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install latest protoc-gen-go + run: go install github.com/golang/protobuf/protoc-gen-go@latest + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: 'gradle' + + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + + - name: Activate dart plugins + run: | + dart pub global activate protoc_plugin + dart pub global activate patrol_cli + + - name: Install dependencies + run: flutter pub get + + - name: Generate app.env + env: + ANDROID_INTERSTITIAL_AD_ID: ${{ secrets.INTERSTITIAL_AD_UNIT_ID }} + IOS_INTERSTITIAL_AD_ID: ${{ secrets.INTERSTITIAL_AD_UNIT_ID_IOS }} + TAPSELL_VIDEO_INTERSTITIAL_ZONE_ID: ${{ secrets.TAPSELL_VIDEO_INTERSTITIAL_ZONE_ID }} + TAPSELL_INTERSTITIAL_ZONE_ID: ${{ secrets.TAPSELL_INTERSTITIAL_ZONE_ID }} + run: | + touch app.env + echo "Android_interstitialAd=$ANDROID_INTERSTITIAL_AD_ID" > app.env + echo "IOS_interstitialAd=$IOS_INTERSTITIAL_AD_ID" >> app.env + echo "VideoInterstitialZoneId=$TAPSELL_VIDEO_INTERSTITIAL_ZONE_ID" >> app.env + echo "InterstitialZoneId=$TAPSELL_INTERSTITIAL_ZONE_ID" >> app.env + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + if: runner.os == 'macOS' + with: + xcode-version: latest-stable + + - name: Install Linux dependencies + if: runner.os == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y network-manager dbus-x11 + sudo apt-get install -y libwebkit2gtk-4.1-dev + sudo apt-get install -y libunwind-dev libstdc++-12-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-d cmake + sudo apt-get install -y libgtk-3-0 libblkid1 liblzma5 + sudo apt-get install -y libpcap-dev libgtk-3-dev libayatana-appindicator3-dev ruby ruby-dev && sudo gem install bundler -v 2.2.26 + + - name: Install the ninja build tool + if: runner.os == 'linux' + uses: seanmiddleditch/gha-setup-ninja@master + + - name: Install WebView2 Runtime + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "MicrosoftEdgeWebView2Setup.exe" + Start-Process -FilePath ".\MicrosoftEdgeWebView2Setup.exe" -ArgumentList "/silent", "/install" -Wait + + - name: Set up MinGW + if: matrix.os == 'windows-latest' + run: choco install mingw -y + + - name: Build for Windows + if: matrix.os == 'windows-latest' + run: | + echo "Building for Windows..." + make windows + make ffigen + + - name: Build linux Libraries + shell: bash + if: runner.os == 'linux' + run: | + make android ANDROID_ARCH=all + make linux + make ffigen + + - name: Build for IOS + if: runner.os == 'macOS' + run: | + make ios + make ffigen + + - name: Running widget tests + run: make widget-tests + + # Run firebase test only on one OS runner + # Does not make sense to run on all OS runners + - name: Running workflow test for android on Firebase test labs + if: runner.os == 'linux' + run: make ci-android-test + + - name: Starting Simulator + id: simulator + if: runner.os == 'macOS' + uses: futureware-tech/simulator-action@v4 + with: + model: 'iPhone 15 Pro' + + - name: Running workflow test for iOS + run: patrol test -d ${{ steps.simulator.outputs.udid }} + if: runner.os == 'macOS' + timeout-minutes: 70 + + - name: Upload xc result + if: runner.os == 'macOS' && always() + uses: actions/upload-artifact@v4 + with: + name: xc-result + path: ${{ github.workspace }}/build/ios + retention-days: 2 + + - name: Running workflow test for linux + uses: coactions/setup-xvfb@v1 + if: runner.os == 'linux' + timeout-minutes: 60 + with: + run: | + sudo systemctl start dbus + sudo systemctl start NetworkManager + make linuxDesktopTest + + - name: Running workflow test for windows + run: make windowsDesktopTest + if: runner.os == 'windows' + timeout-minutes: 60 \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6e8f7d371..65bd7304ff 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ lib/generated_bindings.dart *.env ExportOptions.plist +integration_test/test_bundle.dart +integration_test/test_bundle.dart diff --git a/Makefile b/Makefile index 425f5e6a04..29e73fcd9c 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ test: TEST ?= *_test # integration-test: -# @flutter drive --driver test_driver/integration_driver.dart --debug --flavor prod --target `ls integration_test/$(TEST).dart` +# @flutter drive --driver test_driver/integration_test.dart --debug --flavor prod --target `ls integration_test/$(TEST).dart` APP ?= lantern CAPITALIZED_APP := Lantern @@ -88,6 +88,7 @@ ADB := $(call get-command,adb) OPENSSL := $(call get-command,openssl) GMSAAS := $(call get-command,gmsaas) SENTRY := $(call get-command,sentry-cli) +PATROL := $(call get-command,patrol) BASE64 := $(call get-command,base64) GIT_REVISION_SHORTCODE := $(shell git rev-parse --short HEAD) @@ -746,3 +747,86 @@ clean: rm -f `which gomobile` && \ rm -f `which gobind` rm -Rf "$(FLASHLIGHT_FRAMEWORK_PATH)" "$(INTERMEDIATE_FLASHLIGHT_FRAMEWORK_PATH)" + + +# Test environment scripts and other utilities + +require-patrol: + @if [[ -z "$(PATROL)" ]]; then echo 'patrol-cli is not installed. Please install it with dart pub global activate patrol_cli'; exit 1; fi + + +# Build android apk for test +test-build-android: + @echo "Building apk for test..." + @patrol build android + +# Application test cases + +# Run all native tests +nativeTest: require-patrol + @echo "Running native tests..." + patrol test --target integration_test/features/vpn/vpn_flow_test.dart --dart-define native=true --flavor=prod --verbose + +# Run specific native tests + +runNativeTest: require-patrol + @ARGUMENTS=$(filter-out $@,$(MAKECMDGOALS)); \ + echo "Running patrol native tests on: $$ARGUMENTS" && \ + patrol test --target $$ARGUMENTS --flavor=prod + + +#Runs all integration tests +appWorkflowTest: + @echo "Running all integration tests..." + patrol test + + +# Run specific integration tests +# Usage: make run-specific-integration-tests integration_test/app_startup_flow_test.dart +runTest: + @ARGUMENTS=$(filter-out $@,$(MAKECMDGOALS)); \ + echo "Running tests on: $$ARGUMENTS" && \ + patrol test --target $$ARGUMENTS + + +#------- Desktop test and utils ------------ + +# Run all workflow tests on desktop +maOSWorkflowTest: + @echo "Running all integration tests..." + sh $(CURDIR)/integration_test/run_macos_test.sh + +linuxDesktopTest: + @echo "Running all integration tests..." + sh $(CURDIR)/integration_test/run_linux_test.sh + +windowsDesktopTest: + @echo "Running all integration tests..." + sh $(CURDIR)/integration_test/run_windows_test.sh + +# Run specific tests on desktop +#you can pass DEVICE= (e.g., macOS, linux, windows) +runDesktopTest: + @ARGUMENTS=$(filter-out $@,$(MAKECMDGOALS)); \ + DEVICE=$(DEVICE); \ + if [ -z "$$DEVICE" ]; then \ + echo "Error: DEVICE is not specified. Use DEVICE= (e.g., macOS, linux, android)"; \ + exit 1; \ + fi; \ + echo "Running tests on: $$ARGUMENTS" && \ + flutter test $$ARGUMENTS -d $$DEVICE -r expanded + + +# Run all android test on Firebase test lab +ci-android-test:test-build-android + @echo "Running tests on Firebase test labs..." + sh $(CURDIR)/integration_test/run_android_testlabs.sh + + +#Runs widget tets +widget-tests: + @echo "Running widget tests..." + flutter test test/ + + + diff --git a/README.md b/README.md index 5a08c395cf..829e0a4b85 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Lantern App [![Go Actions Status](https://github.com/getlantern/android-lantern/actions/workflows/go.yml/badge.svg)](https://github.com/getlantern/android-lantern/actions) [![Coverage Status](https://coveralls.io/repos/github/getlantern/android-lantern/badge.svg?t=C4SaZX)](https://coveralls.io/github/getlantern/android-lantern) +# Lantern App +[![Go Actions Status](https://github.com/getlantern/android-lantern/actions/workflows/go.yml/badge.svg)](https://github.com/getlantern/android-lantern/actions/workflows/go.yml) [![Coverage Status](https://coveralls.io/repos/github/getlantern/android-lantern/badge.svg?t=C4SaZX)](https://coveralls.io/github/getlantern/android-lantern)[![Publish releases](https://github.com/getlantern/lantern-client/actions/workflows/release.yml/badge.svg)](https://github.com/getlantern/lantern-client/actions/workflows/release.yml) Lantern APP is an app that uses the [VpnService](https://developer.android.com/reference/android/net/VpnService) API to intercept and reroute all device traffic to the Lantern circumvention tool. @@ -8,45 +9,57 @@ See [docs/replica.md](docs/replica.md). ## Acknowledgements -This application uses ringtone sounds from Mike Koenig available [here](https://soundbible.com/1868-Ringing-Phone.html) -and licensed under the [Creative Commons Attribution License](https://creativecommons.org/licenses/by/3.0/). +This application uses ringtone sounds from Mike Koenig +available [here](https://soundbible.com/1868-Ringing-Phone.html) +and licensed under +the [Creative Commons Attribution License](https://creativecommons.org/licenses/by/3.0/). ## Code Generation + This project includes various pieces of autogenerated code like protocol buffers and routes. All of this code can be generated by running `make codegen` or just `make`. Specific pieces of code can be generated per the below instructions. ### Protocol Buffers + If you update the protocol buffer definitions in protos_shared, make sure to run `make protos` to update the generated dart code. -Note - you might see an error like `Can't load Kernel binary: Invalid SDK hash.`. It seems that one can ignore this. +Note - you might see an error like `Can't load Kernel binary: Invalid SDK hash.`. It seems that one +can ignore this. ## Building and Releasing ### Dependencies -All these dependencies must be in your PATH. Some of this is Android specific, see below for other platforms. +All these dependencies must be in your PATH. Some of this is Android specific, see below for other +platforms. * Java 11 or greater -* [Android Studio](https://developer.android.com/studio?_gl=1*1wowe6v*_up*MQ..&gclid=Cj0KCQjw6auyBhDzARIsALIo6v-bn0juONfkfmQAJtwssRCQWADJMgGfRBisMNTSXHt5CZnyZVSK2Y8aAgCmEALw_wcB&gclsrc=aw.ds) (Android Studio Jellyfish | 2023.3.1 Patch 1) +* [Android Studio](https://developer.android.com/studio?_gl=1*1wowe6v*_up*MQ..&gclid=Cj0KCQjw6auyBhDzARIsALIo6v-bn0juONfkfmQAJtwssRCQWADJMgGfRBisMNTSXHt5CZnyZVSK2Y8aAgCmEALw_wcB&gclsrc=aw.ds) ( + Android Studio Jellyfish | 2023.3.1 Patch 1) * [Xcode](https://developer.apple.com/xcode/resources/) * [Git](https://git-scm.com/downloads) * [Android NDK](#steps-to-run-the-project) - * NDK should be version 26.x, for example 26.0.10792818. + * NDK should be version 26.x, for example 26.0.10792818. * [Git LFS](https://git-lfs.github.com) - - more information in [Usage](#usage) + - more information in [Usage](#usage) * [Flutter (3.24.0)](https://flutter.dev) * [sentry-cli](https://docs.sentry.io/product/cli/installation/) - - This is used for uploading native debug symbols to Sentry + - This is used for uploading native debug symbols to Sentry * [gomobile](https://github.com/golang/go/wiki/Mobile#tools) * [json-server](https://github.com/typicode/json-server) - * Only necessary for testing Replica + * Only necessary for testing Replica * CMake 3.22.1 - * You can get this from Android SDK Manager + * You can get this from Android SDK Manager * [CocoaPods](https://cocoapods.org/) - * Possibly this is only needed on Apple platforms. + * Possibly this is only needed on Apple platforms. * Linux + * [libayatana-appindicator](https://github.com/AyatanaIndicators/libayatana-appindicator) ( + required by [tray_manager](https://github.com/leanflutter/tray_manager#linux-requirements)) + * [gstreamer](https://gstreamer.freedesktop.org/documentation/installing/on-linux.html) ( + required by [flutter plugin audioplayers](https://pub.dev/packages/audioplayers_linux)) + * [libclang-dev](https://apt.llvm.org) (required by [ffigen](https://pub.dev/packages/ffigen)) * [libayatana-appindicator](https://github.com/AyatanaIndicators/libayatana-appindicator) (required by [tray_manager](https://github.com/leanflutter/tray_manager#linux-requirements)) * [gstreamer](https://gstreamer.freedesktop.org/documentation/installing/on-linux.html) (required by [flutter plugin audioplayers](https://pub.dev/packages/audioplayers_linux)) * [libclang-dev](https://apt.llvm.org) (required by [ffigen](https://pub.dev/packages/ffigen)) @@ -68,13 +81,13 @@ All these dependencies must be in your PATH. Some of this is Android specific, s * On the NDK(Side by side) check the latest version of 22.x (not anything newer) * Make sure that you have the latest **Android SDK Command-line Tools** * Finally select the following: - - Android Emulator - - Android SDK Platform-Tools - - Google play APK Expansion Library - - Google play Instant Development SDK - - Google Play Licensing Library - - Google Play Services - - Intel x86 Emulator Accelerator (HAXM installer) + - Android Emulator + - Android SDK Platform-Tools + - Google play APK Expansion Library + - Google play Instant Development SDK + - Google Play Licensing Library + - Google Play Services + - Intel x86 Emulator Accelerator (HAXM installer) * Click on Apply and accept the Terms and Conditions. * Open Xcode first time open Xcode and install necessary components * Download Certificate and provisioning profile from 1Pass [Search [IOS Certificates and profiles](https://my.1password.com/vaults/all/allitems)] @@ -92,7 +105,8 @@ All these dependencies must be in your PATH. Some of this is Android specific, s * `flutter pub get` * `flutter run --flavor prod` -**Note**: If you're using an M1 or M2 chip, navigate to the ios folder and run `arch -x86_64 pod install` +**Note**: If you're using an M1 or M2 chip, navigate to the ios folder and +run `arch -x86_64 pod install` ### ๐Ÿ’ป Running the Project on Desktop @@ -131,19 +145,23 @@ You can easily run emulators directly from the command line with the following: ### ๐Ÿ‘ฉโ€๐Ÿ’ป Using Android Studio? No problem! -We've got you covered! If you prefer using Android Studio, we have already set up the configuration files for you. Just select the prod configuration and hit Run... Get ready to start digging! ๐Ÿ˜„๐Ÿ” +We've got you covered! If you prefer using Android Studio, we have already set up the configuration +files for you. Just select the prod configuration and hit Run... Get ready to start digging! ๐Ÿ˜„๐Ÿ” -You can build an emulator with `./scripts/run_avd.rb`. Here's an example run: `./scripts/run_avd.rb --level=30 --abi=x86 --use_google_apis --window`. +You can build an emulator with `./scripts/run_avd.rb`. Here's an example +run: `./scripts/run_avd.rb --level=30 --abi=x86 --use_google_apis --window`. You'll need Ruby >= 2.3 installed and `colorize` gem (i.e., `gem install colorize`). [//]: # (#### Flutter Logging) [//]: # () + [//]: # (The Flutter component uses [logger](https://pub.dev/packages/logger) package for logging.) [//]: # (See `home.dart#build()` to know where it's configured.) [//]: # () + [//]: # (During development, you'll notice a lot of `GoLog`-tagged code. Feel-free to comment that out during your flutter work.) [//]: # (A sane terminal command (using [pidcat](https://github.com/JakeWharton/pidcat)) is `pidcat org.getlantern.lantern -i GoLog -i System.out -w 3`.) @@ -151,27 +169,33 @@ You'll need Ruby >= 2.3 installed and `colorize` gem (i.e., `gem install coloriz ### Building the InternalSdk (AKA Lantern Core) as a library The core Lantern functionality is written in Go and lives in `./internalsdk`. -It is compiled from Go using [Gomobile](https://github.com/golang/mobile) to appropriate formats for each platform. +It is compiled from Go using [Gomobile](https://github.com/golang/mobile) to appropriate formats for +each platform. #### Android + * To generate AAR run `make android-lib ANDROID_ARCH=all` * For compiled code lives in `./android/app/libs` and is called `liblantern-ARCH.aar`. #### IOS + * To generate XCodeFramework run `make build-framework` For compiled code lives in `./ios/internalsdk/` and is called `Internalsdk.xcframework`. #### Desktop + The desktop app lives under `desktop` .. To build the Go shared library on macOS: -* To build for desktop `make darwin` for macOS, `make windows` for Windows, and `make linux-amd64` for Linux. +* To build for desktop `make darwin` for macOS, `make windows` for Windows, and `make linux-amd64` + for Linux. * Generate the FFI bindings `make ffigen` [//]: # (#### Testing against Lantern's staging servers) [//]: # () + [//]: # (Package the AAR with `make android-lib ANDROID_ARCH=all STAGING=true`) ### Making Android debug builds (Not yet implemented on IOS) @@ -197,10 +221,13 @@ make android-release-install [//]: # (### ๐Ÿšง Making Staging Builds) [//]: # () + [//]: # () + [//]: # (To build mobile for staging, use the STAGING command line argument:) [//]: # () + [//]: # (```) [//]: # (STAGING=true make android-debug android-install) @@ -208,6 +235,7 @@ make android-release-install [//]: # (```) [//]: # () + [//]: # (This will build Flashlight with the same [STAGING flag](https://github.com/getlantern/flashlight/v7/blob/9eb8abbe036e86b9e72a1a938f29e59f75391676/common/const.go#L43), which allows your client to use the [staging pro-server](https://github.com/getlantern/pro-server-neu/blob/fa2859edf213998e15cd7c00461e52fd97a9e570/README.md#L125) instance instead of the production one.) ### ๐ŸŽ‰ Making Release Builds @@ -215,23 +243,29 @@ make android-release-install #### IOS Do this to make a release build: + ``` VERSION=x.x.x make ios-release ``` + **Note**: Replace x.x.x with the version number of your release. This will + - Set the version number in the info.plist file and increment the build number 1 - Upload the DSYM file to Sentry - Open the build folder once the build is complete - #### Android -The Android app is distributed in two ways, as an APK for side-loaded installation and as an app bundle (aab) for distribution on the Google Play Store. The APKs are architecture specific whereas the app bundle contains all 4 architectures (arm and x86 in 32-bit and 64-bit variants). + +The Android app is distributed in two ways, as an APK for side-loaded installation and as an app +bundle (aab) for distribution on the Google Play Store. The APKs are architecture specific whereas +the app bundle contains all 4 architectures (arm and x86 in 32-bit and 64-bit variants). Do this to make a release build: -- Make sure `./android/local.properties` exists. If not, create it to look like this (Replace with your own values. Find `flutter.sdk` by running `flutter doctor -v`): +- Make sure `./android/local.properties` exists. If not, create it to look like this (Replace with + your own values. Find `flutter.sdk` by running `flutter doctor -v`): ``` sdk.dir=/Users/AwesomeLanternDev/Library/Android/sdk @@ -239,20 +273,29 @@ Do this to make a release build: ``` - Download [sentry-cli](https://docs.sentry.io/product/cli/installation/) - - Authenticate with [these credentials](https://my.1password.com/vaults/all/allitems/npsu55phkvbytbomlhnfuhqcii) by running `sentry-cli login`. You need this, else the Makefile task will fail -- Download [this release keystore](https://my.1password.com/vaults/all/allitems/rp5dzcli5ghilzfsanajwz6nqm) file and put it somewhere like `/tmp/mykeystore,jks` -- Replace `~/.gradle/gradle.properties` with the values found [here](https://my.1password.com/vaults/all/allitems/jq67eb556b44gb6nlfjm2yh3tq) - - Make sure to replace `KEYSTORE_FILE` with the location of your keystore (`/tmp/mykeystore,jks` in our case above) + - Authenticate + with [these credentials](https://my.1password.com/vaults/all/allitems/npsu55phkvbytbomlhnfuhqcii) + by running `sentry-cli login`. You need this, else the Makefile task will fail +- +Download [this release keystore](https://my.1password.com/vaults/all/allitems/rp5dzcli5ghilzfsanajwz6nqm) +file and put it somewhere like `/tmp/mykeystore,jks` +- Replace `~/.gradle/gradle.properties` with the values + found [here](https://my.1password.com/vaults/all/allitems/jq67eb556b44gb6nlfjm2yh3tq) + - Make sure to replace `KEYSTORE_FILE` with the location of your keystore (`/tmp/mykeystore,jks` + in our case above) - Run `VERSION= make android-release ANDROID_ARCH=all` - - Or, `DEVELOPMENT_MODE=true make android-release ANDROID_ARCH=all` to enable "Development Mode" which has extra dev features like taking screenshots and dev settings. + - Or, `DEVELOPMENT_MODE=true make android-release ANDROID_ARCH=all` to enable "Development Mode" + which has extra dev features like taking screenshots and dev settings. ### ๐Ÿ“ฆ Building Release Packages and Distributing the App -Lantern-client release and beta packages are built using Continuous Integration (CI). You can create installers for beta, production, or internal testing by adjusting the tag syntax. +Lantern-client release and beta packages are built using Continuous Integration (CI). You can create +installers for beta, production, or internal testing by adjusting the tag syntax. #### Production Release for All Platforms ๐ŸŒ -To release the production version for all platforms (including Android, iOS, and macOS), use the following command: +To release the production version for all platforms (including Android, iOS, and macOS), use the +following command: `git tag -a "lantern-7.0.0" -f -m "Tagging production release"` @@ -264,7 +307,6 @@ Finally, to create an internal build, use "internal", as in: `git tag -a "lantern-7.0.0-internal" -f -m "Tagging internal release"` - #### Platform-Specific Releases ๐Ÿ“ฑ๐Ÿ’ป For releasing to specific platforms, use the appropriate prefix: @@ -285,24 +327,33 @@ After creating a tag, push it to GitHub to trigger the CI/CD pipeline: `git push origin [TAG-NAME]`or `git push origin lantern-7.0.0` -You can then find all built binaries in the [lantern-binaries repository](https://github.com/getlantern/lantern-binaries). +You can then find all built binaries in +the [lantern-binaries repository](https://github.com/getlantern/lantern-binaries). -To publish a release on Google Play, go to the Lantern App on the [Google Play Console](https://play.google.com/console/u/0/developers/4642290275832863621/app/4973965144252805146/app-dashboard?timespan=thirtyDays) and create a new release using the [app bundle](lantern-installer.aab). +To publish a release on Google Play, go to the Lantern App on +the [Google Play Console](https://play.google.com/console/u/0/developers/4642290275832863621/app/4973965144252805146/app-dashboard?timespan=thirtyDays) +and create a new release using the [app bundle](lantern-installer.aab). ### Enabling Auto-Update for a Sideloaded Release -Just because something's been released to prod doesn't mean clients will auto-update, there's an additional step for that. The below will release the current production version to autoupdate. Please make sure the VERSION parameter matches the current production version. +Just because something's been released to prod doesn't mean clients will auto-update, there's an +additional step for that. The below will release the current production version to autoupdate. +Please make sure the VERSION parameter matches the current production version. ``` GH_TOKEN= VERSION=7.2.0 make auto-updates ``` -To find the latest version that's been set for auto updates, check the [lantern](https://github.com/getlantern/lantern/releases) repo. +To find the latest version that's been set for auto updates, check +the [lantern](https://github.com/getlantern/lantern/releases) repo. -You can obtain the GH_TOKEN for releasing auto-updates from [1Password](https://start.1password.com/open/i?a=HHU7O6L7H5E33C6UDFD6Q3SYH4&v=nupvcrpazomdrozlmemsywqfj4&i=qlxf7ffkjnhu7nqkshvwi7ocpm&h=lantern.1password.com). +You can obtain the GH_TOKEN for releasing auto-updates +from [1Password](https://start.1password.com/open/i?a=HHU7O6L7H5E33C6UDFD6Q3SYH4&v=nupvcrpazomdrozlmemsywqfj4&i=qlxf7ffkjnhu7nqkshvwi7ocpm&h=lantern.1password.com). ### Testing Auto-Update with release builds -Sometimes you may need to make a release build with an old version that is eligible for auto-update. You can do that by using the VERSION_CODE environment variable + +Sometimes you may need to make a release build with an old version that is eligible for auto-update. +You can do that by using the VERSION_CODE environment variable ``` APP=lantern VERSION_CODE=1 VERSION=1.0.0 make android-release @@ -344,60 +395,98 @@ ANDROID_ARCH=386 SECRETS_DIR=$PATH_TO_TOO_MANY_SECRETS VERSION=2.0.0-beta1 make SECRETS_DIR=$PATH_TO_TOO_MANY_SECRETS VERSION=2.0.0-beta1 make android-bundle ``` -## Testing +## ๐Ÿงช Testing + +For testing we are using [patrol](https://pub.dev/packages/patrol) framework, Patrol simplifies +interaction with the native layer and offers an extensive set of easy-to-use testing APIs. + +### Integration Testing + +#### Writing Integration Testing + +* Always use our + custom [patrol](https://github.com/getlantern/lantern-client/blob/d5c36eba30e8072c0327eca4eea8472cbfa49cb5/integration_test/utils/test_utils.dart#L54) + method for writing any new integration tests. Similarly, + utilize [appTearDown](https://github.com/getlantern/lantern-client/blob/d5c36eba30e8072c0327eca4eea8472cbfa49cb5/integration_test/utils/test_utils.dart#L95) + for cleanup. These methods ensure compatibility across different environments for mobile and + desktop platforms. +* Structure tests to simulate real user flows, such as changing app settings (e.g., language + preferences) or completing actions +* Ensure tests are compatible with all supported platforms (mobile and desktop). +* If mocks are necessary for specific scenarios, ensure they are clearly documented and cover all + critical edge cases. + +#### Running Integration Testing + +* Make sure you install patrol_cli globally by running `flutter pub global activate patrol_cli` +* Make sure to connect any device or emulator to run the test. + +##### Run Test on Mobile + +> Note: Integration for IOS is not supported at the moment. + +To run all integration the test on Android + +```_sh +make appWorkflowTest +``` + +To run a single test on a mobile -### Flutter +```_sh +make runTest [file_test].dart // Replace testfile with real file +make runTest integration_test/app_startup_flow_test.dart +``` -#### Unit Testing -* Run running Flutter unit tests run `make test` -* To run independent Flutter tests, go to the root of the project and type: `flutter test test/my_folder_test.dart` - * in case that you need the code coverage just add the following argument: `flutter test --coverage test/my_folder_test.dart` +#### Running Test on Firebase Test Lab -#### Integration Testing -You can run integration tests from the integration_test directory against a live app using the following steps: +##### Setup -1. Run the app with these additional run arguments `--dart-define=driver=true --observatory-port 8888 --disable-service-auth-codes` -2. Run the integration test as a dart application -3. When running tests in different locales, change `const simulatedLocale = 'en_US';` in `integration_test_contants.dart` to the desired locale. -4. Go to SETTINGS view and then run `change_language_test.dart`. -5. All tests should start with testing device showing Chats tab. -6. Start by running test `1A`. -7. A handful of tests have specific requirements, marked by a "Test requirements" comment at the start of the test: - 1. `6A_scan_QR_code_test` needs another phone to do the QR scanning process with - 2. `6B_request_flow_test` requires a message request to have just been received - 3. `6C_introductions_test` requires the testing device/emulator to have received an introduction to another contact - 4. `17C_verify_contact_test` requires the most recent message to have been shared in conversation with an unverified contact -8. We will have some duplicate screenshots in there - run `python3 scripts/screenshot_generation_assets/remove_dups.py [your android-lantern-path]/screenshots/` to deduplicate. -9. To generate stitched landscape images for all screenshots in a given test folder, run `python3 scripts/screenshot_generation_assets/merge_screenshots.py [your android-lantern-path]/screenshots/[a locale e.g. en_US]` +* Install `gcloud CLI`. If not, you can install it by following the + instructions [here](https://cloud.google.com/sdk/docs/install). +* Login to your Google Cloud account by running `gcloud auth login`. +* Set the project ID by running `gcloud config set project lantern-android`. -This mechanism for running integration tests follows [this article](https://medium.com/flutter-community/hot-reload-for-flutter-integration-tests-e0478b63bd54). Using this mechanism, you can modify and rerun the integration test without having to redeploy the application. +#### Running the test -WARNING - when running with flutter driver enabled, the on-screen keyboard does not work. +To Run a test on Android device on Firebase Test Lab, you need to run the following command: -WARNING - if you try to run an instance of the app using `--observatory-port` and you already have another instance running with that same observatory-port, the 2nd instance will hang on launch because flutter cannot bind to that port. +```_sh +make ci-android-test +``` +> Note-: If you want to customize which device you want to use change it [here](https://github.com/getlantern/lantern-client/blob/ebc6743840fa3ac5ef82c3d42eee508a2306fd7a/integration_test/run_android_testlabs.sh#L7) -TODO: we need to automate the running of integration tests in a CI environment using Flutter driver. +##### Run Test on Desktop -NOTE โš  : Flutter driver is borderline maintained and clearly the expectation is to move to using `integration_test`. [Here](https://github.com/flutter/flutter/issues/12810) is a good depiction of related conversations. +> Note: Do not use direct patrol for dekstop there are some workaround that we need to do to run the test -#### Generating Mocks +To run all integration the test on desktop -To generate mocks for the interfaces using Mockery, follow these steps: +```_sh +make maOSWorkflowTest // For macOS +make linuxDesktopTest // For Linux +make windowsDesktopTest // For Windows +``` -1. Install mockery: `go install github.com/vektra/mockery/v2@latest` -2. Generate mocks: `make mocks` +To run a single test on a desktop, you need to pass DEVICE=macOS or DEVICE=windows or DEVICE=linux -The generated mocks will be output to the `./mocks` directory. +```_sh +make integration_test/app_startup_flow_test.dart DEVICE=macOS +``` #### Testing Replica -A few Replica tests run [json-server](https://github.com/typicode/json-server) to serve dummy data during tests instead of hitting an actual Replica instance. -The tests should transparently setup and teardown the dummy server but you need to have `json-server` in your PATH. + +A few Replica tests run [json-server](https://github.com/typicode/json-server) to serve dummy data +during tests instead of hitting an actual Replica instance. +The tests should transparently setup and teardown the dummy server but you need to +have `json-server` in your PATH. ### Java/Kotlin * For testing all `android/app/src/test` tests, run `./gradlew :app:test` * For testing all `android/app/src/androidTest` tests, run `./gradlew :app:connectedAndroidTest` -* For testing a specific an `androidTest` test, easiest is to open that file in Android Studio and clicking on the green play button next to the test +* For testing a specific an `androidTest` test, easiest is to open that file in Android Studio and + clicking on the green play button next to the test * For testing the internalsdk package, run `cd ./internalsdk && go test ./...` ### Unit Test Graph @@ -405,15 +494,21 @@ The tests should transparently setup and teardown the dummy server but you need If you wanna visualize the current percentage of code coverage you need to do the following steps. 1. On your `terminal` check if you have installed: `lcov` if not then install. -2. Go to on your terminal `android-lantern/coverage` and type: `genhtml coverage/lcov.info -o coverage/html` that will generate a nice html file with the code coverage of all your files. +2. Go to on your terminal `android-lantern/coverage` and + type: `genhtml coverage/lcov.info -o coverage/html` that will generate a nice html file with the + code coverage of all your files. ## Testing Google Play Payments -If you're trying to test Google Play Payments with a sideloaded build, you will need to satisfy one of the following conditions, otherwise you'll get an error saying "the item you requested is not available for purchase" +If you're trying to test Google Play Payments with a sideloaded build, you will need to satisfy one +of the following conditions, otherwise you'll get an error saying "the item you requested is not +available for purchase" when trying to purchase in-app. -A. Your login for the Google Play Store needs to be a License Tester for our account, as described [here](https://stackoverflow.com/a/55329990) -B. Alternately, you can also try to follow [these steps](https://stackoverflow.com/a/18172192) to make sure that the build is known to Google, but this didn't work for me. +A. Your login for the Google Play Store needs to be a License Tester for our account, as +described [here](https://stackoverflow.com/a/55329990) +B. Alternately, you can also try to follow [these steps](https://stackoverflow.com/a/18172192) to +make sure that the build is known to Google, but this didn't work for me. ``` Make sure to upload the signed APK to developer console. @@ -434,24 +529,35 @@ B. Alternately, you can also try to follow [these steps](https://stackoverflow.c ## Testing Freekassa Payments -You'd most probably wanna run this against Lantern's staging servers **and** turn on testing mode for Freekassa. Unfortunately in Freekassa, once you turn on `Testing Mode`, you can't switch back without affecting live payments. Ideally, contant Freekassa support to see how you can enable a separate testing mode. I'll just mention here some helpful notes while testing Freekassa before we went live with it. - -- Building the Flashlight library (i.e., `internalsdk`) with `make android-lib ANDROID_ARCH=all STAGING=true` is tempting, but it's not gonna work since all staging proxies you'll use (i.e., `fallback-*` proxies did not work at all for me as of today <25-12-2022, soltzen>). - - You'll need to do the incredibly-hacky approach of modifying this function `LanternHttpClient:createProUrl`: - - public static HttpUrl createProUrl(final String uri, final Map params) { - // final String url = String.format("http://localhost/pro%s", uri); - final String url = String.format("https://api-staging.getiantem.org%s", uri); - HttpUrl.Builder builder = HttpUrl.parse(url).newBuilder(); - if (params != null) { - for (Map.Entry param : params.entrySet()) { - builder.addQueryParameter(param.getKey(), param.getValue()); - } - } - return builder.build(); - } - -- You can debug pro-server-neu's staging instance (i.e., `api-staging.getiantem.org`) using a combination of log, telemetry and checking the staging Redis instance (see [here](https://github.com/getlantern/pro-server-neu/blob/c79c1b8da9e418bc4b075392fde9b051c699141d/README.md?plain=1#L125) for more info) +You'd most probably wanna run this against Lantern's staging servers **and** turn on testing mode +for Freekassa. Unfortunately in Freekassa, once you turn on `Testing Mode`, you can't switch back +without affecting live payments. Ideally, contant Freekassa support to see how you can enable a +separate testing mode. I'll just mention here some helpful notes while testing Freekassa before we +went live with it. + +- Building the Flashlight library (i.e., `internalsdk`) + with `make android-lib ANDROID_ARCH=all STAGING=true` is tempting, but it's not gonna work since + all staging proxies you'll use (i.e., `fallback-*` proxies did not work at all for me as of + today <25-12-2022, soltzen>). + - You'll need to do the incredibly-hacky approach of modifying this + function `LanternHttpClient:createProUrl`: + + public static HttpUrl createProUrl(final String uri, final Map params) { + // final String url = String.format("http://localhost/pro%s", uri); + final String url = String.format("https://api-staging.getiantem.org%s", uri); + HttpUrl.Builder builder = HttpUrl.parse(url).newBuilder(); + if (params != null) { + for (Map.Entry param : params.entrySet()) { + builder.addQueryParameter(param.getKey(), param.getValue()); + } + } + return builder.build(); + } + +- You can debug pro-server-neu's staging instance (i.e., `api-staging.getiantem.org`) using a + combination of log, telemetry and checking the staging Redis instance ( + see [here](https://github.com/getlantern/pro-server-neu/blob/c79c1b8da9e418bc4b075392fde9b051c699141d/README.md?plain=1#L125) + for more info) ## Running Appium tests locally @@ -471,10 +577,13 @@ appium driver install --source=npm appium-flutter-driver appium driver install espresso ``` -3. Generate a debug build with `CI=true make android-debug ANDROID_ARCH=all` ... CI needs to be set to true to enable the +3. Generate a debug build with `CI=true make android-debug ANDROID_ARCH=all` ... CI needs to be set + to true to enable the Flutter driver extension. -4. Modify [local_config.json](appium_kotlin/app/src/test/resources/local/local_config.json) to specify the path of a debug build APK on your system, and change `appium:udid` to specify your device ID (you can get this from `adb devices`) +4. Modify [local_config.json](appium_kotlin/app/src/test/resources/local/local_config.json) to + specify the path of a debug build APK on your system, and change `appium:udid` to specify your + device ID (you can get this from `adb devices`) 5. Make sure your device is connected to your computer and then run @@ -490,6 +599,7 @@ To run a specific test, you can do ``` ## Source Dump + Lantern Android source code is made available via source dump tarballs. To create one, run: ``` @@ -498,14 +608,18 @@ VERSION=2.0.0 make sourcedump This will create a file `lantern-android-sources-2.0.0.tar.gz`. -This tarball deliberately excludes UI resources like images and localized strings. It also deliberately excludes 3rd party Java libraries from the libs folder. +This tarball deliberately excludes UI resources like images and localized strings. It also +deliberately excludes 3rd party Java libraries from the libs folder. -The tarball does include vendored Go libraries, including all of the getlantern.org Go libraries. In this tarball, these are all licensed under the GPL +The tarball does include vendored Go libraries, including all of the getlantern.org Go libraries. In +this tarball, these are all licensed under the GPL as explained in [LICENSING.md](LICENSING.md). -All embedded URL literals in the getlantern.org Go code are elided to make it harder for clones to build a working version of Lantern. +All embedded URL literals in the getlantern.org Go code are elided to make it harder for clones to +build a working version of Lantern. -TODO: once we're confident these are working well, we should automate the upload of these to S3 and GitHub along with the upload of releases. +TODO: once we're confident these are working well, we should automate the upload of these to S3 and +GitHub along with the upload of releases. ## Known issues when building the project @@ -519,12 +633,15 @@ TODO: once we're confident these are working well, we should automate the upload Restart your computer. **Android Studio tries to access proxy server** -If you're running Lantern when you start Android Studio, and then turn off Lantern, Android Studio will keep trying to access resources via the proxy (which is no longer running). +If you're running Lantern when you start Android Studio, and then turn off Lantern, Android Studio +will keep trying to access resources via the proxy (which is no longer running). To fix this, restart Android Studio. ## VSCode configurations -If you like that VSCode start running the project without the need of be constantly typing the command. +If you like that VSCode start running the project without the need of be constantly typing the +command. + - Create a folder on the root of your project named: `.vscode` - Inside .vscode create a file named: `launch.json` - Add the following inside `launch.json` @@ -533,7 +650,7 @@ If you like that VSCode start running the project without the need of be constan { "version": "0.2.0", "configurations": [ - { + { "name": "Debug", "program": "lib/main.dart", "request": "launch", @@ -542,7 +659,7 @@ If you like that VSCode start running the project without the need of be constan "args": [ "--no-sound-null-safety", "--flavor", - "prod", + "prod" ] } ] @@ -551,12 +668,13 @@ If you like that VSCode start running the project without the need of be constan ## Linting -This project is formatted and linted with ktlint using the [ktlint-gradle plugin](https://github.com/JLLeitschuh/ktlint-gradle). +This project is formatted and linted with ktlint using +the [ktlint-gradle plugin](https://github.com/JLLeitschuh/ktlint-gradle). -You can install the [ktlint Intellij plugin](https://plugins.jetbrains.com/plugin/15057-ktlint-unofficial-) +You can install +the [ktlint Intellij plugin](https://plugins.jetbrains.com/plugin/15057-ktlint-unofficial-) for some support for linting within Android Studio. - ### Add Commit Hook ``` @@ -565,7 +683,6 @@ for some support for linting within Android Studio. This adds a pre commit hook that lints all staged files upon commit. - ### Manually Auto-format ``` @@ -574,7 +691,6 @@ This adds a pre commit hook that lints all staged files upon commit. This auto-formats all Kotlin files in the project. - ### Manually Check ``` @@ -587,7 +703,9 @@ This manually runs the linter against all Kotlin files in the project. ### Why is there a long-running notification? -We run Lantern as a foreground service so that it remains on and connected with our messaging server. Typical Android applications use Google Play Services for push notifications, so they don't have have to maintain this kind of connection themselves. +We run Lantern as a foreground service so that it remains on and connected with our messaging +server. Typical Android applications use Google Play Services for push notifications, so they don't +have have to maintain this kind of connection themselves. We can't use Google Play Services because: diff --git a/android/.kotlin/sessions/kotlin-compiler-3070100366162913447.salive b/android/.kotlin/sessions/kotlin-compiler-3070100366162913447.salive new file mode 100644 index 0000000000..e69de29bb2 diff --git a/android/app/build.gradle b/android/app/build.gradle index 8aa3374463..a24713c8cc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -78,6 +78,9 @@ android { versionName "9999.99.99" } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner" + testInstrumentationRunnerArguments clearPackageData: "true" + vectorDrawables.useSupportLibrary = true } // kotlin { @@ -192,6 +195,7 @@ android { testOptions { unitTests.returnDefaultValues = true + execution "ANDROIDX_TEST_ORCHESTRATOR" } namespace 'org.getlantern.lantern' } @@ -327,14 +331,16 @@ flutter { source '../..' } -configurations { - debugImplementation.exclude group: "junit", module: "junit" -} +//configurations { +// debugImplementation.exclude group: "junit", module: "junit" +//} dependencies { implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.8" implementation "com.google.protobuf:protobuf-javalite:$protoc_version" implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2' + androidTestImplementation 'junit:junit:4.12' + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugarJdk" implementation fileTree(dir: "libs", include: "liblantern-${androidArch()}.aar") @@ -364,18 +370,20 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14' //Test implementation - androidTestImplementation 'androidx.test:rules:1.6.1' - androidTestImplementation 'androidx.annotation:annotation:1.8.2' - androidTestImplementation 'androidx.test:runner:1.6.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' - androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.6.1' - androidTestImplementation 'androidx.test.ext:junit:1.2.1' - - androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0' + androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" androidTestImplementation 'com.squareup.okhttp3:okhttp:4.9.2' +// androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testImplementation 'junit:junit:4.13.2' - testImplementation "io.mockk:mockk:1.13.5" + testImplementation "io.mockk:mockk:1.13.12" + androidTestUtil "androidx.test:orchestrator:1.5.1" + // androidTestImplementation 'androidx.test:rules:1.6.1' +// androidTestImplementation 'androidx.annotation:annotation:1.8.2' +// androidTestImplementation 'androidx.test:runner:1.6.2' +// androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' +// androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.6.1' +// androidTestImplementation 'androidx.test.ext:junit:1.2.1' + } @@ -426,3 +434,5 @@ sentry { // Default is disabled. includeNativeSources = false } + + diff --git a/android/app/src/androidTest/java/org/getlantern/lantern/test/ApplicationTest.kt b/android/app/src/androidTest/java/org/getlantern/lantern/test/ApplicationTest.kt index e3b5e4d4ab..3aa7b05464 100644 --- a/android/app/src/androidTest/java/org/getlantern/lantern/test/ApplicationTest.kt +++ b/android/app/src/androidTest/java/org/getlantern/lantern/test/ApplicationTest.kt @@ -1,221 +1,221 @@ -package org.getlantern.lantern.test - -import android.Manifest -import android.graphics.Point -import android.os.AsyncTask -import android.os.SystemClock -import android.util.Log -import android.view.View -import androidx.test.InstrumentationRegistry -import androidx.test.espresso.Espresso -import androidx.test.espresso.ViewInteraction -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.filters.LargeTest -import androidx.test.filters.SdkSuppress -import androidx.test.rule.GrantPermissionRule -import androidx.test.runner.AndroidJUnit4 -import androidx.test.uiautomator.UiDevice -import com.kyleduo.switchbutton.SwitchButton -import org.getlantern.lantern.MainActivity -import org.getlantern.lantern.R -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.TypeSafeMatcher -import org.json.JSONException -import org.json.JSONObject -import org.junit.Assert -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import java.net.HttpURLConnection -import java.net.NetworkInterface -import java.net.SocketException -import java.net.URL -import java.util.concurrent.ExecutionException - -@RunWith(AndroidJUnit4::class) -@SdkSuppress(minSdkVersion = 18) -@LargeTest -class ApplicationTest { - @Rule - @JvmField - var grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE) - - @Rule - @JvmField - var mActivityRule = MyActivityTestRule(MainActivity::class.java) - - lateinit var mDevice: UiDevice - var mWatchers = UiWatchers() - - fun resetTestUser() { - var url = URL("https://api.getiantem.org/reset-test-user") - val conn = url.openConnection() as HttpURLConnection - conn.connectTimeout = 10000 - conn.readTimeout = 10000 - conn.requestMethod = "POST" - conn.connect() - conn.outputStream.close() - if (conn.responseCode != 200) { - throw Exception("Unexpected response code resetting test user: " + conn.responseCode) - } - } - - @Before - @Throws(Exception::class) - fun setUp() { - resetTestUser() - - mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - SystemClock.sleep(5000) - TestUtils.clickButtonIfPresent(mDevice, "OK") - mWatchers.registerAnrAndCrashWatchers() - val coordinates = arrayOfNulls(4) - coordinates[0] = Point(248, 1520) - coordinates[1] = Point(248, 929) - coordinates[2] = Point(796, 1520) - coordinates[3] = Point(796, 929) - if (!mDevice.isScreenOn) { - mDevice.wakeUp() - mDevice.swipe(coordinates, 10) - } - } - - @Test - fun turnOnAndOffVPN() { - Log.d(TAG, "Testing turning VPN on and off") - clickButton(R.id.powerLantern) - SystemClock.sleep(1000) - TestUtils.clickButtonIfPresent(mDevice, "OK") - verifyVPNOn() - try { - val json = JSONParse().execute().get() - Assert.assertNotNull("should get JSON result", json) - val ip = json.getString("ip") - Log.d(TAG, "Got IP Address! $ip") - } catch (e: ExecutionException) { - e.printStackTrace() - Assert.fail("JSON fetching task failed") - } catch (e: InterruptedException) { - e.printStackTrace() - Assert.fail("JSON fetching task interrupted") - } catch (e: JSONException) { - e.printStackTrace() - Assert.fail("got incorrect JSON result") - } - Log.d(TAG, "Waiting for view powerLantern to turn off VPN") - clickButton(R.id.powerLantern) - verifyVPNOff() - - // TODO: swipeLeft/swipeRight has no effect. Figure out how to test. - // onView(withId(R.id.powerLantern)).perform(swipeRight()); - // verifyVPNOn(); - - // onView(withId(R.id.powerLantern)).perform(swipeLeft()); - // verifyVPNOff(); - } - - @Test - fun upgradeWithStripe() { - Log.d(TAG, "Testing upgrading to Pro with a Stripe purchase") -// clickButton(R.id.upgradeBtn) - clickButton(R.id.oneYearBtn) - fillField(R.id.emailInput, "ox+testuser@gmail.com") - fillField(R.id.cardInput, "4242424242424242") - fillField(R.id.expirationInput, "03/24") - fillField(R.id.cvcInput, "123") - Espresso.onView(ViewMatchers.withId(R.id.cvcInput)).perform(ViewActions.closeSoftKeyboard()) - clickButton(R.id.continueBtn) - clickButton(R.id.continueToProBtn, attempts = 30, sleep = 1000) - } - - private fun verifyVPNOn() { - SystemClock.sleep(1000) - Espresso.onView(ViewMatchers.withId(R.id.powerLantern)).check( - ViewAssertions.matches(withChecked(true)) - ) - Assert.assertTrue("System VPN should be on", isSystemVPNConnected) - } - - private fun verifyVPNOff() { - SystemClock.sleep(1000) - Espresso.onView(ViewMatchers.withId(R.id.powerLantern)).check( - ViewAssertions.matches(withChecked(false)) - ) - Assert.assertFalse("System VPN should be off", isSystemVPNConnected) - } - - private val isSystemVPNConnected: Boolean - private get() = try { - val intf = NetworkInterface.getByName("tun0") - intf != null - } catch (e: SocketException) { - false - } - - private inner class JSONParse : AsyncTask() { - override fun doInBackground(vararg params: String?): JSONObject { - Log.d(TAG, "Fetching json from $url") - return JsonParser.getJSONFromUrl(url) - } - } - - private fun clickButton(id: Int, attempts: Int = 10, sleep: Long = 500) { - doUntilSuccessful( - attempts, sleep, - { - onView(id).perform(ViewActions.click()) - } - ) - } - - private fun fillField(id: Int, text: String, attempts: Int = 10, sleep: Long = 500) { - doUntilSuccessful( - attempts, sleep, - { - onView(id).perform( - ViewActions.clearText(), - ViewActions.typeText(text) - ) - } - ) - } - - private fun onView(id: Int): ViewInteraction { - return Espresso.onView(ViewMatchers.withId(id)) - } - - private fun doUntilSuccessful(attempts: Int = 10, sleep: Long = 500, fn: () -> Unit) { - lateinit var lastException: Throwable - for (x in 0..attempts) { - try { - return fn() - } catch (e: Throwable) { - lastException = e - SystemClock.sleep(sleep) - // continue - } - } - throw lastException - } - - companion object { - private const val TAG = "ApplicationTest" - private const val url = "https://ifconfig.co/json" - private fun withChecked(checked: Boolean): Matcher { - return object : TypeSafeMatcher() { - public override fun matchesSafely(v: View): Boolean { - val button = v as SwitchButton - return button.isChecked == checked - } - - override fun describeTo(description: Description) { - description.appendText("with correct background color") - } - } - } - } -} +//package org.getlantern.lantern.test +// +//import android.Manifest +//import android.graphics.Point +//import android.os.AsyncTask +//import android.os.SystemClock +//import android.util.Log +//import android.view.View +//import androidx.test.InstrumentationRegistry +//import androidx.test.espresso.Espresso +//import androidx.test.espresso.ViewInteraction +//import androidx.test.espresso.action.ViewActions +//import androidx.test.espresso.assertion.ViewAssertions +//import androidx.test.espresso.matcher.ViewMatchers +//import androidx.test.filters.LargeTest +//import androidx.test.filters.SdkSuppress +//import androidx.test.rule.GrantPermissionRule +//import androidx.test.runner.AndroidJUnit4 +//import androidx.test.uiautomator.UiDevice +//import com.kyleduo.switchbutton.SwitchButton +//import org.getlantern.lantern.MainActivity +//import org.getlantern.lantern.R +//import org.hamcrest.Description +//import org.hamcrest.Matcher +//import org.hamcrest.TypeSafeMatcher +//import org.json.JSONException +//import org.json.JSONObject +//import org.junit.Assert +//import org.junit.Before +//import org.junit.Rule +//import org.junit.Test +//import org.junit.runner.RunWith +//import java.net.HttpURLConnection +//import java.net.NetworkInterface +//import java.net.SocketException +//import java.net.URL +//import java.util.concurrent.ExecutionException +// +//@RunWith(AndroidJUnit4::class) +//@SdkSuppress(minSdkVersion = 18) +//@LargeTest +//class ApplicationTest { +// @Rule +// @JvmField +// var grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE) +// +// @Rule +// @JvmField +// var mActivityRule = MyActivityTestRule(MainActivity::class.java) +// +// lateinit var mDevice: UiDevice +// var mWatchers = UiWatchers() +// +// fun resetTestUser() { +// var url = URL("https://api.getiantem.org/reset-test-user") +// val conn = url.openConnection() as HttpURLConnection +// conn.connectTimeout = 10000 +// conn.readTimeout = 10000 +// conn.requestMethod = "POST" +// conn.connect() +// conn.outputStream.close() +// if (conn.responseCode != 200) { +// throw Exception("Unexpected response code resetting test user: " + conn.responseCode) +// } +// } +// +// @Before +// @Throws(Exception::class) +// fun setUp() { +// resetTestUser() +// +// mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) +// SystemClock.sleep(5000) +// TestUtils.clickButtonIfPresent(mDevice, "OK") +// mWatchers.registerAnrAndCrashWatchers() +// val coordinates = arrayOfNulls(4) +// coordinates[0] = Point(248, 1520) +// coordinates[1] = Point(248, 929) +// coordinates[2] = Point(796, 1520) +// coordinates[3] = Point(796, 929) +// if (!mDevice.isScreenOn) { +// mDevice.wakeUp() +// mDevice.swipe(coordinates, 10) +// } +// } +// +// @Test +// fun turnOnAndOffVPN() { +// Log.d(TAG, "Testing turning VPN on and off") +// clickButton(R.id.powerLantern) +// SystemClock.sleep(1000) +// TestUtils.clickButtonIfPresent(mDevice, "OK") +// verifyVPNOn() +// try { +// val json = JSONParse().execute().get() +// Assert.assertNotNull("should get JSON result", json) +// val ip = json.getString("ip") +// Log.d(TAG, "Got IP Address! $ip") +// } catch (e: ExecutionException) { +// e.printStackTrace() +// Assert.fail("JSON fetching task failed") +// } catch (e: InterruptedException) { +// e.printStackTrace() +// Assert.fail("JSON fetching task interrupted") +// } catch (e: JSONException) { +// e.printStackTrace() +// Assert.fail("got incorrect JSON result") +// } +// Log.d(TAG, "Waiting for view powerLantern to turn off VPN") +// clickButton(R.id.powerLantern) +// verifyVPNOff() +// +// // TODO: swipeLeft/swipeRight has no effect. Figure out how to test. +// // onView(withId(R.id.powerLantern)).perform(swipeRight()); +// // verifyVPNOn(); +// +// // onView(withId(R.id.powerLantern)).perform(swipeLeft()); +// // verifyVPNOff(); +// } +// +// @Test +// fun upgradeWithStripe() { +// Log.d(TAG, "Testing upgrading to Pro with a Stripe purchase") +//// clickButton(R.id.upgradeBtn) +// clickButton(R.id.oneYearBtn) +// fillField(R.id.emailInput, "ox+testuser@gmail.com") +// fillField(R.id.cardInput, "4242424242424242") +// fillField(R.id.expirationInput, "03/24") +// fillField(R.id.cvcInput, "123") +// Espresso.onView(ViewMatchers.withId(R.id.cvcInput)).perform(ViewActions.closeSoftKeyboard()) +// clickButton(R.id.continueBtn) +// clickButton(R.id.continueToProBtn, attempts = 30, sleep = 1000) +// } +// +// private fun verifyVPNOn() { +// SystemClock.sleep(1000) +// Espresso.onView(ViewMatchers.withId(R.id.powerLantern)).check( +// ViewAssertions.matches(withChecked(true)) +// ) +// Assert.assertTrue("System VPN should be on", isSystemVPNConnected) +// } +// +// private fun verifyVPNOff() { +// SystemClock.sleep(1000) +// Espresso.onView(ViewMatchers.withId(R.id.powerLantern)).check( +// ViewAssertions.matches(withChecked(false)) +// ) +// Assert.assertFalse("System VPN should be off", isSystemVPNConnected) +// } +// +// private val isSystemVPNConnected: Boolean +// private get() = try { +// val intf = NetworkInterface.getByName("tun0") +// intf != null +// } catch (e: SocketException) { +// false +// } +// +// private inner class JSONParse : AsyncTask() { +// override fun doInBackground(vararg params: String?): JSONObject { +// Log.d(TAG, "Fetching json from $url") +// return JsonParser.getJSONFromUrl(url) +// } +// } +// +// private fun clickButton(id: Int, attempts: Int = 10, sleep: Long = 500) { +// doUntilSuccessful( +// attempts, sleep, +// { +// onView(id).perform(ViewActions.click()) +// } +// ) +// } +// +// private fun fillField(id: Int, text: String, attempts: Int = 10, sleep: Long = 500) { +// doUntilSuccessful( +// attempts, sleep, +// { +// onView(id).perform( +// ViewActions.clearText(), +// ViewActions.typeText(text) +// ) +// } +// ) +// } +// +// private fun onView(id: Int): ViewInteraction { +// return Espresso.onView(ViewMatchers.withId(id)) +// } +// +// private fun doUntilSuccessful(attempts: Int = 10, sleep: Long = 500, fn: () -> Unit) { +// lateinit var lastException: Throwable +// for (x in 0..attempts) { +// try { +// return fn() +// } catch (e: Throwable) { +// lastException = e +// SystemClock.sleep(sleep) +// // continue +// } +// } +// throw lastException +// } +// +// companion object { +// private const val TAG = "ApplicationTest" +// private const val url = "https://ifconfig.co/json" +// private fun withChecked(checked: Boolean): Matcher { +// return object : TypeSafeMatcher() { +// public override fun matchesSafely(v: View): Boolean { +// val button = v as SwitchButton +// return button.isChecked == checked +// } +// +// override fun describeTo(description: Description) { +// description.appendText("with correct background color") +// } +// } +// } +// } +//} diff --git a/android/app/src/androidTest/java/org/getlantern/lantern/test/DataUsageTests.kt b/android/app/src/androidTest/java/org/getlantern/lantern/test/DataUsageTests.kt index 24b8cac186..d0fedfac03 100644 --- a/android/app/src/androidTest/java/org/getlantern/lantern/test/DataUsageTests.kt +++ b/android/app/src/androidTest/java/org/getlantern/lantern/test/DataUsageTests.kt @@ -2,8 +2,7 @@ package org.getlantern.lantern.test import android.app.NotificationManager import android.content.Context.NOTIFICATION_SERVICE -import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 + import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice @@ -20,123 +19,123 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -@RunWith(AndroidJUnit4::class) -class DataUsageTests { - - @get:Rule - var mainActivityRule = ActivityScenarioRule(MainActivity::class.java) - - private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - - private val notificationManager = - LanternApp.getAppContext().getSystemService(NOTIFICATION_SERVICE) as NotificationManager - - @Before - fun setUp() { - LanternApp.getSession().setPaymentTestMode(true) - notificationManager.cancelAll() - } - - @Test - fun WhenDataCapIsHit_AppShouldShowNotification() { - val bandwidth = Bandwidth(100, 0, 1000, 3600) - val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) - val content = LanternApp.getAppContext().resources.getString( - R.string.data_cap, - bandwidth.expiresAtString - ) - testDataUsageNotification(bandwidth, title, content) - } - - @Test - fun WhenDataUsageIs50_AppShouldShowNotification() { - val bandwidth = Bandwidth(50, 500, 1000, 3600) - val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) - val content = LanternApp.getAppContext().resources.getString( - R.string.data_cap_percent, - bandwidth.remaining, - bandwidth.expiresAtString - ) - testDataUsageNotification(bandwidth, title, content) - } - - @Test - fun WhenDataUsageIs80_AppShouldShowNotification() { - val bandwidth = Bandwidth(80, 200, 1000, 3600) - val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) - val content = LanternApp.getAppContext().resources.getString( - R.string.data_cap_percent, - bandwidth.remaining, - bandwidth.expiresAtString - ) - testDataUsageNotification(bandwidth, title, content) - } - - @Test - fun WhenDataUsageIsReset_AppShouldShowNotification() { - val bandwidth = Bandwidth(0, 1000, 1000, 3600) - val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) - val content = LanternApp.getAppContext().resources.getString(R.string.data_cap_reset) - testDataUsageNotification(bandwidth, title, content) - } - - @Test - fun WhenDataUsageIsOther_AppShouldNOTShowNotification() { - // given the app is running - val notExpectedTitle = - LanternApp.getAppContext().resources.getString(R.string.lantern_notification) - - // open the notification bar - device.openNotification() - - // send data usage update - val bandwidth = Bandwidth(25, 750, 1000, 3600) - LanternApp.getSession().bandwidthUpdate( - bandwidth.percent, - bandwidth.remaining, - bandwidth.allowed, - bandwidth.ttlSeconds - ) - - // then the user should NOT see a push notification - device.wait(Until.hasObject(By.text(notExpectedTitle)), TIMEOUT) - - // with a correct title - val titleObj: UiObject2? = device.findObject(By.text(notExpectedTitle)) - assertNull(titleObj) - } - - private fun testDataUsageNotification( - bandwidth: Bandwidth, - expectedTitle: String, - expectedContent: String - ) { - // given the app is running - - // open the notification bar - device.openNotification() - - // send data usage update - LanternApp.getSession().bandwidthUpdate( - bandwidth.percent, - bandwidth.remaining, - bandwidth.allowed, - bandwidth.ttlSeconds - ) - - // then the user should see a push notification - device.wait(Until.hasObject(By.text(expectedTitle)), TIMEOUT) - - // with a correct title - val titleObj: UiObject2? = device.findObject(By.text(expectedTitle)) - assertNotNull(titleObj) - - // and a correct content - val contentObj: UiObject2? = device.findObject(By.text(expectedContent)) - assertNotNull(contentObj) - } - - companion object { - private const val TIMEOUT = 3000L // 3 seconds - } -} +//@RunWith(AndroidJUnit4::class) +//class DataUsageTests { +// +// @get:Rule +// var mainActivityRule = ActivityScenarioRule(MainActivity::class.java) +// +// private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) +// +// private val notificationManager = +// LanternApp.getAppContext().getSystemService(NOTIFICATION_SERVICE) as NotificationManager +// +// @Before +// fun setUp() { +// LanternApp.session.setPaymentTestMode(true) +// notificationManager.cancelAll() +// } +// +// @Test +// fun WhenDataCapIsHit_AppShouldShowNotification() { +// val bandwidth = Bandwidth(100, 0, 1000, 3600) +// val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) +// val content = LanternApp.getAppContext().resources.getString( +// R.string.data_cap, +// bandwidth.expiresAtString +// ) +// testDataUsageNotification(bandwidth, title, content) +// } +// +// @Test +// fun WhenDataUsageIs50_AppShouldShowNotification() { +// val bandwidth = Bandwidth(50, 500, 1000, 3600) +// val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) +// val content = LanternApp.getAppContext().resources.getString( +// R.string.data_cap_percent, +// bandwidth.remaining, +// bandwidth.expiresAtString +// ) +// testDataUsageNotification(bandwidth, title, content) +// } +// +// @Test +// fun WhenDataUsageIs80_AppShouldShowNotification() { +// val bandwidth = Bandwidth(80, 200, 1000, 3600) +// val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) +// val content = LanternApp.getAppContext().resources.getString( +// R.string.data_cap_percent, +// bandwidth.remaining, +// bandwidth.expiresAtString +// ) +// testDataUsageNotification(bandwidth, title, content) +// } +// +// @Test +// fun WhenDataUsageIsReset_AppShouldShowNotification() { +// val bandwidth = Bandwidth(0, 1000, 1000, 3600) +// val title = LanternApp.getAppContext().resources.getString(R.string.lantern_notification) +// val content = LanternApp.getAppContext().resources.getString(R.string.data_cap_reset) +// testDataUsageNotification(bandwidth, title, content) +// } +// +// @Test +// fun WhenDataUsageIsOther_AppShouldNOTShowNotification() { +// // given the app is running +// val notExpectedTitle = +// LanternApp.getAppContext().resources.getString(R.string.lantern_notification) +// +// // open the notification bar +// device.openNotification() +// +// // send data usage update +// val bandwidth = Bandwidth(25, 750, 1000, 3600) +// LanternApp.getSession().bandwidthUpdate( +// bandwidth.percent, +// bandwidth.remaining, +// bandwidth.allowed, +// bandwidth.ttlSeconds +// ) +// +// // then the user should NOT see a push notification +// device.wait(Until.hasObject(By.text(notExpectedTitle)), TIMEOUT) +// +// // with a correct title +// val titleObj: UiObject2? = device.findObject(By.text(notExpectedTitle)) +// assertNull(titleObj) +// } +// +// private fun testDataUsageNotification( +// bandwidth: Bandwidth, +// expectedTitle: String, +// expectedContent: String +// ) { +// // given the app is running +// +// // open the notification bar +// device.openNotification() +// +// // send data usage update +// LanternApp.getSession().bandwidthUpdate( +// bandwidth.percent, +// bandwidth.remaining, +// bandwidth.allowed, +// bandwidth.ttlSeconds +// ) +// +// // then the user should see a push notification +// device.wait(Until.hasObject(By.text(expectedTitle)), TIMEOUT) +// +// // with a correct title +// val titleObj: UiObject2? = device.findObject(By.text(expectedTitle)) +// assertNotNull(titleObj) +// +// // and a correct content +// val contentObj: UiObject2? = device.findObject(By.text(expectedContent)) +// assertNotNull(contentObj) +// } +// +// companion object { +// private const val TIMEOUT = 3000L // 3 seconds +// } +//} diff --git a/android/app/src/androidTest/java/org/getlantern/lantern/test/MainActivityTest.java b/android/app/src/androidTest/java/org/getlantern/lantern/test/MainActivityTest.java new file mode 100644 index 0000000000..11ec589bdb --- /dev/null +++ b/android/app/src/androidTest/java/org/getlantern/lantern/test/MainActivityTest.java @@ -0,0 +1,35 @@ +package org.getlantern.lantern.test; + +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import pl.leancode.patrol.PatrolJUnitRunner; +import org.getlantern.lantern.MainActivity; + +@RunWith(Parameterized.class) +public class MainActivityTest { + @Parameters(name = "{0}") + public static Object[] testCases() { + PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + // replace "MainActivity.class" with "io.flutter.embedding.android.FlutterActivity.class" + // if in AndroidManifest.xml in manifest/application/activity you have + // android:name="io.flutter.embedding.android.FlutterActivity" + instrumentation.setUp(MainActivity.class); + instrumentation.waitForPatrolAppService(); + return instrumentation.listDartTests(); + } + + public MainActivityTest(String dartTestName) { + this.dartTestName = dartTestName; + } + + private final String dartTestName; + + @Test + public void runDartTest() { + PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation(); + instrumentation.runDartTest(dartTestName); + } +} \ No newline at end of file diff --git a/android/app/src/androidTest/java/org/getlantern/lantern/test/RenewalsTest.java b/android/app/src/androidTest/java/org/getlantern/lantern/test/RenewalsTest.java deleted file mode 100644 index 3b2925442e..0000000000 --- a/android/app/src/androidTest/java/org/getlantern/lantern/test/RenewalsTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.getlantern.lantern.test; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.assertion.ViewAssertions.matches; -import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withId; - -import android.content.Intent; -import android.os.SystemClock; -import android.util.Log; - -import androidx.test.InstrumentationRegistry; -import androidx.test.espresso.NoMatchingViewException; -import androidx.test.rule.ActivityTestRule; -import androidx.test.runner.AndroidJUnit4; -import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject; -import androidx.test.uiautomator.UiSelector; - -import org.getlantern.lantern.MainActivity; -import org.getlantern.lantern.R; -import org.joda.time.DateTime; -import org.joda.time.DateTimeConstants; -import org.joda.time.DateTimeZone; -import org.joda.time.LocalDateTime; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -public class RenewalsTest { - - private static final String TAG = RenewalsTest.class.getName(); - private UiDevice device; - - private String minDatafile = "{\"groups\": [], \"projectId\": \"8504447126\", \"variables\": [{\"defaultValue\": \"true\", \"type\": \"boolean\", \"id\": \"8516291943\", \"key\": \"test_variable\"}], \"version\": \"3\", \"experiments\": [{\"status\": \"Running\", \"key\": \"android_experiment_key\", \"layerId\": \"8499056327\", \"trafficAllocation\": [{\"entityId\": \"8509854340\", \"endOfRange\": 5000}, {\"entityId\": \"8505434669\", \"endOfRange\": 10000}], \"audienceIds\": [], \"variations\": [{\"variables\": [], \"id\": \"8509854340\", \"key\": \"var_1\"}, {\"variables\": [], \"id\": \"8505434669\", \"key\": \"var_2\"}], \"forcedVariations\": {}, \"id\": \"8509139139\"}], \"audiences\": [], \"anonymizeIP\": true, \"attributes\": [], \"revision\": \"7\", \"events\": [{\"experimentIds\": [\"8509139139\"], \"id\": \"8505434668\", \"key\": \"test_event\"}], \"accountId\": \"8362480420\"}"; - - @Rule - public ActivityTestRule mActivityRule = new MyActivityTestRule(MainActivity.class); - - @Before - public void setUp() throws Exception { - device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); - } - - private void checkTextDisplayed(final int id, final String text) { - try { - onView(withId(id)).check(matches(isDisplayed())); - UiObject o = device.findObject(new UiSelector().text(text)); - Assert.assertTrue(o.exists()); - } catch (NoMatchingViewException e) { - Log.e(TAG, "No matching view found", e); - } - } - - private void testRenewal(final LocalDateTime expires, - final String text) { - final boolean isProUser = true; - final boolean expired = false; - final DateTime utc = expires.toDateTime(DateTimeZone.UTC); - final long expiration = utc.getMillis() / DateTimeConstants.MILLIS_PER_SECOND; - mActivityRule.launchActivity(new Intent()); - SystemClock.sleep(5000); - checkTextDisplayed(R.id.renewalHeader, text); - } - - @Test - public void testProExpiresTomorrowRenewal() { - testRenewal(LocalDateTime.now().plusDays(1), - "tomorrow"); - } - - @Test - public void testProExpiresToday() { - testRenewal(LocalDateTime.now(), "today"); - } - - @Test - public void testProExpired() { - testRenewal(LocalDateTime.now().minusDays(1), - "Limited time offer"); - } -} diff --git a/android/app/src/androidTest/java/org/getlantern/lantern/test/ReplicaTest.java b/android/app/src/androidTest/java/org/getlantern/lantern/test/ReplicaTest.java deleted file mode 100644 index d7e9a9f95a..0000000000 --- a/android/app/src/androidTest/java/org/getlantern/lantern/test/ReplicaTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.getlantern.lantern.test; - -import android.content.Context; -import android.util.Log; - -import androidx.test.filters.LargeTest; -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.getlantern.lantern.LanternApp; -import org.getlantern.mobilesdk.Settings; -import org.getlantern.mobilesdk.StartResult; -import org.getlantern.mobilesdk.embedded.EmbeddedLantern; -import org.getlantern.mobilesdk.model.SessionManager; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; - -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -@RunWith(AndroidJUnit4.class) -@LargeTest -public class ReplicaTest extends BaseTest { - // Tests if Replica is initialized properly. - @Test - public void testReplicaIsInitialized() throws Exception { - // Initialize internalsdk - Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - Settings settings = Settings.init(InstrumentationRegistry.getInstrumentation().getTargetContext()); - settings.shouldRunReplica = true; - SessionManager session = LanternApp.getSession(); - session.setReplicaAddr(""); - StartResult result = new EmbeddedLantern().start( - Paths.get(context.getFilesDir().getAbsolutePath(), ".lantern").toString(), - "en_US", settings, session); - - // Wait for replica to start - Thread.sleep(5000); - Log.d("PINEAPPLE", "testReplicaIsInitialized: " + session.getReplicaAddr()); - Assert.assertNotEquals("", session.getReplicaAddr()); - - // Assert that /replica routes yield a 200 - for (Map.Entry entry : new HashMap() {{ - put("replica/heartbeat", "http://" + session.getReplicaAddr() + "/replica/heartbeat"); - put("replica/search", "http://" + session.getReplicaAddr() + "/replica/search?s=hello&page=1&orderBy=relevance&type=web"); - }}.entrySet()) { - OkHttpClient client = new OkHttpClient(); - Request req = new Request.Builder() - .url(entry.getValue()) - .build(); - Response resp = client.newCall(req).execute(); - Assert.assertEquals(200, resp.code()); - } - } -} diff --git a/android/app/src/androidTest/java/org/getlantern/lantern/test/UpdateTest.java b/android/app/src/androidTest/java/org/getlantern/lantern/test/UpdateTest.java index bd44e0dacd..ff34fd2106 100644 --- a/android/app/src/androidTest/java/org/getlantern/lantern/test/UpdateTest.java +++ b/android/app/src/androidTest/java/org/getlantern/lantern/test/UpdateTest.java @@ -1,72 +1,72 @@ -package org.getlantern.lantern.test; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; - -import android.content.Intent; -import android.os.SystemClock; - -import androidx.test.InstrumentationRegistry; -import androidx.test.filters.LargeTest; -import androidx.test.filters.SdkSuppress; -import androidx.test.rule.ActivityTestRule; -import androidx.test.runner.AndroidJUnit4; -import androidx.test.uiautomator.UiDevice; -import androidx.test.uiautomator.UiObject; -import androidx.test.uiautomator.UiSelector; - -import org.getlantern.lantern.MainActivity; -import org.getlantern.lantern.R; -import org.getlantern.lantern.activity.UpdateActivity_; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SdkSuppress(minSdkVersion = 18) -@LargeTest -public class UpdateTest { - @Rule - public ActivityTestRule mMainActivityRule = new MyActivityTestRule(MainActivity.class); - @Rule - public ActivityTestRule mUpdateActivityRule = - new MyActivityTestRule(UpdateActivity_.class, - false /* initialTouchMode */, - false /* launchActivity */); - - UiDevice mDevice; - UiWatchers mWatchers = new UiWatchers(); - String updateURL = "https://github.com/getlantern/lantern/releases/download/3.6.1/update_android_arm.bz2"; - - @Before - public void setUp() throws Exception { - mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); - TestUtils.clickButtonIfPresent(mDevice, "OK"); - mWatchers.registerAnrAndCrashWatchers(); - } - - @Test - public void testUpdate() { - Intent intent = new Intent(); - intent.putExtra("updateUrl", updateURL); - mUpdateActivityRule.launchActivity(intent); - SystemClock.sleep(1000); // leave some time for flashlight to run - onView(withId(R.id.installUpdate)).perform(click()); - waitForDownloadingAPK(); - SystemClock.sleep(1000); - TestUtils.clickButtonIfPresent(mDevice, "OK"); - TestUtils.clickButtonIfPresent(mDevice, "Install"); - SystemClock.sleep(5000); - } - - private void waitForDownloadingAPK() { - String text = mUpdateActivityRule.getActivity(). - getResources().getString(R.string.updating_lantern); - UiObject dialog; - do { - SystemClock.sleep(1000); - dialog = mDevice.findObject(new UiSelector().text(text)); - } while (dialog.exists()); - } -} +//package org.getlantern.lantern.test; +// +//import static androidx.test.espresso.Espresso.onView; +//import static androidx.test.espresso.action.ViewActions.click; +// +//import android.content.Intent; +//import android.os.SystemClock; +// +//import androidx.test.InstrumentationRegistry; +//import androidx.test.filters.LargeTest; +//import androidx.test.filters.SdkSuppress; +//import androidx.test.rule.ActivityTestRule; +//import androidx.test.runner.AndroidJUnit4; +//import androidx.test.uiautomator.UiDevice; +//import androidx.test.uiautomator.UiObject; +//import androidx.test.uiautomator.UiSelector; +// +//import org.getlantern.lantern.MainActivity; +//import org.getlantern.lantern.R; +//import org.getlantern.lantern.activity.UpdateActivity_; +//import org.junit.Before; +//import org.junit.Rule; +//import org.junit.Test; +//import org.junit.runner.RunWith; +// +//@RunWith(AndroidJUnit4.class) +//@SdkSuppress(minSdkVersion = 18) +//@LargeTest +//public class UpdateTest { +// @Rule +// public ActivityTestRule mMainActivityRule = new MyActivityTestRule(MainActivity.class); +// @Rule +// public ActivityTestRule mUpdateActivityRule = +// new MyActivityTestRule(UpdateActivity_.class, +// false /* initialTouchMode */, +// false /* launchActivity */); +// +// UiDevice mDevice; +// UiWatchers mWatchers = new UiWatchers(); +// String updateURL = "https://github.com/getlantern/lantern/releases/download/3.6.1/update_android_arm.bz2"; +// +// @Before +// public void setUp() throws Exception { +// mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); +// TestUtils.clickButtonIfPresent(mDevice, "OK"); +// mWatchers.registerAnrAndCrashWatchers(); +// } +// +// @Test +// public void testUpdate() { +// Intent intent = new Intent(); +// intent.putExtra("updateUrl", updateURL); +// mUpdateActivityRule.launchActivity(intent); +// SystemClock.sleep(1000); // leave some time for flashlight to run +// onView(withId(R.id.installUpdate)).perform(click()); +// waitForDownloadingAPK(); +// SystemClock.sleep(1000); +// TestUtils.clickButtonIfPresent(mDevice, "OK"); +// TestUtils.clickButtonIfPresent(mDevice, "Install"); +// SystemClock.sleep(5000); +// } +// +// private void waitForDownloadingAPK() { +// String text = mUpdateActivityRule.getActivity(). +// getResources().getString(R.string.updating_lantern); +// UiObject dialog; +// do { +// SystemClock.sleep(1000); +// dialog = mDevice.findObject(new UiSelector().text(text)); +// } while (dialog.exists()); +// } +//} diff --git a/android/app/src/main/java/org/getlantern/mobilesdk/Lantern.java b/android/app/src/main/java/org/getlantern/mobilesdk/Lantern.java index c58f99d8b8..d3db1524cf 100644 --- a/android/app/src/main/java/org/getlantern/mobilesdk/Lantern.java +++ b/android/app/src/main/java/org/getlantern/mobilesdk/Lantern.java @@ -224,12 +224,17 @@ private static void copyAssetFile(Context context, private static void initConfigDir(final Context context) { final File dir = new File(context.getFilesDir(), ".lantern"); - final boolean success = dir.mkdir(); - if (success) { - Logger.d(TAG, "Created config directory"); - } else { - Logger.d(TAG, "Error creating config directory"); + try{ + final boolean success = dir.mkdir(); + if (success) { + Logger.d(TAG, "Created config directory"); + } else { + Logger.d(TAG, "Error creating config directory"); + } + }catch (Exception e){ + Logger.e(TAG, "Error while creating config directory", e); } + } public static String configDirFor(Context context, String suffix) { diff --git a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt index a2da9dc829..0383e845d5 100644 --- a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt +++ b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt @@ -125,11 +125,16 @@ class SessionModel internal constructor( result ) } - "checkForUpdates" -> { autoUpdater.checkForUpdates(result) } + "trackUserAction" -> { + val props: Map = mapOf("title" to call.argument("title")!!) + Plausible.event( + call.argument("name")!!, url = call.argument("url")!!, props = props + ) + } "proxyAddr" -> result.success(LanternApp.session.hTTPAddr) diff --git a/android/build.gradle b/android/build.gradle index c7f60b45e9..3ec0fa96e6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '2.0.20' + ext.kotlin_version = '1.9.0' ext.signal_version = '2.8.1' ext.protoc_version = '4.26.1' ext.desugarJdk = '2.0.4' @@ -91,3 +91,4 @@ subprojects { tasks.register("clean", Delete) { delete rootProject.buildDir } + diff --git a/android/settings.gradle b/android/settings.gradle index d043a48c84..676896b2b2 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version '8.6.0' apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false } include ":app" \ No newline at end of file diff --git a/appiumTest.xctestplan b/appiumTest.xctestplan new file mode 100644 index 0000000000..cdb75a3236 --- /dev/null +++ b/appiumTest.xctestplan @@ -0,0 +1,28 @@ +{ + "configurations" : [ + { + "id" : "36E13E8A-BB29-41C9-8449-BB254EDB0C03", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "targetForVariableExpansion" : { + "containerPath" : "container:Runner.xcodeproj", + "identifier" : "97C146ED1CF9000F007C117D", + "name" : "Runner" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:Runner.xcodeproj", + "identifier" : "03E34B742D1E8BD100E19584", + "name" : "RunnerUITests" + } + } + ], + "version" : 1 +} diff --git a/assets/locales/en-us.po b/assets/locales/en-us.po index a5384d30ab..f99eda6564 100644 --- a/assets/locales/en-us.po +++ b/assets/locales/en-us.po @@ -1629,7 +1629,7 @@ msgid "try_lantern_pro" msgstr "Try Lantern for free! " msgid "continue_for_free" -msgstr "Continue for Free " +msgstr "Continue for Free" msgid "recovery_not_found" msgstr "Device not associated to this user" @@ -1833,6 +1833,10 @@ msgstr "Socks Proxy copied" msgid "proxy_everything" msgstr "Proxy Everything" +msgid "activation_lantern_pro_code" +msgstr "Have a Lantern Pro activation code? Click here." + + msgid "errorSubmittingToGooglePlay" msgstr "We are currently experiencing difficulties processing your request for Google Play. Please try again later." diff --git a/dart_test.yml b/dart_test.yml new file mode 100644 index 0000000000..8660c6433b --- /dev/null +++ b/dart_test.yml @@ -0,0 +1,3 @@ +tags: + golden: + timeout: 15s \ No newline at end of file diff --git a/desktop/lib.go b/desktop/lib.go index 5b0c47ff2f..22486d873d 100644 --- a/desktop/lib.go +++ b/desktop/lib.go @@ -53,8 +53,8 @@ func start() *C.char { // Since Go 1.6, panic prints only the stack trace of current goroutine by // default, which may not reveal the root cause. Switch to all goroutines. debug.SetTraceback("all") - // Load application configuration from .env file + log.Debug("Loading .env file") err := godotenv.Load() if err != nil { log.Errorf("Error loading .env file: %v", err) @@ -62,6 +62,7 @@ func start() *C.char { log.Debug("Successfully loaded .env file") } + log.Debug("Logs directory: ") _, err = logging.RotatedLogsUnder(common.DefaultAppName, appdir.Logs(common.DefaultAppName)) if err != nil { log.Error(err) diff --git a/integration_test/0_change_language_test.dart b/integration_test/0_change_language_test.dart deleted file mode 100644 index d5a8899831..0000000000 --- a/integration_test/0_change_language_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:intl/intl.dart'; -import 'package:lantern/i18n/localization_constants.dart'; - -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - final lang = toBeginningOfSentenceCase(displayLanguage(simulatedLocale))!; - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText( - 'settings', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.tapText( - 'language', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.scrollTextUntilVisible(lang); - await driver.tap(find.text(lang)); - await driver.waitForSeconds(2); - }, - ); -} diff --git a/integration_test/10A_contact_info_screen_test.dart b/integration_test/10A_contact_info_screen_test.dart deleted file mode 100644 index dd205c930d..0000000000 --- a/integration_test/10A_contact_info_screen_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'contact_info_screen_1'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Access a contact info screen via long tap', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - await driver.longPressFirstItemInList( - 'chats_messages_list', - ); - - await driver.tapText( - 'view_contact_info', - overwriteTimeout: longWaitTimeout, - ); - - await driver.waitForSeconds(5); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/10B_contact_info_screen_test.dart b/integration_test/10B_contact_info_screen_test.dart deleted file mode 100644 index 101ef40c10..0000000000 --- a/integration_test/10B_contact_info_screen_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'contact_info_screen_2'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Access a contact info screen via top right menu', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - await driver.tapFirstItemInList('chats_messages_list'); - - print('tap on top right menu bar'); - await driver.tapKey( - 'conversation_topbar_more_menu', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText( - 'view_contact_info', - overwriteTimeout: longWaitTimeout, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/11_rename_contact_test.dart b/integration_test/11_rename_contact_test.dart deleted file mode 100644 index 3d30fb591a..0000000000 --- a/integration_test/11_rename_contact_test.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'rename_contact'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Rename a contact', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - await driver.tapFirstItemInList('chats_messages_list'); - - print('tap on top right menu bar'); - await driver.tapKey( - 'conversation_topbar_more_menu', - overwriteTimeout: defaultWaitTimeout, - ); - - print('tap on View Contact Info'); - await driver.tapText( - 'view_contact_info', - overwriteTimeout: defaultWaitTimeout, - ); - - print('click on EDIT'); - await driver.tapText('edit', capitalize: true); - - print('enter new contact name'); - await driver.enterText( - renameContact, - timeout: longWaitTimeout, - ); - - await driver.waitForSeconds(2); - - print('tap SAVE'); - await driver.tapText( - 'save', - capitalize: true, - overwriteTimeout: longWaitTimeout, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/12_delete_contact_test.dart b/integration_test/12_delete_contact_test.dart deleted file mode 100644 index d230a13b90..0000000000 --- a/integration_test/12_delete_contact_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'delete_contact'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Delete a contact', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - await driver.tapFirstItemInList('chats_messages_list'); - - print('tap on top right menu bar'); - await driver.tapKey( - 'conversation_topbar_more_menu', - overwriteTimeout: defaultWaitTimeout, - ); - - await driver.tapText( - 'view_contact_info', - overwriteTimeout: defaultWaitTimeout, - ); - - await driver.tapText('delete_contact', capitalize: true); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/13_block_contact_test.dart b/integration_test/13_block_contact_test.dart deleted file mode 100644 index 219858597f..0000000000 --- a/integration_test/13_block_contact_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'block_contact'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Block a contact', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - await driver.tapFirstItemInList('chats_messages_list'); - - await driver.tapKey( - 'conversation_topbar_more_menu', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText( - 'view_contact_info', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText( - 'block', - capitalize: true, - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapType( - 'Checkbox', - overwriteTimeout: longWaitTimeout, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/14A_react_to_message_test.dart b/integration_test/14A_react_to_message_test.dart deleted file mode 100644 index 02828b7a69..0000000000 --- a/integration_test/14A_react_to_message_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'react_to_message'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'React to message', - () async { - print( - 'This test relies on only one message having the _just now_ timestamp, so lets wait a bit in case other conversations were active recently', - ); - await driver.waitForSeconds(60); - - await driver.openTab('chats', homeFirst: true); - - await driver.screenshotCurrentView(); - - await driver.tapFAB(); - - await driver.tapFirstItemInList('grouped_contact_list'); - - await driver.typeAndSend('test_text'); - - print('long press message we just shared'); - await driver.longPressText('just_now'); - - await driver.tapText('copy_text'); - - await driver.tapText('reply'); - - await driver.typeAndSend('test_reply'); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/14B_delete_for_me_test.dart b/integration_test/14B_delete_for_me_test.dart deleted file mode 100644 index 4f0cedb798..0000000000 --- a/integration_test/14B_delete_for_me_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'delete_for_me'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Delete for me', - () async { - print( - 'This test relies on only one message having the _just now_ timestamp, so lets wait a bit in case other conversations were active recently', - ); - await driver.waitForSeconds(60); - - await driver.openTab('chats', homeFirst: true); - - await driver.tapFAB(); - - await driver.tapFirstItemInList('grouped_contact_list'); - - await driver.typeAndSend('test_text'); - print('long press message we just shared'); - await driver.longPressText('just_now'); - - print('delete for me'); - await driver.tapText('delete_for_me'); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/14C_delete_for_everyone_test.dart b/integration_test/14C_delete_for_everyone_test.dart deleted file mode 100644 index 55a301090b..0000000000 --- a/integration_test/14C_delete_for_everyone_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'delete_for_everyone'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Delete for everyone', - () async { - print( - 'This test relies on only one message having the _just now_ timestamp, so lets wait a bit in case other conversations were active recently', - ); - await driver.waitForSeconds(60); - - await driver.openTab('chats', homeFirst: true); - - await driver.screenshotCurrentView(); - - await driver.tapFAB(); - - await driver.tapFirstItemInList('grouped_contact_list'); - - await driver.typeAndSend('test_text'); - print('long press message we just shared'); - await driver.longPressText('just_now'); - - await driver.tapText('delete_for_everyone'); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/15_copy_recovery_key_test.dart b/integration_test/15_copy_recovery_key_test.dart deleted file mode 100644 index 5dbdc5cf40..0000000000 --- a/integration_test/15_copy_recovery_key_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'copy_recovery_key'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Copy Recovery Key', - () async { - await driver.resetFlagsAndEnrollAgain(skipScreenshot: true); - await driver.openTab('Account'); - await driver.tapText('account_management'); - await driver.tapText('backup_recovery_key'); - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.tapText( - 'copy_recovery_key', - capitalize: true, - ), - screenshotTitle: testName, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/16_recovery_flow_test.dart b/integration_test/16_recovery_flow_test.dart deleted file mode 100644 index b3a80231c3..0000000000 --- a/integration_test/16_recovery_flow_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'recovery_flow_test'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Recovery_flow_test', - () async { - await driver.resetFlags(); - await driver.tapText( - 'chats', - waitText: 'welcome_title', - ); - - await driver.tapText( - 'recover', - capitalize: true, - overwriteTimeout: defaultWaitTimeout, - ); - - print('entering recovery key'); - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.enterText( - recoveryKey, - ), - screenshotTitle: testName, - ); - - await driver.tapText( - 'Submit', - capitalize: true, - overwriteTimeout: defaultWaitTimeout, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/17_verify_contact_test.dart b/integration_test/17_verify_contact_test.dart deleted file mode 100644 index 803173fb2a..0000000000 --- a/integration_test/17_verify_contact_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'verify_contact'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - // Test requirements - // * First message needs to be an unverified contact - group(testName, () { - test( - 'Verify a contact via voice call', - () async { - await driver.openTab('chats', homeFirst: true); - - await driver.addDummyContacts(); - - await driver.tapFAB(); - - await driver.tapFirstItemInList('grouped_contact_list'); - - await driver.tapKey( - 'verification_badge', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText( - 'verify_via_call', - overwriteTimeout: defaultWaitTimeout, - ); - - await driver.tapKey( - 'call_verify_button', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText( - 'mark_as_verified', - capitalize: true, - overwriteTimeout: longWaitTimeout, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/18_disappearing_messages_test.dart b/integration_test/18_disappearing_messages_test.dart deleted file mode 100644 index 35b7372449..0000000000 --- a/integration_test/18_disappearing_messages_test.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'disappearing_messages'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Test disappearing messages settings', - () async { - await driver.openTab('chats', homeFirst: true); - - await driver.tapFAB(); - - await driver.tapFirstItemInList('grouped_contact_list'); - - await driver.typeAndSend( - 'test_disappearing_messages_1', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapKey( - 'conversation_topbar_more_menu', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText( - 'disappearing_messages', - overwriteTimeout: longWaitTimeout, - ); - - final five = await driver.requestData('5'); - final seconds = - (await driver.requestData('longform_seconds')).split(' ')[1]; - await driver.tapText( - '$five $seconds', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText('set', capitalize: true); - - await driver.typeAndSend('test_disappearing_messages_2'); - - await driver.waitForSeconds(5); - await driver.saveScreenshot('final'); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/19_become_pro_test.dart b/integration_test/19_become_pro_test.dart deleted file mode 100644 index 8cd9bd2525..0000000000 --- a/integration_test/19_become_pro_test.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText( - 'Upgrade to Lantern Pro', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.waitForSeconds(2); - }, - ); -} diff --git a/integration_test/1A_chat_number_formats_test.dart b/integration_test/1A_chat_number_formats_test.dart deleted file mode 100644 index d168d1a40a..0000000000 --- a/integration_test/1A_chat_number_formats_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.resetFlagsAndEnrollAgain(); - await driver.openTab('Account'); - await driver.tapText('account_management'); - await driver.tapFirstItemInList('account_management_free_list'); - }, - ); -} diff --git a/integration_test/1B_enroll_leave_Me_note_test.dart b/integration_test/1B_enroll_leave_Me_note_test.dart deleted file mode 100644 index 76fc58e080..0000000000 --- a/integration_test/1B_enroll_leave_Me_note_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('chats', homeFirst: true); - await driver.tapFAB(); - await driver.tapFirstItemInList('grouped_contact_list'); - await driver.typeAndSend('test_text'); - }, - ); -} diff --git a/integration_test/20_vpn_test.dart b/integration_test/20_vpn_test.dart deleted file mode 100644 index 5aa438d63a..0000000000 --- a/integration_test/20_vpn_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('VPN'); - await driver.tapType('FlutterSwitch'); - }, - ); -} diff --git a/integration_test/21_account_management_test.dart b/integration_test/21_account_management_test.dart deleted file mode 100644 index b3544307d7..0000000000 --- a/integration_test/21_account_management_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText('account_management'); - }, - ); -} diff --git a/integration_test/22_invite_test.dart b/integration_test/22_invite_test.dart deleted file mode 100644 index 7f98fe8d7e..0000000000 --- a/integration_test/22_invite_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText('Invite Friends'); - }, - ); -} diff --git a/integration_test/23_download_desktop_test.dart b/integration_test/23_download_desktop_test.dart deleted file mode 100644 index 68d88194e2..0000000000 --- a/integration_test/23_download_desktop_test.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText('desktop_version'); - await driver.tapText('Share link', capitalize: true); - }, - ); -} diff --git a/integration_test/24A_authorize_device_pro_PIN_test.dart b/integration_test/24A_authorize_device_pro_PIN_test.dart deleted file mode 100644 index 4e8275d647..0000000000 --- a/integration_test/24A_authorize_device_pro_PIN_test.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText('Authorize Device for Pro', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.tapText( - 'Link with PIN', - capitalize: true, - overwriteTimeout: defaultWaitTimeout, - ); - }, - ); -} diff --git a/integration_test/24B_authorize_device_pro_email_test.dart b/integration_test/24B_authorize_device_pro_email_test.dart deleted file mode 100644 index 3eeb778861..0000000000 --- a/integration_test/24B_authorize_device_pro_email_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText( - 'Authorize Device for Pro', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.tapText( - 'Link via Email', - capitalize: true, - overwriteTimeout: defaultWaitTimeout, - ); - await driver.typeAndSend('youremail@email.com'); - await driver.tapText( - 'Submit', - capitalize: true, - overwriteTimeout: defaultWaitTimeout, - ); - await driver.screenshotCurrentView(); - }, - ); -} diff --git a/integration_test/25_report_issue_test.dart b/integration_test/25_report_issue_test.dart deleted file mode 100644 index 78b57ee303..0000000000 --- a/integration_test/25_report_issue_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText('Settings'); - await driver.tapText('report_issue'); - // TODO: select issue from dropdown - // TODO: enter text - // TODO: test submit - }, - ); -} diff --git a/integration_test/26_proxy_all_traffic_test.dart b/integration_test/26_proxy_all_traffic_test.dart deleted file mode 100644 index a4c9a61ea2..0000000000 --- a/integration_test/26_proxy_all_traffic_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Account'); - await driver.tapText( - 'Settings', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.tapKey( - 'proxy_all_icon', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.tapText( - 'OK', - overwriteTimeout: defaultWaitTimeout, - ); - await driver.tapType( - 'FlutterSwitch', - overwriteTimeout: defaultWaitTimeout, - ); - }, - ); -} diff --git a/integration_test/27_replica_search_test.dart b/integration_test/27_replica_search_test.dart deleted file mode 100644 index cbed06d2b3..0000000000 --- a/integration_test/27_replica_search_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('Discover'); - final messageContent = await driver.requestData('test_replica_search'); - await driver.waitForSeconds(2); - await driver.tapType( - 'SearchField', - overwriteTimeout: longWaitTimeout, - ); - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.enterText( - messageContent, - timeout: longWaitTimeout, - ), - screenshotTitle: 'sending_message', - ); - await driver.tapKey('submit_text_field'); - - print('viewing a Replica video'); - await driver.waitForSeconds(5); - await driver.screenshotCurrentView(); - await driver.tapFirstItemInList('replica_tab_view'); - await driver.waitForSeconds(5); - await driver.screenshotCurrentView(); - }, - ); -} diff --git a/integration_test/2_add_via_secure_number_test.dart b/integration_test/2_add_via_secure_number_test.dart deleted file mode 100644 index 07c537e3ac..0000000000 --- a/integration_test/2_add_via_secure_number_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - await runTest((driver) async { - await driver.openTab('chats', homeFirst: true); - - await driver.tapFAB(); - - await driver.tapText( - 'add_via_chat_number', - overwriteTimeout: longWaitTimeout, - ); - - await driver.waitForSeconds(2); - - print('entering secure chat number'); - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.enterText( - textThisNumber, - timeout: longWaitTimeout, - ), - screenshotTitle: 'entering_secure_number', - ); - - await driver.tapText( - 'start_chat', - capitalize: true, - overwriteTimeout: longWaitTimeout, - ); - - await driver.enterText(contactNewName); - - await driver.waitForSeconds(2); - - await driver.tapText('Done', capitalize: true); - }); -} diff --git a/integration_test/3_send_first_message_test.dart b/integration_test/3_send_first_message_test.dart deleted file mode 100644 index 8a02d503f7..0000000000 --- a/integration_test/3_send_first_message_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('chats', homeFirst: true); - - await driver.tapFAB(); - - await driver.tapFirstItemInList('grouped_contact_list'); - - print('typing text'); - await driver.typeAndSend('test_hello'); - }, - ); -} diff --git a/integration_test/4_view_attachments_test.dart b/integration_test/4_view_attachments_test.dart deleted file mode 100644 index 22954c85bf..0000000000 --- a/integration_test/4_view_attachments_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('chats', homeFirst: true); - - await driver.addDummyContacts(); - - await driver.sendDummyFiles(); - - await driver.tapFirstItemInList('chats_messages_list'); - - print('tapping on image attachment'); - await driver.tapType( - 'ImageAttachment', - overwriteTimeout: longWaitTimeout, - ); - - await driver.goBack(); - - await driver.tapType( - 'VideoAttachment', - overwriteTimeout: longWaitTimeout, - ); - }, - ); -} diff --git a/integration_test/5_voice_memo_test.dart b/integration_test/5_voice_memo_test.dart deleted file mode 100644 index e747dd5882..0000000000 --- a/integration_test/5_voice_memo_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - await runTest( - (driver) async { - await driver.openTab('chats', homeFirst: true); - - await driver.tapFAB(); - - // Looking for "Me" works for most languages. - // However in some cases (Chinese), "Me" is a single character, and so is the alphabetic marker we use to separate contacts, which confuses the test driver. - // In that case, we can just find the first element of the 'grouped_contact_list' ListBody. - try { - await driver.tapText('me'); - } catch (_) { - print( - 'there was an issue tapping on Me conversation, will find another contact', - ); - await driver.tapFirstItemInList('grouped_contact_list'); - } - - final recorderButtonFinder = find.byValueKey('recorder_button'); - - print('tapping on start record button'); - await driver.longPress(target: recorderButtonFinder); - - print('tapping on stop record button'); - await driver.longPress(target: recorderButtonFinder); - - print('tapping on send'); - await driver.tapKey('send_message'); - }, - ); -} diff --git a/integration_test/6A_scan_QR_code_test.dart b/integration_test/6A_scan_QR_code_test.dart deleted file mode 100644 index 15aeab2854..0000000000 --- a/integration_test/6A_scan_QR_code_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'scan_QR_code'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - // Test requirements - // * Needs another phone to scan the QR code with - group(testName, () { - test( - 'Scan QR code', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.tapFAB(); - - // click on Scan QR Code - await driver.tapText('scan_qr_code'); - - // screenshot and wait - await driver.screenshotCurrentView(); - await driver.waitForSeconds(1); - - // screenshot and wait - await driver.screenshotCurrentView(); - await driver.waitForSeconds(1); - - // screenshot and wait - await driver.screenshotCurrentView(); - await driver.waitForSeconds(1); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/6B_request_flow_test.dart b/integration_test/6B_request_flow_test.dart deleted file mode 100644 index 881b259f24..0000000000 --- a/integration_test/6B_request_flow_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'request_flow'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - // Test requirements - // * Needs to have _just_ received a message request from another user - // TODO (not immediate): we can set up a second test driver so that one message requests the other - group(testName, () { - test( - 'Accept via message request', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - print('open message request'); - await driver.tapText('just_now'); - - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.tapText( - 'accept', - capitalize: true, - overwriteTimeout: longWaitTimeout, - ), - screenshotTitle: 'naming new contact', - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/6C_introductions_test.dart b/integration_test/6C_introductions_test.dart deleted file mode 100644 index 4af8a784ef..0000000000 --- a/integration_test/6C_introductions_test.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'introductions'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - -// * Test requirements -// * Needs to have received a _single_ introduction from another user - // TODO (not immediate): we can set up a second test driver so that one sends an intro to the other - group(testName, () { - test( - 'Accept an introduction', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.tapText('introductions'); - await driver.tapText('reject', capitalize: true); - await driver.tapText('cancel', capitalize: true); - await driver.tapText('accept', capitalize: true); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/7_call_test.dart b/integration_test/7_call_test.dart deleted file mode 100644 index 2713989aa5..0000000000 --- a/integration_test/7_call_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'call'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Call a contact', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - await driver.tapFAB(); - - await driver.tapFirstItemInList('grouped_contact_list'); - - await driver.tapType( - 'CallAction', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText( - 'call', - overwriteTimeout: longWaitTimeout, - ); - - await driver.waitForSeconds(2); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/8A_send_single_intro_test.dart b/integration_test/8A_send_single_intro_test.dart deleted file mode 100644 index c233b7be09..0000000000 --- a/integration_test/8A_send_single_intro_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'integration_test_common.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'send_single_intro'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Send introduction to a single contact', - () async { - await driver.openTab('chats', homeFirst: true); - - await driver.addDummyContacts(); - - print('navigating to New Chat'); - await driver.tapFAB(); - - print('long pressing first contact in list'); - await driver.longPressFirstItemInList('grouped_contact_list'); - - await driver.tapText('introduce_contact'); - - print('select first contact from Introduce list'); - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.tapFirstItemInList('grouped_contact_list'), - screenshotTitle: 'sending_intros', - ); - - // go back to Chats - await driver.goBack(); - - // tap first item which will display the sent introduction - await driver.tapFirstItemInList('chats_messages_list'); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/8B_send_multiple_intros_test.dart b/integration_test/8B_send_multiple_intros_test.dart deleted file mode 100644 index 0862ab9fd1..0000000000 --- a/integration_test/8B_send_multiple_intros_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'send_multiple_intros'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await connect(); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Send introductions to multiple contacts', - () async { - await driver.openTab( - 'chats', - homeFirst: true, - ); - await driver.addDummyContacts(); - - await driver.tapKey( - 'chats_topbar_more_menu', - ); - - // click on Introduce contacts - await driver.tapText('introduce_contacts'); - - // HACK - print('tap secret select all intros key'); - await driver.tapKey( - 'select_all_intros', - overwriteTimeout: longWaitTimeout, - ); - - await driver.tapText('send_introductions', capitalize: true); - - // we are now back in Chats, go to first message to see our sent invitation - await driver.tapFirstItemInList('chats_messages_list'); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/9A_search_contacts_messages_test.dart b/integration_test/9A_search_contacts_messages_test.dart deleted file mode 100644 index 07b62c86bb..0000000000 --- a/integration_test/9A_search_contacts_messages_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'search_contacts_messages'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Search in Messages and Contacts', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - print('tap search icon'); - await driver.tapKey( - 'search_icon', - overwriteTimeout: defaultWaitTimeout, - ); - - print('enter search term'); - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.enterText( - 'test_text', - timeout: longWaitTimeout, - ), - screenshotTitle: testName, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/9B_search_contacts_test.dart b/integration_test/9B_search_contacts_test.dart deleted file mode 100644 index 6eb6bf8915..0000000000 --- a/integration_test/9B_search_contacts_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'integration_test_common.dart'; -import 'integration_test_constants.dart'; - -Future main() async { - late FlutterDriver driver; - final testName = 'search_contacts'; - - setUpAll(() async { - // Connect to a running Flutter application instance. - driver = await FlutterDriver.connect(timeout: const Duration(seconds: 30)); - await driver.initScreenshotsDirectory(testName); - }); - - tearDownAll(() async { - await driver.close(); - }); - - group(testName, () { - test( - 'Search in Contacts', - () async { - await driver.openTab('chats', homeFirst: true); - await driver.screenshotCurrentView(); - - await driver.tapFAB(); - - print('tap search icon'); - await driver.tapType( - 'RoundButton', - overwriteTimeout: defaultWaitTimeout, - ); - - print('enter search term'); - await driver.captureScreenshotDuringFuture( - futureToScreenshot: driver.enterText( - contactNewName.split(' ')[0], - timeout: longWaitTimeout, - ), - screenshotTitle: testName, - ); - }, - timeout: const Timeout(Duration(minutes: 5)), - ); - }); -} diff --git a/integration_test/app_startup_flow_test.dart b/integration_test/app_startup_flow_test.dart new file mode 100644 index 0000000000..aaae7c4128 --- /dev/null +++ b/integration_test/app_startup_flow_test.dart @@ -0,0 +1,36 @@ +import 'package:lantern/core/widgtes/custom_bottom_bar.dart'; +import 'package:lantern/features/home/home.dart'; + +import 'utils/test_utils.dart'; + +/// Looks like patrol does not have native support yet for Linux and Windows +/// so any test that interact with the native layer will / need to use flutter test +/// patrol feature priority:https://patrol.leancode.co/native/feature-parity +/// finder documentation: https://patrol.leancode.co/finders/usage + +///1 +/// if you want to interact with the native layer, then user patrolTest +/// For running patrolTest, you need to run the patrol test --target integration_test/$fileName +///Make sure using this test will run as native test and not as a flutter test +///So use this only when you need to interact with the native layer + +/// 2 +/// if you do not want not to interact with the native layer, then use patrolWidgetTest +/// For running patrolWidgetTest, you need to run the patrol test or flutter test both will work + +// Implement coverage for the test +// https://codewithandrea.com/articles/flutter-test-coverage/ +// https://github.com/flutter/flutter/issues/101031 +// https://stackoverflow.com/questions/61535142/how-to-use-dylibs-from-a-plugin-inside-a-macos-sandboxed-application +// https://github.com/flutter/flutter/issues/135673 + +// IOS Related issues +// https://github.com/leancodepl/patrol/issues/2159 +// https://github.com/leancodepl/patrol/issues/1871 +// https://stackoverflow.com/questions/54875362/firebase-test-lab-ios-app-not-being-installed +// https://stackoverflow.com/questions/54450113/how-to-handle-code-signing-for-ios-testing-on-firebase-test-lab-through-ci +///Make sure to use custom tear down function + +void main() { + +} diff --git a/integration_test/features/account/account_flow_test.dart b/integration_test/features/account/account_flow_test.dart new file mode 100644 index 0000000000..e8cd1e6945 --- /dev/null +++ b/integration_test/features/account/account_flow_test.dart @@ -0,0 +1,139 @@ +import 'package:lantern/features/account/follow_us.dart'; +import 'package:lantern/features/checkout/feature_list.dart'; +import 'package:lantern/features/checkout/plan_details.dart'; +import 'package:lantern/features/vpn/vpn_switch.dart'; + +import '../../utils/test_utils.dart'; + +void main() { + appTearDown( + () async { + await sl.reset(); + }, + ); + + patrol( + 'account page end to end test', + ($) async { + await $(VPNSwitch).waitUntilVisible(); + await $('Account'.i18n).tap(); + await $.pumpAndSettle(); + await $.pump(const Duration(seconds: 2)); + expect($(AppKeys.inviteFriends).visible, equals(true)); + if (sessionModel.proUserNotifier.value ?? false) { + expect($(AppKeys.upgrade_lantern_pro), findsNothing); + expect($(AppKeys.addDevices).visible, equals(true)); + expect($(AppKeys.devices), findsNothing); + } else { + expect($(AppKeys.upgrade_lantern_pro).visible, equals(true)); + expect($(AppKeys.devices).visible, equals(true)); + expect($(AppKeys.addDevices), findsNothing); + } + + if (isMobile()) { + expect($(AppKeys.desktopVersion).visible, equals(true)); + } + expect($(AppKeys.followUs).visible, equals(true)); + expect($(AppKeys.support).visible, equals(true)); + expect($(AppKeys.setting).visible, equals(true)); + + if (!sessionModel.proUserNotifier.value!) { + //check for navigation + await $(AppKeys.upgrade_lantern_pro).tap(); + await $.pumpAndSettle(); + await $.pump(const Duration(seconds: 2)); + + //plans page + expect($(IconButton).visible, true); + expect($(FullScreenDialog).visible, true); + + try { + await $(PlanCard).waitUntilVisible(); + } on WaitUntilVisibleTimeoutException { + ///This is case when user is not able to get the plans + expect($(RetryWidget), findsOneWidget); + expect($(IconButton), findsOneWidget); + await $('refresh'.toUpperCase()).tap(); + await $.pumpAndSettle(); + await $(PlanCard).waitUntilVisible(); + } + + expect($(PlanCard), findsAtLeast(2)); + expect($(PlanCard).at(0).visible, true); + expect($(PlanCard).at(1).visible, true); + expect($(FeatureList), findsOneWidget); + expect($('activation_lantern_pro_code'.i18n).visible, true); + if ((sessionModel.isAuthEnabled.value ?? false) && + (sessionModel.proUserNotifier.value = false)) { + expect($("restore_purchase".i18n.toUpperCase()).visible, true); + } + + // go back + await $(IconButton).tap(); + await $.pumpAndSettle(); + } + + await $(AppKeys.inviteFriends).tap(); + await $.pumpAndSettle(); + await $.pump(const Duration(seconds: 2)); + + //invite friends page + expect($(ListItemFactory), findsOneWidget); + expect($('share_lantern_pro'.i18n).visible, equals(true)); + expect($('share_referral_code'.i18n.toUpperCase()).visible, equals(true)); + expect($(Button).visible, equals(true)); + + await $(IconButton).tap(); + + if (!sessionModel.proUserNotifier.value!) { + // approve device page + await $(AppKeys.devices).tap(); + await $.pumpAndSettle(); + expect($(Button), findsExactly(2)); + expect($('Link with PIN'.i18n.toUpperCase()).visible, true); + expect($('Link via Email'.i18n.toUpperCase()).visible, true); + + await $(IconButton).tap(); + } + + if (isMobile()) { + // desktop version + await $(AppKeys.desktopVersion).tap(); + await $.pumpAndSettle(); + expect($(Button).visible, true); + expect($(CAssetImage).visible, true); + expect($('most_recent_lantern_apps'.i18n), findsOneWidget); + expect($('most_recent_lantern_apps'.i18n).visible, true); + expect($('share_link'.i18n.toUpperCase()).visible, true); + + await $(IconButton).tap(); + } + + // follow us + await $(AppKeys.followUs).tap(); + await $.pumpAndSettle(); + expect($(FollowUs).visible, true); + await $.tester.tapAt(const Offset(10, 10)); + await $.pumpAndSettle(); + + //support + await $(AppKeys.support).tap(); + await $.pumpAndSettle(); + + expect($(AppKeys.reportIssue).visible, true); + expect($(AppKeys.userForum).visible, true); + expect($(AppKeys.faq).visible, true); + + //report issue + await $(AppKeys.reportIssue).tap(); + await $.pumpAndSettle(); + + expect($(CTextField).at(0).visible, true); + expect($(CTextField).at(1).visible, true); + expect($(CTextField), findsExactly(2)); + expect($(DropdownButtonFormField), findsOneWidget); + expect($(DropdownButtonFormField).visible, true); + expect($('send_report'.i18n.toUpperCase()).visible, true); + }, + ); +} diff --git a/integration_test/features/account/language_flow_test.dart b/integration_test/features/account/language_flow_test.dart new file mode 100644 index 0000000000..374bd6cd66 --- /dev/null +++ b/integration_test/features/account/language_flow_test.dart @@ -0,0 +1,44 @@ +import 'package:intl/intl.dart'; +import 'package:lantern/core/localization/localization_constants.dart'; + +import '../../utils/test_utils.dart'; + +const enUs = 'en_US'; +const faIr = 'fa_IR'; +final englishLang = toBeginningOfSentenceCase(displayLanguage(enUs)); +final persianLang = toBeginningOfSentenceCase(displayLanguage(faIr)); + +void main() { + appTearDown( + () async { + await sl.reset(); + }, + ); + + + patrol( + "language end to end test", + ($) async { + await $('Account'.i18n).tap(); + await $.pumpAndSettle(); + await $(AppKeys.setting).tap(); + await $.pumpAndSettle(); + await $(AppKeys.language).tap(); + await $.pumpAndSettle(); + + expect($(ListView).visible, true); + + await $(persianLang).tap(); + await $.pumpAndSettle(); + expect(Localization.locale.toLowerCase(), faIr.toLowerCase()); + + await $(AppKeys.language).waitUntilVisible(); + await $(AppKeys.language).tap(); + await $.pumpAndSettle(); + + await $(englishLang).tap(); + await $.pumpAndSettle(); + expect(Localization.locale.toLowerCase(), enUs.toLowerCase()); + }, + ); +} diff --git a/integration_test/features/account/report_issue_flow_test.dart b/integration_test/features/account/report_issue_flow_test.dart new file mode 100644 index 0000000000..9dcfeaaabc --- /dev/null +++ b/integration_test/features/account/report_issue_flow_test.dart @@ -0,0 +1,45 @@ +import '../../utils/test_utils.dart'; + +void main() { + appTearDown( + () async { + await sl.reset(); + }, + ); + + patrol( + "report issue end to end test", + ($) async { + await $('Account'.i18n).tap(); + await $.pumpAndSettle(); + await $(AppKeys.support).tap(); + await $.pumpAndSettle(); + await $(AppKeys.reportIssue).tap(); + await $.pumpAndSettle(); + + expect($(CTextField), findsExactly(2)); + expect($(DropdownButtonFormField), findsOneWidget); + expect($('send_report'.i18n.toUpperCase()), findsOneWidget); + expect($(Button), findsOneWidget); + + final email = $.tester.widget($(CTextField).at(0)); + final desc = $.tester.widget($(CTextField).at(1)); + final sendReport = $.tester.widget